Unverified Commit 0a2e0a47 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Avoid using `TextAffinity` in `TextBoundary` (#117446)

* Avoid affinity like the plague

* ignore lint

* clean up

* fix test

* review

* Move wordboundary to text painter

* docs

* fix tests
parent 5a87a829
......@@ -144,6 +144,133 @@ enum TextWidthBasis {
longestLine,
}
/// A [TextBoundary] subclass for locating word breaks.
///
/// The underlying implementation uses [UAX #29](https://unicode.org/reports/tr29/)
/// defined default word boundaries.
///
/// The default word break rules can be tailored to meet the requirements of
/// different use cases. For instance, the default rule set keeps horizontal
/// whitespaces together as a single word, which may not make sense in a
/// word-counting context -- "hello world" counts as 3 words instead of 2.
/// An example is the [moveByWordBoundary] variant, which is a tailored
/// word-break locator that more closely matches the default behavior of most
/// platforms and editors when it comes to handling text editing keyboard
/// shortcuts that move or delete word by word.
class WordBoundary extends TextBoundary {
/// Creates a [WordBoundary] with the text and layout information.
WordBoundary._(this._text, this._paragraph);
final InlineSpan _text;
final ui.Paragraph _paragraph;
@override
TextRange getTextBoundaryAt(int position) => _paragraph.getWordBoundary(TextPosition(offset: max(position, 0)));
// Combines two UTF-16 code units (high surrogate + low surrogate) into a
// single code point that represents a supplementary character.
static int _codePointFromSurrogates(int highSurrogate, int lowSurrogate) {
assert(
TextPainter._isHighSurrogate(highSurrogate),
'U+${highSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a high surrogate.',
);
assert(
TextPainter._isLowSurrogate(lowSurrogate),
'U+${lowSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a low surrogate.',
);
const int base = 0x010000 - (0xD800 << 10) - 0xDC00;
return (highSurrogate << 10) + lowSurrogate + base;
}
// The Runes class does not provide random access with a code unit offset.
int? _codePointAt(int index) {
final int? codeUnitAtIndex = _text.codeUnitAt(index);
if (codeUnitAtIndex == null) {
return null;
}
switch (codeUnitAtIndex & 0xFC00) {
case 0xD800:
return _codePointFromSurrogates(codeUnitAtIndex, _text.codeUnitAt(index + 1)!);
case 0xDC00:
return _codePointFromSurrogates(_text.codeUnitAt(index - 1)!, codeUnitAtIndex);
default:
return codeUnitAtIndex;
}
}
static bool _isNewline(int codePoint) {
switch (codePoint) {
case 0x000A:
case 0x0085:
case 0x000B:
case 0x000C:
case 0x2028:
case 0x2029:
return true;
default:
return false;
}
}
bool _skipSpacesAndPunctuations(int offset, bool forward) {
// Use code point since some punctuations are supplementary characters.
// "inner" here refers to the code unit that's before the break in the
// search direction (`forward`).
final int? innerCodePoint = _codePointAt(forward ? offset - 1 : offset);
final int? outerCodeUnit = _text.codeUnitAt(forward ? offset : offset - 1);
// Make sure the hard break rules in UAX#29 take precedence over the ones we
// add below. Luckily there're only 4 hard break rules for word breaks, and
// dictionary based breaking does not introduce new hard breaks:
// https://unicode-org.github.io/icu/userguide/boundaryanalysis/break-rules.html#word-dictionaries
//
// WB1 & WB2: always break at the start or the end of the text.
final bool hardBreakRulesApply = innerCodePoint == null || outerCodeUnit == null
// WB3a & WB3b: always break before and after newlines.
|| _isNewline(innerCodePoint) || _isNewline(outerCodeUnit);
return hardBreakRulesApply || !RegExp(r'[\p{Space_Separator}\p{Punctuation}]', unicode: true).hasMatch(String.fromCharCode(innerCodePoint));
}
/// Returns a [TextBoundary] suitable for handling keyboard navigation
/// commands that change the current selection word by word.
///
/// This [TextBoundary] is used by text widgets in the flutter framework to
/// provide default implementation for text editing shortcuts, for example,
/// "delete to the previous word".
///
/// The implementation applies the same set of rules [WordBoundary] uses,
/// except that word breaks end on a space separator or a punctuation will be
/// skipped, to match the behavior of most platforms. Additional rules may be
/// added in the future to better match platform behaviors.
late final TextBoundary moveByWordBoundary = _UntilTextBoundary(this, _skipSpacesAndPunctuations);
}
class _UntilTextBoundary extends TextBoundary {
const _UntilTextBoundary(this._textBoundary, this._predicate);
final UntilPredicate _predicate;
final TextBoundary _textBoundary;
@override
int? getLeadingTextBoundaryAt(int position) {
if (position < 0) {
return null;
}
final int? offset = _textBoundary.getLeadingTextBoundaryAt(position);
return offset == null || _predicate(offset, false)
? offset
: getLeadingTextBoundaryAt(offset - 1);
}
@override
int? getTrailingTextBoundaryAt(int position) {
final int? offset = _textBoundary.getTrailingTextBoundaryAt(max(position, 0));
return offset == null || _predicate(offset, true)
? offset
: getTrailingTextBoundaryAt(offset);
}
}
/// This is used to cache and pass the computed metrics regarding the
/// caret's size and position. This is preferred due to the expensive
/// nature of the calculation.
......@@ -750,7 +877,7 @@ class TextPainter {
// Creates a ui.Paragraph using the current configurations in this class and
// assign it to _paragraph.
void _createParagraph() {
ui.Paragraph _createParagraph() {
assert(_paragraph == null || _rebuildParagraphForPaint);
final InlineSpan? text = this.text;
if (text == null) {
......@@ -763,8 +890,9 @@ class TextPainter {
_debugMarkNeedsLayoutCallStack = null;
return true;
}());
_paragraph = builder.build();
final ui.Paragraph paragraph = _paragraph = builder.build();
_rebuildParagraphForPaint = false;
return paragraph;
}
void _layoutParagraph(double minWidth, double maxWidth) {
......@@ -861,13 +989,18 @@ class TextPainter {
canvas.drawParagraph(_paragraph!, offset);
}
// Returns true iff the given value is a valid UTF-16 surrogate. The value
// Returns true iff the given value is a valid UTF-16 high surrogate. The value
// must be a UTF-16 code unit, meaning it must be in the range 0x0000-0xFFFF.
//
// See also:
// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
static bool _isUtf16Surrogate(int value) {
return value & 0xF800 == 0xD800;
static bool _isHighSurrogate(int value) {
return value & 0xFC00 == 0xD800;
}
// Whether the given UTF-16 code unit is a low (second) surrogate.
static bool _isLowSurrogate(int value) {
return value & 0xFC00 == 0xDC00;
}
// Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take
......@@ -886,7 +1019,7 @@ class TextPainter {
return null;
}
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
return _isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1;
}
/// Returns the closest offset before `offset` at which the input cursor can
......@@ -897,7 +1030,7 @@ class TextPainter {
return null;
}
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
return _isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1;
}
// Unicode value for a zero width joiner character.
......@@ -916,7 +1049,7 @@ class TextPainter {
const int NEWLINE_CODE_UNIT = 10;
// Check for multi-code-unit glyphs such as emojis or zero width joiner.
final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
final bool needsSearch = _isHighSurrogate(prevCodeUnit) || _isLowSurrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) {
......@@ -966,7 +1099,7 @@ class TextPainter {
final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1));
// Check for multi-code-unit glyphs such as emojis or zero width joiner
final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
final bool needsSearch = _isHighSurrogate(nextCodeUnit) || _isLowSurrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) {
......@@ -1141,6 +1274,18 @@ class TextPainter {
return _paragraph!.getWordBoundary(position);
}
/// {@template flutter.painting.TextPainter.wordBoundaries}
/// Returns a [TextBoundary] that can be used to perform word boundary analysis
/// on the current [text].
///
/// This [TextBoundary] uses word boundary rules defined in [Unicode Standard
/// Annex #29](http://www.unicode.org/reports/tr29/#Word_Boundaries).
/// {@endtemplate}
///
/// Currently word boundary analysis can only be performed after [layout]
/// has been called.
WordBoundary get wordBoundaries => WordBoundary._(text!, _paragraph!);
/// Returns the text range of the line at the given offset.
///
/// The newline (if any) is not returned as part of the range.
......
......@@ -2098,6 +2098,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_setSelection(newSelection, cause);
}
/// {@macro flutter.painting.TextPainter.wordBoundaries}
WordBoundary get wordBoundaries => _textPainter.wordBoundaries;
/// Select a word around the location of the last tap down.
///
/// {@macro flutter.rendering.RenderEditable.selectPosition}
......
......@@ -1559,21 +1559,21 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
switch (granularity) {
case TextGranularity.character:
final String text = range.textInside(fullText);
newPosition = _getNextPosition(CharacterBoundary(text), targetedEdge, forward);
newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, CharacterBoundary(text));
result = SelectionResult.end;
break;
case TextGranularity.word:
final String text = range.textInside(fullText);
newPosition = _getNextPosition(WhitespaceBoundary(text) + WordBoundary(this), targetedEdge, forward);
final TextBoundary textBoundary = paragraph._textPainter.wordBoundaries.moveByWordBoundary;
newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, textBoundary);
result = SelectionResult.end;
break;
case TextGranularity.line:
newPosition = _getNextPosition(LineBreak(this), targetedEdge, forward);
newPosition = _moveToTextBoundaryAtDirection(targetedEdge, forward, LineBoundary(this));
result = SelectionResult.end;
break;
case TextGranularity.document:
final String text = range.textInside(fullText);
newPosition = _getNextPosition(DocumentBoundary(text), targetedEdge, forward);
newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, DocumentBoundary(text));
if (forward && newPosition.offset == range.end) {
result = SelectionResult.next;
} else if (!forward && newPosition.offset == range.start) {
......@@ -1592,15 +1592,43 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
return result;
}
TextPosition _getNextPosition(TextBoundary boundary, TextPosition position, bool forward) {
if (forward) {
return _clampTextPosition(
(PushTextPosition.forward + boundary).getTrailingTextBoundaryAt(position)
);
// Move **beyond** the local boundary of the given type (unless range.start or
// range.end is reached). Used for most TextGranularity types except for
// TextGranularity.line, to ensure the selection movement doesn't get stuck at
// a local fixed point.
TextPosition _moveBeyondTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
final int newOffset = forward
? textBoundary.getTrailingTextBoundaryAt(end.offset) ?? range.end
: textBoundary.getLeadingTextBoundaryAt(end.offset - 1) ?? range.start;
return TextPosition(offset: newOffset);
}
// Move **to** the local boundary of the given type. Typically used for line
// boundaries, such that performing "move to line start" more than once never
// moves the selection to the previous line.
TextPosition _moveToTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
assert(end.offset >= 0);
final int caretOffset;
switch (end.affinity) {
case TextAffinity.upstream:
if (end.offset < 1 && !forward) {
assert (end.offset == 0);
return const TextPosition(offset: 0);
}
final CharacterBoundary characterBoundary = CharacterBoundary(fullText);
caretOffset = math.max(
0,
characterBoundary.getLeadingTextBoundaryAt(range.start + end.offset) ?? range.start,
) - 1;
break;
case TextAffinity.downstream:
caretOffset = end.offset;
break;
}
return _clampTextPosition(
(PushTextPosition.backward + boundary).getLeadingTextBoundaryAt(position),
);
final int offset = forward
? textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? range.end
: textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? range.start;
return TextPosition(offset: offset);
}
MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) {
......
......@@ -137,11 +137,11 @@ class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIn
/// reverse.
///
/// This is typically only used on MacOS.
class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalTextEditingIntent {
class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent].
const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent({
required bool forward,
}) : super(forward);
}) : super(forward, false, true);
}
/// Expands the current selection to the document boundary in the direction
......@@ -154,11 +154,11 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional
///
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
/// moves the extent.
class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
class ExpandSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExpandSelectionToDocumentBoundaryIntent].
const ExpandSelectionToDocumentBoundaryIntent({
required bool forward,
}) : super(forward);
}) : super(forward, false);
}
/// Expands the current selection to the closest line break in the direction
......@@ -173,11 +173,11 @@ class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingInte
///
/// [ExtendSelectionToLineBreakIntent], which is similar but always moves the
/// extent.
class ExpandSelectionToLineBreakIntent extends DirectionalTextEditingIntent {
class ExpandSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExpandSelectionToLineBreakIntent].
const ExpandSelectionToLineBreakIntent({
required bool forward,
}) : super(forward);
}) : super(forward, false);
}
/// Extends, or moves the current selection from the current
......
......@@ -9785,7 +9785,8 @@ void main() {
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 56, affinity: TextAffinity.upstream),
// arrowRight always sets the affinity to downstream.
const TextSelection.collapsed(offset: 56),
);
// Keep moving out.
......@@ -9795,7 +9796,7 @@ void main() {
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 62, affinity: TextAffinity.upstream),
const TextSelection.collapsed(offset: 62),
);
for (int i = 0; i < (66 - 62); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
......@@ -9803,7 +9804,7 @@ void main() {
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
const TextSelection.collapsed(offset: 66),
); // We're at the edge now.
await tester.pumpAndSettle();
......
......@@ -1075,8 +1075,13 @@ void main() {
),
);
selection = paragraph.selections[0];
expect(selection.start, 0); // [how ]are you
expect(selection.end, 4);
if (isBrowser && !isCanvasKit) {
// how [are you\n]
expect(selection, const TextRange(start: 4, end: 12));
} else {
// [how ]are you
expect(selection, const TextRange(start: 0, end: 4));
}
});
test('can granularly extend selection - document', () async {
......
......@@ -45,7 +45,7 @@ void main() {
);
}
group('iOS: do not delete/backspace events', () {
group('iOS: do not handle delete/backspace events', () {
final TargetPlatformVariant iOS = TargetPlatformVariant.only(TargetPlatform.iOS);
final FocusNode editable = FocusNode();
final FocusNode spy = FocusNode();
......
......@@ -576,12 +576,12 @@ void main() {
'Now is the time for\n'
'all good people\n'
'to come to the aid\n'
'of their country',
'of their ',
);
expect(
controller.selection,
const TextSelection.collapsed(offset: 71),
const TextSelection.collapsed(offset: 64),
);
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
......@@ -795,7 +795,7 @@ void main() {
testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async {
controller.text = testSoftwrapText;
// Place the caret at the beginning of the 3rd line.
// Place the caret at the end of the 2nd line.
controller.selection = const TextSelection.collapsed(
offset: 40,
affinity: TextAffinity.upstream,
......@@ -827,12 +827,11 @@ void main() {
await tester.pumpWidget(buildEditableText());
await sendKeyCombination(tester, lineModifierBackspace());
expect(controller.text, testSoftwrapText);
expect(
controller.selection,
const TextSelection.collapsed(offset: 40),
);
expect(controller.text, testSoftwrapText);
}, variant: TargetPlatformVariant.all());
testWidgets('readonly', (WidgetTester tester) async {
......@@ -976,7 +975,7 @@ void main() {
testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async {
controller.text = testSoftwrapText;
// Place the caret at the beginning of the 3rd line.
// Place the caret at the end of the 2nd line.
controller.selection = const TextSelection.collapsed(
offset: 40,
affinity: TextAffinity.upstream,
......@@ -1215,7 +1214,6 @@ void main() {
expect(controller.selection, const TextSelection.collapsed(
offset: 21,
affinity: TextAffinity.upstream,
));
}, variant: TargetPlatformVariant.all());
......@@ -1230,7 +1228,6 @@ void main() {
expect(controller.selection, const TextSelection.collapsed(
offset: 10,
affinity: TextAffinity.upstream,
));
}, variant: allExceptApple);
......@@ -1341,7 +1338,6 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 46, // After "to".
affinity: TextAffinity.upstream,
));
// "good" to "come" is selected.
......@@ -1354,7 +1350,6 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 28, // After "good".
affinity: TextAffinity.upstream,
));
}, variant: allExceptApple);
......@@ -1718,8 +1713,7 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 10,
affinity: TextAffinity.upstream,
offset: 10, // after the first "the"
));
}, variant: macOSOnly);
......@@ -1790,7 +1784,6 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 46, // After "to".
affinity: TextAffinity.upstream,
));
// "good" to "come" is selected.
......@@ -1803,7 +1796,6 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 28, // After "good".
affinity: TextAffinity.upstream,
));
}, variant: macOSOnly);
......@@ -2351,9 +2343,7 @@ void main() {
expect(controller.text, 'testing');
expect(
controller.selection,
const TextSelection.collapsed(
offset: 6,
affinity: TextAffinity.upstream), // should not expand selection
const TextSelection.collapsed(offset: 6), // should not expand selection
reason: selectRight.toString(),
);
}, variant: TargetPlatformVariant.desktop());
......
......@@ -5695,7 +5695,6 @@ void main() {
const TextSelection(
baseOffset: 0,
extentOffset: 6,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -5957,7 +5956,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: testText.length,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6001,7 +5999,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 3,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6193,7 +6190,6 @@ void main() {
const TextSelection(
baseOffset: 10,
extentOffset: 10,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6653,7 +6649,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6678,7 +6673,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6721,7 +6715,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6807,7 +6800,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6832,7 +6824,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6875,7 +6866,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6971,7 +6961,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6996,7 +6985,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7039,7 +7027,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7136,7 +7123,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7182,7 +7168,6 @@ void main() {
const TextSelection(
baseOffset: 23,
extentOffset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7193,7 +7178,6 @@ void main() {
const TextSelection(
baseOffset: 23,
extentOffset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7327,7 +7311,6 @@ void main() {
controller.selection,
equals(const TextSelection.collapsed(
offset: 4,
affinity: TextAffinity.upstream,
)),
);
......@@ -7511,7 +7494,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7535,7 +7517,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7593,7 +7574,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7706,7 +7686,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7730,7 +7709,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7788,7 +7766,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -7900,7 +7877,6 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
);
......@@ -7994,7 +7970,6 @@ void main() {
controller.selection,
equals(const TextSelection.collapsed(
offset: testText.length,
affinity: TextAffinity.upstream,
)),
);
......@@ -8078,7 +8053,6 @@ void main() {
controller.selection,
equals(const TextSelection.collapsed(
offset: testText.length,
affinity: TextAffinity.upstream,
)),
);
......@@ -11036,7 +11010,6 @@ void main() {
controller.selection = const TextSelection(
baseOffset: 15,
extentOffset: 15,
affinity: TextAffinity.upstream,
);
await tester.pump();
expect(controller.selection.isCollapsed, true);
......@@ -11275,7 +11248,6 @@ void main() {
const TextSelection(
baseOffset: 29,
extentOffset: 0,
affinity: TextAffinity.upstream,
),
),
);
......
......@@ -1296,12 +1296,12 @@ void main() {
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control));
await tester.pump();
// Ho[w are you]?
// Ho[w are ]you?
// Good, and you?
// Fine, thank you.
expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 11);
expect(paragraph1.selections[0].end, 8);
expect(paragraph2.selections.length, 0);
}, variant: TargetPlatformVariant.all());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment