Repository: flutter
Issue: [iOS] Unexpected delta deletion range with a Korean keyboard when the IME replaces a character
Labels: a: text input,platform-ios,framework,engine,a: internationalization,has reproducible steps,P2,found in release: 3.16,found in release: 3.19,team-text-input,triaged-text-input

### Steps to reproduce

1. Run the sample code on a physical iOS device
2. Using a Korean keyboard type "ㅅㅛ"
3. Check the logs

### Expected results

The following deletion delta should be generated:

```dart
TextEditingDeltaDeletion(
  oldText: '. ㅅ',
  deletedRange: TextRange(start: 2, end: 3),
  selection: TextSelection.collapsed(
    offset: 2,
    affinity: TextAffinity.downstream,
  ),
  composing: TextRange(start: -1, end: -1),
)
```

### Actual results

The following deletion delta is reported (the deleted range start at offset 1 instead of 2):

```dart
TextEditingDeltaDeletion(
  oldText: '. ㅅ',
  deletedRange: TextRange(start: 1, end: 3),
  selection: TextSelection.collapsed(
    offset: 2,
    affinity: TextAffinity.downstream,
  ),
  composing: TextRange(start: -1, end: -1),
)
```

Background: In the super_editor package, we always send the text editing value to the IME prepended with the string `". "`. We do this to detect the user pressing the delete button at the beginning of the text, because pressing the delete button at the beginning of the editing value doesn't report anything. The `". "` is invisible to the user and when we receive a deletion of the space after the `.`, we consider that the user is pressing the delete button at the beginning of the text. This is used to merge the currently selected paragraph with the previous paragraph. The reported deltas are deleting the invisible characters and then generating a space insertion at an invalid position.


Here's all the deltas reported by Flutter:

First batch:

```dart
[
  TextEditingDeltaInsertion(
    oldText: '. ',
    textInserted: 'ㅅ',
    insertionOffset: 2,
    selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream),
    composing: TextRange(start: -1, end: -1),
  ),
]
```

Second batch:

```dart
[
  TextEditingDeltaNonTextUpdate(
    oldText: '. ㅅ',
    selection: TextSelection(baseOffset: 1, extentOffset: 3, isDirectional: false),
    composing: TextRange(start: -1, end: -1),
  ),
  TextEditingDeltaDeletion(
    oldText: '. ㅅ',
    deletedRange: TextRange(start: 1, end: 3),
    selection: TextSelection.collapsed(
      offset: 1,
      affinity: TextAffinity.downstream,
    ),
    composing: TextRange(start: -1, end: -1),
  ),
  TextEditingDeltaInsertion(
    oldText: '.',
    textInserted: ' ',
    insertionOffset: 1,
    selection: TextSelection.collapsed(
      offset: 2,
      affinity: TextAffinity.downstream,
    ),
    composing: TextRange(start: -1, end: -1),
  ),
  TextEditingDeltaInsertion(
    oldText: '. ',
    textInserted: '쇼',
    insertionOffset: 2,
    selection: TextSelection.collapsed(
      offset: 3,
      affinity: TextAffinity.downstream,
    ),
    composing: TextRange(start: -1, end: -1),
  )
]
```

### Code sample

<details><summary>Code sample</summary>

