Revert "More EditableText docs (#66864)" (#68025)

This reverts commit daa6b2cc29b5ff1f166660f9e70fb38d6c0de6cc.
diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart
index c98f5a2..b273db7 100644
--- a/packages/flutter/lib/src/services/text_input.dart
+++ b/packages/flutter/lib/src/services/text_input.dart
@@ -756,17 +756,7 @@
   /// Gets the current text input.
   TextEditingValue get textEditingValue;
 
-  /// Indicates that the user has requested the delegate to replace its current
-  /// text editing state with [value].
-  ///
-  /// The new [value] is treated as user input and thus may subject to input
-  /// formatting.
-  ///
-  /// See also:
-  ///
-  /// * [EditableTextState.textEditingValue]: an implementation that applies
-  ///   additional pre-processing to the specified [value], before updating the
-  ///   text editing state.
+  /// Sets the current text input (replaces the whole line).
   set textEditingValue(TextEditingValue value);
 
   /// Hides the text selection toolbar.
@@ -794,7 +784,6 @@
 /// See also:
 ///
 ///  * [TextInput.attach]
-///  * [EditableText], a [TextInputClient] implementation.
 abstract class TextInputClient {
   /// Abstract const constructor. This constructor enables subclasses to provide
   /// const constructors so that they can be used in const expressions.
@@ -816,9 +805,6 @@
   AutofillScope? get currentAutofillScope;
 
   /// Requests that this client update its editing state to the given value.
-  ///
-  /// The new [value] is treated as user input and thus may subject to input
-  /// formatting.
   void updateEditingValue(TextEditingValue value);
 
   /// Requests that this client perform the given action.
@@ -846,10 +832,7 @@
 ///
 /// See also:
 ///
-///  * [TextInput.attach], a method used to establish a [TextInputConnection]
-///    between the system's text input and a [TextInputClient].
-///  * [EditableText], a [TextInputClient] that connects to and interacts with
-///    the system's text input using a [TextInputConnection].
+///  * [TextInput.attach]
 class TextInputConnection {
   TextInputConnection._(this._client)
       : assert(_client != null),
@@ -906,8 +889,7 @@
     TextInput._instance._updateConfig(configuration);
   }
 
-  /// Requests that the text input control change its internal state to match
-  /// the given state.
+  /// Requests that the text input control change its internal state to match the given state.
   void setEditingState(TextEditingValue value) {
     assert(attached);
     TextInput._instance._setEditingState(value);
@@ -1060,57 +1042,9 @@
 
 /// An low-level interface to the system's text input control.
 ///
-/// To start interacting with the system's text input control, call [attach] to
-/// establish a [TextInputConnection] between the system's text input control
-/// and a [TextInputClient]. The majority of commands available for
-/// interacting with the text input control reside in the returned
-/// [TextInputConnection]. The communication between the system text input and
-/// the [TextInputClient] is asynchronous.
-///
-/// The platform text input plugin (which represents the system's text input)
-/// and the [TextInputClient] usually maintain their own text editing states
-/// ([TextEditingValue]) separately. They must be kept in sync as long as the
-/// [TextInputClient] is connected. The following methods can be used to send
-/// [TextEditingValue] to update the other party, when either party's text
-/// editing states change:
-///
-/// * The [TextInput.attach] method allows a [TextInputClient] to establish a
-///   connection to the text input. An optional field in its `configuration`
-///   parameter can be used to specify an initial value for the platform text
-///   input plugin's [TextEditingValue].
-///
-/// * The [TextInputClient] sends its [TextEditingValue] to the platform text
-///   input plugin using [TextInputConnection.setEditingState].
-///
-/// * The platform text input plugin sends its [TextEditingValue] to the
-///   connected [TextInputClient] via a "TextInput.setEditingState" message.
-///
-/// * When autofill happens on a disconnected [TextInputClient], the platform
-///   text input plugin sends the [TextEditingValue] to the connected
-///   [TextInputClient]'s [AutofillScope], and the [AutofillScope] will further
-///   relay the value to the correct [TextInputClient].
-///
-/// When synchronizing the [TextEditingValue]s, the communication may get stuck
-/// in an infinite when both parties are trying to send their own update. To
-/// mitigate the problem, only [TextInputClient]s are allowed to alter the
-/// received [TextEditingValue]s while platform text input plugins are to accept
-/// the received [TextEditingValue]s unmodified. More specifically:
-///
-/// * When a [TextInputClient] receives a new [TextEditingValue] from the
-///   platform text input plugin, it's allowed to modify the value (for example,
-///   apply [TextInputFormatter]s). If it decides to do so, it must send the
-///   updated [TextEditingValue] back to the platform text input plugin to keep
-///   the [TextEditingValue]s in sync.
-///
-/// * When the platform text input plugin receives a new value from the
-///   connected [TextInputClient], it must accept the new value as-is, to avoid
-///   sending back an updated value.
-///
 /// See also:
 ///
 ///  * [TextField], a widget in which the user may enter text.
-///  * [EditableText], a [TextInputClient] that connects to [TextInput] when it
-///    wants to take user input from the keyboard.
 class TextInput {
   TextInput._() {
     _channel = SystemChannels.textInput;
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index ddd12b3..97a8dd1 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -320,19 +320,6 @@
 /// movement. This widget does not provide any focus management (e.g.,
 /// tap-to-focus).
 ///
-/// ## Handling User Input
-///
-/// Currently the user may change the text this widget contains via keyboard or
-/// the text selection menu. When the user inserted or deleted text, you will be
-/// notified of the change and get a chance to modify the new text value:
-///
-/// * The [inputFormatters] will be first applied to the user input.
-///
-/// * The [controller]'s [TextEditingController.value] will be updated with the
-///   formatted result, and the [controller]'s listeners will be notified.
-///
-/// * The [onChanged] callback, if specified, will be called last.
-///
 /// ## Input Actions
 ///
 /// A [TextInputAction] can be provided to customize the appearance of the
@@ -1095,9 +1082,7 @@
   /// {@template flutter.widgets.editableText.inputFormatters}
   /// Optional input validation and formatting overrides.
   ///
-  /// Formatters are run in the provided order when the text input changes. When
-  /// this parameter changes, the new formatters will not be applied until the
-  /// next time the user inserts or deletes text.
+  /// Formatters are run in the provided order when the text input changes.
   /// {@endtemplate}
   final List<TextInputFormatter>? inputFormatters;
 
@@ -1652,66 +1637,61 @@
     _clipboardStatus?.removeListener(_onChangedClipboardStatus);
     _clipboardStatus?.dispose();
     super.dispose();
-    assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
   }
 
   // TextInputClient implementation:
 
-  /// The last known [TextEditingValue] of the platform text input plugin.
-  ///
-  /// This value is updated when the platform text input plugin sends a new
-  /// update via [updateEditingValue], or when [EditableText] calls
-  /// [TextInputConnection.setEditingState] to overwrite the platform text input
-  /// plugin's [TextEditingValue].
-  ///
-  /// Used in [_updateRemoteEditingValueIfNeeded] to determine whether the
-  /// remote value is outdated and needs updating.
-  TextEditingValue? _lastKnownRemoteTextEditingValue;
+  // _lastFormattedUnmodifiedTextEditingValue tracks the last value
+  // that the formatter ran on and is used to prevent double-formatting.
+  TextEditingValue? _lastFormattedUnmodifiedTextEditingValue;
+  // _lastFormattedValue tracks the last post-format value, so that it can be
+  // reused without rerunning the formatter when the input value is repeated.
+  TextEditingValue? _lastFormattedValue;
+  // _receivedRemoteTextEditingValue is the direct value last passed in
+  // updateEditingValue. This value does not get updated with the formatted
+  // version.
+  TextEditingValue? _receivedRemoteTextEditingValue;
 
   @override
   TextEditingValue get currentTextEditingValue => _value;
 
+  bool _updateEditingValueInProgress = false;
+
   @override
   void updateEditingValue(TextEditingValue value) {
-    // This method handles text editing state updates from the platform text
-    // input plugin. The [EditableText] may not have the focus or an open input
-    // connection, as autofill can update a disconnected [EditableText].
-
+    _updateEditingValueInProgress = true;
     // Since we still have to support keyboard select, this is the best place
     // to disable text updating.
     if (!_shouldCreateInputConnection) {
+      _updateEditingValueInProgress = false;
       return;
     }
-
     if (widget.readOnly) {
       // In the read-only case, we only care about selection changes, and reject
       // everything else.
       value = _value.copyWith(selection: value.selection);
     }
-    _lastKnownRemoteTextEditingValue = value;
+    _receivedRemoteTextEditingValue = value;
+    if (value.text != _value.text) {
+      hideToolbar();
+      _showCaretOnScreen();
+      _currentPromptRectRange = null;
+      if (widget.obscureText && value.text.length == _value.text.length + 1) {
+        _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
+        _obscureLatestCharIndex = _value.selection.baseOffset;
+      }
+    }
 
     if (value == _value) {
       // This is possible, for example, when the numeric keyboard is input,
       // the engine will notify twice for the same value.
       // Track at https://github.com/flutter/flutter/issues/65811
+      _updateEditingValueInProgress = false;
       return;
-    }
-
-    if (value.text == _value.text && value.composing == _value.composing) {
+    } else if (value.text == _value.text && value.composing == _value.composing && value.selection != _value.selection) {
       // `selection` is the only change.
       _handleSelectionChanged(value.selection, renderEditable, SelectionChangedCause.keyboard);
     } else {
-      hideToolbar();
-      _currentPromptRectRange = null;
-
-      if (_hasInputConnection) {
-        _showCaretOnScreen();
-        if (widget.obscureText && value.text.length == _value.text.length + 1) {
-          _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
-          _obscureLatestCharIndex = _value.selection.baseOffset;
-        }
-      }
-
       _formatAndSetValue(value);
     }
 
@@ -1721,6 +1701,7 @@
       _stopCursorTimer(resetCharTicks: false);
       _startCursorTimer();
     }
+    _updateEditingValueInProgress = false;
   }
 
   @override
@@ -1878,52 +1859,33 @@
     }
 
     // Invoke optional callback with the user's submitted content.
-    try {
-      widget.onSubmitted?.call(_value.text);
-    } catch (exception, stack) {
-      FlutterError.reportError(FlutterErrorDetails(
-        exception: exception,
-        stack: stack,
-        library: 'widgets',
-        context: ErrorDescription('while calling onSubmitted for $action'),
-      ));
+    if (widget.onSubmitted != null) {
+      try {
+        widget.onSubmitted!(_value.text);
+      } catch (exception, stack) {
+        FlutterError.reportError(FlutterErrorDetails(
+          exception: exception,
+          stack: stack,
+          library: 'widgets',
+          context: ErrorDescription('while calling onSubmitted for $action'),
+        ));
+      }
     }
   }
 
-  int _batchEditDepth = 0;
-
-  /// Begins a new batch edit, within which new updates made to the text editing
-  /// value will not be sent to the platform text input plugin.
-  ///
-  /// Batch edits nest. When the outmost batch edit finishes, [endBatchEdit]
-  /// will attempt to send [currentTextEditingValue] to the text input plugin if
-  /// it detected a change.
-  void beginBatchEdit() {
-    _batchEditDepth += 1;
-  }
-
-  /// Ends the current batch edit started by the last call to [beginBatchEdit],
-  /// and send [currentTextEditingValue] to the text input plugin if needed.
-  ///
-  /// Throws an error in debug mode if this [EditableText] is not in a batch
-  /// edit.
-  void endBatchEdit() {
-    _batchEditDepth -= 1;
-    assert(
-      _batchEditDepth >= 0,
-      'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.',
-    );
-    _updateRemoteEditingValueIfNeeded();
-  }
-
   void _updateRemoteEditingValueIfNeeded() {
-    if (_batchEditDepth > 0 || !_hasInputConnection)
+    if (!_hasInputConnection)
       return;
     final TextEditingValue localValue = _value;
-    if (localValue == _lastKnownRemoteTextEditingValue)
+    // We should not update back the value notified by the remote(engine) in reverse, this is redundant.
+    // Unless we modify this value for some reason during processing, such as `TextInputFormatter`.
+    if (_updateEditingValueInProgress && localValue == _receivedRemoteTextEditingValue)
       return;
+    // In other cases, as long as the value of the [widget.controller.value] is modified,
+    // `setEditingState` should be called as we do not want to skip sending real changes
+    // to the engine.
+    // Also see https://github.com/flutter/flutter/issues/65059#issuecomment-690254379
     _textInputConnection!.setEditingState(localValue);
-    _lastKnownRemoteTextEditingValue = localValue;
   }
 
   TextEditingValue get _value => widget.controller.value;
@@ -1987,7 +1949,7 @@
     return RevealedOffset(rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
   }
 
-  bool get _hasInputConnection => _textInputConnection?.attached ?? false;
+  bool get _hasInputConnection => _textInputConnection != null && _textInputConnection!.attached;
   bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false;
   bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null;
 
@@ -1997,6 +1959,7 @@
     }
     if (!_hasInputConnection) {
       final TextEditingValue localValue = _value;
+      _lastFormattedUnmodifiedTextEditingValue = localValue;
 
       // When _needsAutofill == true && currentAutofillScope == null, autofill
       // is allowed but saving the user input from the text field is
@@ -2037,7 +2000,8 @@
     if (_hasInputConnection) {
       _textInputConnection!.close();
       _textInputConnection = null;
-      _lastKnownRemoteTextEditingValue = null;
+      _lastFormattedUnmodifiedTextEditingValue = null;
+      _receivedRemoteTextEditingValue = null;
     }
   }
 
@@ -2055,7 +2019,8 @@
     if (_hasInputConnection) {
       _textInputConnection!.connectionClosedReceived();
       _textInputConnection = null;
-      _lastKnownRemoteTextEditingValue = null;
+      _lastFormattedUnmodifiedTextEditingValue = null;
+      _receivedRemoteTextEditingValue = null;
       _finalizeEditing(TextInputAction.done, shouldUnfocus: true);
     }
   }
@@ -2119,15 +2084,17 @@
       );
       _selectionOverlay!.handlesVisible = widget.showSelectionHandles;
       _selectionOverlay!.showHandles();
-      try {
-        widget.onSelectionChanged?.call(selection, cause);
-      } catch (exception, stack) {
-        FlutterError.reportError(FlutterErrorDetails(
-          exception: exception,
-          stack: stack,
-          library: 'widgets',
-          context: ErrorDescription('while calling onSelectionChanged for $cause'),
-        ));
+      if (widget.onSelectionChanged != null) {
+        try {
+          widget.onSelectionChanged!(selection, cause);
+        } catch (exception, stack) {
+          FlutterError.reportError(FlutterErrorDetails(
+            exception: exception,
+            stack: stack,
+            library: 'widgets',
+            context: ErrorDescription('while calling onSelectionChanged for $cause'),
+          ));
+        }
       }
     }
   }
@@ -2215,35 +2182,53 @@
     _lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
   }
 
-  late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
+  _WhitespaceDirectionalityFormatter? _whitespaceFormatter;
 
   void _formatAndSetValue(TextEditingValue value) {
+    _whitespaceFormatter ??= _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
+
     // Check if the new value is the same as the current local value, or is the same
     // as the pre-formatting value of the previous pass (repeat call).
-    final bool textChanged = _value.text != value.text || _value.composing != value.composing;
+    final bool textChanged = _value.text != value.text;
+    final bool isRepeat = value == _lastFormattedUnmodifiedTextEditingValue;
 
-    if (textChanged) {
+    // There's no need to format when starting to compose or when continuing
+    // an existing composition.
+    final bool isComposing = value.composing.isValid;
+    final bool isPreviouslyComposing = _lastFormattedUnmodifiedTextEditingValue?.composing.isValid ?? false;
+
+    if ((textChanged || (!isComposing && isPreviouslyComposing)) &&
+        widget.inputFormatters != null &&
+        widget.inputFormatters!.isNotEmpty) {
       // Only format when the text has changed and there are available formatters.
       // Pass through the formatter regardless of repeat status if the input value is
       // different than the stored value.
-      value = widget.inputFormatters?.fold<TextEditingValue>(
-        value,
-        (TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
-      ) ?? value;
-
+      for (final TextInputFormatter formatter in widget.inputFormatters!) {
+        value = formatter.formatEditUpdate(_value, value);
+      }
       // Always pass the text through the whitespace directionality formatter to
       // maintain expected behavior with carets on trailing whitespace.
-      value = _whitespaceFormatter.formatEditUpdate(_value, value);
+      value = _whitespaceFormatter!.formatEditUpdate(_value, value);
+      _lastFormattedValue = value;
     }
 
-    // Put all optional user callback invocations in a batch edit to prevent
-    // sending multiple `TextInput.updateEditingValue` messages.
-    beginBatchEdit();
+    if (value == _value) {
+      // If the value was modified by the formatter, the remote should be notified to keep in sync,
+      // if not modified, it will short-circuit.
+      _updateRemoteEditingValueIfNeeded();
+    } else {
+      // Setting _value here ensures the selection and composing region info is passed.
+      _value = value;
+    }
 
-    _value = value;
-    if (textChanged) {
+    // Use the last formatted value when an identical repeat pass is detected.
+    if (isRepeat && textChanged && _lastFormattedValue != null) {
+      _value = _lastFormattedValue!;
+    }
+
+    if (textChanged && widget.onChanged != null) {
       try {
-        widget.onChanged?.call(value.text);
+        widget.onChanged!(value.text);
       } catch (exception, stack) {
         FlutterError.reportError(FlutterErrorDetails(
           exception: exception,
@@ -2253,8 +2238,7 @@
         ));
       }
     }
-
-    endBatchEdit();
+    _lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
   }
 
   void _onCursorColorTick() {
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 7491fbc..01f8942 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -4736,246 +4736,6 @@
     );
   });
 
-  group('batch editing', () {
-    final TextEditingController controller = TextEditingController(text: testText);
-    final EditableText editableText = EditableText(
-      showSelectionHandles: true,
-      maxLines: 2,
-      controller: controller,
-      focusNode: FocusNode(),
-      cursorColor: Colors.red,
-      backgroundCursorColor: Colors.blue,
-      style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
-      keyboardType: TextInputType.text,
-    );
-
-    final Widget widget = MediaQuery(
-      data: const MediaQueryData(),
-      child: Directionality(
-        textDirection: TextDirection.ltr,
-        child: editableText,
-      ),
-    );
-
-    testWidgets('batch editing works', (WidgetTester tester) async {
-      await tester.pumpWidget(widget);
-
-      // Connect.
-      await tester.showKeyboard(find.byType(EditableText));
-
-      final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
-      state.updateEditingValue(const TextEditingValue(text: 'remote value'));
-      tester.testTextInput.log.clear();
-
-      state.beginBatchEdit();
-
-      controller.text = 'new change 1';
-      expect(state.currentTextEditingValue.text, 'new change 1');
-      expect(tester.testTextInput.log, isEmpty);
-
-      // Nesting.
-      state.beginBatchEdit();
-      controller.text = 'new change 2';
-      expect(state.currentTextEditingValue.text, 'new change 2');
-      expect(tester.testTextInput.log, isEmpty);
-
-      // End the innermost batch edit. Not yet.
-      state.endBatchEdit();
-      expect(tester.testTextInput.log, isEmpty);
-
-      controller.text = 'new change 3';
-      expect(state.currentTextEditingValue.text, 'new change 3');
-      expect(tester.testTextInput.log, isEmpty);
-
-      // Finish the outermost batch edit.
-      state.endBatchEdit();
-      expect(tester.testTextInput.log, hasLength(1));
-      expect(
-        tester.testTextInput.log,
-        contains(matchesMethodCall('TextInput.setEditingState', args: containsPair('text', 'new change 3'))),
-      );
-    });
-
-    testWidgets('batch edits need to be nested properly', (WidgetTester tester) async {
-      await tester.pumpWidget(widget);
-
-      // Connect.
-      await tester.showKeyboard(find.byType(EditableText));
-
-      final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
-      state.updateEditingValue(const TextEditingValue(text: 'remote value'));
-      tester.testTextInput.log.clear();
-
-      String errorString;
-      try {
-        state.endBatchEdit();
-      } catch (e) {
-        errorString = e.toString();
-      }
-
-      expect(errorString, contains('Unbalanced call to endBatchEdit'));
-    });
-
-     testWidgets('catch unfinished batch edits on disposal', (WidgetTester tester) async {
-      await tester.pumpWidget(widget);
-
-      // Connect.
-      await tester.showKeyboard(find.byType(EditableText));
-
-      final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
-      state.updateEditingValue(const TextEditingValue(text: 'remote value'));
-      tester.testTextInput.log.clear();
-
-      state.beginBatchEdit();
-      expect(tester.takeException(), isNull);
-
-      await tester.pumpWidget(Container());
-      expect(tester.takeException(), isNotNull);
-    });
-  });
-
-  group('EditableText does not send editing values more than once', () {
-    final TextEditingController controller = TextEditingController(text: testText);
-    final EditableText editableText = EditableText(
-      showSelectionHandles: true,
-      maxLines: 2,
-      controller: controller,
-      focusNode: FocusNode(),
-      cursorColor: Colors.red,
-      backgroundCursorColor: Colors.blue,
-      style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
-      keyboardType: TextInputType.text,
-      inputFormatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(6)],
-      onChanged: (String s) => controller.text += ' onChanged',
-    );
-
-    final Widget widget = MediaQuery(
-      data: const MediaQueryData(),
-      child: Directionality(
-        textDirection: TextDirection.ltr,
-        child: editableText,
-      ),
-    );
-
-    controller.addListener(() {
-      if (!controller.text.endsWith('listener'))
-        controller.text += ' listener';
-    });
-
-    testWidgets('input from text input plugin', (WidgetTester tester) async {
-      await tester.pumpWidget(widget);
-
-      // Connect.
-      await tester.showKeyboard(find.byType(EditableText));
-      tester.testTextInput.log.clear();
-
-      final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
-      state.updateEditingValue(const TextEditingValue(text: 'remoteremoteremote'));
-
-      // Apply in order: length formatter -> listener -> onChanged -> listener.
-      expect(controller.text, 'remote listener onChanged listener');
-      final List<TextEditingValue> updates = tester.testTextInput.log
-        .where((MethodCall call) => call.method == 'TextInput.setEditingState')
-        .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
-        .toList(growable: false);
-
-      expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remote listener onChanged listener')]);
-
-      tester.testTextInput.log.clear();
-
-      // If by coincidence the text input plugin sends the same value back,
-      // do nothing.
-      state.updateEditingValue(const TextEditingValue(text: 'remote listener onChanged listener'));
-      expect(controller.text, 'remote listener onChanged listener');
-      expect(tester.testTextInput.log, isEmpty);
-    });
-
-    testWidgets('input from text selection menu', (WidgetTester tester) async {
-      await tester.pumpWidget(widget);
-
-      // Connect.
-      await tester.showKeyboard(find.byType(EditableText));
-      tester.testTextInput.log.clear();
-
-      final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
-      state.textEditingValue = const TextEditingValue(text: 'remoteremoteremote');
-
-      // Apply in order: length formatter -> listener -> onChanged -> listener.
-      expect(controller.text, 'remote listener onChanged listener');
-      final List<TextEditingValue> updates = tester.testTextInput.log
-        .where((MethodCall call) => call.method == 'TextInput.setEditingState')
-        .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
-        .toList(growable: false);
-
-      expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remote listener onChanged listener')]);
-
-      tester.testTextInput.log.clear();
-    });
-
-    testWidgets('input from controller', (WidgetTester tester) async {
-      await tester.pumpWidget(widget);
-
-      // Connect.
-      await tester.showKeyboard(find.byType(EditableText));
-      tester.testTextInput.log.clear();
-
-      controller.text = 'remoteremoteremote';
-      final List<TextEditingValue> updates = tester.testTextInput.log
-        .where((MethodCall call) => call.method == 'TextInput.setEditingState')
-        .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
-        .toList(growable: false);
-
-      expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remoteremoteremote listener')]);
-    });
-
-    testWidgets('input from changing controller', (WidgetTester tester) async {
-      final TextEditingController controller = TextEditingController(text: testText);
-      Widget build({ TextEditingController textEditingController }) {
-        return MediaQuery(
-          data: const MediaQueryData(),
-          child: Directionality(
-            textDirection: TextDirection.ltr,
-            child: EditableText(
-              showSelectionHandles: true,
-              maxLines: 2,
-              controller: textEditingController ?? controller,
-              focusNode: FocusNode(),
-              cursorColor: Colors.red,
-              backgroundCursorColor: Colors.blue,
-              style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
-              keyboardType: TextInputType.text,
-              inputFormatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(6)],
-            ),
-          ),
-        );
-      }
-
-      await tester.pumpWidget(build());
-
-      // Connect.
-      await tester.showKeyboard(find.byType(EditableText));
-      tester.testTextInput.log.clear();
-      await tester.pumpWidget(build(textEditingController: TextEditingController(text: 'new text')));
-
-      List<TextEditingValue> updates = tester.testTextInput.log
-        .where((MethodCall call) => call.method == 'TextInput.setEditingState')
-        .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
-        .toList(growable: false);
-
-      expect(updates, const <TextEditingValue>[TextEditingValue(text: 'new text')]);
-
-      tester.testTextInput.log.clear();
-      await tester.pumpWidget(build(textEditingController: TextEditingController(text: 'new new text')));
-
-      updates = tester.testTextInput.log
-        .where((MethodCall call) => call.method == 'TextInput.setEditingState')
-        .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
-        .toList(growable: false);
-
-      expect(updates, const <TextEditingValue>[TextEditingValue(text: 'new new text')]);
-    });
-  });
-
   testWidgets('input imm channel calls are ordered correctly', (WidgetTester tester) async {
     const String testText = 'flutter is the best!';
     final TextEditingController controller = TextEditingController(text: testText);
@@ -5475,12 +5235,12 @@
     expect(formatter.formatCallCount, 3);
     state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2))); // No text change, does not format
     expect(formatter.formatCallCount, 3);
