Skip to content
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

[SuperEditor][web] Fix text input after a deletion (Resolves #1224) #1269

Merged
merged 16 commits into from
Aug 17, 2023

Conversation

angelosilvestre
Copy link
Collaborator

[SuperEditor][web] Fix text input after a deletion. Resolves #1224

On web, if we quickly press backspace and type a character at the end of a paragraph we get the following exception and the text input crashes:

Couldn't map an IME position to a document position.

The issue is that we are handling the deletion twice on web. In IME mode, Flutter reports both the deletion delta and the backspace keypress.

This PR changes defaultImeKeyboardActions to include a different version of deleteUpstreamContentWithBackspace, which does nothing when running on web.

@angelosilvestre
Copy link
Collaborator Author

@brian-superlist Could you please try this PR?

@@ -903,7 +903,7 @@ final defaultImeKeyboardActions = <DocumentKeyboardAction>[
deleteToStartOfLineWithCmdBackspaceOnMac,
deleteWordUpstreamWithAltBackspaceOnMac,
deleteWordUpstreamWithControlBackspaceOnWindowsAndLinux,
deleteUpstreamContentWithBackspace,
deleteUpstreamContentWithBackspaceOnNonWeb,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please add appropriate tests that replicate the web behavior of sending both to ensure the problem is solved?

@brian-superlist
Copy link
Contributor

brian-superlist commented Aug 2, 2023

@angelosilvestre Thanks for the follow-up. This one is working better, typing generally doesn't get stuck, but it looks like there are two issues:

Issue 1: Cannot remove a node by pressing delete key

Reproduction Steps:

  1. Create a new blank node
  2. Set selecetion at beginning of node
  3. Press the Backspace Key (Delete key on Mac)

Expected: Node is deleted and selection returns to end of previous node
Actual: Nothing happens

Screen.Recording.2023-08-02.at.5.10.16.PM.mov

Logs

Error: Assertion failed: org-dartlang-sdk:///lib/ui/text.dart:547:16
start >= -1
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/ui/text.dart 547:26                                                           new
lib/_engine/engine/text_editing/text_editing.dart 573:48                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall

Issue 2: Selected Text + Deletion does not work correctly

Reproduction Steps:

  1. Add some content to the editor
  2. Select that content
  3. Press the Backspace Key (Delete key on Mac)

Expected: Selected Content is Deleted
Actual: Wrong content is deleted

This also does not work if you type normal characters instead of pressing the backspace / delete key.

Screen.Recording.2023-08-02.at.5.12.21.PM.mov

Logs

No error logs while pressing backspace/delete. The following logs are printed when you start typing other characters. If this is a separate problem with selection on web, I can file a new issue. However, I did not observe this behavior on main.

Another exception was thrown: RangeError (end): Invalid value: Not in inclusive range 35..45: 29

@angelosilvestre
Copy link
Collaborator Author

Issue 2: Selected Text + Deletion does not work correctly

I was able to fix this issue. It's the same root cause as the first issue. We are changing the selection when we are handling the deltas and also on key events.

I applied the same solution. However, I've found a flutter web issue: even when we are expanding the selection upstream (shift + left arrow), the IME reports a downstream selection. This causes the caret to be display at the wrong position.

I filed flutter/flutter#131906.

@matthew-carroll

@brian-superlist
Copy link
Contributor

brian-superlist commented Aug 7, 2023

Thanks, @angelosilvestre. Overall, working better than the previous version!

I found one problem with the current PR. Is it related to the Flutter issue you mentioned?

Reproduction Steps

  1. Select some text
  2. Replace with different content
  3. Create a new empty node
  4. Unable to delete it
Screen.Recording.2023-08-07.at.10.48.27.AM.mov

Logs

The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from `dart:ui_web` instead.
Initializing logger: ExampleApp
(33.244) ExampleApp > FINE: Showing text format toolbar
(33.262) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(33.263) ExampleApp > FINE: Anchor is null. Building an empty box.
(33.282) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(33.283) ExampleApp > FINE: Anchor is non-null: Offset(238.0, 322.8), child: ValueListenableBuilder<DocumentSelection?>
(33.284) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a792d5f7-f1ac-4829-a7d0-07193cc7e0e3", position: (TextNodePosition(offset: 69, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a792d5f7-f1ac-4829-a7d0-07193cc7e0e3", position: (TextNodePosition(offset: 70, affinity: TextAffinity.downstream)))
(33.588) ExampleApp > FINE: Showing text format toolbar
(33.590) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a792d5f7-f1ac-4829-a7d0-07193cc7e0e3", position: (TextNodePosition(offset: 59, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a792d5f7-f1ac-4829-a7d0-07193cc7e0e3", position: (TextNodePosition(offset: 70, affinity: TextAffinity.downstream)))
(33.604) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(33.605) ExampleApp > FINE: Anchor is non-null: Offset(194.6, 322.8), child: ValueListenableBuilder<DocumentSelection?>
Error: Assertion failed: org-dartlang-sdk:///lib/ui/text.dart:547:16
start >= -1
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/ui/text.dart 547:26                                                           new
lib/_engine/engine/text_editing/text_editing.dart 573:48                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/ui/text.dart:547:16
start >= -1
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/ui/text.dart 547:26                                                           new
lib/_engine/engine/text_editing/text_editing.dart 573:48                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall

@angelosilvestre
Copy link
Collaborator Author

@brian-superlist Thanks. I will investigate.

@angelosilvestre
Copy link
Collaborator Author

@brian-superlist This issue seems to be related to flutter/flutter#131023

I pushed a commit in this PR related to the new line duplication.

@brian-superlist
Copy link
Contributor

Great, text editing is feeling much better overall! The next problem I've found relates to multiple nodes.

Problem 1

  1. Create a new node
  2. Select the text in that node and some text from the node before
  3. Type in a character

Expected: Selected text is removed and replaced with the character I've typed
Actual: Nothing happens and the editor is broken

Screen.Recording.2023-08-08.at.10.26.10.AM.mov
(5.229) ExampleApp > FINE: Showing text format toolbar
The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from `dart:ui_web` instead.
Initializing logger: ExampleApp
The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from `dart:ui_web` instead.
Initializing logger: ExampleApp
(17.323) ExampleApp > FINE: Showing text format toolbar
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:480:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 480:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 574:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1372:56                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall

Problem 2

Selection across nodes does not work.

  1. Create a new node
  2. Add some text
  3. Use the keyboard to select all the text from the new node and try to select text from previous node

Expected: Text is selected and can be modified
Actual: Text selection is wrong

Screen.Recording.2023-08-08.at.10.30.06.AM.mov
The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from `dart:ui_web` instead.
Initializing logger: ExampleApp
(13.273) ExampleApp > FINE: Showing text format toolbar
(14.069) ExampleApp > FINE: Showing text format toolbar
(14.083) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.084) ExampleApp > FINE: Anchor is null. Building an empty box.
(14.100) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.100) ExampleApp > FINE: Anchor is non-null: Offset(255.6, 109.7), child: ValueListenableBuilder<DocumentSelection?>
(14.101) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 6, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 7, affinity: TextAffinity.downstream)))
(14.322) ExampleApp > FINE: Showing text format toolbar
(14.326) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 5, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 7, affinity: TextAffinity.downstream)))
(14.340) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.340) ExampleApp > FINE: Anchor is non-null: Offset(250.7, 109.7), child: ValueListenableBuilder<DocumentSelection?>
(14.349) ExampleApp > FINE: Showing text format toolbar
(14.351) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 4, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 7, affinity: TextAffinity.downstream)))
(14.363) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.363) ExampleApp > FINE: Anchor is non-null: Offset(248.5, 109.7), child: ValueListenableBuilder<DocumentSelection?>
(14.388) ExampleApp > FINE: Showing text format toolbar
(14.393) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 3, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 7, affinity: TextAffinity.downstream)))
(14.406) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.406) ExampleApp > FINE: Anchor is non-null: Offset(245.5, 109.7), child: ValueListenableBuilder<DocumentSelection?>
(14.423) ExampleApp > FINE: Showing text format toolbar
(14.427) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 2, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 7, affinity: TextAffinity.downstream)))
(14.439) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.439) ExampleApp > FINE: Anchor is non-null: Offset(240.9, 109.7), child: ValueListenableBuilder<DocumentSelection?>
(14.454) ExampleApp > FINE: Showing text format toolbar
(14.459) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 1, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 7, affinity: TextAffinity.downstream)))
(14.471) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.471) ExampleApp > FINE: Anchor is non-null: Offset(236.1, 109.7), child: ValueListenableBuilder<DocumentSelection?>
(14.705) ExampleApp > FINE: Showing text format toolbar
(14.710) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 0, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "b465b12f-e724-457c-a318-d4b7da9bede4", position: (TextNodePosition(offset: 7, affinity: TextAffinity.downstream)))
(14.724) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(14.725) ExampleApp > FINE: Anchor is non-null: Offset(231.2, 109.7), child: ValueListenableBuilder<DocumentSelection?>

@angelosilvestre
Copy link
Collaborator Author

Problem 1

This seems to be the same as flutter/flutter#131023

