-
Notifications
You must be signed in to change notification settings - Fork 6k
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
TextEditingDelta Support for the Web #28527
TextEditingDelta Support for the Web #28527
Conversation
…here there is an edit that occurs before the current cursor position
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 mainly want to make sure that there's no simpler way to do inferDeltaState
. I've left some questions on there. Glad to hear it's working though.
this.composingExtent = -1, | ||
}); | ||
|
||
static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState? lastTextEditingDeltaState) { |
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 it make this any simpler if you used inputType? Or is that not possible...
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 attempted to use inputType
however there is some normalization I do in order to get the behavior of the Web to fit into the processing we do on the framework side that doesn't allow for this.
For example, when trying to use insertCompositionText
I discovered it is called for the entire composed word.
So say we have hello|
and we delete character by character starting at the end. In this case insertCompositionText
is called for every deletion. However on Android & iOS, when we delete the final character h
this is reported as a regular deletion, and not one from the composing region.
newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset); | ||
} | ||
|
||
// If we are composing then set the delta range to the composing region we captured in compositionupdate. |
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: Line length.
this.baseOffset = -1, | ||
this.extentOffset = -1, | ||
this.composingOffset = -1, | ||
this.composingExtent = -1, |
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.
Why are the names slightly different than the frameworks' TextDelta, is it to match how the browser's events present the information?
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 just tried to match the EditingState
parameters in this case.
final AutofillInfo? autofill; | ||
|
||
final EngineAutofillForm? autofillGroup; | ||
|
||
final TextCapitalizationConfig textCapitalization; | ||
} | ||
|
||
typedef OnChangeCallback = void Function(EditingState? editingState); | ||
typedef OnChangeCallback = void Function(EditingState? editingState, TextEditingDeltaState? editingDeltaState); |
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 it makes sense to add this parameter rather than creating a new type as long as nothing is broken by it.
// We can assume the deltaText for additions and replacements to the text value | ||
// are accurate. What may not be accurate is the range of the delta. | ||
// | ||
// We can think of the newEditingState as our source of truth. |
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.
Is this whole comment explaining the situation of double space inserting a period? Maybe add that as an example in the comment somewhere. And/or, be sure that this is thoroughly tested so that future developers can understand your intention (and not break 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.
Add a test for this case and the accent menu case.
// 2. Apply matches/replacement to oldText until oldText matches the | ||
// new editing state's text value. | ||
final RegExp deltaTextPattern = RegExp(r'' + newTextEditingDeltaState.deltaText + r''); | ||
for (final Match match in deltaTextPattern.allMatches(newEditingState.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.
So in the example of double space inserting a period, the matches here will be all periods in the text, and you're trying to find the one that is new?
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.
in the case of double space inserting a period the deltaText
we receive from beforeinput
is actually .
. So we search for a period followed by a whitespace in the new editing state. The new editing state comes directly from the DOM
so it can be seen as the source of truth.
When we have the matched ranges we iterate through them and apply the deltaText
to the oldText
using these matched ranges as the deltaRange
. The range that results in our oldText
being equal to the value of the new editing state is the correct deltaRange
that we should send to the framework.
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 the code looks great, but I tried running this and was able to crash it in two different ways:
- In the middle of the text, insert a bunch of spaces. Sometimes the spaces don't seem to work, sometimes they insert a period (which I think should not happen on desktop web), and eventually it crashes.
Video of crash #1
Screen.Recording.2022-01-28.at.11.21.30.AM.mov
- Insert "ni" on the Chinese keyboard and replace it with 你. It crashes.
Video of crash #2
Screen.Recording.2022-01-28.at.11.25.02.AM.mov
I'm using my fork of the rich text editing demo to test this. There may also be problems with the demo that are contributing to this...
/// The change between the last editing state and the current editing state | ||
/// of a text field. This is packaged into a JSON and sent to the framework | ||
/// to be processed into a concrete [TextEditingDelta]. |
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: Will this actually end up on our docs website? Maybe follow the style guide on docs by splitting this into two paragraphs just in case.
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 don't think so. But I agree, I fixed it.
/// we have a non empty deltaText, and that we did not have an active selection. An active selection | ||
/// would mean we are not composing. |
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 never realized that you couldn't make a selection while composing, but you seem to be right.
_editingDeltaState = newTextEditingDeltaState; | ||
onChange!(lastEditingState, _editingDeltaState); | ||
// Flush delta after it has been sent to framework. | ||
_editingDeltaState = null; |
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 makes a lot more sense to me at a glance now, thanks for putting up with my pickiness.
/// when applied to the last editing state. If it is not then we use our new editing state as the source of truth, | ||
/// and use regex to find the correct deltaStart and deltaEnd. | ||
static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState lastTextEditingDeltaState) { | ||
final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState.copyWith(); |
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.
Thanks for doing this, I think it's comforting to know there's nothing fancy going on with modifying the inputs when I see this copy.
|
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 👍
Thanks for looking at those two crashes. The double space thing seems better to me now, but I hope to get time to really polish up that demo so that we can better test this kind of thing in a realistic environment.
// new editing state's text value. | ||
final bool isPeriodInsertion = newTextEditingDeltaState.deltaText.contains('.'); | ||
final RegExp deltaTextPattern = isPeriodInsertion? | ||
RegExp(r'\' + newTextEditingDeltaState.deltaText + r'') |
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.
Does deltaText
contain unescaped user-supplied text? If so, then RegExp
will parse it like a regular expression. For example, what happens if the user enters .*
?
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.
Yes it does contain unescaped text. I think a better solution here would be to use RegExp.escape(deltaText)
which will give us an escaped version of the given text.
final bool isPeriodInsertion = newTextEditingDeltaState.deltaText.contains('.'); | ||
final RegExp deltaTextPattern = isPeriodInsertion? | ||
RegExp(r'\' + newTextEditingDeltaState.deltaText + r'') | ||
: RegExp(r'' + newTextEditingDeltaState.deltaText + r''); |
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.
There's something wrong with formatting. Consider putting ?
before the output expression, same as :
, e.g.:
final RegExp deltaTextPattern = isPeriodInsertion
? case1
: case2
this.baseOffset = -1, | ||
this.extentOffset = -1, | ||
this.composingOffset = -1, | ||
this.composingExtent = -1, |
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.
Are the empty strings and -1 meaningful values, or are they here only to make the fields non-null? Would it make sense to mark the parameters with required
?
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.
required
wouldn't work in this case since we don't create the entire TextEditingDelta
at any single point. It goes through beforeInput
-> onInput
-> inferDeltaState
.
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.
On the framework side -1
for deltaStart
and deltaEnd
is currently used by TextEditingDelta
to identify a NonTextUpdate
. I made the selection
and composing
nullable now.
/// | ||
/// For a deletion we calculate the length of the deleted text by comparing the new | ||
/// and last editing states. We subtract this from the deltaEnd that we set when beforeinput | ||
/// was fired to determine out deltaStart. |
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: s/out/our/ (although I'd prefer "the")
nit: backtick the deltaStart
final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; | ||
|
||
if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { | ||
// We are removing 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.
nit: consider extracting complex conditions into named variables. Less needs to be explained this way. E.g.:
final bool isTextBeingRemoved = newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1;
if (isTextBeingRemoved) {
...
int deltaStart; | ||
|
||
/// The position in the text field where the change ends. | ||
int deltaEnd; |
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.
If empty strings and -1 are meaningful values for these fields, let's explain what they mean. Something tells me that the 3 fields above could be combined into a TextEditingDelta(text, start, end)
struct and the default here expressed as TextEditingDelta? delta;
. I may be missing something though.
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 since the selection and composing region change when the text is mutated they should be together with deltaStart
,deltaEnd
, and deltaText
in one class.
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 if LGT @justinmc
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.
Updated LGTM 👍
I feel confident about merging this without a recent LGTM from Mouad, but we should check in with him when he's back.
|
||
final ui.TextRange normalizedRange = ui.TextRange(start: math.min(replacedRange.start, replacedRange.end), end: math.max(replacedRange.start, replacedRange.end)); | ||
|
||
final String newText = normalizedRange.textBefore(originalText) + replacementText + normalizedRange.textAfter(originalText); |
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: Maybe just return this directly instead of assigning to a variable.
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.
sgtm
* 2a8f388 Roll Skia from a424b619bc40 to ec0af1664478 (6 revisions) (flutter/engine#31352) * ba8f1c5 Add clang-analyzer-* and clang-diagnostic-* to .clang-tidy (flutter/engine#31291) * 1eafb40 Use H5vcc CanvasKit implementation if it is detected. (flutter/engine#31191) * 6d99e90 Roll Dart SDK from 55c93c732da9 to b7eea441d7d1 (4 revisions) (flutter/engine#31353) * 935b46a Roll Fuchsia Mac SDK from 5_CZ81mTD... to 5CmPcHTb1... (flutter/engine#31356) * 244bb25 Roll Skia from ec0af1664478 to 9cb74e90792d (4 revisions) (flutter/engine#31357) * b6b5759 Roll Skia from 9cb74e90792d to 81d4b5d5b45d (6 revisions) (flutter/engine#31360) * 0f6db11 Roll Skia from 81d4b5d5b45d to 1f813e4c7f6d (1 revision) (flutter/engine#31362) * 321a482 Fix AccessibilityBridge crash due to invalid access during ReplaceSemanticsObject (flutter/engine#31351) * 2cc3abb Manual roll of ICU (flutter/engine#31132) * c1e843d Roll Fuchsia Linux SDK from 4VEg4eRJS... to YGS2LvlDy... (flutter/engine#31364) * fe08734 Roll Dart SDK from b7eea441d7d1 to 7e3310bbe1ed (2 revisions) (flutter/engine#31365) * 2062e57 Add trackpad gesture PointerData types (flutter/engine#28571) * 24fe585 Roll Skia from 1f813e4c7f6d to 5a2135af5623 (1 revision) (flutter/engine#31366) * 11bf86a Migrate string encoding conversions to FML (flutter/engine#31334) * 6d9f479 Roll Skia from 5a2135af5623 to 21a92dff8fdc (3 revisions) (flutter/engine#31368) * 417a05e Roll Dart SDK from 7e3310bbe1ed to a9cfcc289ed4 (1 revision) (flutter/engine#31369) * b04ed63 Change link to felt documentation (flutter/engine#31312) * 3e7515a Roll Fuchsia Mac SDK from 5CmPcHTb1... to ZA5ZzabQM... (flutter/engine#31370) * 90effff Revert "Add trackpad gesture PointerData types (#28571)" (flutter/engine#31375) * 3764a8f Change support for VM service message from "The Dart VM Service is listening" to "The Dart VM service is listening" (flutter/engine#31361) * 3d629c5 Fix html gradient rendering (#97762) (flutter/engine#31355) * 963c449 [Android] Show deprecation warnings for Android tests (flutter/engine#31246) * 0be895c Roll Dart SDK from a9cfcc289ed4 to 0041431f5ec7 (1 revision) (flutter/engine#31372) * 18f2faf Roll Skia from 21a92dff8fdc to c5d3326d767d (7 revisions) (flutter/engine#31373) * 1b762d3 Roll Fuchsia Linux SDK from YGS2LvlDy... to yDo1mhBKz... (flutter/engine#31374) * 7b6a7b6 Roll Skia from c5d3326d767d to b6dfd97c5290 (10 revisions) (flutter/engine#31377) * f55b161 [a11y] Delegate UTF8ToUTF16 to FML (flutter/engine#31376) * d36d25f TextEditingDelta Support for the Web (flutter/engine#28527) * aa17186 [fml] Use common FML string encoding utils (flutter/engine#31378) * 97d9ec3 Renamed the scenario tests target to be generic emulator tests. (flutter/engine#28919) * 902717f Roll Skia from b6dfd97c5290 to e1e2a858205f (3 revisions) (flutter/engine#31380) * 877f820 Roll Dart SDK from 0041431f5ec7 to a15d19a0d914 (2 revisions) (flutter/engine#31381) * 2d16729 Roll Skia from e1e2a858205f to 74ce095463e1 (2 revisions) (flutter/engine#31383) * 8d3c0fb [web] PathRef: do not use == with doubles in assertions (flutter/engine#31382) * d1164c1 Move recipes to repository folders. (flutter/engine#31367) * e031e07 Roll Skia from 74ce095463e1 to 82d65d0487bd (1 revision) (flutter/engine#31384) * 5140a44 Define thread priority enum and set thread priority for all threads in Engine (flutter/engine#30605) * 1e46918 Roll Dart SDK from a15d19a0d914 to a3736d4e9b1b (1 revision) (flutter/engine#31397) * 8a28948 Roll Fuchsia Linux SDK from yDo1mhBKz... to UHV3HWM3d... (flutter/engine#31398) * ca86c79 Roll Fuchsia Mac SDK from ZA5ZzabQM... to jjxstNbgZ... (flutter/engine#31399)
* 2a8f388 Roll Skia from a424b619bc40 to ec0af1664478 (6 revisions) (flutter/engine#31352) * ba8f1c5 Add clang-analyzer-* and clang-diagnostic-* to .clang-tidy (flutter/engine#31291) * 1eafb40 Use H5vcc CanvasKit implementation if it is detected. (flutter/engine#31191) * 6d99e90 Roll Dart SDK from 55c93c732da9 to b7eea441d7d1 (4 revisions) (flutter/engine#31353) * 935b46a Roll Fuchsia Mac SDK from 5_CZ81mTD... to 5CmPcHTb1... (flutter/engine#31356) * 244bb25 Roll Skia from ec0af1664478 to 9cb74e90792d (4 revisions) (flutter/engine#31357) * b6b5759 Roll Skia from 9cb74e90792d to 81d4b5d5b45d (6 revisions) (flutter/engine#31360) * 0f6db11 Roll Skia from 81d4b5d5b45d to 1f813e4c7f6d (1 revision) (flutter/engine#31362) * 321a482 Fix AccessibilityBridge crash due to invalid access during ReplaceSemanticsObject (flutter/engine#31351) * 2cc3abb Manual roll of ICU (flutter/engine#31132) * c1e843d Roll Fuchsia Linux SDK from 4VEg4eRJS... to YGS2LvlDy... (flutter/engine#31364) * fe08734 Roll Dart SDK from b7eea441d7d1 to 7e3310bbe1ed (2 revisions) (flutter/engine#31365) * 2062e57 Add trackpad gesture PointerData types (flutter/engine#28571) * 24fe585 Roll Skia from 1f813e4c7f6d to 5a2135af5623 (1 revision) (flutter/engine#31366) * 11bf86a Migrate string encoding conversions to FML (flutter/engine#31334) * 6d9f479 Roll Skia from 5a2135af5623 to 21a92dff8fdc (3 revisions) (flutter/engine#31368) * 417a05e Roll Dart SDK from 7e3310bbe1ed to a9cfcc289ed4 (1 revision) (flutter/engine#31369) * b04ed63 Change link to felt documentation (flutter/engine#31312) * 3e7515a Roll Fuchsia Mac SDK from 5CmPcHTb1... to ZA5ZzabQM... (flutter/engine#31370) * 90effff Revert "Add trackpad gesture PointerData types (flutter#28571)" (flutter/engine#31375) * 3764a8f Change support for VM service message from "The Dart VM Service is listening" to "The Dart VM service is listening" (flutter/engine#31361) * 3d629c5 Fix html gradient rendering (flutter#97762) (flutter/engine#31355) * 963c449 [Android] Show deprecation warnings for Android tests (flutter/engine#31246) * 0be895c Roll Dart SDK from a9cfcc289ed4 to 0041431f5ec7 (1 revision) (flutter/engine#31372) * 18f2faf Roll Skia from 21a92dff8fdc to c5d3326d767d (7 revisions) (flutter/engine#31373) * 1b762d3 Roll Fuchsia Linux SDK from YGS2LvlDy... to yDo1mhBKz... (flutter/engine#31374) * 7b6a7b6 Roll Skia from c5d3326d767d to b6dfd97c5290 (10 revisions) (flutter/engine#31377) * f55b161 [a11y] Delegate UTF8ToUTF16 to FML (flutter/engine#31376) * d36d25f TextEditingDelta Support for the Web (flutter/engine#28527) * aa17186 [fml] Use common FML string encoding utils (flutter/engine#31378) * 97d9ec3 Renamed the scenario tests target to be generic emulator tests. (flutter/engine#28919) * 902717f Roll Skia from b6dfd97c5290 to e1e2a858205f (3 revisions) (flutter/engine#31380) * 877f820 Roll Dart SDK from 0041431f5ec7 to a15d19a0d914 (2 revisions) (flutter/engine#31381) * 2d16729 Roll Skia from e1e2a858205f to 74ce095463e1 (2 revisions) (flutter/engine#31383) * 8d3c0fb [web] PathRef: do not use == with doubles in assertions (flutter/engine#31382) * d1164c1 Move recipes to repository folders. (flutter/engine#31367) * e031e07 Roll Skia from 74ce095463e1 to 82d65d0487bd (1 revision) (flutter/engine#31384) * 5140a44 Define thread priority enum and set thread priority for all threads in Engine (flutter/engine#30605) * 1e46918 Roll Dart SDK from a15d19a0d914 to a3736d4e9b1b (1 revision) (flutter/engine#31397) * 8a28948 Roll Fuchsia Linux SDK from yDo1mhBKz... to UHV3HWM3d... (flutter/engine#31398) * ca86c79 Roll Fuchsia Mac SDK from ZA5ZzabQM... to jjxstNbgZ... (flutter/engine#31399)
Description
Design Doc
This change aims to collect the changes made to the editing value in the web text input plugin, and send them to the framework through a platform channel.
Framework PR: flutter/flutter#88477
Related Issues
Partially fixes flutter/flutter#87972
Tests
Added tests to verify delta inference.
Pre-launch Checklist
writing and running engine tests.
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.