Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 77 commits into from
Jan 30, 2023

Conversation

Renzo-Olivares
Copy link
Contributor

Fixes #114180

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].
  • All existing and new tests are passing.

@flutter-dashboard flutter-dashboard bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels Dec 5, 2022
@Renzo-Olivares Renzo-Olivares marked this pull request as ready for review December 12, 2022 20:38
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: getTextBoundaryAt(position).start,
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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!

Copy link
Contributor

@justinmc justinmc left a 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.
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "returning" => "returned"

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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));
});
Copy link
Contributor

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.

Copy link
Contributor

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;
Copy link
Contributor

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.

@Renzo-Olivares Renzo-Olivares force-pushed the paragraph-boundary branch 2 times, most recently from a75d332 to d259028 Compare January 9, 2023 19:13
/// line terminator that encloses the desired paragraph.
@override
int? getLeadingTextBoundaryAt(int position) {
final List<int> codeUnits = _text.codeUnits;
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong Jan 10, 2023

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);

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Jan 10, 2023

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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);

@Renzo-Olivares Renzo-Olivares force-pushed the paragraph-boundary branch 2 times, most recently from 04b6842 to cbab75a Compare January 12, 2023 20:41
@Renzo-Olivares
Copy link
Contributor Author

Renzo-Olivares commented Jan 12, 2023

@justinmc @LongCatIsLooong This is ready for a re-review

edit: found a bug

staus: not ready for review.

@Renzo-Olivares Renzo-Olivares force-pushed the paragraph-boundary branch 2 times, most recently from 3bed680 to b4aed45 Compare January 19, 2023 22:19
/// that follows the line terminator that encloses the desired paragraph.
@override
int? getLeadingTextBoundaryAt(int position) {
if (position < 0 || position > _text.length) {
Copy link
Contributor

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).

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@LongCatIsLooong LongCatIsLooong Jan 24, 2023

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?

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Jan 24, 2023

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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--;
Copy link
Contributor

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;
Copy link
Contributor

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);

Copy link
Contributor

@justinmc justinmc left a 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', () {
Copy link
Contributor

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));
});
Copy link
Contributor

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 👍

@Renzo-Olivares Renzo-Olivares force-pushed the paragraph-boundary branch 2 times, most recently from 18d952a to aa7d942 Compare January 22, 2023 23:51
@Renzo-Olivares Renzo-Olivares force-pushed the paragraph-boundary branch 3 times, most recently from 6a358e0 to 0c5d0a1 Compare January 25, 2023 20:14
@Renzo-Olivares Renzo-Olivares added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 30, 2023
@auto-submit auto-submit bot merged commit 6c12e39 into flutter:master Jan 30, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 31, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 31, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 31, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 31, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 31, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 31, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 31, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 1, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 1, 2023
auto-submit bot pushed a commit to flutter/plugins that referenced this pull request Feb 1, 2023
* 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

TextField keyboard shortcut alt + shift + arrow down on MacOS should extend to paragraph boundary
3 participants