Problem 2

We do need a fix on our side, but we need to receive the selection affinity correctly in order to fix this (flutter/flutter#131906)

@matthew-carroll
Copy link
Contributor

@angelosilvestre @brian-superlist - Please let me know when you think this is ready for my final review to merge in.

@brian-superlist
Copy link
Contributor

brian-superlist commented Aug 10, 2023

I don't know the internals of Super Editor well, but I'm a little curious if problem 2 is really related only to the Flutter affinity bug? For example, I can also create the problem starting from a node, moving the caret from left to right.

If it definitely is, I'd say this one is good to go until we fix the underlying Flutter issues

Reproduction Steps:

  1. Place cursor near end of node
  2. Use shift + right arrow to select text until the end of the node end of node
  3. Use shift + right arrow to continue selection to next node

Expected: Text in the next node is selected
Actual: No text in the next node is selected

Screen.Recording.2023-08-10.at.11.33.23.AM.mov
(29.146) ExampleApp > FINE: Showing text format toolbar
(29.161) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(29.162) ExampleApp > FINE: Anchor is null. Building an empty box.
(29.178) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(29.179) ExampleApp > FINE: Anchor is non-null: Offset(248.1, 310.7), child: ValueListenableBuilder<DocumentSelection?>
(29.180) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 64, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 65, affinity: TextAffinity.downstream)))
(29.338) ExampleApp > FINE: Showing text format toolbar
(29.339) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 64, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 66, affinity: TextAffinity.downstream)))
(29.354) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(29.354) ExampleApp > FINE: Anchor is non-null: Offset(252.8, 310.7), child: ValueListenableBuilder<DocumentSelection?>
(29.533) ExampleApp > FINE: Showing text format toolbar
(29.537) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 64, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 67, affinity: TextAffinity.downstream)))
(29.549) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(29.549) ExampleApp > FINE: Anchor is non-null: Offset(257.8, 310.7), child: ValueListenableBuilder<DocumentSelection?>
(29.729) ExampleApp > FINE: Showing text format toolbar
(29.730) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 64, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 68, affinity: TextAffinity.downstream)))
(29.741) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(29.741) ExampleApp > FINE: Anchor is non-null: Offset(262.5, 310.7), child: ValueListenableBuilder<DocumentSelection?>
(29.896) ExampleApp > FINE: Showing text format toolbar
(29.897) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 64, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 69, affinity: TextAffinity.downstream)))
(29.907) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(29.907) ExampleApp > FINE: Anchor is non-null: Offset(267.3, 310.7), child: ValueListenableBuilder<DocumentSelection?>
(30.482) ExampleApp > FINE: Showing text format toolbar
(30.487) ExampleApp > FINE: Building toolbar. Selection: [DocumentSelection] - 
  base: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 64, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "a4f499a1-e357-4358-8c4b-1e4c196a7575", position: (TextNodePosition(offset: 70, affinity: TextAffinity.downstream)))
(30.502) ExampleApp > FINE: (Re)Building _PositionedToolbar widget due to anchor change
(30.502) ExampleApp > FINE: Anchor is non-null: Offset(269.6, 310.7), child: ValueListenableBuilder<DocumentSelection?>

@angelosilvestre
Copy link
Collaborator Author

@brian-superlist I have a fix for moving the caret left to right. I should push it today.

@brian-superlist
Copy link
Contributor

Thanks @angelosilvestre!

As some general feedback, I found many of these issues reported in this thread by doing very quick functional testing (< 3 minutes). To speed up PRs like this in the future and reduce these back-and-forth rounds of time-delayed feedback, I'd ask you to do more thorough functional testing on your side before asking for functional testing on our side.

Before we merge this one in, I'd recommend taking 5-10 minutes to run the example app on web, use the editor to write more content "like a normal user," and see if you run into any issues that I might have missed.

@angelosilvestre
Copy link
Collaborator Author

@brian-superlist Will do!

@brian-superlist
Copy link
Contributor

@angelosilvestre Thanks 😄

@matthew-carroll
Copy link
Contributor

Before we merge this one in, I'd recommend taking 5-10 minutes to run the example app on web, use the editor to write more content "like a normal user," and see if you run into any issues that I might have missed.

This is really something that should be handled by tests. I don't really want any humans on any side spending a lot of time being test robots. Are we simply lacking appropriate tests? Or is this fundamentally a web issue, and therefore dependent on web integration tests?

@brian-superlist
Copy link
Contributor

brian-superlist commented Aug 10, 2023

This is really something that should be handled by tests.

Thanks Matt. Yes, I complete agree -- we should certainly have test coverage for these issues and not rely on repeated manual QA to check regressions.

