Skip to content

[web] Assertion failure when inserting new lines #131023

@angelosilvestre

Description

@angelosilvestre

Is there an existing issue for this?

Steps to reproduce

  1. Clone repository https://github.com/angelosilvestre/super_editor/tree/1219_newline_insertion (branch 1219_newline_insertion)
  2. Run the example app on web
  3. Place the caret at the first paragraph
  4. Press enter
  5. Type "a"
  6. Press enter again

Expected results

New line is inserted

Actual results

A crash happens.

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    super.key,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with DeltaTextInputClient {
  TextInputConnection? _imeConnection;

  final String placeholder = '. ';

  TextEditingValue _currentTextEditingValue = const TextEditingValue(
    text: 'Press ENTER, then type "a" and press ENTER again',
    selection: TextSelection.collapsed(offset: 48),
  );

  @override
  void initState() {
    super.initState();
    _attachToIme();
  }

  @override
  void dispose() {
    _detachFromIme();
    super.dispose();
  }

  @override
  void performAction(TextInputAction action) {
    if (action == TextInputAction.newline) {
      // Here, we would create a new empty paragraph.
      // Use a placeholder so we can detect deletions.
      final newValue = TextEditingValue(
        text: placeholder,
        selection: const TextSelection.collapsed(offset: 2),
      );

      _imeConnection?.setEditingState(newValue);

      setState(() {
        _currentTextEditingValue = newValue;
      });
    }
  }

  @override
  void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
    // What we think the current value is.
    TextEditingValue newValue = _currentTextEditingValue.copyWith();

    // What the IME thinks the current value is.
    TextEditingValue imeEditingValue = _currentTextEditingValue.copyWith();

    for (final delta in textEditingDeltas) {
      print('Applying delta: $delta');

      // Compute the value that the IME thinks it's the current state.
      imeEditingValue = delta.apply(imeEditingValue);

      if (delta is TextEditingDeltaInsertion) {
        newValue = _applyInsertion(newValue, delta);
      } else {
        newValue = delta.apply(newValue);
      }
    }

    print('New editing value: $newValue');

    if (imeEditingValue != newValue) {
      // We diverged from the IME.
      //
      // Send our value back to the IME.
      _imeConnection!.setEditingState(newValue);
    }

    setState(() {
      _currentTextEditingValue = newValue;
    });
  }

  TextEditingValue _applyInsertion(TextEditingValue currentValue, TextEditingDeltaInsertion delta) {
    if (delta.textInserted == '\n') {
      // The user pressed ENTER.
      //
      // On web, both performAction and updateEditingValueWithDeltas are called.
      //
      // As we handle the new line in performAction we ignore it here.
      return currentValue;
    }

    if (delta.oldText == placeholder) {
      // The user typed in an empty paragraph.
      // The newly inserted text replaces the placeholder.
      return TextEditingValue(
        text: delta.textInserted,
        selection: const TextSelection.collapsed(offset: 1),
      );
    }

    return delta.apply(currentValue);
  }

  void _attachToIme() {
    if (_imeConnection != null && _imeConnection!.attached) {
      return;
    }

    const config = TextInputConfiguration(
      enableDeltaModel: true,
      inputType: TextInputType.multiline,
      inputAction: TextInputAction.newline,
    );
    _imeConnection = TextInput.attach(this, config);
    _imeConnection!.setEditingState(_currentTextEditingValue);
    _imeConnection!.show();
  }

  void _detachFromIme() {
    _imeConnection?.close();
    _imeConnection = null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Focus(
        autofocus: true,
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('Text: ${_currentTextEditingValue.text}'),
              const SizedBox(height: 30),
              Text('Selection: ${_currentTextEditingValue.selection}'),
              const SizedBox(height: 30),
              Text('Selected text: ${_currentTextEditingValue.selection.textInside(_currentTextEditingValue.text)}'),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void connectionClosed() {}

  @override
  AutofillScope? get currentAutofillScope => null;

  @override
  TextEditingValue? get currentTextEditingValue => _currentTextEditingValue;

  @override
  void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}

  @override
  void insertContent(KeyboardInsertedContent content) {}

  @override
  void insertTextPlaceholder(Size size) {}

  @override
  void performPrivateCommand(String action, Map<String, dynamic> data) {}

  @override
  void performSelector(String selectorName) {}

  @override
  void removeTextPlaceholder() {}

  @override
  void showAutocorrectionPromptRect(int start, int end) {}

  @override
  void showToolbar() {}

  @override
  void updateEditingValue(TextEditingValue value) {}

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {}
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
Launching lib/main.dart on Chrome in debug mode...
This app is linked to the debug service: ws://127.0.0.1:38203/GHhd3P4elAU=/ws
Debug service listening on ws://127.0.0.1:38203/GHhd3P4elAU=/ws
Connecting to VM Service at ws://127.0.0.1:38203/GHhd3P4elAU=/ws
The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from `dart:ui_web` instead.
Initializing logger: ExampleApp

>received TextInput.setClient: {inputType: {name: TextInputType.multiline, signed: null, decimal: null}, readOnly: false, obscureText: false, autocorrect: true, smartDashesType: 1, smartQuotesType: 1, enableSuggestions: true, enableInteractiveSelection: true, actionLabel: null, inputAction: TextInputAction.newline, textCapitalization: TextCapitalization.sentences, keyboardAppearance: Brightness.light, enableIMEPersonalizedLearning: true, contentCommitMimeTypes: [], enableDeltaModel: true}

>received TextInput.setEditingState: {text: Original text, selectionBase: 13, selectionExtent: 13, composingBase: -1, composingExtent: -1}

>handleChange called
type: selectionchange
newEditingState: {text: Original text, selectionBase: 13, selectionExtent: 13, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState("Original text", base:13, extent:13, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState("Original text", base:13, extent:13, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: true
infered deltaState: {deltas: [{oldText: Original text, deltaText: , deltaStart: -1, deltaEnd: -1, selectionBase: 13, selectionExtent: 13, composingBase: -1, composingExtent: -1}]}

>handleChange called
type: selectionchange
newEditingState: {text: Original text, selectionBase: 13, selectionExtent: 13, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState("Original text", base:13, extent:13, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState("Original text", base:13, extent:13, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: true
infered deltaState: {deltas: [{oldText: Original text, deltaText: , deltaStart: -1, deltaEnd: -1, selectionBase: 13, selectionExtent: 13, composingBase: -1, composingExtent: -1}]}

performAction called: TextInputAction.newline

>received TextInput.setEditingState: {text: >>, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}

>handleChange called
type: input
newEditingState: {text: >>
, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>
", base:3, extent:3, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: false
replacing "Original text" with "
"
originalText: Original text
replacementText:

replacedRange: TextRange(start: 2, end: 2)
isDeltaVerified: false
isMatchWithinOldTextBounds: true
replacing "Original text" with "
"
originalText: Original text
replacementText:

replacedRange: TextRange(start: 2, end: 3)
infered deltaState: {deltas: [{oldText: Original text, deltaText:
, deltaStart: 2, deltaEnd: 2, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}]}

>updateEditingStateWithDeltas called
delta: {deltas: [{oldText: Original text, deltaText:
, deltaStart: 2, deltaEnd: 2, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}]}

>received TextInput.setEditingState: {text: >>, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}

>handleChange called
type: selectionchange
newEditingState: {text: >>, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: true
infered deltaState: {deltas: [{oldText: >>, deltaText: , deltaStart: -1, deltaEnd: -1, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}]}

>handleChange called
type: selectionchange
newEditingState: {text: >>, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: true
infered deltaState: {deltas: [{oldText: >>, deltaText: , deltaStart: -1, deltaEnd: -1, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}]}

>handleChange called
type: selectionchange
newEditingState: {text: >>, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: true
infered deltaState: {deltas: [{oldText: >>, deltaText: , deltaStart: -1, deltaEnd: -1, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}]}

>handleChange called
type: input
newEditingState: {text: >>a, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>a", base:3, extent:3, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: false
replacing ">>" with "a"
originalText: >>
replacementText: a
replacedRange: TextRange(start: 2, end: 2)
isDeltaVerified: true
infered deltaState: {deltas: [{oldText: >>, deltaText: a, deltaStart: 2, deltaEnd: 2, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}]}

>updateEditingStateWithDeltas called
delta: {deltas: [{oldText: >>, deltaText: a, deltaStart: 2, deltaEnd: 2, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}]}

>received TextInput.setEditingState: {text: a, selectionBase: 1, selectionExtent: 1, composingBase: -1, composingExtent: -1}

>handleChange called
type: selectionchange
newEditingState: {text: a, selectionBase: 1, selectionExtent: 1, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState("a", base:1, extent:1, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState("a", base:1, extent:1, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: true
infered deltaState: {deltas: [{oldText: a, deltaText: , deltaStart: -1, deltaEnd: -1, selectionBase: 1, selectionExtent: 1, composingBase: -1, composingExtent: -1}]}

>handleChange called
type: selectionchange
newEditingState: {text: a, selectionBase: 1, selectionExtent: 1, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState("a", base:1, extent:1, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState("a", base:1, extent:1, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: true
infered deltaState: {deltas: [{oldText: a, deltaText: , deltaStart: -1, deltaEnd: -1, selectionBase: 1, selectionExtent: 1, composingBase: -1, composingExtent: -1}]}

performAction called: TextInputAction.newline

>received TextInput.setEditingState: {text: >>, selectionBase: 2, selectionExtent: 2, composingBase: -1, composingExtent: -1}

>handleChange called
type: input
newEditingState: {text: >>
, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>
", base:3, extent:3, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: false
replacing "a" with "
"
originalText: a
replacementText:

replacedRange: TextRange(start: 2, end: 2)

>handleChange called
type: selectionchange
newEditingState: {text: >>
, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>
", base:3, extent:3, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: false
replacing "a" with "
"
originalText: a
replacementText:

replacedRange: TextRange(start: 2, end: 2)
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:470:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 470:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 583:11                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1373:33                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall

>handleChange called
type: selectionchange
newEditingState: {text: >>
, selectionBase: 3, selectionExtent: 3, composingBase: -1, composingExtent: -1}
infering delta state
lastEditingState: EditingState(">>", base:2, extent:2, composingBase:-1, composingExtent:-1)}
newEditingState: EditingState(">>
", base:3, extent:3, composingBase:-1, composingExtent:-1)}
isTextBeingRemoved: false
isDeltaRangeEmpty: false
replacing "a" with "
"
originalText: a
replacementText:

replacedRange: TextRange(start: 2, end: 2)
Error: Assertion failed: org-dartlang-sdk:///lib/_engine/engine/text_editing/text_editing.dart:470:10
replacedRange.start <= originalText.length && replacedRange.end <= originalText.length
is not true
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 35:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 470:89                          _replace
lib/_engine/engine/text_editing/text_editing.dart 583:11                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1373:33                         handleChange
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 574:37  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 579:39  dcall

Flutter Doctor output

Doctor output
[✓] Flutter (Channel master, 3.13.0-6.0.pre.24, on Ubuntu 22.04.2 LTS 5.15.90.1-microsoft-standard-WSL2,
    locale C.UTF-8)
    • Flutter version 3.13.0-6.0.pre.24 on channel master at /home/wsl/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision e29f2b98af (2 days ago), 2023-07-19 00:10:17 +0200
    • Engine revision 71bbecee30
    • Dart version 3.1.0 (build 3.1.0-323.0.dev)
    • DevTools version 2.25.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/linux#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.


[✓] Chrome - develop for the web
    • Chrome at google-chrome

[✗] Linux toolchain - develop for Linux desktop
    ✗ clang++ is required for Linux development.
      It is likely available from your distribution (e.g.: apt install clang), or can be downloaded from
      https://releases.llvm.org/
    ✗ CMake is required for Linux development.
      It is likely available from your distribution (e.g.: apt install cmake), or can be downloaded from
      https://cmake.org/download/
    ✗ ninja is required for Linux development.
      It is likely available from your distribution (e.g.: apt install ninja-build), or can be downloaded
      from https://github.com/ninja-build/ninja/releases
    • pkg-config version 0.29.2
    ✗ GTK 3.0 development libraries are required for Linux development.
      They are likely available from your distribution (e.g.: apt install libgtk-3-dev)

[!] Android Studio (not installed)
    • Android Studio not found; download from https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/docs/get-started/install/linux#android-setup for detailed instructions).

[✓] Connected device (2 available)
    • Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.2 LTS 5.15.90.1-microsoft-standard-WSL2
    • Chrome (web)    • chrome • web-javascript • Google Chrome 115.0.5790.98

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

! Doctor found issues in 3 categories.

Metadata

Metadata

Labels

P2Important issues not 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.13Found to occur in 3.13has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-webWeb applications specificallyr: fixedIssue is closed as already fixed in a newer versionteam-webOwned by Web platform team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions