Extra unexpected text input when selection end is touch mark which inclusive is 'false' with IME Mode

录屏2024-08-07 16.53.25

Is there a solution to solve this problem?

I reported this issue a month ago and also found this issue on GitHub. It is a problem unique to the Chrome browser, and there has been no response so far. I investigated the code and found that the code at this point handles the DOM very aggressively. The entire mechanism is also not friendly to decorations. It’s very likely that this part of the code needs to be rewritten to solve the problem, especially since it was written three years ago and hasn’t changed since then.

This ‘aggressive’ code solves an actual problem (giving IME input the proper marks ahead of time, because once composition started the library cannot touch the DOM near the cursor anymore without aborting/corrupting the composition). Chrome’s IME is extremely brittle, and whenever anything happens that it cannot handle, it starts doing this kind of text duplication.

At some point, moving ProseMirror over to use the new EditContext feature may sidestep this kind of nonsense, by taking IME out of the editor’s DOM. But that’s a major project, and a lot of Chrome-specific code, so I’m not sure when I’ll be able to get to that.

2 Likes

@marijn

I am currently resolving this issue by inserting an img decoration with side=-1 into the text node where the cursor is located via a Plugin. However, due to the lack of extensive test cases, I have only tested it in my case. Do you think this approach is correct?

If possible, can this design replace the ‘aggressive’ code?

The code:

import { Plugin, Selection, TextSelection } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { Node } from 'prosemirror-model'

export const cursorPlugin = () => {
  const getDecoration = (doc: Node, selection: Selection) => {
    if (!(selection instanceof TextSelection)) {
      return DecorationSet.empty
    }
    if (!selection.empty) {
      return DecorationSet.empty
    }
    const pos = selection.from
    return DecorationSet.create(doc, [
      Decoration.widget(
        pos,
        () => {
          const cursor = document.createElement('img')
          cursor.classList.add('ProseMirror-separator')
          return cursor
        },
        {
          key: pos.toString(),
          side: -1,
        }
      ),
    ])
  }
  return new Plugin({
    state: {
      init(_, instance) {
        return getDecoration(instance.doc, instance.selection)
      },
      apply(tr) {
        return getDecoration(tr.doc, tr.selection)
      },
    },
    props: {
      decorations(state) {
        return this.getState(state)
      },
    },
  })
}

So I guess the code just isn’t aggressive enough then.

Doing some testing, what I’m seeing is that Chrome starts randomly expelling characters from the link during composition. You can reproduce this without ProseMirror by creating an editable element like this…

<div contenteditable=true><a href=/>one</a></div>

… selecting the ‘e’, and, with simplified Chinese IME, start typing ‘e’ characters. At the third, the last character of the composition (俄) is no longer part of the link. Once you commit the composition, all characters are moved out of the link. This kind of thing throws ProseMirror off (it assumes composition happens inside a single text node), causing it to redraw parts of the composed text, which triggers Chrome’s text duplication behavior.

This text expands the cursor wrapping logic to also kick in when the selection isn’t empty. It seems to help.

Unfortunately, this patch didn’t work which still duplicate the composing letter :joy:

Steps:

  1. selection the mark’s [end - 1, end], the DOM: <a>example</a>
  2. select last ‘e’.
  3. start Composition and press ‘f’, the DOM seem: <a>exampl</a><a>f</a>
  4. continue to press ‘f’, the DOM seem: <a>examplf</a>f
  5. press ‘f’,the DOM seem: <a>examplf</a>f'f'f

This is my expection: When selection is not empty, the marks in the entire composition process are based on selection.$from.marks. Can this be achieved? :face_with_monocle:

Which browser, platform, and which exact IME are you testing with?

Actually, I could reproduce this on Linux too—it’ll still sometimes keep the composed text inside the link, despite the barrier <img> node. This patch deletes the selection in that situation, to force it to create the new text outside of the link.

Chrome 119, MacOS 14.5, SougouPinyin IME.

Could you publish a new version package then i can test this patch because I can’t get this patch. thx.

I’ve tagged a version 1.33.11

This patch is works. thx :grinning:

@marijn Sorry to bother you again. If we stop checking mutations at the compositionstart event and instead record the mutations, then recalculate them at the compositionend event, would that mean we no longer need to keep deleting elements during the composition process?

That would mean the entire editor would need to be kept in some weird frozen state while composition happens, and that extensions cannot react to composed text as it comes in (for example when autocompleting). I tried that with old versions of CodeMirror, it was not a good system.