Adjacent highlightWhitespace() marker spans become one span containing IME preedit text during composition

Hi,

I found a composition-time rendering issue with CodeMirror’s built-in highlightWhitespace() extension on Windows with a Japanese IME.

I’m not sure whether the DOM behavior itself is expected as part of CodeMirror’s composition-preservation strategy, so I’m posting this here before treating it as a bug.

Minimal reproduction

Environment:

  • Windows 11
  • Chrome 152.0.7977.65 (64-bit)
  • ATOK Japanese IME
  • @codemirror/view 6.43.9
  • @codemirror/state 6.7.1

The editor has highlightWhitespace() enabled:

import { highlightWhitespace } from "@codemirror/view"

// Include highlightWhitespace() in the editor extensions.

The document contains five ASCII spaces (U+0020):

     

Steps:

  1. Place the cursor between the second and third spaces.
  2. Enable the Japanese IME.
  3. Start composing text, for example gross (entered as full-width IME preedit characters).
  4. Leave the composition active without committing it.

Expected

The five whitespace markers remain visually stable while the composition is active.

Actual

Before composition, the five spaces are rendered as five separate cm-highlightSpace spans.

At compositionstart:

marker.count = 5

After the first composition update:

marker.count = 4

One of the remaining marker spans now contains two spaces and the IME preedit text.

For example, after the first composition update:

marker[0].className = "cm-highlightSpace"
marker[0].textContent = "  g"
marker[0].rect.width = 30px
selection.anchor.insideMarker = true

The DOM selection is inside that same cm-highlightSpace span.

The built-in style is still applied:

background-image = radial-gradient(...)
background-repeat = repeat
background-size = auto

Visually, instead of five stable per-space markers, the span that now contains both spaces and the preedit text paints an enlarged dot behind the preedit text.

[attach before / during / after image here]

The circled area shows the span containing the preedit text painting a single enlarged gradient dot over the larger box (background-size: auto), instead of one marker per space.

The underlying document is not corrupted. The number of U+0020 spaces remains five before and after composition.

At the compositionend snapshot, the marker count is still 4. In my run, a further DOM child-list mutation happened about 60 ms later, after which the normal rendering returned.

Additional observations

I originally encountered this while implementing a custom per-character Decoration.mark() for U+3000 IDEOGRAPHIC SPACE. I observed the same high-level behavior there: decorated whitespace and IME preedit text can end up inside the same mark-decoration span during composition.

I reproduced that custom-decoration behavior with ATOK, Microsoft IME, and Google Japanese Input on Windows.

I also observe span merging in Firefox 154.0.1, but the visual result differs: Firefox does not paint the enlarged gradient, so the merged span simply shows no marker. The Firefox behavior appeared timing-dependent in my tests and I could not reproduce it reliably, so the numbers above are all from Chrome.

Windows US-International dead-key input did not reproduce this in my test, since it did not go through the same IME composition path.

Questions

Is this DOM restructuring around an active composition an expected consequence of CodeMirror’s composition-preservation strategy?

If so, what is the recommended way to implement per-character visual markers that need to remain visually stable while IME composition is active?

If anyone with a Chinese or Korean IME can check whether this reproduces on their setup, that would also help determine whether it is specific to Japanese IMEs.

I am using Chinese IME, encountered some decoration issues before, and so might be able to look into the issue together. Can you post a link to the code sample (like Try CodeMirror) that can reproduce the issue?

Thanks! Here is a reproduction: at codemirror/try

Place the caret between the second and third space, then start composing. What I’d like to know is whether marker.count drops from 5 to 4 on your setup, and whether one span ends up containing both spaces and the preedit text.

From my quick testing on macOS 15.7.5 with Rime, all browsers behave differently with none expected behavior: latest Firefox, latest Chrome and recent Safari.

At the beginning of composition, the IME either outputs a placeholder character (looks like a full-width space on Firefox and Safari) or the preedit text is put inline (the “ㄓ” in Chrome).

Additional observations:

  • On Firefox, the character is captured by the mark decoration. Either the second or the third space decoration disappears.
  • On Chrome, it looks like what as your sample case. The leading space decoration disappears, and the preedit goes “into” the sequent mark decoration.
  • On Safari the placeholder is even selected. From the highlight the placeholder seems to have gone to the second decoration.

I can share my experience: In my recent attempt to make a symbol input extension, I want to replicate the IME behavior I observed, to insert a placeholder character, wrap it in a mark decoration for styling, and replace it with the committed text when the user hits enter. I witnessed some inconsistent behavior like above. At last I just gave up, simply turning my extension off whenever view.composing is true. Might not apply to your case, though.

Thanks a lot for testing this — that is a much wider matrix than I could cover.

First, a correction to my original post. I wrote there that Firefox does not paint the enlarged marker. That was wrong. I re-measured on Windows 11 / Firefox 154.0.1 with ATOK, and the merged span does expand:

preedit "お" -> marker[1].rect.width = 30px

preedit "おお" -> 45px

preedit "おおきく" -> 75px

preedit "おおきくなるよ" -> 120px

(baseline for a single space is 7.5px)

More interestingly, your description of the per-engine difference matches what I measure exactly. With the caret between the second and third space:

Chrome (Windows 11, ATOK):

marker[0].textContent = " "

marker[1..3] = " " each → the first two spans merge, preedit appended at the end

Firefox (Windows 11, ATOK):

marker[0].textContent = " "

marker[1].textContent = " "

marker[2..3] = " " each → the second and third spans merge, preedit sandwiched between them

So in Chrome the leading space decoration is the one that loses its marker, and in Firefox it is the second or third — which is precisely what you observed on macOS with Rime. Same OS and same IME on my side, with only the browser changing, so the grouping appears to be determined by the rendering engine rather than by the IME.

In both engines marker.count goes 5 → 4, two spaces end up inside the merged span, and the document itself is unchanged (five U+0020 before and after). At compositionend the count is still 4; a further DOM child-list mutation about 50 ms later restores the normal rendering.

Your last paragraph is the part I find most telling. You arrived here from a symbol input extension wrapping a placeholder in a mark decoration; I got here from visualizing U+3000 in a text editor. Two unrelated use cases, same failure mode.

I will try your workaround (disabling the decoration while view.composing is true) and report back. My worry was that changing decorations mid-composition might disturb the composition itself, but if it has been working for you in practice, that concern may be unfounded.

Which brings me back to my original question, now better supported than when I first asked it:

Is this DOM restructuring around an active composition an expected consequence of CodeMirror’s composition-preservation strategy?

And if it is expected, is disabling decorations while view.composing is true the recommended approach, or is there a better pattern for per-character visual markers that need to stay stable during composition?