Skip to content

Fix evil motion crossing into stale whitespace cells (#246)#250

Open
dakra wants to merge 1 commit into
mainfrom
fix/issue-246-evil-motion
Open

Fix evil motion crossing into stale whitespace cells (#246)#250
dakra wants to merge 1 commit into
mainfrom
fix/issue-246-evil-motion

Conversation

@dakra
Copy link
Copy Markdown
Owner

@dakra dakra commented May 9, 2026

Summary

Fixes the operator misfires reported in #246 (and supersedes the diagnosis from #218):

  • Improve code quality, adaptive redraw, and CI coverage #1 ('cw' then 'dw' over-deletes in zsh / pi) — clamp evil's forward / end-of-line motions at the terminal cursor's buffer position so they can't walk into trailing whitespace cells left over from a non-CSI-K redraw.
  • #2a ('0' / '^' lands on the prompt without OSC 133) — fall back to sending readline 'C-a' when the cursor's row carries no 'ghostel-prompt' property; '--sync-point-on-next-redraw' lands point at the new cursor.
  • #2b (custom normal-state passthrough binding doesn't move point) — add public 'evil-ghostel-send-and-follow' as an opt-in wrapper for user bindings; doesn't change the redraw advice's default point preservation.
  • Replace manual lint CI with melpazoid #3 (sporadic operator misfires) — same root cause as Improve code quality, adaptive redraw, and CI coverage #1; covered by the clamp.

Mechanism

zsh's zle and prompt_toolkit redraw inline deletions by overwriting with spaces rather than emitting CSI K. The deleted columns end up as real space characters in the Emacs buffer past the live end of input. evil's 'evil-forward-word-begin' treats those cells as content and walks off the input row to the next buffer line; the subsequent 'dw' from the wrong row sends down-arrows through the PTY, which zsh interprets as history-next, scrubbing the input.

The clamp narrows the buffer's upper bound to the cursor's buffer position before the motion runs. When point is past the cursor (scrollback) the narrow is skipped so it can't cut point out of the visible region.

Test plan

  • 'make -j4 all test-evil' — clean (lint, byte-compile, 85 evil-ghostel + 149 elisp + 331 native tests pass)
  • Live zsh repro (drives a real subprocess + libghostty pipeline): before — 'w' jumped to line 2, 'dw' triggered zsh history-jump; after — 'w' holds, 'dw' is a no-op
  • 7 new ERT tests covering the clamp, scrollback no-clamp, line-mode no-clamp, end-to-end cw->dw repro, 'C-a' fallback, scrollback fall-through, and 'send-and-follow'
  • Final live verification in interactive emacsclient against noctuid's actual workflow

Out of scope

  • Reproducing #2b — not yet repro'd. The helper unblocks user-installed passthroughs without changing defaults; if their case isn't fully fixed by the helper we can iterate.
  • Auto-sourcing the shell integration (the proper fix for #2a is OSC 133; the 'C-a' fallback is the safety net).

zsh's zle and prompt_toolkit redraw inline deletions by overwriting
with spaces rather than emitting CSI K, leaving the Emacs buffer with
trailing whitespace cells past the live end of input.  evil's word
motion treated those cells as content and walked off the input row -
so 'word word word<esc>bbcw<esc>w' jumped to the next buffer line,
and the following 'dw' sent down-arrows through the PTY, triggering
zsh history navigation that scrubbed the input.

Clamp 'evil-forward-word-begin' / '-word-end' / '-WORD-begin' /
'-WORD-end', 'evil-end-of-line', 'evil-end-of-visual-line', and
'evil-last-non-blank' via a 'save-restriction' narrow whose upper
bound is the terminal cursor's buffer position.  When point is past
the cursor (scrollback) the clamp is skipped so it can't cut point
out of the visible region.

Also fall back to readline C-a from 'evil-beginning-of-line' /
'evil-first-non-blank' when the cursor's row carries no
'ghostel-prompt' property - so '0' / '^' lands at input start even
when the shell isn't sourcing the OSC 133 integration.
'--sync-point-on-next-redraw' carries point to the new cursor.

Add 'evil-ghostel-send-and-follow' so user-defined normal-state
passthrough bindings can opt into the same redraw-time point sync;
the redraw advice deliberately preserves point in normal state to
keep column-only motions ('^', '$', '0') from being undone by
incidental redraws, which is the right default but blocks 'send
key, follow with point' use cases.

Fixes #246
@dakra dakra force-pushed the fix/issue-246-evil-motion branch 3 times, most recently from 7b9caef to 3bcb7ed Compare May 10, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant