IME: fix post-commit caret for replacement commits; trace IME pushes

ApplyIMECommitToModel computed the caret pushed to the IME right
after a commit as Range.End + len(text). That is correct for a plain
insertion (Start == End) but wrong for a replacement — the case a
swipe/autocorrect over a selection produces: the inserted text starts
at Range.Start, so the caret is Range.Start + len(text). A 4-rune
replacement (the 'samet' autocorrect on the phone) pushed the caret
4 runes past the inserted text, desynchronizing the IME's caret from
ours at exactly the moment its local model was being updated; the
persistent 4-rune drift that followed (every commit 4 runes ahead,
then the empty-fix-up loop) matches this.

The logic side (applyIMECommitBytes) already computed the correct
caret; this aligns the renderer's immediate push with it.

Also log every IME push (snippet restarts, selection updates, commit
pushes) from the renderer: the phone incident's logcat rotated before
it could be re-analyzed, so the next occurrence must be
reconstructable from what we actually pushed to the IME.
This commit is contained in:
Greg Pomerantz 2026-09-13 18:58:27 -04:00
parent cb8ebc003f
commit 51344b9a5b

View File

@ -24,6 +24,7 @@ import (
"gioui.org/unit"
"gioui.org/widget"
"golang.org/x/image/math/fixed"
"log"
)
// maxInt32 is a large value used as MaxWidth for single-line text layout.
@ -330,7 +331,15 @@ func (r *Renderer) ApplyIMECommitToModel(gtx layout.Context, ev key.EditEvent) {
r.imeHoldActive = true
r.imeHoldEditSeq = uint64(r.lastEditSeq)
r.imeHoldFrames = 0
caret := ev.Range.End + utf8.RuneCountInString(ev.Text)
// The post-commit caret is at the END of the inserted text, which
// starts at Range.Start: for an insertion (Start == End) that is
// Range.End + len, but for a replacement (a swipe/autocorrect over a
// selection) it is Range.Start + len — Range.End + len would land
// (End-Start) runes past the inserted text and desynchronize the IME's
// caret from ours (observed on the phone: a 4-rune replacement left
// every subsequent commit 4 runes ahead of the caret).
caret := ev.Range.Start + utf8.RuneCountInString(ev.Text)
log.Printf("IME PUSH commit-range=[%d,%d) text=%q -> sel caret=%d", ev.Range.Start, ev.Range.End, ev.Text, caret)
r.lastSelStart, r.lastSelCaret = -1, caret
r.lastIMECaretPos = r.imeCaretPos
gtx.Execute(key.SelectionCmd{
@ -405,6 +414,7 @@ func (r *Renderer) FlushIME(gtx layout.Context, tf TextField) {
// this runs — see ApplyIMECommitToModel — so a normal keystroke
// never reaches this branch; it is the re-anchoring path: scroll,
// tap, file switch, snapped commit, external edit.)
log.Printf("IME PUSH snippet=[%d,%d) len=%d (restart)", snippet.Range.Start, snippet.Range.End, utf8.RuneCountInString(snippet.Text))
r.lastSelStart, r.lastSelCaret = -1, -1
gtx.Execute(key.SnippetCmd{Tag: tf.id, Snippet: snippet})
}
@ -428,6 +438,7 @@ func (r *Renderer) FlushIME(gtx layout.Context, tf TextField) {
r.lastSelStart = selStart
r.lastSelCaret = selEnd
r.lastIMECaretPos = r.imeCaretPos
log.Printf("IME PUSH sel=[%d,%d)", selStart, selEnd)
rng := key.Range{Start: selStart, End: selEnd}
if selStart < 0 {
rng = key.Range{Start: selEnd, End: selEnd}