```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: TextInputExample(),
      ),
    ),
  );
}

class TextInputExample extends StatefulWidget {
  const TextInputExample({super.key});

  @override
  State<TextInputExample> createState() => _TextInputExampleState();
}

class _TextInputExampleState extends State<TextInputExample> implements DeltaTextInputClient {
  final FocusNode _focusNode = FocusNode();
  final GlobalKey _textKey = GlobalKey();

  TextInputConnection? _inputConnection;
  TextEditingValue _currentEditingValue = const TextEditingValue(
    text: '. ',
    selection: TextSelection.collapsed(offset: 2),
  );

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _attach();
    });
  }

  @override
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }

  @override
  void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
    print('Received ${textEditingDeltas.length} deltas');
    TextEditingValue newEditingValue = _currentEditingValue;

    for (final delta in textEditingDeltas) {
      print(delta);
      newEditingValue = delta.apply(newEditingValue);
    }

    setState(() {
      _currentEditingValue = newEditingValue;
    });
  }

  void _attach() {
    if (_inputConnection != null) {
      return;
    }
    _inputConnection = TextInput.attach(
      this,
      const TextInputConfiguration(
        enableDeltaModel: true,
        inputType: TextInputType.multiline,
        textCapitalization: TextCapitalization.sentences,
        inputAction: TextInputAction.newline,
        keyboardAppearance: Brightness.light,
      ),
    );
    _inputConnection!.show();
    _inputConnection!.setEditingState(_currentEditingValue);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Focus(
        focusNode: _focusNode,
        autofocus: true,
        child: DecoratedBox(
          decoration: BoxDecoration(border: Border.all()),
          child: Text(
            key: _textKey,
            _currentEditingValue.text,
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }

  @override
  void connectionClosed() {}

  @override
  AutofillScope? get currentAutofillScope => null;

  @override
  TextEditingValue? get currentTextEditingValue => null;

  @override
  void insertTextPlaceholder(Size size) {}

  @override
  void performAction(TextInputAction action) {}

  @override
  void performPrivateCommand(String action, Map<String, dynamic> data) {}

  @override
  void removeTextPlaceholder() {}

  @override
  void showAutocorrectionPromptRect(int start, int end) {}

  @override
  void showToolbar() {}

  @override
  void updateEditingValue(TextEditingValue value) {}

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {}

  @override
  void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}

  @override
  void insertContent(KeyboardInsertedContent content) {}

  @override
  void performSelector(String selectorName) {}
}

```

</details>


### Screenshots or Video

_No response_

### Logs

<details open><summary>Logs</summary>

```console
Connecting to VM Service at ws://127.0.0.1:61816/Kn70Oolwy_4=/ws
flutter: Received 1 deltas
flutter: TextEditingDeltaInsertion#1eb9d(oldText: ., textInserted: ㅅ, insertionOffset: 2, selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Received 4 deltas
flutter: TextEditingDeltaNonTextUpdate#a30b9(oldText: . ㅅ, selection: TextSelection(baseOffset: 1, extentOffset: 3, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: TextEditingDeltaDeletion#6ae2a(oldText: . ㅅ, textDeleted:  ㅅ, deletedRange: TextRange(start: 1, end: 3), selection: TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: TextEditingDeltaInsertion#7edeb(oldText: ., textInserted:, insertionOffset: 1, selection: TextSelection.collapsed(offset: 2, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: TextEditingDeltaInsertion#6d651(oldText: ., textInserted: 쇼, insertionOffset: 2, selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
Application finished.

Exited.
```

</details>


### Flutter Doctor output

<details><summary>Doctor output</summary>

```console
[✓] Flutter (Channel master, 3.19.0-15.0.pre.27, on macOS 14.2.1
    23C71 darwin-arm64, locale pt-BR)
    • Flutter version 3.19.0-15.0.pre.27 on channel master at
      /Users/angelosilvestre/dev/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision e1e1c36d49 (24 hours ago), 2024-01-31
      14:43:03 -0800
    • Engine revision 5b89189b8b
    • Dart version 3.4.0 (build 3.4.0-96.0.dev)
    • DevTools version 2.31.0

[✓] Android toolchain - develop for Android devices (Android SDK
    version 34.0.0)
    • Android SDK at /Users/angelosilvestre/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Applications/Android
      Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build
      17.0.6+0-17.0.6b829.9-10027231)
    • All Android licenses accepted.

[!] Xcode - develop for iOS and macOS (Xcode 15.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15C500b
    ! CocoaPods 1.12.1 out of date (1.13.0 is recommended).
        CocoaPods is used to retrieve the iOS and macOS platform
        side's plugin code that responds to your plugin usage on the
        Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To upgrade see
      https://guides.cocoapods.org/using/getting-started.html#updati
      ng-cocoapods for instructions.

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google
      Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build
      17.0.6+0-17.0.6b829.9-10027231)

[✓] VS Code (version 1.86.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.80.0

[✓] Connected device (3 available)
    • macOS (desktop)                 • macos                 •
      darwin-arm64   • macOS 14.2.1 23C71 darwin-arm64
    • Mac Designed for iPad (desktop) • mac-designed-for-ipad •
      darwin         • macOS 14.2.1 23C71 darwin-arm64
    • Chrome (web)                    • chrome                •
      web-javascript • Google Chrome 121.0.6167.85
    ! Error: Browsing on the local area network for iPhone de
      Patricia. Ensure the device is unlocked and attached with a
      cable or associated with the same local area network as this
      Mac.
      The device must be opted into Developer Mode to connect
      wirelessly. (code -27)

[✓] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.
```

</details>