Skip to content

ADR 0051: Global animation consistency, and three Vim-mode polish fixes

Status

Accepted

Context

Direct bug reports after using ADR 0047–0050's work for real: Visual-mode navigation didn't glide at all; Shift+A (jump to end of line, enter Insert) showed no transition; the typing pop-in animation (ADR 0049) "doesn't work at all"; and three more asks — Visual mode should use the same block cursor as Normal, the block cursor's capped opacity (ADR 0047) was wrong and should be full/normal, and : should open the command line directly in Vim mode instead of requiring Ctrl+;.

Decision

The typing pop-in's real bug: snapAnimationToTarget() was clearing its own animation

insertText() pushes a fresh TypingAnimation entry, then — for the plain-character and Tab cases — keyPressEvent called snapAnimationToTarget() right after. ADR 0049 had made that function also clear m_typingAnimations (reasoning: an undo/redo or Vim command shouldn't let a stale pop-in keep animating over content it's no longer about). The result: every single typed character's animation was wiped in the same keystroke that created it, before a single frame ever rendered any progress. The earlier "verified live" screenshot in ADR 0049 was a false positive — it compared the typed character's brightness against the gutter line-number's own (deliberately dimmed) alpha tier, not against that same character at a later frame, so a real regression read as a pass.

Fixed by moving the clear to only where it's actually needed: applyUndoResult() (shared by undo()/redo()), the one case that truly can invalidate an in-flight animation's byte range with unrelated content. snapAnimationToTarget() itself no longer touches m_typingAnimations at all.

Typing now glides too, not just the character pop-in

Separately, keyPressEvent had an isEdit flag that forced plain character insertion and Tab to snap the caret position instantly (ADR 0017's original reasoning: gliding can't keep pace with fast repeated small jumps). Direct instruction this round: make animation consistent everywhere, including typing. Removed isEdit entirely — typing's caret now eases through m_renderedCaretPos like every other cursor move (arrows, Backspace, Enter already worked this way).

Vim mode was snapping after almost everything

The actual cause of "Visual mode doesn't animate" and "Shift+A doesn't animate": handleVimNormalOrVisualKey() called snapAnimationToTarget() after every resolved key — plain motions (h j k l 0 ^ $ w b e, gg, G) in both Normal and Visual mode, the Visual-mode operator switch (v x d y c), and the Normal-mode mode-entry/operator switch (i a I A o O x p P u, and starting d/y/c). Every one of these forced an instant snap regardless of animations, unlike arrow-key navigation, which never did. Removed the call from all of these paths, plus from the Vim mutation primitives that had their own (vimDeleteRange, vimDeleteLines, vimPasteAfter, vimPasteBefore, vimOpenLineAbove) and from runCommand's :<digits> ex-jump — all of Vim mode now glides exactly like the rest of the app, with no special-cased exceptions left. undo()/redo() keep their snap (a restored cursor position can be anywhere in the document — an arbitrary jump, not a discrete one-step motion — so instant sync remains the sensible default there, matching every other non-Vim structural reload in this file, e.g. openFile).

Visual mode gets the block cursor too

blockCursor's condition widened from m_vimMode == VimMode::Normal to m_vimMode != VimMode::Insert — Normal and Visual now render identically (Insert keeps the bar, since it's still "an insertion point"). Visual's own selection highlight and the block cursor compose fine together; no rendering changes needed beyond the condition itself.

Block cursor: full opacity, not capped

ADR 0047's kVimBlockCursorMaxAlpha = 200 (~78% peak) is gone — direct feedback called the capped-opacity decision wrong. The block now uses the exact same caretAlpha (0–255, full breathing range) the bar caret always used. The knocked-out glyph drawn on top (also unchanged) already guarantees the character stays legible regardless of the block's opacity, so nothing else needed to change.

: opens the command line directly in Vim Normal/Visual mode

Real vim's own convention. Added as a new check near the top of handleVimNormalOrVisualKey() (alongside g/G/motions, before the mode-specific branches, so it works identically from Normal or Visual): c == ':' calls m_commandLine->openCommandLine(). Purely additive — the existing app-wide Ctrl+; (ADR 0025) still works everywhere, Vim mode or not; this just adds the more natural Vim-idiomatic bare-colon trigger where it's otherwise a dead, swallowed key. Insert mode is untouched — a bare : there still types as a literal character, since this check only runs inside Vim's own Normal/Visual dispatch.

Consequences

Verified live: a G jump (5 lines) sampled across three frames shows the block cursor genuinely mid-transit at 20ms, further along at 50ms, and settled by 300ms — real interpolation, not an instant jump with a lucky screenshot. The identical motion from Visual mode shows the same progressive glide with the selection extending underneath it. A typed character sampled at 20ms vs. settled shows a visibly dimmer/smaller glyph early and a fully saturated one once settled — the pop-in genuinely renders progressively now. Visual mode shows the same block cursor as Normal, at full (uncapped) opacity. : opens the command line from Normal mode; the same keystroke in Insert mode still inserts a literal :. ctest --test-dir build still 9/9 throughout (no core-layer change).