I don't really want any humans on any side spending a lot of time being test robots.

Again, I agree.

As we are on such different timezones, feedback cycles can be a bit slow. For example, you or Angelo might fix something in your morning or early afternoon, but often we won't be able to provide feedback until your next day (because it's evening or night time in Europe). Therefore, on more exploratory fixes like this one, it'd be great to do a bit more functional testing, find the more obvious bugs, write test cases for them, fix those bugs + make the test cases pass, and then pass it over to our side for more functional verification. Overall, I think that could lead to faster feedback loops.

Does that seem reasonable? Happy to discuss this over slack or google meet as well to ensure we understand each other.

@matthew-carroll
Copy link
Contributor

@angelosilvestre can you check whether we're missing widget tests for this, or whether we need integration tests?

If the problem is that we need a web integration test, can you try writing a single integration test that covers one of these interactions and see if it works? I've run into major problems with web integration tests in the past, but perhaps they're usable now.

@angelosilvestre
Copy link
Collaborator Author

With the changes from this PR we will need to add some tests for web. However, there are some places where we check for kIsWeb and we aren't able to override this value.

We can introduce an overridable value instead of checking kIsWeb or we can use integration tests. But, even with integration tests, I'm not sure if we would get the assertion failures from some of the issues mentioned in this PR, because the input events would be totally generated by flutter itself.

@matthew-carroll
Copy link
Contributor

because the input events would be totally generated by flutter itself

Aren't we primarily dealing with selection changes? Wouldn't an integration test use the real IME to do that?

This PR accumulated a number of different issues. It's possible that some of those can't reasonably be tested, but the issues that can be tested, should be tested, because the alternative is to have people manually do these things over and over every time we release.

@angelosilvestre
Copy link
Collaborator Author

I think it's good now.

There are three remaining issues that are flutter issues:

  • Place the caret at the end of a paragraph
  • Press enter
  • Type a single letter
  • Press enter again
  • The editor crashes

This is caused by flutter/flutter#131023 and seems to be solved by flutter/engine#44595.

  • Place the caret at a paragraph
  • Shift + left arrow until selection expands to a character from the previous node
  • Press backspace
  • The editor crashes

I added this issue to the flutter ticket and it seems the fix is in progress: flutter/flutter#131023 (comment)

  • Place the caret at the middle of a word
  • Press shift + left arrow to expand the selection upstream
  • The selection expands, but the caret don't move

This is caused by flutter/flutter#131906.

@matthew-carroll
Copy link
Contributor

@angelosilvestre you enumerated the issues that this PR doesn't fix. Can you please enumerate what this PR does fix?

@angelosilvestre
Copy link
Collaborator Author

@matthew-carroll

  • Fix a crash after pressing backspace
  • Fix expanding the selection upstream
  • Fix new line duplication on enter
  • Fix new line duplication on enter on task nodes

@angelosilvestre
Copy link
Collaborator Author

Solved conflicts.

@@ -432,7 +432,6 @@ class _ExampleEditorState extends State<ExampleEditor> {
],
gestureMode: _gestureMode,
inputSource: _inputSource,
keyboardActions: _inputSource == TextInputSource.ime ? defaultImeKeyboardActions : defaultKeyboardActions,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We don't need it because the constructor already does the same.