-    state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 1, end: 2))); // Composing change triggers reformat
-    expect(formatter.formatCallCount, 4);
+    state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 1, end: 2))); // Composing change does not reformat
+    expect(formatter.formatCallCount, 3);
     expect(formatter.lastOldValue.composing, const TextRange(start: -1, end: -1));
-    expect(formatter.lastNewValue.composing, const TextRange(start: 1, end: 2)); // The new composing was registered in formatter.
+    expect(formatter.lastNewValue.composing, const TextRange(start: -1, end: -1)); // Since did not format, the new composing was not registered in formatter.
     state.updateEditingValue(const TextEditingValue(text: '01234', selection: TextSelection.collapsed(offset: 2))); // Formats, with oldValue containing composing region.
-    expect(formatter.formatCallCount, 5);
+    expect(formatter.formatCallCount, 4);
     expect(formatter.lastOldValue.composing, const TextRange(start: 1, end: 2));
     expect(formatter.lastNewValue.composing, const TextRange(start: -1, end: -1));
 
@@ -5491,10 +5251,8 @@
       '[2]: normal aaaa',
       '[3]: 012, 0123',
       '[3]: normal aaaaaa',
-      '[4]: 0123, 0123',
-      '[4]: normal aaaaaaaa',
-      '[5]: 0123, 01234',
-      '[5]: normal aaaaaaaaaa',
+      '[4]: 0123, 01234',
+      '[4]: normal aaaaaaaa'
     ];
 
     expect(formatter.log, referenceLog);