From 86885dd0bd24e151348016b0744cabe4d11ccb38 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Wed, 8 Sep 2021 01:57:50 -0700 Subject: [PATCH 01/65] Initial implementation of TextEditingDeltaState for the web --- .../src/engine/text_editing/text_editing.dart | 102 +++++++++++++++++- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 3eee473f2894d..128b569ff2fd7 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:html' as html; +import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; @@ -439,6 +440,51 @@ class AutofillInfo { } } +class TextEditingDeltaState { + const TextEditingDeltaState({ + this.oldText = '', + this.deltaText = '', + this.deltaStart = -1, + this.deltaEnd = -1, + this.baseOffset = -1, + this.extentOffset = -1, + }); + + final String oldText; + final String deltaText; + final int deltaStart; + final int deltaEnd; + final int baseOffset; + final int extentOffset; + + Map toFlutter() => { + 'oldText': oldText, + 'deltaText': deltaText, + 'deltaStart': deltaStart, + 'deltaEnd': deltaEnd, + 'selectionBase': baseOffset, + 'selectionExtent': extentOffset, + }; + + TextEditingDeltaState copyWith({ + String? oldText, + String? deltaText, + int? deltaStart, + int? deltaEnd, + int? baseOffset, + int? extentOffset, + }) { + return TextEditingDeltaState( + oldText: oldText ?? this.oldText, + deltaText: deltaText ?? this.deltaText, + deltaStart: deltaStart ?? this.deltaStart, + deltaEnd: deltaEnd ?? this.deltaEnd, + baseOffset: baseOffset ?? this.baseOffset, + extentOffset: extentOffset ?? this.extentOffset, + ); + } +} + /// The current text and selection state of a text field. class EditingState { EditingState({this.text, int? baseOffset, int? extentOffset}) : @@ -610,6 +656,7 @@ class InputConfiguration { const TextCapitalizationConfig.defaultCapitalization(), this.autofill, this.autofillGroup, + this.enableDeltaModel = false, }); InputConfiguration.fromFrameworkMessage( @@ -633,7 +680,8 @@ class InputConfiguration { autofillGroup = EngineAutofillForm.fromFrameworkMessage( flutterInputConfiguration.tryJson('autofill'), flutterInputConfiguration.tryList('fields'), - ); + ), + enableDeltaModel = flutterInputConfiguration.tryBool('enableDeltaModel') ?? false; /// The type of information being edited in the input control. final EngineInputType inputType; @@ -658,6 +706,8 @@ class InputConfiguration { /// supported by Safari. final bool autocorrect; + final bool enableDeltaModel; + final AutofillInfo? autofill; final EngineAutofillForm? autofillGroup; @@ -665,7 +715,7 @@ class InputConfiguration { final TextCapitalizationConfig textCapitalization; } -typedef OnChangeCallback = void Function(EditingState? editingState); +typedef OnChangeCallback = void Function(EditingState? editingState, TextEditingDeltaState? editingDeltaState); typedef OnActionCallback = void Function(String? inputAction); /// Provides HTML DOM functionality for editable text. @@ -849,6 +899,8 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { late InputConfiguration inputConfiguration; EditingState? lastEditingState; + TextEditingDeltaState? lastTextEditingDeltaState; + /// Styles associated with the editable text. EditableTextStyle? style; @@ -947,6 +999,22 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { subscriptions.add(html.document.onSelectionChange.listen(handleChange)); + activeDomElement.addEventListener('beforeinput', (event) { + String eventData = js_util.getProperty(event, 'data').toString(); + if (eventData == 'null') { + eventData = ''; + } + final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); + + if (eventData == '') { + lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: (lastEditingState!.extentOffset ?? -1) - 1, deltaEnd: lastEditingState!.extentOffset); + } else { + lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: lastEditingState!.extentOffset, deltaEnd: lastEditingState!.extentOffset); + } + print('beforeinput delta: ' + js_util.getProperty(event, 'data').toString() + ' delta length: ' + js_util.getProperty(event, 'data').toString().length.toString()); + print('target ranges: ' + js_util.callMethod(event, 'getTargetRanges', []).toString()); + }); + // Refocus on the activeDomElement after blur, so that user can keep editing the // text field. subscriptions.add(activeDomElement.onBlur.listen((_) { @@ -978,6 +1046,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { isEnabled = false; lastEditingState = null; + lastTextEditingDeltaState = null; style = null; geometry = null; @@ -1022,10 +1091,12 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { assert(isEnabled); final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); + final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); if (newEditingState != lastEditingState) { lastEditingState = newEditingState; - onChange!(lastEditingState); + lastTextEditingDeltaState = newTextEditingDeltaState; + onChange!(lastEditingState, lastTextEditingDeltaState); } } @@ -1731,6 +1802,20 @@ class TextEditingChannel { ); } + /// Sends the 'TextInputClient.updateEditingStateWithDeltas' message to the framework. + void updateEditingStateWithDelta(int? clientId, TextEditingDeltaState? editingDeltaState) { + EnginePlatformDispatcher.instance.invokeOnPlatformMessage( + 'flutter/textinput', + const JSONMethodCodec().encodeMethodCall( + MethodCall('TextInputClient.updateEditingStateWithDeltas', [ + clientId, + editingDeltaState!.toFlutter(), + ]), + ), + _emptyCallback, + ); + } + /// Sends the 'TextInputClient.performAction' message to the framework. void performAction(int? clientId, String? inputAction) { EnginePlatformDispatcher.instance.invokeOnPlatformMessage( @@ -1824,8 +1909,15 @@ class HybridTextEditing { isEditing = true; strategy.enable( configuration!, - onChange: (EditingState? editingState) { - channel.updateEditingState(_clientId, editingState); + onChange: (EditingState? editingState, TextEditingDeltaState? editingDeltaState) { + if (configuration!.enableDeltaModel) { + print('delta model is enabled'); + editingDeltaState = editingDeltaState!.copyWith(baseOffset: editingState!.baseOffset, extentOffset: editingState.extentOffset); + channel.updateEditingStateWithDelta(_clientId, editingDeltaState); + } else { + print('we should never be here'); + channel.updateEditingState(_clientId, editingState); + } }, onAction: (String? inputAction) { channel.performAction(_clientId, inputAction); From 9ef2c43855154860b5d14eb74ecc775ef5d926b7 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 02:04:24 -0700 Subject: [PATCH 02/65] Capture composing region through compositionupdate and handle cases where there is an edit that occurs before the current cursor position --- .../src/engine/text_editing/text_editing.dart | 185 ++++++++++++++++-- 1 file changed, 171 insertions(+), 14 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 128b569ff2fd7..42d4d2995d97c 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -448,6 +448,8 @@ class TextEditingDeltaState { this.deltaEnd = -1, this.baseOffset = -1, this.extentOffset = -1, + this.composingOffset = -1, + this.composingExtent = -1, }); final String oldText; @@ -456,6 +458,8 @@ class TextEditingDeltaState { final int deltaEnd; final int baseOffset; final int extentOffset; + final int composingOffset; + final int composingExtent; Map toFlutter() => { 'oldText': oldText, @@ -464,6 +468,8 @@ class TextEditingDeltaState { 'deltaEnd': deltaEnd, 'selectionBase': baseOffset, 'selectionExtent': extentOffset, + // 'composingBase': composingOffset, + // 'composingExtent': composingExtent, }; TextEditingDeltaState copyWith({ @@ -473,6 +479,8 @@ class TextEditingDeltaState { int? deltaEnd, int? baseOffset, int? extentOffset, + int? composingOffset, + int? composingExtent, }) { return TextEditingDeltaState( oldText: oldText ?? this.oldText, @@ -481,6 +489,8 @@ class TextEditingDeltaState { deltaEnd: deltaEnd ?? this.deltaEnd, baseOffset: baseOffset ?? this.baseOffset, extentOffset: extentOffset ?? this.extentOffset, + composingOffset: composingOffset ?? this.composingOffset, + composingExtent: composingExtent ?? this.composingExtent, ); } } @@ -999,20 +1009,22 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { subscriptions.add(html.document.onSelectionChange.listen(handleChange)); - activeDomElement.addEventListener('beforeinput', (event) { - String eventData = js_util.getProperty(event, 'data').toString(); - if (eventData == 'null') { - eventData = ''; - } - final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); + activeDomElement.addEventListener('beforeinput', handleBeforeInput); - if (eventData == '') { - lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: (lastEditingState!.extentOffset ?? -1) - 1, deltaEnd: lastEditingState!.extentOffset); - } else { - lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: lastEditingState!.extentOffset, deltaEnd: lastEditingState!.extentOffset); - } - print('beforeinput delta: ' + js_util.getProperty(event, 'data').toString() + ' delta length: ' + js_util.getProperty(event, 'data').toString().length.toString()); - print('target ranges: ' + js_util.callMethod(event, 'getTargetRanges', []).toString()); + activeDomElement.addEventListener('compositionstart', handleCompositionStart); + + activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate); + + activeDomElement.addEventListener('compositionend', handleCompositionEnd); + + activeDomElement.addEventListener('candidatewindowshow', (event) { + print('hello from candidatewindowshow'); + print('leaving candidatewindowshow'); + }); + + activeDomElement.addEventListener('candidatewindowhide', (event) { + print('hello from candidatewindowhide'); + print('leaving candidatewindowhide'); }); // Refocus on the activeDomElement after blur, so that user can keep editing the @@ -1087,17 +1099,154 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _appendedToForm = true; } + /// Replaces a range of text in the original string with the text given in the + /// replacement string. + String _replace(String originalText, String replacementText, int start, int end) { + final String textStart = originalText.substring(0, start); + final String textEnd = originalText.substring(end, originalText.length); + final String newText = textStart + replacementText + textEnd; + return newText; + } + void handleChange(html.Event event) { assert(isEnabled); + // To verify the range of our delta we should compare the newEditingState's + // current text with the delta applied to the oldText. If they differ then capture + // the correct range from the newEditingState's text value. + // + // 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. + print('hello from inside handleChange'); + print('input type: ' + js_util.getProperty(event, 'inputType').toString()); + print('isComposing: ' + js_util.getProperty(event, 'isComposing').toString()); final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); + TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); + + final bool previousSelectionWasCollapsed = lastEditingState!.baseOffset == lastEditingState!.extentOffset; + + if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { + // We are removing text. + final int deletedLength = newTextEditingDeltaState.oldText.length - newEditingState.text!.length; + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.deltaEnd - deletedLength); + } else if (newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { + // We are replacing text at a selection. + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset); + } + + final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; + if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); + } + + print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); + print('last extenOffset: ' + lastEditingState!.extentOffset.toString()); + + final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; + if (!isDeltaRangeEmpty) { + // At this point the delta has been built and is ready to be sent to the framework. + // Before we send it off we should verify that this delta results in the current + // editing state. If it does not then we should update our delta accordingly. + // + // Our editing state can be seen as our source of truth. + final String textAfterDelta = _replace( + newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, + newTextEditingDeltaState.deltaStart, + newTextEditingDeltaState.deltaEnd); + final bool isDeltaVerified = textAfterDelta == newEditingState.text!; + + if (!isDeltaVerified) { + // Find all matches for deltaText. + // Find the correct range for our delta to arrive at the current editing state. + final RegExp deltaTextPattern = RegExp(r'' + newTextEditingDeltaState.deltaText + r''); + for (final Match match in deltaTextPattern.allMatches(newEditingState.text!)) { + String textAfterMatch; + int actualEnd; + final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= newTextEditingDeltaState.oldText.length; + if (!isMatchWithinOldTextBounds) { + actualEnd = match.start + newTextEditingDeltaState.deltaText.length - 1; + textAfterMatch = _replace( + newTextEditingDeltaState.oldText, + newTextEditingDeltaState.deltaText, + match.start, + actualEnd, + ); + } else { + actualEnd = match.end; + textAfterMatch = _replace( + newTextEditingDeltaState.oldText, + newTextEditingDeltaState.deltaText, + match.start, + actualEnd, + ); + } + + if (textAfterMatch == newEditingState.text!) { + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: match.start, deltaEnd: actualEnd); + break; + } + } + } + } if (newEditingState != lastEditingState) { lastEditingState = newEditingState; lastTextEditingDeltaState = newTextEditingDeltaState; onChange!(lastEditingState, lastTextEditingDeltaState); + // Flush delta after it has been sent to framework. + lastTextEditingDeltaState = TextEditingDeltaState(oldText: lastEditingState!.text!); } + + print('leaving handleChange'); + } + + void handleBeforeInput(html.Event event) { + print('hello from beforeinput'); + print('input type: ' + js_util.getProperty(event, 'inputType').toString()); + print('isComposing: ' + js_util.getProperty(event, 'isComposing').toString()); + String eventData = js_util.getProperty(event, 'data').toString(); + final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); + + if (eventData == 'null') { + lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: '', deltaEnd: lastEditingState!.extentOffset); + } else { + lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: lastEditingState!.extentOffset, deltaEnd: lastEditingState!.extentOffset); + } + print('beforeinput delta: ' + js_util.getProperty(event, 'data').toString() + ' delta length: ' + js_util.getProperty(event, 'data').toString().length.toString()); + print('target ranges: ' + js_util.callMethod(event, 'getTargetRanges', []).toString()); + print('leaving beforeinput'); + } + + void handleCompositionStart(html.Event event) { + final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); + print('hello from compositionstart'); + print('current baseOffset: ' + newEditingState.baseOffset.toString()); + print('current extentOffset: ' + newEditingState.extentOffset.toString()); + print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); + print('last extentOffset: ' + lastEditingState!.extentOffset.toString()); + print('leaving compositionstart'); + } + + void handleCompositionUpdate(html.Event event) { + final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); + final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); + lastTextEditingDeltaState = newDeltaState.copyWith(composingOffset: newEditingState.baseOffset, composingExtent: newEditingState.extentOffset); + print('hello from compositionupdate'); + print('current baseOffset: ' + newEditingState.baseOffset.toString()); + print('current extentOffset: ' + newEditingState.extentOffset.toString()); + print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); + print('last extenOffset: ' + lastEditingState!.extentOffset.toString()); + print('leaving compositionupdate'); + } + + void handleCompositionEnd(html.Event event) { + final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); + print('hello from compositionend'); + print('current baseOffset: ' + newEditingState.baseOffset.toString()); + print('current extentOffset: ' + newEditingState.extentOffset.toString()); + print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); + print('last extenOffset: ' + lastEditingState!.extentOffset.toString()); + print('leaving compositionend'); } void maybeSendAction(html.Event event) { @@ -1240,6 +1389,14 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy { subscriptions.add(html.document.onSelectionChange.listen(handleChange)); + activeDomElement.addEventListener('beforeinput', handleBeforeInput); + + activeDomElement.addEventListener('compositionstart', handleCompositionStart); + + activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate); + + activeDomElement.addEventListener('compositionend', handleCompositionEnd); + // Position the DOM element after it is focused. subscriptions.add(activeDomElement.onFocus.listen((_) { // Cancel previous timer if exists. From b93b043c74b8ac5d61bac729bc2064555480672f Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 02:09:16 -0700 Subject: [PATCH 03/65] clean up unused code --- .../src/engine/text_editing/text_editing.dart | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 42d4d2995d97c..a4f2ba833620a 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1011,22 +1011,8 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { activeDomElement.addEventListener('beforeinput', handleBeforeInput); - activeDomElement.addEventListener('compositionstart', handleCompositionStart); - activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate); - activeDomElement.addEventListener('compositionend', handleCompositionEnd); - - activeDomElement.addEventListener('candidatewindowshow', (event) { - print('hello from candidatewindowshow'); - print('leaving candidatewindowshow'); - }); - - activeDomElement.addEventListener('candidatewindowhide', (event) { - print('hello from candidatewindowhide'); - print('leaving candidatewindowhide'); - }); - // Refocus on the activeDomElement after blur, so that user can keep editing the // text field. subscriptions.add(activeDomElement.onBlur.listen((_) { @@ -1217,16 +1203,6 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { print('leaving beforeinput'); } - void handleCompositionStart(html.Event event) { - final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - print('hello from compositionstart'); - print('current baseOffset: ' + newEditingState.baseOffset.toString()); - print('current extentOffset: ' + newEditingState.extentOffset.toString()); - print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); - print('last extentOffset: ' + lastEditingState!.extentOffset.toString()); - print('leaving compositionstart'); - } - void handleCompositionUpdate(html.Event event) { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); @@ -1239,16 +1215,6 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { print('leaving compositionupdate'); } - void handleCompositionEnd(html.Event event) { - final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - print('hello from compositionend'); - print('current baseOffset: ' + newEditingState.baseOffset.toString()); - print('current extentOffset: ' + newEditingState.extentOffset.toString()); - print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); - print('last extenOffset: ' + lastEditingState!.extentOffset.toString()); - print('leaving compositionend'); - } - void maybeSendAction(html.Event event) { if (event is html.KeyboardEvent) { if (inputConfiguration.inputType.submitActionOnEnter && @@ -1391,12 +1357,8 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy { activeDomElement.addEventListener('beforeinput', handleBeforeInput); - activeDomElement.addEventListener('compositionstart', handleCompositionStart); - activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate); - activeDomElement.addEventListener('compositionend', handleCompositionEnd); - // Position the DOM element after it is focused. subscriptions.add(activeDomElement.onFocus.listen((_) { // Cancel previous timer if exists. From d3201a16347b937838be5b7f535d234d238f0de8 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 02:10:26 -0700 Subject: [PATCH 04/65] clean up rest of logs --- .../src/engine/text_editing/text_editing.dart | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index a4f2ba833620a..4a18f851aa861 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1102,10 +1102,6 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { // // 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. - print('hello from inside handleChange'); - print('input type: ' + js_util.getProperty(event, 'inputType').toString()); - print('isComposing: ' + js_util.getProperty(event, 'isComposing').toString()); - final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); @@ -1125,9 +1121,6 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); } - print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); - print('last extenOffset: ' + lastEditingState!.extentOffset.toString()); - final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; if (!isDeltaRangeEmpty) { // At this point the delta has been built and is ready to be sent to the framework. @@ -1182,14 +1175,9 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { // Flush delta after it has been sent to framework. lastTextEditingDeltaState = TextEditingDeltaState(oldText: lastEditingState!.text!); } - - print('leaving handleChange'); } void handleBeforeInput(html.Event event) { - print('hello from beforeinput'); - print('input type: ' + js_util.getProperty(event, 'inputType').toString()); - print('isComposing: ' + js_util.getProperty(event, 'isComposing').toString()); String eventData = js_util.getProperty(event, 'data').toString(); final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); @@ -1198,21 +1186,12 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { } else { lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: lastEditingState!.extentOffset, deltaEnd: lastEditingState!.extentOffset); } - print('beforeinput delta: ' + js_util.getProperty(event, 'data').toString() + ' delta length: ' + js_util.getProperty(event, 'data').toString().length.toString()); - print('target ranges: ' + js_util.callMethod(event, 'getTargetRanges', []).toString()); - print('leaving beforeinput'); } void handleCompositionUpdate(html.Event event) { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); lastTextEditingDeltaState = newDeltaState.copyWith(composingOffset: newEditingState.baseOffset, composingExtent: newEditingState.extentOffset); - print('hello from compositionupdate'); - print('current baseOffset: ' + newEditingState.baseOffset.toString()); - print('current extentOffset: ' + newEditingState.extentOffset.toString()); - print('last baseOffset: ' + lastEditingState!.baseOffset.toString()); - print('last extenOffset: ' + lastEditingState!.extentOffset.toString()); - print('leaving compositionupdate'); } void maybeSendAction(html.Event event) { @@ -2030,11 +2009,9 @@ class HybridTextEditing { configuration!, onChange: (EditingState? editingState, TextEditingDeltaState? editingDeltaState) { if (configuration!.enableDeltaModel) { - print('delta model is enabled'); editingDeltaState = editingDeltaState!.copyWith(baseOffset: editingState!.baseOffset, extentOffset: editingState.extentOffset); channel.updateEditingStateWithDelta(_clientId, editingDeltaState); } else { - print('we should never be here'); channel.updateEditingState(_clientId, editingState); } }, From 9f99a66fda71b27659179a5d463932c2d6694214 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 02:15:15 -0700 Subject: [PATCH 05/65] Make sure we initialize oldText in beforeInput --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 4a18f851aa861..880c3f98eeb2a 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1179,7 +1179,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void handleBeforeInput(html.Event event) { String eventData = js_util.getProperty(event, 'data').toString(); - final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); + final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); if (eventData == 'null') { lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: '', deltaEnd: lastEditingState!.extentOffset); From 8d693178bcc86900e3716d8051f6fe9f8a7d3052 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 02:24:14 -0700 Subject: [PATCH 06/65] Clean up comments --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 880c3f98eeb2a..0e2b0e9df74af 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -468,8 +468,6 @@ class TextEditingDeltaState { 'deltaEnd': deltaEnd, 'selectionBase': baseOffset, 'selectionExtent': extentOffset, - // 'composingBase': composingOffset, - // 'composingExtent': composingExtent, }; TextEditingDeltaState copyWith({ From 3ba145c4a7e40da4b75d3c8421dd821c1ef14f65 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 02:31:13 -0700 Subject: [PATCH 07/65] more defaults --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 0e2b0e9df74af..d146a462dc080 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1188,7 +1188,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void handleCompositionUpdate(html.Event event) { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(); + final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); lastTextEditingDeltaState = newDeltaState.copyWith(composingOffset: newEditingState.baseOffset, composingExtent: newEditingState.extentOffset); } From 728055a15ccf15b3c2559960f2cda208e36cc183 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 02:44:28 -0700 Subject: [PATCH 08/65] Add more comments --- .../src/engine/text_editing/text_editing.dart | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index d146a462dc080..df0d9a92d08bd 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1094,12 +1094,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void handleChange(html.Event event) { assert(isEnabled); - // To verify the range of our delta we should compare the newEditingState's - // current text with the delta applied to the oldText. If they differ then capture - // the correct range from the newEditingState's text value. - // - // 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. + final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); @@ -1114,6 +1109,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset); } + // If we are composing then set the delta range to the composing region we captured in compositionupdate. final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); @@ -1121,11 +1117,14 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; if (!isDeltaRangeEmpty) { - // At this point the delta has been built and is ready to be sent to the framework. - // Before we send it off we should verify that this delta results in the current - // editing state. If it does not then we should update our delta accordingly. + // To verify the range of our delta we should compare the newEditingState's + // text with the delta applied to the oldText. If they differ then capture + // the correct delta range from the newEditingState's text value. + // + // 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. // - // Our editing state can be seen as our source of truth. + // We can think of the newEditingState as our source of truth. final String textAfterDelta = _replace( newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, newTextEditingDeltaState.deltaStart, @@ -1133,8 +1132,9 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final bool isDeltaVerified = textAfterDelta == newEditingState.text!; if (!isDeltaVerified) { - // Find all matches for deltaText. - // Find the correct range for our delta to arrive at the current editing state. + // 1. Find all matches for deltaText. + // 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!)) { String textAfterMatch; @@ -1180,8 +1180,14 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); if (eventData == 'null') { + // When event.data is 'null' we have a deletion. + // The deltaStart is set in handleChange because there is where we get access + // to the new selection baseOffset which is our new deltaStart. lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: '', deltaEnd: lastEditingState!.extentOffset); } else { + // When event.data is not 'null' we we will begin by considering this delta as an insertion + // at the selection extentOffset. This may change due to logic in handleChange to handle + // composition and other IME behaviors. lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: lastEditingState!.extentOffset, deltaEnd: lastEditingState!.extentOffset); } } From efbdf5bbf4d955ff3a388c94a3f50ff11f8dd816 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 10:17:32 -0700 Subject: [PATCH 09/65] Move delta inferrence logic to TextEditingDeltaState --- .../src/engine/text_editing/text_editing.dart | 162 +++++++++--------- 1 file changed, 84 insertions(+), 78 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index df0d9a92d08bd..96b76f6deaa20 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -440,6 +440,15 @@ class AutofillInfo { } } +/// Replaces a range of text in the original string with the text given in the +/// replacement string. +String _replace(String originalText, String replacementText, int start, int end) { + final String textStart = originalText.substring(0, start); + final String textEnd = originalText.substring(end, originalText.length); + final String newText = textStart + replacementText + textEnd; + return newText; +} + class TextEditingDeltaState { const TextEditingDeltaState({ this.oldText = '', @@ -452,6 +461,80 @@ class TextEditingDeltaState { this.composingExtent = -1, }); + static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState? lastTextEditingDeltaState) { + TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); + + final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; + + if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { + // We are removing text. + final int deletedLength = newTextEditingDeltaState.oldText.length - newEditingState.text!.length; + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.deltaEnd - deletedLength); + } else if (newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { + // We are replacing text at a selection. + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset); + } + + // If we are composing then set the delta range to the composing region we captured in compositionupdate. + final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; + if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); + } + + final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; + if (!isDeltaRangeEmpty) { + // To verify the range of our delta we should compare the newEditingState's + // text with the delta applied to the oldText. If they differ then capture + // the correct delta range from the newEditingState's text value. + // + // 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. + final String textAfterDelta = _replace( + newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, + newTextEditingDeltaState.deltaStart, + newTextEditingDeltaState.deltaEnd); + final bool isDeltaVerified = textAfterDelta == newEditingState.text!; + + if (!isDeltaVerified) { + // 1. Find all matches for deltaText. + // 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!)) { + String textAfterMatch; + int actualEnd; + final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= newTextEditingDeltaState.oldText.length; + if (!isMatchWithinOldTextBounds) { + actualEnd = match.start + newTextEditingDeltaState.deltaText.length - 1; + textAfterMatch = _replace( + newTextEditingDeltaState.oldText, + newTextEditingDeltaState.deltaText, + match.start, + actualEnd, + ); + } else { + actualEnd = match.end; + textAfterMatch = _replace( + newTextEditingDeltaState.oldText, + newTextEditingDeltaState.deltaText, + match.start, + actualEnd, + ); + } + + if (textAfterMatch == newEditingState.text!) { + newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: match.start, deltaEnd: actualEnd); + break; + } + } + } + } + + return newTextEditingDeltaState; + } + final String oldText; final String deltaText; final int deltaStart; @@ -1083,88 +1166,11 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _appendedToForm = true; } - /// Replaces a range of text in the original string with the text given in the - /// replacement string. - String _replace(String originalText, String replacementText, int start, int end) { - final String textStart = originalText.substring(0, start); - final String textEnd = originalText.substring(end, originalText.length); - final String newText = textStart + replacementText + textEnd; - return newText; - } - void handleChange(html.Event event) { assert(isEnabled); final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); - - final bool previousSelectionWasCollapsed = lastEditingState!.baseOffset == lastEditingState!.extentOffset; - - if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { - // We are removing text. - final int deletedLength = newTextEditingDeltaState.oldText.length - newEditingState.text!.length; - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.deltaEnd - deletedLength); - } else if (newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { - // We are replacing text at a selection. - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset); - } - - // If we are composing then set the delta range to the composing region we captured in compositionupdate. - final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; - if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); - } - - final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; - if (!isDeltaRangeEmpty) { - // To verify the range of our delta we should compare the newEditingState's - // text with the delta applied to the oldText. If they differ then capture - // the correct delta range from the newEditingState's text value. - // - // 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. - final String textAfterDelta = _replace( - newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, - newTextEditingDeltaState.deltaStart, - newTextEditingDeltaState.deltaEnd); - final bool isDeltaVerified = textAfterDelta == newEditingState.text!; - - if (!isDeltaVerified) { - // 1. Find all matches for deltaText. - // 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!)) { - String textAfterMatch; - int actualEnd; - final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= newTextEditingDeltaState.oldText.length; - if (!isMatchWithinOldTextBounds) { - actualEnd = match.start + newTextEditingDeltaState.deltaText.length - 1; - textAfterMatch = _replace( - newTextEditingDeltaState.oldText, - newTextEditingDeltaState.deltaText, - match.start, - actualEnd, - ); - } else { - actualEnd = match.end; - textAfterMatch = _replace( - newTextEditingDeltaState.oldText, - newTextEditingDeltaState.deltaText, - match.start, - actualEnd, - ); - } - - if (textAfterMatch == newEditingState.text!) { - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: match.start, deltaEnd: actualEnd); - break; - } - } - } - } + final TextEditingDeltaState newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, lastTextEditingDeltaState); if (newEditingState != lastEditingState) { lastEditingState = newEditingState; From 1c330f3e22192dd45dcf6f1e9f937b509102df23 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Thu, 9 Sep 2021 10:31:39 -0700 Subject: [PATCH 10/65] Add new listeners to rest of strategies --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 96b76f6deaa20..0d6c7e66861f3 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1479,6 +1479,10 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy { subscriptions.add(html.document.onSelectionChange.listen(handleChange)); + activeDomElement.addEventListener('beforeinput', handleBeforeInput); + + activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate); + subscriptions.add(activeDomElement.onBlur.listen((_) { if (domRenderer.windowHasFocus) { // Chrome on Android will hide the onscreen keyboard when you tap outside @@ -1531,6 +1535,10 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy { subscriptions.add(activeDomElement.onKeyDown.listen(maybeSendAction)); + activeDomElement.addEventListener('beforeinput', handleBeforeInput); + + activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate); + // Detects changes in text selection. // // In Firefox, when cursor moves, neither selectionChange nor onInput From 0dd249a75ee35f221d94c246c7f329cc46963273 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 03:37:40 -0700 Subject: [PATCH 11/65] Fix existing tests --- .../src/engine/text_editing/text_editing.dart | 2 +- .../test/engine/semantics/text_field_test.dart | 16 ++++++++-------- lib/web_ui/test/text_editing_test.dart | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 0d6c7e66861f3..524ab8020d6da 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1182,7 +1182,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { } void handleBeforeInput(html.Event event) { - String eventData = js_util.getProperty(event, 'data').toString(); + final String eventData = js_util.getProperty(event, 'data').toString(); final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); if (eventData == 'null') { diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index 018969d7864a1..340c8d38cb21d 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -104,7 +104,7 @@ void testMain() { int actionCount = 0; strategy.enable( singlelineConfig, - onChange: (_) { + onChange: (_, _) { changeCount++; }, onAction: (_) { @@ -164,7 +164,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_) {}, + onChange: (_, _) {}, onAction: (_) {}, ); final SemanticsObject textFieldSemantics = createTextFieldSemantics( @@ -192,7 +192,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_) {}, + onChange: (_, _) {}, onAction: (_) {}, ); @@ -228,7 +228,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_) {}, + onChange: (_, _) {}, onAction: (_) {}, ); @@ -275,7 +275,7 @@ void testMain() { strategy.enable( multilineConfig, - onChange: (_) {}, + onChange: (_, _) {}, onAction: (_) {}, ); createTextFieldSemantics( @@ -291,7 +291,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_) {}, + onChange: (_, _) {}, onAction: (_) {}, ); @@ -314,7 +314,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_) {}, + onChange: (_, _) {}, onAction: (_) {}, ); @@ -382,7 +382,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_) {}, + onChange: (_, _) {}, onAction: (_) {}, ); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index e9f09b061bfa3..76fbf11a67fb9 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -46,7 +46,7 @@ final InputConfiguration multilineConfig = InputConfiguration( final Map flutterMultilineConfig = createFlutterConfig('multiline'); -void trackEditingState(EditingState? editingState) { +void trackEditingState(EditingState? editingState, TextEditingDeltaState? textEditingDeltaState) { lastEditingState = editingState; } From 37b757ef770de7d8c165f8d9cc488da1ee079f53 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 03:54:59 -0700 Subject: [PATCH 12/65] Fix tests --- .../test/engine/semantics/text_field_test.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index 340c8d38cb21d..062b02fb56b60 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -104,7 +104,7 @@ void testMain() { int actionCount = 0; strategy.enable( singlelineConfig, - onChange: (_, _) { + onChange: (_, __) { changeCount++; }, onAction: (_) { @@ -164,7 +164,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_, _) {}, + onChange: (_, __) {}, onAction: (_) {}, ); final SemanticsObject textFieldSemantics = createTextFieldSemantics( @@ -192,7 +192,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_, _) {}, + onChange: (_, __) {}, onAction: (_) {}, ); @@ -228,7 +228,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_, _) {}, + onChange: (_, __) {}, onAction: (_) {}, ); @@ -275,7 +275,7 @@ void testMain() { strategy.enable( multilineConfig, - onChange: (_, _) {}, + onChange: (_, __) {}, onAction: (_) {}, ); createTextFieldSemantics( @@ -291,7 +291,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_, _) {}, + onChange: (_, __) {}, onAction: (_) {}, ); @@ -314,7 +314,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_, _) {}, + onChange: (_, __) {}, onAction: (_) {}, ); @@ -382,7 +382,7 @@ void testMain() { strategy.enable( singlelineConfig, - onChange: (_, _) {}, + onChange: (_, __) {}, onAction: (_) {}, ); From fd1396fb7a957ffd2242f1bfb5e2f86ff48d6d01 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 09:06:41 -0700 Subject: [PATCH 13/65] Add lastTextEditingDeltaState to test --- lib/web_ui/test/text_editing_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 76fbf11a67fb9..44f5f35bf73f8 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -31,6 +31,7 @@ const MethodCodec codec = JSONMethodCodec(); DefaultTextEditingStrategy? editingStrategy; EditingState? lastEditingState; +TextEditingState? lastTextEditingDeltaState; String? lastInputAction; final InputConfiguration singlelineConfig = InputConfiguration( @@ -48,6 +49,7 @@ final Map flutterMultilineConfig = void trackEditingState(EditingState? editingState, TextEditingDeltaState? textEditingDeltaState) { lastEditingState = editingState; + lastTextEditingDeltaState = textEditingDeltaState; } void trackInputAction(String? inputAction) { @@ -61,6 +63,7 @@ void main() { void testMain() { tearDown(() { lastEditingState = null; + lastTextEditingDeltaState = null; lastInputAction = null; cleanTextEditingStrategy(); cleanTestFlags(); From cb6ef966e1670d074df8d88aba778645bef1ccc8 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 10:06:06 -0700 Subject: [PATCH 14/65] fix tests --- lib/web_ui/test/text_editing_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 44f5f35bf73f8..dff992f039172 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -31,7 +31,7 @@ const MethodCodec codec = JSONMethodCodec(); DefaultTextEditingStrategy? editingStrategy; EditingState? lastEditingState; -TextEditingState? lastTextEditingDeltaState; +TextEditingDeltaState? lastTextEditingDeltaState; String? lastInputAction; final InputConfiguration singlelineConfig = InputConfiguration( From 4f610e0c604989b41a5df6c2099c9fcf88b5d1ee Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 10:38:10 -0700 Subject: [PATCH 15/65] Add some preliminary tests for TextEditingDeltaState --- lib/web_ui/test/text_editing_test.dart | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index dff992f039172..4b57a36ed9e3a 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2143,6 +2143,59 @@ void testMain() { expect(editingState1 != editingState3, isTrue); }); }); + + group('TextEditingDeltaState', () { + test('Verify correct delta is inferred', () { + final EditingState newEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); + final EditingState lastEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + + final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); + + expect(textEditingDeltaState.oldText, 'worl'); + expect(textEditingDeltaState.deltaText, 'd'); + expect(textEditingDeltaState.deltaStart, 4); + expect(textEditingDeltaState.deltaEnd, 4); + expect(textEditingDeltaState.baseOffset, 5); + expect(textEditingDeltaState.extentOffset, 5); + expect(textEditingDeltaState.composingOffset, -1); + expect(textEditingDeltaState.composingExtent, -1); + }); + + test('Verify correct delta is inferred for double space to insert a period', () { + final EditingState newEditState = EditingState(text: 'hello.', baseOffset: 7, extentOffset: 7); + final EditingState lastEditState = EditingState(text: 'hello', baseOffset: 6, extentOffset: 6); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello', deltaText: '.', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + + final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); + + expect(textEditingDeltaState.oldText, 'hello '); + expect(textEditingDeltaState.deltaText, '. '); + expect(textEditingDeltaState.deltaStart, 5); + expect(textEditingDeltaState.deltaEnd, 6); + expect(textEditingDeltaState.baseOffset, 7); + expect(textEditingDeltaState.extentOffset, 7); + expect(textEditingDeltaState.composingOffset, -1); + expect(textEditingDeltaState.composingExtent, -1); + }); + + test('Verify correct delta is inferred for accent menu', () { + final EditingState newEditState = EditingState(text: 'à', baseOffset: 1, extentOffset: 1); + final EditingState lastEditState = EditingState(text: 'a', baseOffset: 1, extentOffset: 1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'a', deltaText: 'à', deltaStart: 1, deltaEnd: 1, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + + final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); + + expect(textEditingDeltaState.oldText, 'a'); + expect(textEditingDeltaState.deltaText, 'à'); + expect(textEditingDeltaState.deltaStart, 0); + expect(textEditingDeltaState.deltaEnd, 1); + expect(textEditingDeltaState.baseOffset, 1); + expect(textEditingDeltaState.extentOffset, 1); + expect(textEditingDeltaState.composingOffset, -1); + expect(textEditingDeltaState.composingExtent, -1); + }); + }); } KeyboardEvent dispatchKeyboardEvent( From f772bf164c99f4ab4bf8c6e51e43d2eddd3cd156 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 10:52:25 -0700 Subject: [PATCH 16/65] Send as list to framework --- .../src/engine/text_editing/text_editing.dart | 16 ++++++++++------ lib/web_ui/test/text_editing_test.dart | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 524ab8020d6da..240f0fccf43e4 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -545,12 +545,16 @@ class TextEditingDeltaState { final int composingExtent; Map toFlutter() => { - 'oldText': oldText, - 'deltaText': deltaText, - 'deltaStart': deltaStart, - 'deltaEnd': deltaEnd, - 'selectionBase': baseOffset, - 'selectionExtent': extentOffset, + 'batchDeltas': [ + { + 'oldText': oldText, + 'deltaText': deltaText, + 'deltaStart': deltaStart, + 'deltaEnd': deltaEnd, + 'selectionBase': baseOffset, + 'selectionExtent': extentOffset, + }, + ], }; TextEditingDeltaState copyWith({ diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 4b57a36ed9e3a..ade9f8dbd93e8 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2145,7 +2145,7 @@ void testMain() { }); group('TextEditingDeltaState', () { - test('Verify correct delta is inferred', () { + test('Verify correct delta is inferred - insertion', () { final EditingState newEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); final EditingState lastEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); @@ -2162,6 +2162,23 @@ void testMain() { expect(textEditingDeltaState.composingExtent, -1); }); + test('Verify correct delta is inferred - deletion', () { + final EditingState newEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); + final EditingState lastEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'world', deltaText: '', deltaStart: 4, deltaEnd: 5, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + + final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); + + expect(textEditingDeltaState.oldText, 'world'); + expect(textEditingDeltaState.deltaText, ''); + expect(textEditingDeltaState.deltaStart, 4); + expect(textEditingDeltaState.deltaEnd, 5); + expect(textEditingDeltaState.baseOffset, 4); + expect(textEditingDeltaState.extentOffset, 4); + expect(textEditingDeltaState.composingOffset, -1); + expect(textEditingDeltaState.composingExtent, -1); + }); + test('Verify correct delta is inferred for double space to insert a period', () { final EditingState newEditState = EditingState(text: 'hello.', baseOffset: 7, extentOffset: 7); final EditingState lastEditState = EditingState(text: 'hello', baseOffset: 6, extentOffset: 6); From f6e4d321319f8bed1d56dd365fac6ef3ee51422f Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 11:01:10 -0700 Subject: [PATCH 17/65] Add composing region test --- lib/web_ui/test/text_editing_test.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index ade9f8dbd93e8..c1dc50f1a8aeb 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2179,6 +2179,23 @@ void testMain() { expect(textEditingDeltaState.composingExtent, -1); }); + test('Verify correct delta is inferred - composing region replacement', () { + final EditingState newEditState = EditingState(text: '你好吗', baseOffset: 3, extentOffset: 3); + final EditingState lastEditState = EditingState(text: 'ni hao ma', baseOffset: 9, extentOffset: 9); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'ni hao ma', deltaText: '你好吗', deltaStart: 9, deltaEnd: 9, baseOffset: -1, extentOffset: -1, composingOffset: 0, extentOffset: 9); + + final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); + + expect(textEditingDeltaState.oldText, 'ni hao ma'); + expect(textEditingDeltaState.deltaText, '你好吗'); + expect(textEditingDeltaState.deltaStart, 0); + expect(textEditingDeltaState.deltaEnd, 9); + expect(textEditingDeltaState.baseOffset, 3); + expect(textEditingDeltaState.extentOffset, 3); + expect(textEditingDeltaState.composingOffset, -1); + expect(textEditingDeltaState.composingExtent, -1); + }); + test('Verify correct delta is inferred for double space to insert a period', () { final EditingState newEditState = EditingState(text: 'hello.', baseOffset: 7, extentOffset: 7); final EditingState lastEditState = EditingState(text: 'hello', baseOffset: 6, extentOffset: 6); From 704d4f92ac7e5cbc4e39209339ef1af656c62af0 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 11:07:56 -0700 Subject: [PATCH 18/65] Address nits --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 240f0fccf43e4..61898d253ead0 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -475,7 +475,8 @@ class TextEditingDeltaState { newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset); } - // If we are composing then set the delta range to the composing region we captured in compositionupdate. + // If we are composing then set the delta range to the composing region we + // captured in compositionupdate. final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); From 577aa591f7e47d5acb8610eba74b0b04a701d283 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 11:20:45 -0700 Subject: [PATCH 19/65] Update tests --- lib/web_ui/test/text_editing_test.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index c1dc50f1a8aeb..317202969ce34 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2148,7 +2148,7 @@ void testMain() { test('Verify correct delta is inferred - insertion', () { final EditingState newEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); final EditingState lastEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2165,7 +2165,7 @@ void testMain() { test('Verify correct delta is inferred - deletion', () { final EditingState newEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); final EditingState lastEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'world', deltaText: '', deltaStart: 4, deltaEnd: 5, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'world', deltaText: '', deltaStart: 4, deltaEnd: 5, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2182,7 +2182,7 @@ void testMain() { test('Verify correct delta is inferred - composing region replacement', () { final EditingState newEditState = EditingState(text: '你好吗', baseOffset: 3, extentOffset: 3); final EditingState lastEditState = EditingState(text: 'ni hao ma', baseOffset: 9, extentOffset: 9); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'ni hao ma', deltaText: '你好吗', deltaStart: 9, deltaEnd: 9, baseOffset: -1, extentOffset: -1, composingOffset: 0, extentOffset: 9); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'ni hao ma', deltaText: '你好吗', deltaStart: 9, deltaEnd: 9, baseOffset: -1, extentOffset: -1, composingOffset: 0, composingExtent: 9); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2199,7 +2199,7 @@ void testMain() { test('Verify correct delta is inferred for double space to insert a period', () { final EditingState newEditState = EditingState(text: 'hello.', baseOffset: 7, extentOffset: 7); final EditingState lastEditState = EditingState(text: 'hello', baseOffset: 6, extentOffset: 6); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello', deltaText: '.', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello', deltaText: '.', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2216,7 +2216,7 @@ void testMain() { test('Verify correct delta is inferred for accent menu', () { final EditingState newEditState = EditingState(text: 'à', baseOffset: 1, extentOffset: 1); final EditingState lastEditState = EditingState(text: 'a', baseOffset: 1, extentOffset: 1); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'a', deltaText: 'à', deltaStart: 1, deltaEnd: 1, baseOffset: -1, extentOffset: -1, composingOffset: -1, extentOffset: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'a', deltaText: 'à', deltaStart: 1, deltaEnd: 1, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); From 0dcf1f2f6c74aae9220022ec74e2cca0a11717ce Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 12:04:32 -0700 Subject: [PATCH 20/65] Try to fix tests --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 61898d253ead0..751b8e2d5c945 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -462,7 +462,7 @@ class TextEditingDeltaState { }); static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState? lastTextEditingDeltaState) { - TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); + TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text ?? ''); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; From a2e55258f440c2aff73f5416b69be3f44c57ca85 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 12:17:22 -0700 Subject: [PATCH 21/65] Prefer const with constant constructors --- .../lib/src/engine/text_editing/text_editing.dart | 2 +- lib/web_ui/test/text_editing_test.dart | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 751b8e2d5c945..d877228d0f723 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -477,7 +477,7 @@ class TextEditingDeltaState { // If we are composing then set the delta range to the composing region we // captured in compositionupdate. - final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; + final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; //Maybe use event.isComposing? if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); } diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 317202969ce34..a79cdd6dea9b9 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2148,7 +2148,7 @@ void testMain() { test('Verify correct delta is inferred - insertion', () { final EditingState newEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); final EditingState lastEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2165,7 +2165,7 @@ void testMain() { test('Verify correct delta is inferred - deletion', () { final EditingState newEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); final EditingState lastEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'world', deltaText: '', deltaStart: 4, deltaEnd: 5, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'world', deltaText: '', deltaStart: 4, deltaEnd: 5, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2182,7 +2182,7 @@ void testMain() { test('Verify correct delta is inferred - composing region replacement', () { final EditingState newEditState = EditingState(text: '你好吗', baseOffset: 3, extentOffset: 3); final EditingState lastEditState = EditingState(text: 'ni hao ma', baseOffset: 9, extentOffset: 9); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'ni hao ma', deltaText: '你好吗', deltaStart: 9, deltaEnd: 9, baseOffset: -1, extentOffset: -1, composingOffset: 0, composingExtent: 9); + const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'ni hao ma', deltaText: '你好吗', deltaStart: 9, deltaEnd: 9, baseOffset: -1, extentOffset: -1, composingOffset: 0, composingExtent: 9); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2199,7 +2199,7 @@ void testMain() { test('Verify correct delta is inferred for double space to insert a period', () { final EditingState newEditState = EditingState(text: 'hello.', baseOffset: 7, extentOffset: 7); final EditingState lastEditState = EditingState(text: 'hello', baseOffset: 6, extentOffset: 6); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello', deltaText: '.', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello', deltaText: '.', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2216,7 +2216,7 @@ void testMain() { test('Verify correct delta is inferred for accent menu', () { final EditingState newEditState = EditingState(text: 'à', baseOffset: 1, extentOffset: 1); final EditingState lastEditState = EditingState(text: 'a', baseOffset: 1, extentOffset: 1); - final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'a', deltaText: 'à', deltaStart: 1, deltaEnd: 1, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'a', deltaText: 'à', deltaStart: 1, deltaEnd: 1, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); From 95fe97b0c5450e8c495553f5cee8f2ff0d2977c0 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 12:18:25 -0700 Subject: [PATCH 22/65] Clean up comments --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index d877228d0f723..751b8e2d5c945 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -477,7 +477,7 @@ class TextEditingDeltaState { // If we are composing then set the delta range to the composing region we // captured in compositionupdate. - final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; //Maybe use event.isComposing? + final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); } From 592121586c82fe9fb11f67f81a73238d68c2a8ce Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Mon, 13 Sep 2021 12:30:18 -0700 Subject: [PATCH 23/65] Specify types --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 751b8e2d5c945..287e7a9eb9718 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -546,7 +546,7 @@ class TextEditingDeltaState { final int composingExtent; Map toFlutter() => { - 'batchDeltas': [ + 'batchDeltas': >[ { 'oldText': oldText, 'deltaText': deltaText, From a2a8b9dbf99c8f6886a4a592c76c92b183f8a6d0 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Tue, 14 Sep 2021 01:02:31 -0700 Subject: [PATCH 24/65] fix tests --- .../src/engine/text_editing/text_editing.dart | 7 +- lib/web_ui/test/text_editing_test.dart | 91 ++++++++++++++++--- 2 files changed, 81 insertions(+), 17 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 287e7a9eb9718..d50fad823281a 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -462,7 +462,7 @@ class TextEditingDeltaState { }); static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState? lastTextEditingDeltaState) { - TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text ?? ''); + TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; @@ -1175,7 +1175,10 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { assert(isEnabled); final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - final TextEditingDeltaState newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, lastTextEditingDeltaState); + TextEditingDeltaState? newTextEditingDeltaState; + if (inputConfiguration.enableDeltaModel) { + newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, lastTextEditingDeltaState); + } if (newEditingState != lastEditingState) { lastEditingState = newEditingState; diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index a79cdd6dea9b9..4be52c94b994a 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -1498,6 +1498,63 @@ void testMain() { hideKeyboard(); }); + test('Syncs the editing state back to Flutter - delta model', () { + final MethodCall setClient = MethodCall( + 'TextInput.setClient', [123, createFlutterConfig('text', enableDeltaModel: true)]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + 'text': '', + 'selectionBase': -1, + 'selectionExtent': -1, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + final InputElement input = textEditing!.strategy.domElement! as InputElement; + + input.value = 'something'; + input.dispatchEvent(Event.eventType('Event', 'input')); + + spy.messages.clear(); + + input.setSelectionRange(2, 5); + if (browserEngine == BrowserEngine.firefox) { + final Event keyup = KeyboardEvent('keyup'); + textEditing!.strategy.domElement!.dispatchEvent(keyup); + } else { + document.dispatchEvent(Event.eventType('Event', 'selectionchange')); + } + + expect(spy.messages, hasLength(1)); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.updateEditingStateWithDeltas'); + expect( + spy.messages[0].methodArguments, + [ + 123, // Client ID + { + 'batchDeltas': [ + { + 'oldText': 'something', + 'deltaText': '', + 'deltaStart': -1, + 'deltaEnd': -1, + 'selectionBase': 2, + 'selectionExtent': 5, + } + ], + } + ], + ); + spy.messages.clear(); + + hideKeyboard(); + }); + test('multiTextField Autofill sync updates back to Flutter', () { // Create a configuration with an AutofillGroup of four text fields. const String hintForFirstElement = 'familyName'; @@ -2145,6 +2202,8 @@ void testMain() { }); group('TextEditingDeltaState', () { + // The selection baseOffset and extentOffset are not inferred by + // TextEditingDeltaState.inferDeltaState so we do not verify them here. test('Verify correct delta is inferred - insertion', () { final EditingState newEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); final EditingState lastEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); @@ -2156,8 +2215,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, 'd'); expect(textEditingDeltaState.deltaStart, 4); expect(textEditingDeltaState.deltaEnd, 4); - expect(textEditingDeltaState.baseOffset, 5); - expect(textEditingDeltaState.extentOffset, 5); + expect(textEditingDeltaState.baseOffset, -1); + expect(textEditingDeltaState.extentOffset, -1); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); @@ -2173,8 +2232,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, ''); expect(textEditingDeltaState.deltaStart, 4); expect(textEditingDeltaState.deltaEnd, 5); - expect(textEditingDeltaState.baseOffset, 4); - expect(textEditingDeltaState.extentOffset, 4); + expect(textEditingDeltaState.baseOffset, -1); + expect(textEditingDeltaState.extentOffset, -1); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); @@ -2190,16 +2249,16 @@ void testMain() { expect(textEditingDeltaState.deltaText, '你好吗'); expect(textEditingDeltaState.deltaStart, 0); expect(textEditingDeltaState.deltaEnd, 9); - expect(textEditingDeltaState.baseOffset, 3); - expect(textEditingDeltaState.extentOffset, 3); - expect(textEditingDeltaState.composingOffset, -1); - expect(textEditingDeltaState.composingExtent, -1); + expect(textEditingDeltaState.baseOffset, -1); + expect(textEditingDeltaState.extentOffset, -1); + expect(textEditingDeltaState.composingOffset, 0); + expect(textEditingDeltaState.composingExtent, 9); }); test('Verify correct delta is inferred for double space to insert a period', () { - final EditingState newEditState = EditingState(text: 'hello.', baseOffset: 7, extentOffset: 7); - final EditingState lastEditState = EditingState(text: 'hello', baseOffset: 6, extentOffset: 6); - const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello', deltaText: '.', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + final EditingState newEditState = EditingState(text: 'hello. ', baseOffset: 7, extentOffset: 7); + final EditingState lastEditState = EditingState(text: 'hello ', baseOffset: 6, extentOffset: 6); + const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello ', deltaText: '. ', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2207,8 +2266,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, '. '); expect(textEditingDeltaState.deltaStart, 5); expect(textEditingDeltaState.deltaEnd, 6); - expect(textEditingDeltaState.baseOffset, 7); - expect(textEditingDeltaState.extentOffset, 7); + expect(textEditingDeltaState.baseOffset, -1); + expect(textEditingDeltaState.extentOffset, -1); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); @@ -2224,8 +2283,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, 'à'); expect(textEditingDeltaState.deltaStart, 0); expect(textEditingDeltaState.deltaEnd, 1); - expect(textEditingDeltaState.baseOffset, 1); - expect(textEditingDeltaState.extentOffset, 1); + expect(textEditingDeltaState.baseOffset, -1); + expect(textEditingDeltaState.extentOffset, -1); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); @@ -2339,6 +2398,7 @@ Map createFlutterConfig( String? placeholderText, List? autofillHintsForFields, bool decimal = false, + bool enableDeltaModel = false, }) { return { 'inputType': { @@ -2355,6 +2415,7 @@ Map createFlutterConfig( if (autofillEnabled && autofillHintsForFields != null) 'fields': createFieldValues(autofillHintsForFields, autofillHintsForFields), + 'enableDeltaModel': enableDeltaModel, }; } From 1fbab86b46f2f00034dbd17673cd6208af0ebf59 Mon Sep 17 00:00:00 2001 From: Renzo-Olivares Date: Tue, 14 Sep 2021 03:01:34 -0700 Subject: [PATCH 25/65] Specify type annotations --- lib/web_ui/test/text_editing_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 4be52c94b994a..26e7d07d696f9 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -1537,8 +1537,8 @@ void testMain() { [ 123, // Client ID { - 'batchDeltas': [ - { + 'batchDeltas': >[ + { 'oldText': 'something', 'deltaText': '', 'deltaStart': -1, From 03d63c3b11542033636edb77df9dd741c4f9ae64 Mon Sep 17 00:00:00 2001 From: Renzo Date: Tue, 11 Jan 2022 22:11:31 -0800 Subject: [PATCH 26/65] batchDeltas -> deltas --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 5e36925a0c446..27960d2250a14 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -547,7 +547,7 @@ class TextEditingDeltaState { final int composingExtent; Map toFlutter() => { - 'batchDeltas': >[ + 'deltas': >[ { 'oldText': oldText, 'deltaText': deltaText, From 51eabc88f30548e0836f0443bf41838f5a18cfb0 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 12 Jan 2022 13:40:26 -0800 Subject: [PATCH 27/65] Make eventData nullable so we dont compare with a 'null' string --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 27960d2250a14..06e3a41fb9c4e 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1191,11 +1191,11 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { } void handleBeforeInput(html.Event event) { - final String eventData = js_util.getProperty(event, 'data').toString(); + final String? eventData = js_util.getProperty(event, 'data') as String?; final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); - if (eventData == 'null') { - // When event.data is 'null' we have a deletion. + if (eventData == null) { + // When event.data is null we have a deletion. // The deltaStart is set in handleChange because there is where we get access // to the new selection baseOffset which is our new deltaStart. lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: '', deltaEnd: lastEditingState!.extentOffset); From 125580e224b133b54d88d2b5477c758ff23c3115 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 12 Jan 2022 14:29:38 -0800 Subject: [PATCH 28/65] Make TextEditingDeltaState mutable to avoid multiple copies --- .../src/engine/text_editing/text_editing.dart | 115 ++++++++---------- 1 file changed, 51 insertions(+), 64 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 06e3a41fb9c4e..d2429e1176235 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -451,7 +451,7 @@ String _replace(String originalText, String replacementText, int start, int end) } class TextEditingDeltaState { - const TextEditingDeltaState({ + TextEditingDeltaState({ this.oldText = '', this.deltaText = '', this.deltaStart = -1, @@ -462,28 +462,27 @@ class TextEditingDeltaState { this.composingExtent = -1, }); - static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState? lastTextEditingDeltaState) { - TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); - + static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState lastTextEditingDeltaState) { final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; - if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { + if (lastTextEditingDeltaState.deltaText.isEmpty && lastTextEditingDeltaState.deltaEnd != -1) { // We are removing text. - final int deletedLength = newTextEditingDeltaState.oldText.length - newEditingState.text!.length; - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.deltaEnd - deletedLength); - } else if (newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { + final int deletedLength = lastTextEditingDeltaState.oldText.length - newEditingState.text!.length; + lastTextEditingDeltaState.deltaStart = lastTextEditingDeltaState.deltaEnd - deletedLength; + } else if (lastTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { // We are replacing text at a selection. - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset); + lastTextEditingDeltaState.deltaStart = lastEditingState!.baseOffset!; } // If we are composing then set the delta range to the composing region we // captured in compositionupdate. - final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; - if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent); + final bool isCurrentlyComposing = lastTextEditingDeltaState.composingOffset != -1 && lastTextEditingDeltaState.composingOffset != lastTextEditingDeltaState.composingExtent; + if (lastTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { + lastTextEditingDeltaState.deltaStart = lastTextEditingDeltaState.composingOffset; + lastTextEditingDeltaState.deltaEnd = lastTextEditingDeltaState.composingExtent; } - final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; + final bool isDeltaRangeEmpty = lastTextEditingDeltaState.deltaStart == -1 && lastTextEditingDeltaState.deltaStart == lastTextEditingDeltaState.deltaEnd; if (!isDeltaRangeEmpty) { // To verify the range of our delta we should compare the newEditingState's // text with the delta applied to the oldText. If they differ then capture @@ -494,57 +493,58 @@ class TextEditingDeltaState { // // We can think of the newEditingState as our source of truth. final String textAfterDelta = _replace( - newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, - newTextEditingDeltaState.deltaStart, - newTextEditingDeltaState.deltaEnd); + lastTextEditingDeltaState.oldText, lastTextEditingDeltaState.deltaText, + lastTextEditingDeltaState.deltaStart, + lastTextEditingDeltaState.deltaEnd); final bool isDeltaVerified = textAfterDelta == newEditingState.text!; if (!isDeltaVerified) { // 1. Find all matches for deltaText. // 2. Apply matches/replacement to oldText until oldText matches the // new editing state's text value. - final RegExp deltaTextPattern = RegExp(r'' + newTextEditingDeltaState.deltaText + r''); + final RegExp deltaTextPattern = RegExp(r'' + lastTextEditingDeltaState.deltaText + r''); for (final Match match in deltaTextPattern.allMatches(newEditingState.text!)) { String textAfterMatch; int actualEnd; - final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= newTextEditingDeltaState.oldText.length; + final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= lastTextEditingDeltaState.oldText.length; if (!isMatchWithinOldTextBounds) { - actualEnd = match.start + newTextEditingDeltaState.deltaText.length - 1; + actualEnd = match.start + lastTextEditingDeltaState.deltaText.length - 1; textAfterMatch = _replace( - newTextEditingDeltaState.oldText, - newTextEditingDeltaState.deltaText, + lastTextEditingDeltaState.oldText, + lastTextEditingDeltaState.deltaText, match.start, actualEnd, ); } else { actualEnd = match.end; textAfterMatch = _replace( - newTextEditingDeltaState.oldText, - newTextEditingDeltaState.deltaText, + lastTextEditingDeltaState.oldText, + lastTextEditingDeltaState.deltaText, match.start, actualEnd, ); } if (textAfterMatch == newEditingState.text!) { - newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: match.start, deltaEnd: actualEnd); + lastTextEditingDeltaState.deltaStart = match.start; + lastTextEditingDeltaState.deltaEnd = actualEnd; break; } } } } - return newTextEditingDeltaState; + return lastTextEditingDeltaState; } - final String oldText; - final String deltaText; - final int deltaStart; - final int deltaEnd; - final int baseOffset; - final int extentOffset; - final int composingOffset; - final int composingExtent; + String oldText; + String deltaText; + int deltaStart; + int deltaEnd; + int baseOffset; + int extentOffset; + int composingOffset; + int composingExtent; Map toFlutter() => { 'deltas': >[ @@ -558,28 +558,6 @@ class TextEditingDeltaState { }, ], }; - - TextEditingDeltaState copyWith({ - String? oldText, - String? deltaText, - int? deltaStart, - int? deltaEnd, - int? baseOffset, - int? extentOffset, - int? composingOffset, - int? composingExtent, - }) { - return TextEditingDeltaState( - oldText: oldText ?? this.oldText, - deltaText: deltaText ?? this.deltaText, - deltaStart: deltaStart ?? this.deltaStart, - deltaEnd: deltaEnd ?? this.deltaEnd, - baseOffset: baseOffset ?? this.baseOffset, - extentOffset: extentOffset ?? this.extentOffset, - composingOffset: composingOffset ?? this.composingOffset, - composingExtent: composingExtent ?? this.composingExtent, - ); - } } /// The current text and selection state of a text field. @@ -997,6 +975,12 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { EditingState? lastEditingState; TextEditingDeltaState? lastTextEditingDeltaState; + TextEditingDeltaState get editingDelta { + if (lastTextEditingDeltaState == null){ + lastTextEditingDeltaState = TextEditingDeltaState(oldText: lastEditingState!.text!); + } + return lastTextEditingDeltaState!; + } /// Styles associated with the editable text. EditableTextStyle? style; @@ -1178,7 +1162,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState? newTextEditingDeltaState; if (inputConfiguration.enableDeltaModel) { - newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, lastTextEditingDeltaState); + newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, editingDelta); } if (newEditingState != lastEditingState) { @@ -1186,31 +1170,33 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { lastTextEditingDeltaState = newTextEditingDeltaState; onChange!(lastEditingState, lastTextEditingDeltaState); // Flush delta after it has been sent to framework. - lastTextEditingDeltaState = TextEditingDeltaState(oldText: lastEditingState!.text!); + lastTextEditingDeltaState = null; } } void handleBeforeInput(html.Event event) { final String? eventData = js_util.getProperty(event, 'data') as String?; - final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); if (eventData == null) { // When event.data is null we have a deletion. // The deltaStart is set in handleChange because there is where we get access // to the new selection baseOffset which is our new deltaStart. - lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: '', deltaEnd: lastEditingState!.extentOffset); + editingDelta.deltaText = ''; + editingDelta.deltaEnd = lastEditingState!.extentOffset!; } else { - // When event.data is not 'null' we we will begin by considering this delta as an insertion + // When event.data is not null we we will begin by considering this delta as an insertion // at the selection extentOffset. This may change due to logic in handleChange to handle // composition and other IME behaviors. - lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: lastEditingState!.extentOffset, deltaEnd: lastEditingState!.extentOffset); + editingDelta.deltaText = eventData; + editingDelta.deltaStart = lastEditingState!.extentOffset!; + editingDelta.deltaEnd = lastEditingState!.extentOffset!; } } void handleCompositionUpdate(html.Event event) { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!); - lastTextEditingDeltaState = newDeltaState.copyWith(composingOffset: newEditingState.baseOffset, composingExtent: newEditingState.extentOffset); + editingDelta.composingOffset = newEditingState.baseOffset!; + editingDelta.composingExtent = newEditingState.extentOffset!; } void maybeSendAction(html.Event event) { @@ -2036,7 +2022,8 @@ class HybridTextEditing { configuration!, onChange: (EditingState? editingState, TextEditingDeltaState? editingDeltaState) { if (configuration!.enableDeltaModel) { - editingDeltaState = editingDeltaState!.copyWith(baseOffset: editingState!.baseOffset, extentOffset: editingState.extentOffset); + editingDeltaState?.baseOffset = editingState!.baseOffset!; + editingDeltaState?.extentOffset = editingState!.extentOffset!; channel.updateEditingStateWithDelta(_clientId, editingDeltaState); } else { channel.updateEditingState(_clientId, editingState); From 26668cb00fdabdd7f2f4730fe28a6389e1a666ba Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 12 Jan 2022 15:29:37 -0800 Subject: [PATCH 29/65] Fix analyzer --- .../lib/src/engine/text_editing/text_editing.dart | 6 ++---- lib/web_ui/test/text_editing_test.dart | 10 +++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index d2429e1176235..02a7b91c3c117 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -976,9 +976,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { TextEditingDeltaState? lastTextEditingDeltaState; TextEditingDeltaState get editingDelta { - if (lastTextEditingDeltaState == null){ - lastTextEditingDeltaState = TextEditingDeltaState(oldText: lastEditingState!.text!); - } + lastTextEditingDeltaState ??= TextEditingDeltaState(oldText: lastEditingState!.text!); return lastTextEditingDeltaState!; } @@ -1175,7 +1173,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { } void handleBeforeInput(html.Event event) { - final String? eventData = js_util.getProperty(event, 'data') as String?; + final String? eventData = js_util.getProperty(event, 'data') as String?; if (eventData == null) { // When event.data is null we have a deletion. diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 74da52a55a11f..55f2a926380a0 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2207,7 +2207,7 @@ void testMain() { test('Verify correct delta is inferred - insertion', () { final EditingState newEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); final EditingState lastEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); - const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'worl', deltaText: 'd', deltaStart: 4, deltaEnd: 4, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2224,7 +2224,7 @@ void testMain() { test('Verify correct delta is inferred - deletion', () { final EditingState newEditState = EditingState(text: 'worl', baseOffset: 4, extentOffset: 4); final EditingState lastEditState = EditingState(text: 'world', baseOffset: 5, extentOffset: 5); - const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'world', deltaText: '', deltaStart: 4, deltaEnd: 5, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'world', deltaText: '', deltaStart: 4, deltaEnd: 5, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2241,7 +2241,7 @@ void testMain() { test('Verify correct delta is inferred - composing region replacement', () { final EditingState newEditState = EditingState(text: '你好吗', baseOffset: 3, extentOffset: 3); final EditingState lastEditState = EditingState(text: 'ni hao ma', baseOffset: 9, extentOffset: 9); - const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'ni hao ma', deltaText: '你好吗', deltaStart: 9, deltaEnd: 9, baseOffset: -1, extentOffset: -1, composingOffset: 0, composingExtent: 9); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'ni hao ma', deltaText: '你好吗', deltaStart: 9, deltaEnd: 9, baseOffset: -1, extentOffset: -1, composingOffset: 0, composingExtent: 9); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2258,7 +2258,7 @@ void testMain() { test('Verify correct delta is inferred for double space to insert a period', () { final EditingState newEditState = EditingState(text: 'hello. ', baseOffset: 7, extentOffset: 7); final EditingState lastEditState = EditingState(text: 'hello ', baseOffset: 6, extentOffset: 6); - const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello ', deltaText: '. ', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'hello ', deltaText: '. ', deltaStart: 6, deltaEnd: 6, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); @@ -2275,7 +2275,7 @@ void testMain() { test('Verify correct delta is inferred for accent menu', () { final EditingState newEditState = EditingState(text: 'à', baseOffset: 1, extentOffset: 1); final EditingState lastEditState = EditingState(text: 'a', baseOffset: 1, extentOffset: 1); - const TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'a', deltaText: 'à', deltaStart: 1, deltaEnd: 1, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); + final TextEditingDeltaState deltaState = TextEditingDeltaState(oldText: 'a', deltaText: 'à', deltaStart: 1, deltaEnd: 1, baseOffset: -1, extentOffset: -1, composingOffset: -1, composingExtent: -1); final TextEditingDeltaState textEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditState, lastEditState, deltaState); From 5a33dbfec729429c225e230c69697cd6f91bbd9c Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 12 Jan 2022 16:07:03 -0800 Subject: [PATCH 30/65] Fix test --- lib/web_ui/test/text_editing_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 55f2a926380a0..8b876f33c69eb 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -1537,7 +1537,7 @@ void testMain() { [ 123, // Client ID { - 'batchDeltas': >[ + 'deltas': >[ { 'oldText': 'something', 'deltaText': '', From e47a3ed761aa1c0cff34836e8d6ce835c2976142 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 12 Jan 2022 16:50:09 -0800 Subject: [PATCH 31/65] Use safe browser api instead of directly accessing js_util --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 02a7b91c3c117..5e5a5c7dae762 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; @@ -1173,7 +1172,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { } void handleBeforeInput(html.Event event) { - final String? eventData = js_util.getProperty(event, 'data') as String?; + final String? eventData = getJsProperty(event, 'data') as String?; if (eventData == null) { // When event.data is null we have a deletion. From 04020c2176250bc8288a9fec8e5d8915550602b8 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 19 Jan 2022 13:35:02 -0800 Subject: [PATCH 32/65] remove last prefix from editingDeltaState --- .../src/engine/text_editing/text_editing.dart | 16 +++++++++------- lib/web_ui/test/text_editing_test.dart | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 5e5a5c7dae762..91de694322fca 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -973,10 +973,10 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { late InputConfiguration inputConfiguration; EditingState? lastEditingState; - TextEditingDeltaState? lastTextEditingDeltaState; + TextEditingDeltaState? _editingDelta; TextEditingDeltaState get editingDelta { - lastTextEditingDeltaState ??= TextEditingDeltaState(oldText: lastEditingState!.text!); - return lastTextEditingDeltaState!; + _editingDelta ??= TextEditingDeltaState(oldText: lastEditingState!.text!); + return _editingDelta!; } /// Styles associated with the editable text. @@ -1112,7 +1112,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { isEnabled = false; lastEditingState = null; - lastTextEditingDeltaState = null; + _editingDelta = null; style = null; geometry = null; @@ -1155,6 +1155,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void handleChange(html.Event event) { assert(isEnabled); + print('handling regular input'); final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState? newTextEditingDeltaState; @@ -1164,15 +1165,16 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { if (newEditingState != lastEditingState) { lastEditingState = newEditingState; - lastTextEditingDeltaState = newTextEditingDeltaState; - onChange!(lastEditingState, lastTextEditingDeltaState); + _editingDelta = newTextEditingDeltaState; + onChange!(lastEditingState, editingDelta); // Flush delta after it has been sent to framework. - lastTextEditingDeltaState = null; + _editingDelta = null; } } void handleBeforeInput(html.Event event) { final String? eventData = getJsProperty(event, 'data') as String?; + print('handle before input'); if (eventData == null) { // When event.data is null we have a deletion. diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 8b876f33c69eb..17160efab4fb7 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -31,7 +31,7 @@ const MethodCodec codec = JSONMethodCodec(); DefaultTextEditingStrategy? editingStrategy; EditingState? lastEditingState; -TextEditingDeltaState? lastTextEditingDeltaState; +TextEditingDeltaState? editingDeltaState; String? lastInputAction; final InputConfiguration singlelineConfig = InputConfiguration( @@ -49,7 +49,7 @@ final Map flutterMultilineConfig = void trackEditingState(EditingState? editingState, TextEditingDeltaState? textEditingDeltaState) { lastEditingState = editingState; - lastTextEditingDeltaState = textEditingDeltaState; + editingDeltaState = textEditingDeltaState; } void trackInputAction(String? inputAction) { @@ -63,7 +63,7 @@ void main() { void testMain() { tearDown(() { lastEditingState = null; - lastTextEditingDeltaState = null; + editingDeltaState = null; lastInputAction = null; cleanTextEditingStrategy(); cleanTestFlags(); From 13ca351eb6bcadbe27eedd1596abbca226d14dda Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 19 Jan 2022 13:37:16 -0800 Subject: [PATCH 33/65] Remove logs --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 91de694322fca..2dd080690f60f 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1155,7 +1155,6 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void handleChange(html.Event event) { assert(isEnabled); - print('handling regular input'); final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState? newTextEditingDeltaState; @@ -1174,7 +1173,6 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void handleBeforeInput(html.Event event) { final String? eventData = getJsProperty(event, 'data') as String?; - print('handle before input'); if (eventData == null) { // When event.data is null we have a deletion. From be578c361cb74527be831f4ccbdfe720e774021f Mon Sep 17 00:00:00 2001 From: Renzo Date: Fri, 21 Jan 2022 16:32:14 -0800 Subject: [PATCH 34/65] fix merge --- .../src/engine/text_editing/text_editing.dart | 73 ++++++++++++------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index d87835e5d0600..864f224df6742 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -442,10 +442,16 @@ class AutofillInfo { /// Replaces a range of text in the original string with the text given in the /// replacement string. -String _replace(String originalText, String replacementText, int start, int end) { - final String textStart = originalText.substring(0, start); - final String textEnd = originalText.substring(end, originalText.length); - final String newText = textStart + replacementText + textEnd; +String _replace(String originalText, String replacementText, ui.TextRange replacedRange) { + assert(replacedRange.isValid); + assert(replacedRange.start <= originalText.length && replacedRange.end <= originalText.length); + + 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); + return newText; } @@ -453,6 +459,7 @@ class TextEditingDeltaState { TextEditingDeltaState({ this.oldText = '', this.deltaText = '', + this.inputType = '', this.deltaStart = -1, this.deltaEnd = -1, this.baseOffset = -1, @@ -475,8 +482,8 @@ class TextEditingDeltaState { // If we are composing then set the delta range to the composing region we // captured in compositionupdate. - final bool isCurrentlyComposing = lastTextEditingDeltaState.composingOffset != -1 && lastTextEditingDeltaState.composingOffset != lastTextEditingDeltaState.composingExtent; - if (lastTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { + final bool isCurrentlyComposing = lastTextEditingDeltaState.inputType == 'insertCompositionText'; + if (isCurrentlyComposing) { lastTextEditingDeltaState.deltaStart = lastTextEditingDeltaState.composingOffset; lastTextEditingDeltaState.deltaEnd = lastTextEditingDeltaState.composingExtent; } @@ -491,10 +498,10 @@ class TextEditingDeltaState { // are accurate. What may not be accurate is the range of the delta. // // We can think of the newEditingState as our source of truth. + final ui.TextRange replacementRange = ui.TextRange(start: lastTextEditingDeltaState.deltaStart, end: lastTextEditingDeltaState.deltaEnd); final String textAfterDelta = _replace( lastTextEditingDeltaState.oldText, lastTextEditingDeltaState.deltaText, - lastTextEditingDeltaState.deltaStart, - lastTextEditingDeltaState.deltaEnd); + replacementRange); final bool isDeltaVerified = textAfterDelta == newEditingState.text!; if (!isDeltaVerified) { @@ -511,16 +518,20 @@ class TextEditingDeltaState { textAfterMatch = _replace( lastTextEditingDeltaState.oldText, lastTextEditingDeltaState.deltaText, - match.start, - actualEnd, + ui.TextRange( + start: match.start, + end: actualEnd, + ), ); } else { actualEnd = match.end; textAfterMatch = _replace( lastTextEditingDeltaState.oldText, lastTextEditingDeltaState.deltaText, - match.start, - actualEnd, + ui.TextRange( + start: match.start, + end: actualEnd, + ), ); } @@ -538,6 +549,7 @@ class TextEditingDeltaState { String oldText; String deltaText; + String inputType; int deltaStart; int deltaEnd; int baseOffset; @@ -1177,20 +1189,29 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { void handleBeforeInput(html.Event event) { final String? eventData = getJsProperty(event, 'data') as String?; - - if (eventData == null) { - // When event.data is null we have a deletion. - // The deltaStart is set in handleChange because there is where we get access - // to the new selection baseOffset which is our new deltaStart. - editingDelta.deltaText = ''; - editingDelta.deltaEnd = lastEditingState!.extentOffset!; - } else { - // When event.data is not null we we will begin by considering this delta as an insertion - // at the selection extentOffset. This may change due to logic in handleChange to handle - // composition and other IME behaviors. - editingDelta.deltaText = eventData; - editingDelta.deltaStart = lastEditingState!.extentOffset!; - editingDelta.deltaEnd = lastEditingState!.extentOffset!; + final String? inputType = getJsProperty(event, 'inputType') as String?; + + if (inputType != null) { + editingDelta.inputType = inputType; + + if (inputType.contains('delete')) { + // The deltaStart is set in handleChange because there is where we get access + // to the new selection baseOffset which is our new deltaStart. + editingDelta.deltaText = ''; + editingDelta.deltaEnd = lastEditingState!.extentOffset!; + } else if (inputType == 'insertLineBreak'){ + // event.data is null, so we manually set deltaText as a line break by setting it to '\n'. + editingDelta.deltaText = '\n'; + editingDelta.deltaStart = lastEditingState!.extentOffset!; + editingDelta.deltaEnd = lastEditingState!.extentOffset!; + } else if (eventData != null) { + // When event.data is not null we we will begin by considering this delta as an insertion + // at the selection extentOffset. This may change due to logic in handleChange to handle + // composition and other IME behaviors. + editingDelta.deltaText = eventData; + editingDelta.deltaStart = lastEditingState!.extentOffset!; + editingDelta.deltaEnd = lastEditingState!.extentOffset!; + } } } From 2df95b01744f3f730a739a121dc0c927184ba873 Mon Sep 17 00:00:00 2001 From: Renzo Date: Fri, 21 Jan 2022 16:34:46 -0800 Subject: [PATCH 35/65] fix whitespace --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 864f224df6742..30b30ac2ea70b 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -446,9 +446,7 @@ String _replace(String originalText, String replacementText, ui.TextRange replac assert(replacedRange.isValid); assert(replacedRange.start <= originalText.length && replacedRange.end <= originalText.length); - final ui.TextRange normalizedRange = ui.TextRange( - start: math.min(replacedRange.start, replacedRange.end), - end: math.max(replacedRange.start, replacedRange.end)); + 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); From ac3407b9130c6412f74069439a7e3ddd6d9db2e1 Mon Sep 17 00:00:00 2001 From: Renzo Date: Mon, 24 Jan 2022 11:13:23 -0800 Subject: [PATCH 36/65] revert composing changes --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 30b30ac2ea70b..1b140a344638a 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -480,8 +480,8 @@ class TextEditingDeltaState { // If we are composing then set the delta range to the composing region we // captured in compositionupdate. - final bool isCurrentlyComposing = lastTextEditingDeltaState.inputType == 'insertCompositionText'; - if (isCurrentlyComposing) { + final bool isCurrentlyComposing = lastTextEditingDeltaState.composingOffset != -1 && lastTextEditingDeltaState.composingOffset != lastTextEditingDeltaState.composingExtent; + if (lastTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { lastTextEditingDeltaState.deltaStart = lastTextEditingDeltaState.composingOffset; lastTextEditingDeltaState.deltaEnd = lastTextEditingDeltaState.composingExtent; } From 6ced401f02affb8fcc061ae60a5b546dfba4d0b8 Mon Sep 17 00:00:00 2001 From: Renzo Date: Tue, 25 Jan 2022 13:43:19 -0800 Subject: [PATCH 37/65] update comments --- .../lib/src/engine/text_editing/text_editing.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 1b140a344638a..8c8accfa5afa6 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -457,7 +457,6 @@ class TextEditingDeltaState { TextEditingDeltaState({ this.oldText = '', this.deltaText = '', - this.inputType = '', this.deltaStart = -1, this.deltaEnd = -1, this.baseOffset = -1, @@ -471,10 +470,15 @@ class TextEditingDeltaState { if (lastTextEditingDeltaState.deltaText.isEmpty && lastTextEditingDeltaState.deltaEnd != -1) { // We are removing text. + // When text is deleted outside of the composing region or is cut using the native toolbar, + // we calculate the length of the deleted text by comparing the new and old editing state lengths. + // This value is then subtracted from the end position of the delta to capture the deleted range. final int deletedLength = lastTextEditingDeltaState.oldText.length - newEditingState.text!.length; lastTextEditingDeltaState.deltaStart = lastTextEditingDeltaState.deltaEnd - deletedLength; } else if (lastTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { // We are replacing text at a selection. + // When a selection of text is replaced by a copy/paste operation we set the starting range + // of the delta to be the beginning of the selection of the previous editing state. lastTextEditingDeltaState.deltaStart = lastEditingState!.baseOffset!; } @@ -547,7 +551,6 @@ class TextEditingDeltaState { String oldText; String deltaText; - String inputType; int deltaStart; int deltaEnd; int baseOffset; @@ -1190,15 +1193,13 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final String? inputType = getJsProperty(event, 'inputType') as String?; if (inputType != null) { - editingDelta.inputType = inputType; - if (inputType.contains('delete')) { // The deltaStart is set in handleChange because there is where we get access // to the new selection baseOffset which is our new deltaStart. editingDelta.deltaText = ''; editingDelta.deltaEnd = lastEditingState!.extentOffset!; } else if (inputType == 'insertLineBreak'){ - // event.data is null, so we manually set deltaText as a line break by setting it to '\n'. + // event.data is null on a line break, so we manually set deltaText as a line break by setting it to '\n'. editingDelta.deltaText = '\n'; editingDelta.deltaStart = lastEditingState!.extentOffset!; editingDelta.deltaEnd = lastEditingState!.extentOffset!; From 15868f67ef93ef0406cbc912fb47eb08f90602ae Mon Sep 17 00:00:00 2001 From: Renzo Date: Tue, 25 Jan 2022 13:44:12 -0800 Subject: [PATCH 38/65] remove trailing whitespace --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 8c8accfa5afa6..99e1f044c2373 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -470,7 +470,7 @@ class TextEditingDeltaState { if (lastTextEditingDeltaState.deltaText.isEmpty && lastTextEditingDeltaState.deltaEnd != -1) { // We are removing text. - // When text is deleted outside of the composing region or is cut using the native toolbar, + // When text is deleted outside of the composing region or is cut using the native toolbar, // we calculate the length of the deleted text by comparing the new and old editing state lengths. // This value is then subtracted from the end position of the delta to capture the deleted range. final int deletedLength = lastTextEditingDeltaState.oldText.length - newEditingState.text!.length; From 059fe7418e045cbf192ee1be2e6a9f8f973dac2f Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 10:51:08 -0800 Subject: [PATCH 39/65] Add docs for TextEditingDeltaState --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 99e1f044c2373..859797f7cdd5c 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -453,6 +453,9 @@ String _replace(String originalText, String replacementText, ui.TextRange replac return newText; } +/// 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]. class TextEditingDeltaState { TextEditingDeltaState({ this.oldText = '', From 26e676a56f23cc255007a26ef27de70e685f84fb Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 11:28:46 -0800 Subject: [PATCH 40/65] Normalize delta naming and use a copy instead of modifying function arguments --- .../src/engine/text_editing/text_editing.dart | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 859797f7cdd5c..a3f958206c96e 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -469,31 +469,32 @@ class TextEditingDeltaState { }); static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState lastTextEditingDeltaState) { + final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState; final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; - if (lastTextEditingDeltaState.deltaText.isEmpty && lastTextEditingDeltaState.deltaEnd != -1) { + if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { // We are removing text. // When text is deleted outside of the composing region or is cut using the native toolbar, // we calculate the length of the deleted text by comparing the new and old editing state lengths. // This value is then subtracted from the end position of the delta to capture the deleted range. - final int deletedLength = lastTextEditingDeltaState.oldText.length - newEditingState.text!.length; - lastTextEditingDeltaState.deltaStart = lastTextEditingDeltaState.deltaEnd - deletedLength; - } else if (lastTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { + final int deletedLength = newTextEditingDeltaState.oldText.length - newEditingState.text!.length; + newTextEditingDeltaState.deltaStart = newTextEditingDeltaState.deltaEnd - deletedLength; + } else if (newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { // We are replacing text at a selection. // When a selection of text is replaced by a copy/paste operation we set the starting range // of the delta to be the beginning of the selection of the previous editing state. - lastTextEditingDeltaState.deltaStart = lastEditingState!.baseOffset!; + newTextEditingDeltaState.deltaStart = lastEditingState!.baseOffset!; } // If we are composing then set the delta range to the composing region we // captured in compositionupdate. - final bool isCurrentlyComposing = lastTextEditingDeltaState.composingOffset != -1 && lastTextEditingDeltaState.composingOffset != lastTextEditingDeltaState.composingExtent; - if (lastTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { - lastTextEditingDeltaState.deltaStart = lastTextEditingDeltaState.composingOffset; - lastTextEditingDeltaState.deltaEnd = lastTextEditingDeltaState.composingExtent; + final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; + if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { + newTextEditingDeltaState.deltaStart = newTextEditingDeltaState.composingOffset; + newTextEditingDeltaState.deltaEnd = newTextEditingDeltaState.composingExtent; } - final bool isDeltaRangeEmpty = lastTextEditingDeltaState.deltaStart == -1 && lastTextEditingDeltaState.deltaStart == lastTextEditingDeltaState.deltaEnd; + final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; if (!isDeltaRangeEmpty) { // To verify the range of our delta we should compare the newEditingState's // text with the delta applied to the oldText. If they differ then capture @@ -503,9 +504,9 @@ class TextEditingDeltaState { // are accurate. What may not be accurate is the range of the delta. // // We can think of the newEditingState as our source of truth. - final ui.TextRange replacementRange = ui.TextRange(start: lastTextEditingDeltaState.deltaStart, end: lastTextEditingDeltaState.deltaEnd); + final ui.TextRange replacementRange = ui.TextRange(start: newTextEditingDeltaState.deltaStart, end: newTextEditingDeltaState.deltaEnd); final String textAfterDelta = _replace( - lastTextEditingDeltaState.oldText, lastTextEditingDeltaState.deltaText, + newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, replacementRange); final bool isDeltaVerified = textAfterDelta == newEditingState.text!; @@ -513,16 +514,16 @@ class TextEditingDeltaState { // 1. Find all matches for deltaText. // 2. Apply matches/replacement to oldText until oldText matches the // new editing state's text value. - final RegExp deltaTextPattern = RegExp(r'' + lastTextEditingDeltaState.deltaText + r''); + final RegExp deltaTextPattern = RegExp(r'' + newTextEditingDeltaState.deltaText + r''); for (final Match match in deltaTextPattern.allMatches(newEditingState.text!)) { String textAfterMatch; int actualEnd; - final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= lastTextEditingDeltaState.oldText.length; + final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= newTextEditingDeltaState.oldText.length; if (!isMatchWithinOldTextBounds) { - actualEnd = match.start + lastTextEditingDeltaState.deltaText.length - 1; + actualEnd = match.start + newTextEditingDeltaState.deltaText.length - 1; textAfterMatch = _replace( - lastTextEditingDeltaState.oldText, - lastTextEditingDeltaState.deltaText, + newTextEditingDeltaState.oldText, + newTextEditingDeltaState.deltaText, ui.TextRange( start: match.start, end: actualEnd, @@ -531,8 +532,8 @@ class TextEditingDeltaState { } else { actualEnd = match.end; textAfterMatch = _replace( - lastTextEditingDeltaState.oldText, - lastTextEditingDeltaState.deltaText, + newTextEditingDeltaState.oldText, + newTextEditingDeltaState.deltaText, ui.TextRange( start: match.start, end: actualEnd, @@ -541,15 +542,15 @@ class TextEditingDeltaState { } if (textAfterMatch == newEditingState.text!) { - lastTextEditingDeltaState.deltaStart = match.start; - lastTextEditingDeltaState.deltaEnd = actualEnd; + newTextEditingDeltaState.deltaStart = match.start; + newTextEditingDeltaState.deltaEnd = actualEnd; break; } } } } - return lastTextEditingDeltaState; + return newTextEditingDeltaState; } String oldText; @@ -993,10 +994,10 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { late InputConfiguration inputConfiguration; EditingState? lastEditingState; - TextEditingDeltaState? _editingDelta; - TextEditingDeltaState get editingDelta { - _editingDelta ??= TextEditingDeltaState(oldText: lastEditingState!.text!); - return _editingDelta!; + TextEditingDeltaState? _editingDeltaState; + TextEditingDeltaState get editingDeltaState { + _editingDeltaState ??= TextEditingDeltaState(oldText: lastEditingState!.text!); + return _editingDeltaState!; } /// Styles associated with the editable text. @@ -1132,7 +1133,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { isEnabled = false; lastEditingState = null; - _editingDelta = null; + _editingDeltaState = null; style = null; geometry = null; @@ -1179,15 +1180,15 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState? newTextEditingDeltaState; if (inputConfiguration.enableDeltaModel) { - newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, editingDelta); + newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, editingDeltaState); } if (newEditingState != lastEditingState) { lastEditingState = newEditingState; - _editingDelta = newTextEditingDeltaState; - onChange!(lastEditingState, editingDelta); + _editingDeltaState = newTextEditingDeltaState; + onChange!(lastEditingState, editingDeltaState); // Flush delta after it has been sent to framework. - _editingDelta = null; + _editingDeltaState = null; } } @@ -1199,28 +1200,28 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { if (inputType.contains('delete')) { // The deltaStart is set in handleChange because there is where we get access // to the new selection baseOffset which is our new deltaStart. - editingDelta.deltaText = ''; - editingDelta.deltaEnd = lastEditingState!.extentOffset!; + editingDeltaState.deltaText = ''; + editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } else if (inputType == 'insertLineBreak'){ // event.data is null on a line break, so we manually set deltaText as a line break by setting it to '\n'. - editingDelta.deltaText = '\n'; - editingDelta.deltaStart = lastEditingState!.extentOffset!; - editingDelta.deltaEnd = lastEditingState!.extentOffset!; + editingDeltaState.deltaText = '\n'; + editingDeltaState.deltaStart = lastEditingState!.extentOffset!; + editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } else if (eventData != null) { // When event.data is not null we we will begin by considering this delta as an insertion // at the selection extentOffset. This may change due to logic in handleChange to handle // composition and other IME behaviors. - editingDelta.deltaText = eventData; - editingDelta.deltaStart = lastEditingState!.extentOffset!; - editingDelta.deltaEnd = lastEditingState!.extentOffset!; + editingDeltaState.deltaText = eventData; + editingDeltaState.deltaStart = lastEditingState!.extentOffset!; + editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } } } void handleCompositionUpdate(html.Event event) { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); - editingDelta.composingOffset = newEditingState.baseOffset!; - editingDelta.composingExtent = newEditingState.extentOffset!; + editingDeltaState.composingOffset = newEditingState.baseOffset!; + editingDeltaState.composingExtent = newEditingState.extentOffset!; } void maybeSendAction(html.Event event) { From 91962261a5f5c99805961942a8015b5507bdf54a Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 11:54:36 -0800 Subject: [PATCH 41/65] Update selection of delta in inferDeltaState instead of onChange --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index a3f958206c96e..ece7923b54447 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -550,6 +550,10 @@ class TextEditingDeltaState { } } + // Update selection of the delta using information from the new editing state. + newTextEditingDeltaState.baseOffset = newEditingState.baseOffset!; + newTextEditingDeltaState.extentOffset = newEditingState.extentOffset!; + return newTextEditingDeltaState; } @@ -2047,8 +2051,6 @@ class HybridTextEditing { configuration!, onChange: (EditingState? editingState, TextEditingDeltaState? editingDeltaState) { if (configuration!.enableDeltaModel) { - editingDeltaState?.baseOffset = editingState!.baseOffset!; - editingDeltaState?.extentOffset = editingState!.extentOffset!; channel.updateEditingStateWithDelta(_clientId, editingDeltaState); } else { channel.updateEditingState(_clientId, editingState); From 38bc244c4a94707df6ffbb9b6ca670d0df7bdf76 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 12:41:58 -0800 Subject: [PATCH 42/65] Fix tests, previously the selection was not set in inferDeltaState, now it is so the tests should reflect this change --- lib/web_ui/test/text_editing_test.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 7b97d8d519b75..486b3e87c57fc 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2249,8 +2249,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, 'd'); expect(textEditingDeltaState.deltaStart, 4); expect(textEditingDeltaState.deltaEnd, 4); - expect(textEditingDeltaState.baseOffset, -1); - expect(textEditingDeltaState.extentOffset, -1); + expect(textEditingDeltaState.baseOffset, 5); + expect(textEditingDeltaState.extentOffset, 5); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); @@ -2266,8 +2266,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, ''); expect(textEditingDeltaState.deltaStart, 4); expect(textEditingDeltaState.deltaEnd, 5); - expect(textEditingDeltaState.baseOffset, -1); - expect(textEditingDeltaState.extentOffset, -1); + expect(textEditingDeltaState.baseOffset, 4); + expect(textEditingDeltaState.extentOffset, 4); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); @@ -2283,8 +2283,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, '你好吗'); expect(textEditingDeltaState.deltaStart, 0); expect(textEditingDeltaState.deltaEnd, 9); - expect(textEditingDeltaState.baseOffset, -1); - expect(textEditingDeltaState.extentOffset, -1); + expect(textEditingDeltaState.baseOffset, 3); + expect(textEditingDeltaState.extentOffset, 3); expect(textEditingDeltaState.composingOffset, 0); expect(textEditingDeltaState.composingExtent, 9); }); @@ -2300,8 +2300,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, '. '); expect(textEditingDeltaState.deltaStart, 5); expect(textEditingDeltaState.deltaEnd, 6); - expect(textEditingDeltaState.baseOffset, -1); - expect(textEditingDeltaState.extentOffset, -1); + expect(textEditingDeltaState.baseOffset, 7); + expect(textEditingDeltaState.extentOffset, 7); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); @@ -2317,8 +2317,8 @@ void testMain() { expect(textEditingDeltaState.deltaText, 'à'); expect(textEditingDeltaState.deltaStart, 0); expect(textEditingDeltaState.deltaEnd, 1); - expect(textEditingDeltaState.baseOffset, -1); - expect(textEditingDeltaState.extentOffset, -1); + expect(textEditingDeltaState.baseOffset, 1); + expect(textEditingDeltaState.extentOffset, 1); expect(textEditingDeltaState.composingOffset, -1); expect(textEditingDeltaState.composingExtent, -1); }); From cc0bb16723474a18d97baa8a5740340a99989ee4 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 13:33:27 -0800 Subject: [PATCH 43/65] Make a copy of delta instead of modifying function arguments --- .../src/engine/text_editing/text_editing.dart | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index ece7923b54447..0a1c00c015583 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -469,7 +469,7 @@ class TextEditingDeltaState { }); static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState lastTextEditingDeltaState) { - final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState; + final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState.copyWith(); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { @@ -578,6 +578,28 @@ class TextEditingDeltaState { }, ], }; + + TextEditingDeltaState copyWith({ + String? oldText, + String? deltaText, + int? deltaStart, + int? deltaEnd, + int? baseOffset, + int? extentOffset, + int? composingOffset, + int? composingExtent, + }) { + return TextEditingDeltaState( + oldText: oldText ?? this.oldText, + deltaText: deltaText ?? this.deltaText, + deltaStart: deltaStart ?? this.deltaStart, + deltaEnd: deltaEnd ?? this.deltaEnd, + baseOffset: baseOffset ?? this.baseOffset, + extentOffset: extentOffset ?? this.extentOffset, + composingOffset: composingOffset ?? this.composingOffset, + composingExtent: composingExtent ?? this.composingExtent, + ); + } } /// The current text and selection state of a text field. @@ -1183,8 +1205,8 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState? newTextEditingDeltaState; - if (inputConfiguration.enableDeltaModel) { - newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, editingDeltaState); + if (inputConfiguration.enableDeltaModel) { + newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, editingDeltaState); } if (newEditingState != lastEditingState) { From b7a91da3abe558c2955c6887dd46ea63e3ed667f Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 13:34:39 -0800 Subject: [PATCH 44/65] remove whitespace --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 0a1c00c015583..049a51253bb3b 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1205,8 +1205,8 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { final EditingState newEditingState = EditingState.fromDomElement(activeDomElement); TextEditingDeltaState? newTextEditingDeltaState; - if (inputConfiguration.enableDeltaModel) { - newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, editingDeltaState); + if (inputConfiguration.enableDeltaModel) { + newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, editingDeltaState); } if (newEditingState != lastEditingState) { From a600e9e567a7ef78bb9bb8dd3e5c86c06076f098 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 13:55:06 -0800 Subject: [PATCH 45/65] Move some logic into inferDeltaState --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 049a51253bb3b..f8fefd2554ff3 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -472,6 +472,9 @@ class TextEditingDeltaState { final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState.copyWith(); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; + newTextEditingDeltaState.deltaStart = lastEditingState!.extentOffset!; + newTextEditingDeltaState.deltaEnd = lastEditingState.extentOffset!; + if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { // We are removing text. // When text is deleted outside of the composing region or is cut using the native toolbar, @@ -1227,19 +1230,14 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { // The deltaStart is set in handleChange because there is where we get access // to the new selection baseOffset which is our new deltaStart. editingDeltaState.deltaText = ''; - editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } else if (inputType == 'insertLineBreak'){ // event.data is null on a line break, so we manually set deltaText as a line break by setting it to '\n'. editingDeltaState.deltaText = '\n'; - editingDeltaState.deltaStart = lastEditingState!.extentOffset!; - editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } else if (eventData != null) { // When event.data is not null we we will begin by considering this delta as an insertion // at the selection extentOffset. This may change due to logic in handleChange to handle // composition and other IME behaviors. editingDeltaState.deltaText = eventData; - editingDeltaState.deltaStart = lastEditingState!.extentOffset!; - editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } } } From 98fad4709b9f153e47bc56ef6a2606f99e37eee2 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 13:55:47 -0800 Subject: [PATCH 46/65] whitespace --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index f8fefd2554ff3..4fbde87542442 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -474,7 +474,7 @@ class TextEditingDeltaState { newTextEditingDeltaState.deltaStart = lastEditingState!.extentOffset!; newTextEditingDeltaState.deltaEnd = lastEditingState.extentOffset!; - + if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { // We are removing text. // When text is deleted outside of the composing region or is cut using the native toolbar, From 786a52810cf59ed2e06be7100314d779807863d7 Mon Sep 17 00:00:00 2001 From: Renzo Date: Wed, 26 Jan 2022 14:10:07 -0800 Subject: [PATCH 47/65] analyzer fix --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 4fbde87542442..7238b65a96820 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -486,7 +486,7 @@ class TextEditingDeltaState { // We are replacing text at a selection. // When a selection of text is replaced by a copy/paste operation we set the starting range // of the delta to be the beginning of the selection of the previous editing state. - newTextEditingDeltaState.deltaStart = lastEditingState!.baseOffset!; + newTextEditingDeltaState.deltaStart = lastEditingState.baseOffset!; } // If we are composing then set the delta range to the composing region we From 3f260ce865e550443b0f290915d14ae7717b583a Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 13:26:31 -0800 Subject: [PATCH 48/65] Revert "analyzer fix" This reverts commit 786a52810cf59ed2e06be7100314d779807863d7. --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 7238b65a96820..4fbde87542442 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -486,7 +486,7 @@ class TextEditingDeltaState { // We are replacing text at a selection. // When a selection of text is replaced by a copy/paste operation we set the starting range // of the delta to be the beginning of the selection of the previous editing state. - newTextEditingDeltaState.deltaStart = lastEditingState.baseOffset!; + newTextEditingDeltaState.deltaStart = lastEditingState!.baseOffset!; } // If we are composing then set the delta range to the composing region we From b9db35a7b102515903690347875db762b7641959 Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 13:26:38 -0800 Subject: [PATCH 49/65] Revert "whitespace" This reverts commit 98fad4709b9f153e47bc56ef6a2606f99e37eee2. --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 4fbde87542442..f8fefd2554ff3 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -474,7 +474,7 @@ class TextEditingDeltaState { newTextEditingDeltaState.deltaStart = lastEditingState!.extentOffset!; newTextEditingDeltaState.deltaEnd = lastEditingState.extentOffset!; - + if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { // We are removing text. // When text is deleted outside of the composing region or is cut using the native toolbar, From 41765f8060828db5086c0329e208121eb778eb3e Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 13:26:44 -0800 Subject: [PATCH 50/65] Revert "Move some logic into inferDeltaState" This reverts commit a600e9e567a7ef78bb9bb8dd3e5c86c06076f098. --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index f8fefd2554ff3..049a51253bb3b 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -472,9 +472,6 @@ class TextEditingDeltaState { final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState.copyWith(); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; - newTextEditingDeltaState.deltaStart = lastEditingState!.extentOffset!; - newTextEditingDeltaState.deltaEnd = lastEditingState.extentOffset!; - if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { // We are removing text. // When text is deleted outside of the composing region or is cut using the native toolbar, @@ -1230,14 +1227,19 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { // The deltaStart is set in handleChange because there is where we get access // to the new selection baseOffset which is our new deltaStart. editingDeltaState.deltaText = ''; + editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } else if (inputType == 'insertLineBreak'){ // event.data is null on a line break, so we manually set deltaText as a line break by setting it to '\n'. editingDeltaState.deltaText = '\n'; + editingDeltaState.deltaStart = lastEditingState!.extentOffset!; + editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } else if (eventData != null) { // When event.data is not null we we will begin by considering this delta as an insertion // at the selection extentOffset. This may change due to logic in handleChange to handle // composition and other IME behaviors. editingDeltaState.deltaText = eventData; + editingDeltaState.deltaStart = lastEditingState!.extentOffset!; + editingDeltaState.deltaEnd = lastEditingState!.extentOffset!; } } } From 8ff4f2d4fbebd6a089f1c7c988debe54af620c6b Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 13:25:17 -0800 Subject: [PATCH 51/65] pass _editingDeltaState instead of editingDeltaState to onChange for clarity --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 049a51253bb3b..358f1ed20a40d 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1212,7 +1212,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { if (newEditingState != lastEditingState) { lastEditingState = newEditingState; _editingDeltaState = newTextEditingDeltaState; - onChange!(lastEditingState, editingDeltaState); + onChange!(lastEditingState, _editingDeltaState); // Flush delta after it has been sent to framework. _editingDeltaState = null; } From f4a858a0429bd922b70654c1c577a0949787e1d3 Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 13:40:13 -0800 Subject: [PATCH 52/65] Add docs for beforeinput --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 358f1ed20a40d..f6406ac3c4e35 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1219,6 +1219,14 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { } void handleBeforeInput(html.Event event) { + // In some cases the beforeinput event is not fired such as when the selection + // of a text field is updated. In this case only the oninput event is fired. + // We still want a delta generated in these cases so we can properly update + // the selection. We begin to set the deltaStart and deltaEnd in beforeinput + // because a change in the selection will not have a delta range, it will only + // have a baseOffset and extentOffset. If these are set inside of inferDeltaState + // then the method will incorrectly report a deltaStart and deltaEnd for a non + // text update delta. final String? eventData = getJsProperty(event, 'data') as String?; final String? inputType = getJsProperty(event, 'inputType') as String?; From cbb6b6be6d2352a82b848f4e62b8850faae5ca5b Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 13:56:38 -0800 Subject: [PATCH 53/65] Add docs for inferDeltaState --- .../src/engine/text_editing/text_editing.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index f6406ac3c4e35..fcf10e3187996 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -468,6 +468,23 @@ class TextEditingDeltaState { this.composingExtent = -1, }); + /// Infers the correct delta values based on information from the new editing state + /// and the last editing state. + /// + /// 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. + /// + /// For a replacement at a selection we set the deltaStart to be the beginning of the selection + /// from the last editing state. + /// + /// For the composing region we check if a composing range was captured by the compositionupdate event, + /// we have a non empty deltaText, and that we did not have an active selection. An active selection + /// would mean we are not composing. + /// + /// We then verify that the delta we collected results in the text contained within the new editing state + /// 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(); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; From b59660087526f25879292428d23921991f0fefe3 Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 13:57:15 -0800 Subject: [PATCH 54/65] whitespace --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index fcf10e3187996..19df1be472072 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -477,7 +477,7 @@ class TextEditingDeltaState { /// /// For a replacement at a selection we set the deltaStart to be the beginning of the selection /// from the last editing state. - /// + /// /// For the composing region we check if a composing range was captured by the compositionupdate event, /// we have a non empty deltaText, and that we did not have an active selection. An active selection /// would mean we are not composing. From 5a8a5f75463c33a378dd2d85749e57a640288129 Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 14:08:43 -0800 Subject: [PATCH 55/65] Add more docs --- .../src/engine/text_editing/text_editing.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 19df1be472072..fd2175236d883 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -574,13 +574,31 @@ class TextEditingDeltaState { return newTextEditingDeltaState; } + /// The text before the text field was updated. String oldText; + + /// The text that is being inserted/replaced into the text field. + /// This will be an empty string for deletions and non text updates + /// such as selection updates. String deltaText; + + /// The position in the text field where the change begins. int deltaStart; + + /// The position in the text field where the change ends. int deltaEnd; + + /// The updated selection offset, or starting position of the selection + /// in the text field. int baseOffset; + + /// The extent of the selection. int extentOffset; + + /// The starting position of the composing region. int composingOffset; + + /// The extent of the composing region. int composingExtent; Map toFlutter() => { From 6645a0bf01e9b4454f38f6b596741eb2b9460bc4 Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 14:19:18 -0800 Subject: [PATCH 56/65] update docs --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index fd2175236d883..1be06f411d8dc 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -588,17 +588,16 @@ class TextEditingDeltaState { /// The position in the text field where the change ends. int deltaEnd; - /// The updated selection offset, or starting position of the selection - /// in the text field. + /// The updated starting position of the selection in the text field. int baseOffset; - /// The extent of the selection. + /// The updated terminating position of the selection. int extentOffset; /// The starting position of the composing region. int composingOffset; - /// The extent of the composing region. + /// The terminating position of the composing region. int composingExtent; Map toFlutter() => { From da90e99292da41098c55778697240fef0492262e Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 27 Jan 2022 14:22:04 -0800 Subject: [PATCH 57/65] update docs --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 1be06f411d8dc..91dc3e9038a04 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -591,7 +591,7 @@ class TextEditingDeltaState { /// The updated starting position of the selection in the text field. int baseOffset; - /// The updated terminating position of the selection. + /// The updated terminating position of the selection in the text field. int extentOffset; /// The starting position of the composing region. From b5e0b55e11f466a4537670715d7ad0193f9463a2 Mon Sep 17 00:00:00 2001 From: Renzo Date: Fri, 28 Jan 2022 20:50:02 -0800 Subject: [PATCH 58/65] Fix for insertion of a period following a double space within old text bounds --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 91dc3e9038a04..ac1980c725d8c 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -547,7 +547,7 @@ class TextEditingDeltaState { ), ); } else { - actualEnd = match.end; + actualEnd = match.end - 1; textAfterMatch = _replace( newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, From 7a5aff94fb78ff2dab44b0b0089a9027912b29e3 Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Mon, 31 Jan 2022 00:15:32 -0800 Subject: [PATCH 59/65] Fix accent insertion --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index ac1980c725d8c..c731a52c2b045 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -531,7 +531,10 @@ class TextEditingDeltaState { // 1. Find all matches for deltaText. // 2. Apply matches/replacement to oldText until oldText matches the // new editing state's text value. - final RegExp deltaTextPattern = RegExp(r'' + newTextEditingDeltaState.deltaText + r''); + final bool isPeriodInsertion = newTextEditingDeltaState.deltaText == '. '; + final RegExp deltaTextPattern = isPeriodInsertion? + RegExp(r'\' + newTextEditingDeltaState.deltaText + r'') + : RegExp(r'' + newTextEditingDeltaState.deltaText + r''); for (final Match match in deltaTextPattern.allMatches(newEditingState.text!)) { String textAfterMatch; int actualEnd; @@ -547,7 +550,7 @@ class TextEditingDeltaState { ), ); } else { - actualEnd = match.end - 1; + actualEnd = actualEnd = isPeriodInsertion? match.end - 1 : match.end; textAfterMatch = _replace( newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, From 9d97c88eed8d32178b3429bfc27576ba128fd756 Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Mon, 31 Jan 2022 10:11:30 -0800 Subject: [PATCH 60/65] clean up comments --- .../lib/src/engine/text_editing/text_editing.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index c731a52c2b045..dd4d7f659adf8 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -454,7 +454,9 @@ String _replace(String originalText, String replacementText, ui.TextRange replac } /// 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 +/// of a text field. +/// +/// This is packaged into a JSON and sent to the framework /// to be processed into a concrete [TextEditingDelta]. class TextEditingDeltaState { TextEditingDeltaState({ @@ -521,6 +523,10 @@ class TextEditingDeltaState { // are accurate. What may not be accurate is the range of the delta. // // We can think of the newEditingState as our source of truth. + // + // This verification is needed for cases such as the insertion of a period + // after a double space, and the insertion of an accented character through + // a native composing menu. final ui.TextRange replacementRange = ui.TextRange(start: newTextEditingDeltaState.deltaStart, end: newTextEditingDeltaState.deltaEnd); final String textAfterDelta = _replace( newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText, @@ -531,7 +537,7 @@ class TextEditingDeltaState { // 1. Find all matches for deltaText. // 2. Apply matches/replacement to oldText until oldText matches the // new editing state's text value. - final bool isPeriodInsertion = newTextEditingDeltaState.deltaText == '. '; + final bool isPeriodInsertion = newTextEditingDeltaState.deltaText.contains('.'); final RegExp deltaTextPattern = isPeriodInsertion? RegExp(r'\' + newTextEditingDeltaState.deltaText + r'') : RegExp(r'' + newTextEditingDeltaState.deltaText + r''); From 0fc279de681505054241afcced7ab0a2b6d96a57 Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Wed, 9 Feb 2022 12:41:26 -0800 Subject: [PATCH 61/65] Address comments for clarity aand regexp --- .../src/engine/text_editing/text_editing.dart | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index dd4d7f659adf8..3301f35988510 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -474,32 +474,32 @@ class TextEditingDeltaState { /// and the last editing state. /// /// 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. + /// and last editing states. We subtract this from the `deltaEnd` that we set when beforeinput + /// was fired to determine the `deltaStart`. /// - /// For a replacement at a selection we set the deltaStart to be the beginning of the selection + /// For a replacement at a selection we set the `deltaStart` to be the beginning of the selection /// from the last editing state. /// /// For the composing region we check if a composing range was captured by the compositionupdate event, - /// we have a non empty deltaText, and that we did not have an active selection. An active selection + /// we have a non empty `deltaText`, and that we did not have an active selection. An active selection /// would mean we are not composing. /// /// We then verify that the delta we collected results in the text contained within the new editing state /// 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. + /// and use regex to find the correct `deltaStart` and `deltaEnd`. static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState lastTextEditingDeltaState) { final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState.copyWith(); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; + final bool isTextBeingRemoved = newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1; + final bool isTextBeingChangedAtActiveSelection = newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed; - if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) { - // We are removing text. + if (isTextBeingRemoved) { // When text is deleted outside of the composing region or is cut using the native toolbar, // we calculate the length of the deleted text by comparing the new and old editing state lengths. // This value is then subtracted from the end position of the delta to capture the deleted range. final int deletedLength = newTextEditingDeltaState.oldText.length - newEditingState.text!.length; newTextEditingDeltaState.deltaStart = newTextEditingDeltaState.deltaEnd - deletedLength; - } else if (newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) { - // We are replacing text at a selection. + } else if (isTextBeingChangedAtActiveSelection) { // When a selection of text is replaced by a copy/paste operation we set the starting range // of the delta to be the beginning of the selection of the previous editing state. newTextEditingDeltaState.deltaStart = lastEditingState!.baseOffset!; @@ -538,9 +538,7 @@ class TextEditingDeltaState { // 2. Apply matches/replacement to oldText until oldText matches the // new editing state's text value. final bool isPeriodInsertion = newTextEditingDeltaState.deltaText.contains('.'); - final RegExp deltaTextPattern = isPeriodInsertion? - RegExp(r'\' + newTextEditingDeltaState.deltaText + r'') - : RegExp(r'' + newTextEditingDeltaState.deltaText + r''); + final RegExp deltaTextPattern = RegExp(RegExp.escape(newTextEditingDeltaState.deltaText)); for (final Match match in deltaTextPattern.allMatches(newEditingState.text!)) { String textAfterMatch; int actualEnd; From fc823e93743dc88036ec0931868eef9e166d09ad Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Thu, 10 Feb 2022 10:58:23 -0800 Subject: [PATCH 62/65] Make composing and selection nullable --- .../src/engine/text_editing/text_editing.dart | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 3301f35988510..3eacdcf44d988 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -464,10 +464,10 @@ class TextEditingDeltaState { this.deltaText = '', this.deltaStart = -1, this.deltaEnd = -1, - this.baseOffset = -1, - this.extentOffset = -1, - this.composingOffset = -1, - this.composingExtent = -1, + this.baseOffset, + this.extentOffset, + this.composingOffset, + this.composingExtent, }); /// Infers the correct delta values based on information from the new editing state @@ -507,10 +507,10 @@ class TextEditingDeltaState { // If we are composing then set the delta range to the composing region we // captured in compositionupdate. - final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; + final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != null && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent; if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) { - newTextEditingDeltaState.deltaStart = newTextEditingDeltaState.composingOffset; - newTextEditingDeltaState.deltaEnd = newTextEditingDeltaState.composingExtent; + newTextEditingDeltaState.deltaStart = newTextEditingDeltaState.composingOffset!; + newTextEditingDeltaState.deltaEnd = newTextEditingDeltaState.composingExtent!; } final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd; @@ -596,16 +596,16 @@ class TextEditingDeltaState { int deltaEnd; /// The updated starting position of the selection in the text field. - int baseOffset; + int? baseOffset; /// The updated terminating position of the selection in the text field. - int extentOffset; + int? extentOffset; /// The starting position of the composing region. - int composingOffset; + int? composingOffset; /// The terminating position of the composing region. - int composingExtent; + int? composingExtent; Map toFlutter() => { 'deltas': >[ From 6bf2ca7167370ac0783b53c11429da04aace0220 Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Thu, 10 Feb 2022 11:44:16 -0800 Subject: [PATCH 63/65] update docs --- .../lib/src/engine/text_editing/text_editing.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 3eacdcf44d988..7f83d801d9e92 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -474,19 +474,19 @@ class TextEditingDeltaState { /// and the last editing state. /// /// 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 the `deltaStart`. + /// and last editing states. We subtract this from the [deltaEnd] that we set when beforeinput + /// was fired to determine the [deltaStart]. /// - /// For a replacement at a selection we set the `deltaStart` to be the beginning of the selection + /// For a replacement at a selection we set the [deltaStart] to be the beginning of the selection /// from the last editing state. /// /// For the composing region we check if a composing range was captured by the compositionupdate event, - /// we have a non empty `deltaText`, and that we did not have an active selection. An active selection + /// we have a non empty [deltaText], and that we did not have an active selection. An active selection /// would mean we are not composing. /// /// We then verify that the delta we collected results in the text contained within the new editing state /// 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`. + /// and use regex to find the correct [deltaStart] and [deltaEnd]. static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState lastTextEditingDeltaState) { final TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState.copyWith(); final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset; @@ -590,9 +590,13 @@ class TextEditingDeltaState { String deltaText; /// The position in the text field where the change begins. + /// + /// Has a default value of -1 to signify an empty range. int deltaStart; /// The position in the text field where the change ends. + /// + /// Has a default value of -1 to signify an empty range. int deltaEnd; /// The updated starting position of the selection in the text field. From acbc0de7ada9b8db9c62465ac299b28c4cc4dc0a Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Thu, 10 Feb 2022 11:45:24 -0800 Subject: [PATCH 64/65] whitespace --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 7f83d801d9e92..76aa8f8313863 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -595,7 +595,7 @@ class TextEditingDeltaState { int deltaStart; /// The position in the text field where the change ends. - /// + /// /// Has a default value of -1 to signify an empty range. int deltaEnd; From 0bb7f30d2285301bc1ed1038084b19aac7be5a0f Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Thu, 10 Feb 2022 12:15:04 -0800 Subject: [PATCH 65/65] address comments --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 76aa8f8313863..1f7918577f932 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -448,9 +448,7 @@ String _replace(String originalText, String replacementText, ui.TextRange replac 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); - - return newText; + return normalizedRange.textBefore(originalText) + replacementText + normalizedRange.textAfter(originalText); } /// The change between the last editing state and the current editing state