@@ -232,6 +233,18 @@ ExecutionInstruction anyCharacterOrDestructiveKeyToDeleteSelection({
return ExecutionInstruction.haltExecution;
}

ExecutionInstruction deleteUpstreamContentWithBackspaceWithIme({
Copy link
Contributor

Choose a reason for hiding this comment

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

This name isn't accurate. This handler isn't about IME - it's about web. But also, why do we need a new handler that just adds an if-statement? Can't we add the isWeb conditional to the existing handler?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We must ignore the key event only if we use IME as the input source. When using keyboard as the input source we still need to handle backspace.

Inside the key handler we don't know which input source is being used. We could add it to SuperEditorContext, then we would be able to just modify the existing handler.

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case, I think the way we've handled things like this in the past is to define keyboard handlers that block execution, rather than composing conditional handlers around other handlers. Those blocking handlers are probably easier to name accurately. I bet that approach will also prove easier for people to understand.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Understood. I'll use that approach.

return moveUpDownLeftAndRightWithArrowKeys(editContext: editContext, keyEvent: keyEvent);
}

ExecutionInstruction moveUpAndDownWithArrowKeysOnWeb({
Copy link
Contributor

Choose a reason for hiding this comment

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

What's special about these web versions of arrow handlers vs the ones we've already implemented? I don't see anything obvious that looks unique here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When using web with IME as the input source, left and right arrow keys generate non-text deltas which change the position. We need to handle only up and down arrow keys to move between lines and nodes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean that we've duplicated all the vertical arrow key behavior just because our original handler included both horizontal and vertical implementations?

If so, we should separate the original handler into two handlers. One for horizontal and one for vertical. Then we can leave the vertical one alone, and we can introduce a blocking handler for the horizontal case on web.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated.

// we forward the newline action to performAction.
if (defaultTargetPlatform == TargetPlatform.android || kIsWeb) {
if (defaultTargetPlatform == TargetPlatform.android) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we get this condition wrong from the beginning, or did Flutter change something and now web doesn't do this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think something has changed, but I would need to test with previous Flutter versions to confirm it.

@@ -750,6 +751,19 @@ ExecutionInstruction backspaceToClearParagraphBlockType({
return didClearBlockType ? ExecutionInstruction.haltExecution : ExecutionInstruction.continueExecution;
}

ExecutionInstruction enterToInsertBlockNewlineWithIme({
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question here as elsewhere - why create a new handler? Can we just add the web conditional to the existing handler?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When we use IME as the input source we still need to handle this key events, even on web.

/// Overrides the value of [isWeb].
///
/// This is intended to be used in tests.
bool? debugIsWebOverride;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add the appropriate annotation for test related APIs. And then you don't need to state that in the comments.

Also, I'm not sure a bool is the right type for this. Shouldn't the developer be able to override this value in both directions? Either force it to "yes we're on web" or force it to "no we're not on web"? That would require a 3 value type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Please add the appropriate annotation for test related APIs. And then you don't need to state that in the comments.

Do you mean visibleForTesting?

Also, I'm not sure a bool is the right type for this. Shouldn't the developer be able to override this value in both directions? Either force it to "yes we're on web" or force it to "no we're not on web"? That would require a 3 value type.

As this is a nullable bool, this is already possible:

null: use isWeb value
true: we are on web
false: we are not on web

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added a dart doc, but if you think we should use an enum we can change it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah. visibleForTesting.

In general, it's probably not a good idea to give significance to a null bool. That condition is likely to be overlooked. You can do a nullable enum, but a nullable bool is just begging for confusion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated.

@@ -416,7 +416,7 @@ class TestSuperEditorConfigurator {
imeOverrides: _config.imeOverrides,
keyboardActions: [
..._config.prependedKeyboardActions,
...defaultKeyboardActions,
...(_config.inputSource == TextInputSource.ime ? defaultImeKeyboardActions : defaultKeyboardActions),
Copy link
Contributor

Choose a reason for hiding this comment

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

Test tools should not be making SuperEditor policy decisions. We pass _config.inputSource into SuperEditor. If any such decision needs to be made, it should be made by SuperEditor. This configuration system only exists to let testers provide non-default values for SuperEditor properties.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

SuperEditor already has this policy, but as we are always setting a value for keyboardActions we are overriding it and always using defaultKeyboardActions, even when using IME.

Copy link
Contributor

Choose a reason for hiding this comment

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

Are we doing this with any other configuration property? Or is this the first one that we've pulled out from SuperEditor into the configuration system?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It seems it's just for this property. We do a similar thing for the component builders, but as we have only one list of defaultComponentBuilders this isn't a problem.

@@ -85,6 +84,46 @@ void testWidgetsOnDesktop(
testWidgetsOnLinux("$description (on Linux)", test, skip: skip, variant: variant);
}

/// A widget test that runs a variant for every desktop platform, e.g.,
/// Mac, Windows, Linux and for macOS Web.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is running on Mac web going to be sufficient? This would seem to suggest that when it comes to web behaviors, the underlying platform doesn't matter. But that's not accurate, is it? Aren't web shortcuts and selection movements typically the same as the underlying platform?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Modified to run on all platforms on web.

Copy link
Contributor

@matthew-carroll matthew-carroll left a comment

Choose a reason for hiding this comment

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

LGTM

@angelosilvestre
Copy link
Collaborator Author

@matthew-carroll Rebased onto main to fix a compilation error due to the change in the AttributedText constructor.

@matthew-carroll matthew-carroll merged commit 66ab331 into superlistapp:main Aug 17, 2023
@angelosilvestre angelosilvestre deleted the 1224_fix_input_web branch August 17, 2023 00:58
angelosilvestre added a commit to angelosilvestre/super_editor that referenced this pull request Aug 17, 2023
angelosilvestre added a commit to angelosilvestre/super_editor that referenced this pull request Aug 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Web text input works inconsistently
3 participants