-
Notifications
You must be signed in to change notification settings - Fork 28.7k
Introduce ParagraphBoundary subclass for text editing #116549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce ParagraphBoundary subclass for text editing #116549
Conversation
80828f2
to
fa37dcd
Compare
46400c1
to
f057b3e
Compare
@override | ||
TextPosition getLeadingTextBoundaryAt(TextPosition position) { | ||
return TextPosition( | ||
offset: getTextBoundaryAt(position).start, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: consider implementing these 2 separately. getTextBoundaryAt
always searches for both boundaries instead of the one in the specified direction.
case 0xA: // line feed | ||
case 0xB: // vertical feed | ||
case 0xC: // form feed | ||
case 0xD: // carriage return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When there is a CRLF you shouldn't break after CR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean the CR should come before the LF in this switch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
huh the unicode website is offline. You should not break after CR if it's followed by a LF.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't 0x2028 and 0x2029 also supposed to be on this list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks for catching that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with nits 👍
// `textPosition`. The `textPosition` is bounded by either a line terminator | ||
// in each direction of the text, or if there is no line terminator in a given | ||
// direction then the bound extends to the start/end of the document in that | ||
// direction. The returning range includes the line terminator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these comments use three slashes? I guess as it is right now, users on the docs site will see the docs for TextBoundary.getTextBoundaryAt, and these comments won't show up anywhere. Is that intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "returning" => "returned"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"the line terminator" => "both line terminators" or "the line terminator in both directions"
Right, it includes both, one at each end?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About my first comment, actually I think the docs site will show both the current docs and inherited docs? See: https://master-api.flutter.dev/flutter/dart-core/List/first.html
So I guess if you did add triple slashes here, the docs site would show both. Just letting you know in case you choose to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with triple slashes here, but I noticed the other TextBoundary
subclasses do not have any additional documentation, so maybe I should just leave it as double slash? Do you have any thoughts @LongCatIsLooong ?
// The range includes the line terminator. | ||
expect(boundary.getLeadingTextBoundaryAt(position), const TextPosition(offset: 0)); | ||
expect(boundary.getTrailingTextBoundaryAt(position), const TextPosition(offset: 12, affinity: TextAffinity.upstream)); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: If you think it would be useful, maybe test one or more edge cases of getTextBoundaryAt, like what happens if you pass it a series of newline characters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you've updated this to include a bunch of good edge cases now 👍
/// line terminator that encloses the desired paragraph. | ||
@override | ||
TextPosition getLeadingTextBoundaryAt(TextPosition position) { | ||
final Iterator<int> codeUnitIter = _text.codeUnits.iterator; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use indices to iterate since you'll need them anyways? I think that will make the implementation way simpler.
a75d332
to
d259028
Compare
/// line terminator that encloses the desired paragraph. | ||
@override | ||
int? getLeadingTextBoundaryAt(int position) { | ||
final List<int> codeUnits = _text.codeUnits; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this work:
if (position < 0) {
return null;
}
if (position >= _text.length) {
return getLeadingTextBoundaryAt(_text.length - 1);
}
assert(_text.isNotEmpty);
int index = position;
if (index > 0 && codeUnits[index] == LF && codeUnits[index - 1] == CR) index -= 2;
else if (isNewline(codeUnits[index])) index -= 1;
while (index > 0 && !isNewline(codeUnits[index])) {
index -= 1;
}
return max(index, 0);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fails for something like abcd efg hi\r\n\n\n\n\n\n\n\n\n\n\n\njklmno\npqrstuv
, given position
18. The expected output is 18 for the leading boundary, but we get 17 instead because we skip the new line at the initial position. Still trying to figure out some solution for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for that code! Helped simplify the logic a bunch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I think the while
part needs to be fixed (had an off-by-one moment there):
while (index > 0) {
if (isNewline(codeUnits[index])) {
return index + 1;
}
index -= 1;
}
return max(index, 0);
04b6842
to
cbab75a
Compare
@justinmc @LongCatIsLooong This is ready for a re-review edit: found a bug staus: not ready for review. |
3bed680
to
b4aed45
Compare
/// that follows the line terminator that encloses the desired paragraph. | ||
@override | ||
int? getLeadingTextBoundaryAt(int position) { | ||
if (position < 0 || position > _text.length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By definition this finds the largest index < position. So position > _text.length
shouldn't return null (same for the other method).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't we be unable to iterate through the text if the given position is not contained within the text? Or in that case do we set the position to the end of the text and start iterating from there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eot is also considered a paragraph break I think. The convention is to return the largest index regardless of the bounds, but I don't know if there're significant benefits over returning null when the index is OOB.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if someone where to use ParagraphBoundary
a stand-alone class with a text of 'hello world'. If they use 100
for the position given to getLeadingBoundaryAt
and getTrailingBoundaryAt
, then they would expect null bounds since position
100
does not exist within the text.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Returns the offset of the closest text boundary before or at the given
/// `position`, or null if no boundaries can be found.
///
/// The return value, if not null, is usually less than or equal to `position`.
int? getLeadingTextBoundaryAt(int position);
This is the definition so by that definition it should return the closest offset, "returning null if OOB" currently is not a promise the interface makes, I think it's OK to make that change but to keep it consistent all subclasses would need to change their behavior. But there doesn't seem to be a huge benefit that you gain from switching from one to another?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh okay that makes sense. Sorry I was misunderstanding before, I am fine with keeping the definition with how it is. I have updated the implementation to reflect this.
Pretty much from how I understand it, the implementation should only return null
when position
is OOB
in the direction that we are searching for a boundary.
getLeadingTextBoundaryAt
should return null if position
is OOB
when iterating through the text in the backwards direction. position < 0
.
getTrailingTextBoundaryAt
should return null if position
is OOB
when iterating in the forward direction. position >= _text.length
. This matches what I see from DocumentBoundary/CharacterBoundary
though LineBoundary.getTrailingTextBoundaryAt
uses end >= 0 ? end : null;
.
I also added the two cases below, What do you think?
When getLeadingTextBoundaryAt
is given a position
that is OOB
in the forwards direction, then the closest text boundary would be the end of the text.
When getTrailingTextBoundaryAt
is given a position
that is OOB
in the backwards direction, then the closest text boundary is the beginning of the text 0
.
if (position < 0 || position > _text.length) { | ||
return null; | ||
} | ||
if (position == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the text is empty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure if we should be returning null
or 0
in the case (I'm leaning towards 0 since that would have getTextBoundaryAt
return a collapsed TextRange at 0). Do you have any opinions on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with 0
for the reason above.
if (index > 0 && codeUnits[index] == 0xA && codeUnits[index - 1] == 0xD) { | ||
index -= 2; | ||
} else if (TextLayoutMetrics.isLineTerminator(codeUnits[index]) && !TextLayoutMetrics.isLineTerminator(codeUnits[index - 1])) { | ||
index--; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: index -= 1
for consistency (also this is the preferred style according to the style guide). Same for the code below.
/// line terminator that encloses the desired paragraph. | ||
@override | ||
int? getLeadingTextBoundaryAt(int position) { | ||
final List<int> codeUnits = _text.codeUnits; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I think the while
part needs to be fixed (had an off-by-one moment there):
while (index > 0) {
if (isNewline(codeUnits[index])) {
return index + 1;
}
index -= 1;
}
return max(index, 0);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM when Weiyu's comments are addressed 👍
@@ -146,6 +146,66 @@ void main() { | |||
expect(boundary.getTextBoundaryAt(3), TestTextLayoutMetrics.lineAt3); | |||
}); | |||
|
|||
test('paragraph boundary works', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Up to you if you think this is any more clear, but you could turn this test into group('paragraph boundary', ...
and then make each independent case below into its own small test.
// The range includes the line terminator. | ||
expect(boundary.getLeadingTextBoundaryAt(position), const TextPosition(offset: 0)); | ||
expect(boundary.getTrailingTextBoundaryAt(position), const TextPosition(offset: 12, affinity: TextAffinity.upstream)); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you've updated this to include a bunch of good edge cases now 👍
18d952a
to
aa7d942
Compare
6a358e0
to
0c5d0a1
Compare
e521fa7
to
17b1be2
Compare
* 472b887 0ec8e2802 Roll Fuchsia Mac SDK from 9y7C2oamTv6Py4JSC... to EAFnGijD0l5QxaPxF... (flutter/engine#39236) (flutter/flutter#119461) * 15cd00f a7bb0e410 Roll Fuchsia Linux SDK from 1D63BqURfJdG4r3CK... to xTXbcsPr5GJvFSLha... (flutter/engine#39238) (flutter/flutter#119482) * 530c3f2 [Re-land#2] Button padding M3 (flutter/flutter#119498) * 17eb2e8 Ability to disable the browser's context menu on web (flutter/flutter#118194) * df8ad3d roll packages (flutter/flutter#119370) * b68cebd roll packages (flutter/flutter#119530) * 59d80dc [Android] Add explicit exported tag to Linux_android flavors test (flutter/flutter#117542) * 458b298 Refactoring to use `ver` command instead of `systeminfo` (flutter/flutter#119304) * 54405bf fixes PointerEventConverter to handle malformed scrolling event (flutter/flutter#118124) * e69ea6d Support flipping mouse scrolling axes through modifier keys (flutter/flutter#115610) * 92df6b4 396c7fd0b Reland "Remove references to Observatory (#38919)" (flutter/engine#39139) (flutter/flutter#119546) * 7477d7a Reland "Add --serve-observatory flag to run, attach, and test (#118402)" (flutter/flutter#119529) * 6c12e39 Introduce ParagraphBoundary subclass for text editing (flutter/flutter#116549) * b227df3 Hint text semantics to be excluded in a11y read out if hintText is not visible. (flutter/flutter#119198) * 18c7f8a Fix typo in --machine help text (flutter/flutter#119563) * 329f86a Make a few values non-nullable in cupertino (flutter/flutter#119478) * c4520bc b2efe0175 [web] Expose felt flag for building CanvasKit Chromium (flutter/engine#39201) (flutter/flutter#119567) * 8898f4f Marks Mac_android run_debug_test_android to be unflaky (flutter/flutter#117468) * 1f0b6fb Remove deprecated AppBar/SliverAppBar/AppBarTheme.textTheme member (flutter/flutter#119253) * edaeec8 Roll Flutter Engine from b2efe01754ef to 5011144c0b46 (3 revisions) (flutter/flutter#119578) * 865dc5c Roll Flutter Engine from 5011144c0b46 to daa8eeb7fc0b (2 revisions) (flutter/flutter#119584) * 1148a2a Migrate EditableTextState from addPostFrameCallbacks to compositionCallbacks (flutter/flutter#119359) * 2340902 Roll Flutter Engine from daa8eeb7fc0b to 77218818138f (3 revisions) (flutter/flutter#119586) * 65900b7 Remove deprecated AnimatedSize.vsync parameter (flutter/flutter#119186) * 5b6572f Add debug diagnostics to channels integration test (flutter/flutter#119579) * 504e565 Roll Flutter Engine from 77218818138f to 9448f2966c11 (3 revisions) (flutter/flutter#119592) * 7ba4406 Revert "[Re-land#2] Button padding M3 (#119498)" (flutter/flutter#119597) * 2c34a88 Roll Flutter Engine from 9448f2966c11 to 72abe0e4b828 (3 revisions) (flutter/flutter#119603) * df0ab40 Roll Plugins from ff84c44 to 9da327c (15 revisions) (flutter/flutter#119629) * 67d07a6 [flutter_tools] Fix parsing of existing DDS URIs from exceptions (flutter/flutter#119506) * d272a3a Reland: [macos] add flavor options to tool commands (flutter/flutter#119564) * a16d82c aa00da3c1 Roll Skia from fc31f43cc40a to 3c6eb76a683a (1 revision) (flutter/engine#39280) (flutter/flutter#119605) * f6b0c6d Use first Dart VM Service found with mDNS if there are duplicates (flutter/flutter#119545) * d4c7485 Make Decoration.padding non-nullable (flutter/flutter#119581) * 2fccf4d Remove MediaQuery from WidgetsApp (flutter/flutter#119377) * 9b3b9cf Roll Flutter Engine from aa00da3c1612 to cd2e8885e491 (6 revisions) (flutter/flutter#119639) * 6a54059 Make MultiChildRenderObjectWidget const (flutter/flutter#119195) * e2b3d89 Fix CupertinoNavigationBar should create a backward compatible Annota… (flutter/flutter#119515) * 7bf95f4 1aaf3db31 Roll Dart SDK from 4fdbc7c28141 to 9bcc1773ebf0 (1 revision) (flutter/engine#39290) (flutter/flutter#119640) * 0e22aca Add support for image insertion on Android (flutter/flutter#110052) * ff22813 separatorBuilder can't return null (flutter/flutter#119566) * 60c1f29 2471f430f Update buildroot to c02da5072d1bb2. (flutter/engine#39292) (flutter/flutter#119645) * fbe9ff3 Disable an inaccurate test assertion that will be fixed by an engine roll (flutter/flutter#119653) * 8f90e2a Roll Flutter Engine from 2471f430ff4b to bb7b7006f4a3 (2 revisions) (flutter/flutter#119655) * 3884381 Make gen-l10n error handling independent of logger state (flutter/flutter#119644) * 198a51a Migrate the Material Date pickers to M3 Reprise (flutter/flutter#119033) * dc86565 Roll Flutter Engine from bb7b7006f4a3 to 521b975449ba (4 revisions) (flutter/flutter#119670) * 82df235 Undo making Flex,Row,Column const (flutter/flutter#119669) * 6f9a896 Roll Flutter Engine from 521b975449ba to 38913c5484cf (2 revisions) (flutter/flutter#119675) * 8d0af36 🥅 Produce warning instead of error for storage base url overrides (flutter/flutter#119595) * 3894d24 1703a3966 Roll Skia from c29211525dac to 654f4805e8b8 (21 revisions) (flutter/engine#39309) (flutter/flutter#119683) * a752c2f Expose enableIMEPersonalizedLearning on CupertinoSearchTextField (flutter/flutter#119439) * e1f0b1d d92e23cb5 Roll Skia from 654f4805e8b8 to da41cf18f651 (1 revision) (flutter/engine#39311) (flutter/flutter#119686) * 97d273c CupertinoThemeData equality (flutter/flutter#119480) * 4167835 5b549950f Roll Fuchsia Linux SDK from 71lEeibIyrq0V8jId... to TFcelQ5SwrzkcYK2d... (flutter/engine#39312) (flutter/flutter#119688) * b4a6e34 0d87b1562 Roll Dart SDK from 8b57d23a7246 to de03e1f41b50 (1 revision) (flutter/engine#39313) (flutter/flutter#119695) * 3af30ff Roll Flutter Engine from 0d87b156265c to c08a286d60e9 (3 revisions) (flutter/flutter#119706) * d278808 [Re-land] Exposed tooltip longPress (flutter/flutter#118796)
Fixes #114180
Pre-launch Checklist
///
).