Skip to content

[iOS] Typing two dashes generate unexpected deltas #129061

@angelosilvestre

Description

@angelosilvestre

Is there an existing issue for this?

Steps to reproduce

  1. Run the sample code on iOS.
  2. Press "Attach to IME"
  3. Type "-"
  4. Type another "-"

Expected results

Receive a replacement delta, or both deltas in a single call to updateEditingValueWithDeltas.

Actual results

A call to updateEditingValueWithDeltas with a deletion delta followed by a call to updateEditingValueWithDeltas with an insertion delta.

Code sample

Code sample
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({Key? key}) : super(key: key);

  @override
  State<TextInputExample> createState() => _TextInputExampleState();
}

class _TextInputExampleState extends State<TextInputExample> implements DeltaTextInputClient {
  final emptyParagraphPlaceholder = '. ';
  final firstParagraphText = '';

  TextInputConnection? _inputConnection;
  late TextEditingValue _currentEditingValue;

  @override
  void initState() {
    super.initState();
    _currentEditingValue = _placeHolderEditingValue;
  }

  @override
  void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
    print('Received ${textEditingDeltas.length} deltas');
    TextEditingValue newEditingValue = _currentEditingValue;

    for (final delta in textEditingDeltas) {
      print('IME: received delta $delta');
      if (delta.oldText != _currentEditingValue.text) {
        print('Out of sync with IME');
        print('OS text - "${delta.oldText}"');
        print('OS selection - ${delta.selection}');
        print('OS composing - ${delta.composing}');

        print('App text - "${_currentEditingValue.text}"');
        print('App selection - ${_currentEditingValue.selection}');
        print('App composing - ${_currentEditingValue.composing}');
      }
      if (delta is TextEditingDeltaInsertion) {
        newEditingValue = _applyInsertionDelta(newEditingValue, delta);
      } else if (delta is TextEditingDeltaDeletion) {
        newEditingValue = _applyDeletionDelta(newEditingValue, delta);
      } else if (delta is TextEditingDeltaNonTextUpdate) {
        newEditingValue = _applyNonTextDelta(newEditingValue, delta);
      } else {
        newEditingValue = delta.apply(newEditingValue);
      }
      if (newEditingValue.text.isEmpty) {
        newEditingValue = _placeHolderEditingValue;
      }
    }

