diff --git a/internal/editor/state.go b/internal/editor/state.go index 4557422..2391c6e 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -3290,8 +3290,16 @@ func textPosOnLineAtX(visualLine int, x float64) (int, bool) { if visualLine < len(layout.VisualLineStarts) { return base + layout.VisualLineStarts[visualLine], true } - // No line start recorded for this line (finger beyond the content): - // clamp to the last non-empty line. + // No line start recorded for this line (finger below the last line): + // the insertion point there is the end of the visible window — what + // the phantom trailing-line start used to provide (the shaper no + // longer records one at the window end; see the VisualLineStarts + // capture in drawWrappedText). A layout with NO line starts at all + // (degenerate/synthetic) has no window end to point at: clamp to + // the last non-empty line instead. + if len(layout.VisualLineStarts) > 0 { + return base + len(TheState.Editor.IMEWindowText), true + } for i := len(groups) - 1; i >= 0; i-- { if len(groups[i].indices) > 0 { visualLine = i diff --git a/internal/ui/render.go b/internal/ui/render.go index d849b85..ec05028 100644 --- a/internal/ui/render.go +++ b/internal/ui/render.go @@ -1276,7 +1276,25 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w flushLine() if g.Flags&text.FlagLineBreak != 0 { lineCount++ - layout.VisualLineStarts = append(layout.VisualLineStarts, byteOffset) + // Record the next visual line's start ONLY when a real line + // follows the break: a real "\n" cluster (zero width, one + // rune — including the final one of a newline-terminated + // file, whose trailing empty line is real) or a soft wrap + // with content after it. The shaper ALSO flags the last + // visible glyph of a final line without a trailing newline, + // and the synthetic end-of-text glyph after a trailing + // newline: both sit at len(str) with no line after them. + // Recording them creates a phantom visual line: taps below + // the content map onto it (harmless — it returns the EOF + // byte), but CaretPoint resolves the caret through it and + // draws the caret on that phantom line — one line BELOW + // where the typed text lands, so the letter appeared a line + // above the cursor. + isRealNewline := g.Advance == 0 && g.Runes > 0 + softWrap := g.Advance != 0 && byteOffset < len(str) + if isRealNewline || softWrap { + layout.VisualLineStarts = append(layout.VisualLineStarts, byteOffset) + } } } }