mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
319 lines
12 KiB
Dart
319 lines
12 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style license that can be
|
||
// found in the LICENSE file.
|
||
|
||
import 'package:flutter/rendering.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
|
||
class _ConsistentTextRangeImplementationMatcher extends Matcher {
|
||
_ConsistentTextRangeImplementationMatcher(int length)
|
||
: range = TextRange(start: -1, end: length + 1),
|
||
assert(length >= 0);
|
||
|
||
final TextRange range;
|
||
@override
|
||
Description describe(Description description) {
|
||
return description.add(
|
||
'The implementation of TextBoundary.getTextBoundaryAt is consistent with its other methods.',
|
||
);
|
||
}
|
||
|
||
@override
|
||
Description describeMismatch(
|
||
dynamic item,
|
||
Description mismatchDescription,
|
||
Map<dynamic, dynamic> matchState,
|
||
bool verbose,
|
||
) {
|
||
final boundary = matchState['textBoundary'] as TextBoundary;
|
||
final position = matchState['position'] as int;
|
||
final int leading = boundary.getLeadingTextBoundaryAt(position) ?? -1;
|
||
final int trailing = boundary.getTrailingTextBoundaryAt(position) ?? -1;
|
||
|
||
return mismatchDescription.add(
|
||
'at position $position, expected ${TextRange(start: leading, end: trailing)} but got ${boundary.getTextBoundaryAt(position)}',
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||
for (int i = range.start; i <= range.end; i++) {
|
||
final int? leading = (item as TextBoundary).getLeadingTextBoundaryAt(i);
|
||
final int? trailing = item.getTrailingTextBoundaryAt(i);
|
||
final TextRange boundary = item.getTextBoundaryAt(i);
|
||
final bool consistent = boundary.start == (leading ?? -1) && boundary.end == (trailing ?? -1);
|
||
if (!consistent) {
|
||
matchState['textBoundary'] = item;
|
||
matchState['position'] = i;
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
Matcher _hasConsistentTextRangeImplementationWithinRange(int length) =>
|
||
_ConsistentTextRangeImplementationMatcher(length);
|
||
|
||
void main() {
|
||
test('Character boundary works', () {
|
||
const boundary = CharacterBoundary('abc');
|
||
expect(boundary, _hasConsistentTextRangeImplementationWithinRange(3));
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(-1), null);
|
||
expect(boundary.getTrailingTextBoundaryAt(-1), 0);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(0), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(0), 1);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(1), 1);
|
||
expect(boundary.getTrailingTextBoundaryAt(1), 2);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(2), 2);
|
||
expect(boundary.getTrailingTextBoundaryAt(2), 3);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(3), 3);
|
||
expect(boundary.getTrailingTextBoundaryAt(3), null);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(4), 3);
|
||
expect(boundary.getTrailingTextBoundaryAt(4), null);
|
||
});
|
||
|
||
test('Character boundary works with grapheme', () {
|
||
const text = 'a❄︎c';
|
||
const boundary = CharacterBoundary(text);
|
||
expect(boundary, _hasConsistentTextRangeImplementationWithinRange(text.length));
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(-1), null);
|
||
expect(boundary.getTrailingTextBoundaryAt(-1), 0);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(0), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(0), 1);
|
||
|
||
// The `❄` takes two character length.
|
||
expect(boundary.getLeadingTextBoundaryAt(1), 1);
|
||
expect(boundary.getTrailingTextBoundaryAt(1), 3);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(2), 1);
|
||
expect(boundary.getTrailingTextBoundaryAt(2), 3);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(3), 3);
|
||
expect(boundary.getTrailingTextBoundaryAt(3), 4);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(text.length), text.length);
|
||
expect(boundary.getTrailingTextBoundaryAt(text.length), null);
|
||
});
|
||
|
||
test('wordBoundary.moveByWordBoundary', () {
|
||
const text =
|
||
'ABC ABC\n' // [0, 10)
|
||
'AÁ Á\n' // [10, 20)
|
||
' \n' // [20, 30)
|
||
'ABC!!!ABC\n' // [30, 40)
|
||
' !ABC !!\n' // [40, 50)
|
||
'A 𑗋𑗋 A\n'; // [50, 60)
|
||
|
||
final textPainter = TextPainter()
|
||
..textDirection = TextDirection.ltr
|
||
..text = const TextSpan(text: text)
|
||
..layout();
|
||
|
||
final TextBoundary boundary = textPainter.wordBoundaries.moveByWordBoundary;
|
||
|
||
// 4 points to the 2nd whitespace in the first line.
|
||
// Don't break between horizontal spaces and letters/numbers.
|
||
expect(boundary.getLeadingTextBoundaryAt(4), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(4), 9);
|
||
|
||
// Works when words are starting/ending with a combining diacritical mark.
|
||
expect(boundary.getLeadingTextBoundaryAt(14), 10);
|
||
expect(boundary.getTrailingTextBoundaryAt(14), 19);
|
||
|
||
// Do break before and after newlines.
|
||
expect(boundary.getLeadingTextBoundaryAt(24), 20);
|
||
expect(boundary.getTrailingTextBoundaryAt(24), 29);
|
||
|
||
// Do not break on punctuations.
|
||
expect(boundary.getLeadingTextBoundaryAt(34), 30);
|
||
expect(boundary.getTrailingTextBoundaryAt(34), 39);
|
||
|
||
// Ok to break if next to punctuations or separating spaces.
|
||
expect(boundary.getLeadingTextBoundaryAt(44), 43);
|
||
expect(boundary.getTrailingTextBoundaryAt(44), 46);
|
||
|
||
// 44 points to a low surrogate of a punctuation.
|
||
expect(boundary.getLeadingTextBoundaryAt(54), 50);
|
||
expect(boundary.getTrailingTextBoundaryAt(54), 59);
|
||
});
|
||
|
||
test('line boundary works', () {
|
||
final boundary = LineBoundary(TestTextLayoutMetrics());
|
||
expect(boundary.getLeadingTextBoundaryAt(3), TestTextLayoutMetrics.lineAt3.start);
|
||
expect(boundary.getTrailingTextBoundaryAt(3), TestTextLayoutMetrics.lineAt3.end);
|
||
expect(boundary.getTextBoundaryAt(3), TestTextLayoutMetrics.lineAt3);
|
||
});
|
||
|
||
group('paragraph boundary', () {
|
||
test('works for simple cases', () {
|
||
const textA = 'abcd efg hi\njklmno\npqrstuv';
|
||
const boundaryA = ParagraphBoundary(textA);
|
||
|
||
// Position enclosed inside of paragraph, 'abcd efg h|i\n'.
|
||
const position = 10;
|
||
|
||
// The range includes the line terminator.
|
||
expect(boundaryA.getLeadingTextBoundaryAt(position), 0);
|
||
expect(boundaryA.getTrailingTextBoundaryAt(position), 12);
|
||
|
||
// This text includes a carriage return followed by a line feed.
|
||
const textB = 'abcd efg hi\r\njklmno\npqrstuv';
|
||
const boundaryB = ParagraphBoundary(textB);
|
||
expect(boundaryB.getLeadingTextBoundaryAt(position), 0);
|
||
expect(boundaryB.getTrailingTextBoundaryAt(position), 13);
|
||
|
||
const textF =
|
||
'Now is the time for\n' // 20
|
||
'all good people\n' // 20 + 16 => 36
|
||
'to come to the aid\n' // 36 + 19 => 55
|
||
'of their country.'; // 55 + 17 => 72
|
||
const boundaryF = ParagraphBoundary(textF);
|
||
const positionF = 11;
|
||
expect(boundaryF.getLeadingTextBoundaryAt(positionF), 0);
|
||
expect(boundaryF.getTrailingTextBoundaryAt(positionF), 20);
|
||
});
|
||
|
||
test('works for consecutive line terminators involving CRLF', () {
|
||
const textI =
|
||
'Now is the time for\n' // 20
|
||
'all good people\n\r\n' // 20 + 16 => 38
|
||
'to come to the aid\n' // 38 + 19 => 57
|
||
'of their country.'; // 57 + 17 => 74
|
||
const boundaryI = ParagraphBoundary(textI);
|
||
const positionI = 56; // \n at the end of the third line.
|
||
const positionJ = 38; // t at beginning of third line.
|
||
const positionK = 37; // \n at end of second line.
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionI), 38);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionI), 57);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionJ), 38);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionJ), 57);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionK), 36);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionK), 38);
|
||
});
|
||
|
||
test('works for consecutive line terminators', () {
|
||
const textI =
|
||
'Now is the time for\n' // 20
|
||
'all good people\n\n' // 20 + 16 => 37
|
||
'to come to the aid\n' // 37 + 19 => 56
|
||
'of their country.'; // 56 + 17 => 73
|
||
const boundaryI = ParagraphBoundary(textI);
|
||
const positionI = 55; // \n at the end of the third line.
|
||
const positionJ = 37; // t at beginning of third line.
|
||
const positionK = 36; // \n at end of second line.
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionI), 37);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionI), 56);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionJ), 37);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionJ), 56);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionK), 36);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionK), 37);
|
||
});
|
||
|
||
test('leading boundary works for consecutive CRLF', () {
|
||
// This text includes multiple consecutive carriage returns followed by line feeds (CRLF).
|
||
const textH = 'abcd efg hi\r\n\r\n\r\n\r\n\r\n\r\n\r\n\n\n\n\n\njklmno\npqrstuv';
|
||
const boundaryH = ParagraphBoundary(textH);
|
||
const positionH = 18;
|
||
expect(boundaryH.getLeadingTextBoundaryAt(positionH), 17);
|
||
expect(boundaryH.getTrailingTextBoundaryAt(positionH), 19);
|
||
});
|
||
|
||
test('trailing boundary works for consecutive CRLF', () {
|
||
// This text includes multiple consecutive carriage returns followed by line feeds (CRLF).
|
||
const textG = 'abcd efg hi\r\n\n\n\n\n\n\r\n\r\n\r\n\r\n\n\n\n\n\njklmno\npqrstuv';
|
||
const boundaryG = ParagraphBoundary(textG);
|
||
const positionG = 18;
|
||
expect(boundaryG.getLeadingTextBoundaryAt(positionG), 18);
|
||
expect(boundaryG.getTrailingTextBoundaryAt(positionG), 20);
|
||
});
|
||
|
||
test('works when position is between two CRLF', () {
|
||
const textE = 'abcd efg hi\r\nhello\r\n\n';
|
||
const boundaryE = ParagraphBoundary(textE);
|
||
// Position enclosed inside of paragraph, 'abcd efg hi\r\nhello\r\n\n'.
|
||
const positionE = 16;
|
||
expect(boundaryE.getLeadingTextBoundaryAt(positionE), 13);
|
||
expect(boundaryE.getTrailingTextBoundaryAt(positionE), 20);
|
||
});
|
||
|
||
test('works for multiple consecutive line terminators', () {
|
||
// This text includes multiple consecutive line terminators.
|
||
const textC = 'abcd efg hi\r\n\n\n\n\n\n\n\n\n\n\n\njklmno\npqrstuv';
|
||
const boundaryC = ParagraphBoundary(textC);
|
||
// Position enclosed inside of paragraph, 'abcd efg hi\r\n\n\n\n\n\n|\n\n\n\n\n\njklmno\npqrstuv'.
|
||
const positionC = 18;
|
||
expect(boundaryC.getLeadingTextBoundaryAt(positionC), 18);
|
||
expect(boundaryC.getTrailingTextBoundaryAt(positionC), 19);
|
||
|
||
const textD = 'abcd efg hi\r\n\n\n\n';
|
||
const boundaryD = ParagraphBoundary(textD);
|
||
// Position enclosed inside of paragraph, 'abcd efg hi\r\n\n|\n\n'.
|
||
const positionD = 14;
|
||
expect(boundaryD.getLeadingTextBoundaryAt(positionD), 14);
|
||
expect(boundaryD.getTrailingTextBoundaryAt(positionD), 15);
|
||
});
|
||
});
|
||
|
||
test('document boundary works', () {
|
||
const text = 'abcd efg hi\njklmno\npqrstuv';
|
||
const boundary = DocumentBoundary(text);
|
||
expect(boundary, _hasConsistentTextRangeImplementationWithinRange(text.length));
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(-1), null);
|
||
expect(boundary.getTrailingTextBoundaryAt(-1), text.length);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(0), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(0), text.length);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(10), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(10), text.length);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(text.length), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(text.length), null);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(text.length + 1), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(text.length + 1), null);
|
||
});
|
||
}
|
||
|
||
class TestTextLayoutMetrics extends TextLayoutMetrics {
|
||
static const TextSelection lineAt3 = TextSelection(baseOffset: 0, extentOffset: 10);
|
||
static const TextRange wordBoundaryAt3 = TextRange(start: 4, end: 7);
|
||
|
||
@override
|
||
TextSelection getLineAtOffset(TextPosition position) {
|
||
if (position.offset == 3) {
|
||
return lineAt3;
|
||
}
|
||
throw UnimplementedError();
|
||
}
|
||
|
||
@override
|
||
TextPosition getTextPositionAbove(TextPosition position) {
|
||
throw UnimplementedError();
|
||
}
|
||
|
||
@override
|
||
TextPosition getTextPositionBelow(TextPosition position) {
|
||
throw UnimplementedError();
|
||
}
|
||
|
||
@override
|
||
TextRange getWordBoundary(TextPosition position) {
|
||
if (position.offset == 3) {
|
||
return wordBoundaryAt3;
|
||
}
|
||
throw UnimplementedError();
|
||
}
|
||
}
|