    _syncWithOS(newEditingValue);
    setState(() {
      _currentEditingValue = newEditingValue;
    });
  }

  void _syncWithOS(TextEditingValue textEditingValue) async {
    _inputConnection?.setEditingState(textEditingValue);
  }

  void _run() {
    _detach();
    _attach();

    _syncWithOS(_currentEditingValue);
  }

  void _attach() {
    if (_inputConnection != null) {
      return;
    }
    _inputConnection = TextInput.attach(
      this,
      const TextInputConfiguration(
        enableDeltaModel: true,
        inputType: TextInputType.multiline,
        inputAction: TextInputAction.newline,
      ),
    );
    _inputConnection!.show();
  }

  void _detach() {
    _inputConnection?.close();
    _inputConnection = null;
  }

  /// Placehold which the editor sends to the framework to represent an empty paragraph.
  TextEditingValue get _placeHolderEditingValue => TextEditingValue(
        text: emptyParagraphPlaceholder,
        selection: const TextSelection.collapsed(offset: 2),
        composing: const TextRange(start: -1, end: -1),
      );

  TextEditingValue _applyInsertionDelta(TextEditingValue current, TextEditingDeltaInsertion delta) {
    TextEditingValue newEditingValue = current;
    if (delta.oldText == emptyParagraphPlaceholder) {
      // The text contains only the placeholder, so we are in an empty paragraph.
      // The inserted text should be the new text.
      newEditingValue = TextEditingValue(
        text: delta.textInserted,
        composing: const TextRange(start: -1, end: -1),
        selection: const TextSelection.collapsed(offset: 1),
      );
    } else {
      newEditingValue = delta.apply(current);
    }

    return newEditingValue;
  }

  TextEditingValue _applyDeletionDelta(TextEditingValue current, TextEditingDeltaDeletion delta) {
    return delta.apply(current);
  }

  TextEditingValue _applyNonTextDelta(TextEditingValue current, TextEditingDeltaNonTextUpdate delta) {
    print("IME: Non-text change:");
    print("IME: OS-side text      - ${delta.oldText}");
    print("IME: OS-side selection - ${delta.selection}");
    print("IME: OS-side composing - ${delta.composing}");
    return current.copyWith(composing: delta.composing);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            _currentEditingValue.toString(),
            textAlign: TextAlign.center,
          ),
          ElevatedButton(
            onPressed: _run,
            child: const Text('Attach to IME'),
          ),
        ],
      ),
    );
  }

  @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) {
    // TODO: implement didChangeInputControl
  }

  @override
  void insertContent(KeyboardInsertedContent content) {
    // TODO: implement insertContent
  }

  @override
  void performSelector(String selectorName) {
    // TODO: implement performSelector
  }
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
Launching lib/main.dart on iPhone Xs Max in debug mode...
Xcode build done.                                            7,9s
Connecting to VM Service at ws://127.0.0.1:49869/vbH1MpsAuWA=/ws
flutter: Received 1 deltas
flutter: IME: received delta TextEditingDeltaInsertion#a006d(oldText: ., textInserted: -, insertionOffset: 2, selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Received 1 deltas
flutter: IME: received delta TextEditingDeltaNonTextUpdate#93e41(oldText: -, selection: TextSelection(baseOffset: 0, extentOffset: 1, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: IME: Non-text change:
flutter: IME: OS-side text      - -
flutter: IME: OS-side selection - TextSelection(baseOffset: 0, extentOffset: 1, isDirectional: false)
flutter: IME: OS-side composing - TextRange(start: -1, end: -1)
flutter: Received 1 deltas
flutter: IME: received delta TextEditingDeltaDeletion#17112(oldText: -, textDeleted: -, deletedRange: TextRange(start: 0, end: 1), selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Received 1 deltas
flutter: IME: received delta TextEditingDeltaInsertion#c5e9d(oldText, textInserted: —, insertionOffset: 0, selection: TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Out of sync with IME
flutter: OS text - ""
flutter: OS selection - TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false)
flutter: OS composing - TextRange(start: -1, end: -1)
flutter: App text - ". "
flutter: App selection - TextSelection.collapsed(offset: 2, affinity: TextAffinity.downstream, isDirectional: false)
flutter: App composing - TextRange(start: -1, end: -1)

Flutter Doctor output

Doctor output
[✓] Flutter (Channel master, 3.12.0-4.0.pre.110, on macOS 12.6.5 21G531 darwin-x64, locale pt-BR)
    • Flutter version 3.12.0-4.0.pre.110 on channel master at /Users/user223563/Downloads/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision c29b570678 (12 hours ago), 2023-06-16 20:07:25 -0400
    • Engine revision 7ffa1355f7
    • Dart version 3.1.0 (build 3.1.0-213.0.dev)
    • DevTools version 2.24.0

[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.dev/docs/get-started/install/macos#android-setup for detailed instructions).
      If the Android SDK has been installed to a custom location, please use
      `flutter config --android-sdk` to update to that location.


[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14C18
    • CocoaPods version 1.12.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.1)
    • 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 11.0.15+0-b2043.56-8887301)

[✓] IntelliJ IDEA Community Edition (version 2020.3.3)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • 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

[✓] VS Code (version 1.75.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.66.0

[✓] Connected device (3 available)
    • iPhone Xs Max (mobile) • 8983A467-63BE-408C-A44C-8D898C7A8471 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-12-4 (simulator)
    • macOS (desktop)        • macos                                • darwin-x64     • macOS 12.6.5 21G531 darwin-x64
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome 114.0.5735.133

[✓] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.

Metadata

Metadata

Labels

P1High-priority issues at the top of the work lista: text inputEntering text in a text field or keyboard related problemsengineflutter/engine related. See also e: labels.found in release: 3.10Found to occur in 3.10found in release: 3.12Found to occur in 3.12frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-iosiOS applications specificallyr: fixedIssue is closed as already fixed in a newer versionteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions