feat(editor): complete vim command surface and fix J join (#1222)#1224
Merged
Conversation
…ands Split monolithic VimEngineTests/VimVisualModeTests into 14 domain-focused files covering motions (h/j/k/l/w/W/b/B/e/E/ge/gE/0/dollar/caret/underscore/gg/G/H/M/L/%), insert entry, operators, paste, replace (r/R), case change, indent, undo/redo/repeat, count prefix, command-line, visual mode, marks, registers, find-char (f/F/t/T/;/,), join (J/gJ). Adds VimTextBuffer protocol methods (matchingBracket, big-word motions, wordEndBackward, visibleLineRange, indentString) with implementations in both VimTextBufferAdapter and VimTextBufferMock. Extends VimMode with .replace case and VimOperator with .lowercase/.uppercase/.toggleCase/.indent/.outdent. Tests for unimplemented commands fail to drive subsequent commits. Refs #1222
…/G first-non-blank, count multiplication Adds capital-letter shortcuts (D for d$, Y for yy, C for c$, X for delete-before-cursor, S for cc, s for delete-char-and-insert) and the big-word motion family (W, B, E, ge, gE). Fixes gg/G to land on first non-blank per vim spec. Operator + linewise motion (dG, dgg) now works via executeLinewiseOperator. Count prefix on operator and motion now multiply (2d3w deletes 6 words). Inclusive motion no longer consumes a trailing newline.
Adds J (join lines with single space) and gJ (join without space) commands in Normal and Visual modes. Implements vim semantics: strips leading whitespace from the next line for J, preserves it for gJ, suppresses the inserted space when adjacent to whitespace or before a closing paren, leaves the cursor on the join point, supports count prefix (3J joins 3 lines). In Visual mode J joins every line covered by the selection. Fixes #1222
…rators, >>/<< indent, %, H/M/L screen motions, ?, gi Adds the full find-character family (f, F, t, T, ;, ,) with operator combos (dfo, cto, yfo). Single-char replace (r) and the dedicated Replace overwrite mode (R, exiting via Escape). Case operators: standalone ~ with count, the case operators gu/gU/g~ with motion (gu$, gUw, etc.), and the doubled line forms guu/gUU/g~~. Indent operators >> and <<. Bracket-match motion %. Screen motions H/M/L using the new visibleLineRange protocol method. Reverse search prompt ? and gi resume-insert.
…epeat, U line undo, visual mode parity
Adds the complete marks subsystem (m{a-z} set, '{a-z} jump to line, `{a-z} jump to exact offset, `` and '' jump to last position). Named registers "{a-z} write/read with uppercase variant appending; numbered registers "0.."9 implement vim's standard delete ring ("0 holds last yank). The . repeat command replays the last edit through a typed VimDotKind. U undoes every edit on the current line since the cursor arrived. Visual mode gains o (swap anchor/cursor), ~ (toggle case), u/U (lowercase/uppercase), r{char} (replace selection), I (insert at start), A (append at end), and paste-replace where p/P over a selection replaces the selection with the register contents. Linewise change keeps the trailing newline. Paste accepts a count prefix.
…teals key window Two interceptor bugs caused Esc to silently no-op when the cursor sat past the last char of the buffer (typically right after typing ';' at end-of-query) and the autocomplete popup was the key window: 1. The willCloseNotification observer spawned a Task to read NSApp.currentEvent. By the time the Task ran, the triggering Esc keyDown had rotated out, so the keyCode == 53 check failed and engine.process(Esc) was skipped. The observer now reads currentEvent synchronously inside the .main-queue block and uses MainActor.assumeIsolated instead of Task. 2. handleKeyEvent guarded on event.window === textView.window. When a child popup is the key window, the Esc keyDown's event.window is the popup, so handleKeyEvent bailed out before the engine could see Esc. The guard now allows Esc keyDowns from child popups of the editor window to reach the engine (mode switch happens first, then the event propagates so the popup also closes naturally). Adds engine-level regression tests for Esc at offset == buffer.length on buffers both with and without trailing newlines. Refs #1222
…ponder chain The SwiftUI "Clear Selection" menu binds Esc to NSResponder.cancelOperation. macOS dispatches menu key equivalents through performKeyEquivalent before the per-app local event monitor, so when the cursor sat right after typing ';' at end-of-buffer, the menu shortcut fired first and the local event monitor never saw Esc — vim was stuck in insert. The fix has two parts. (1) VimKeyInterceptor.handleKeyEvent no longer gates Esc on event.window: isEditorFocused already proves we're the focused editor, and Esc is the one event we always want to consume in non-normal modes. (2) A new public hook handleEscapeFromExternalSource() lets the SwiftUI menu route Esc through the engine first, falling back to cancelOperation only when no editor is in a non-normal vim mode. Wired via EditorEventRouter.handleVimEscapeFromMenu() so the "Clear Selection" Button checks the active editor before dispatching the responder-chain action. Adds Swift Testing cases for handleEscapeFromExternalSource covering normal-mode noop, insert→normal, and replace→normal transitions. Refs #1222
Pressing w (or W) from the last word of the buffer used to land the cursor one position past the last char, leaving a visible block-cursor highlight on the empty space after ';'. Per vim's last-word-on-last-line rule, a bare (non-operator) word motion must clamp to a valid normal-mode cursor position: never past buffer.length, never on a newline. Adds clampToContentPosition() and applies it at the tail of wordForward and bigWordForward when no operator is pending. Operator-targeted motions (dw, cw, yw) still report the post-content offset so they can delete through the last char. Adds three regression tests covering: w on ';' at end of "SELECT * FROM users;", w on a single-word no-newline buffer, and w on a single-word trailing-newline buffer.
…ert-edits, number adjust, macros, scroll, sentence/paragraph, edge cases
Adds 128 tests across 9 new files, all consistent with standard vim semantics. Roughly 33 already pass (boundary cases the engine handles correctly today) and 95 are failing TDD specs that drive the next round of engine work:
- Text objects (iw/aw/iW/aW/i"/a"/i'/a'/i(/a(/i{/a{/i[/a[/i</a</it/at/ip/ap) in operator and visual modes
- Search (/, ?, n, N, *, # word-under-cursor) with whole-word matching, wrap-around, and operator combos
- Insert-mode editing (Ctrl+W word-back-delete, Ctrl+U line-start-delete, Ctrl+H backspace, Ctrl+T/D indent/outdent)
- Number adjust (Ctrl+A increment, Ctrl+X decrement) including count, negative, hex, digit-rollover, cursor placement
- Visual reselection: gv, `<, `>
- Sentence/paragraph motions: ( ), { }, [[ ]]
- Macros: q/@/@@ with count, multiple registers, recording across visual operations
- Scroll motions: Ctrl+d/u/f/b, Ctrl+e/y, zt/zz/zb, gj/gk
- Edge cases: empty buffer safety, single-char buffer, end-of-buffer regressions, unicode, CRLF, extreme counts, mode-switch idempotency
…s, scroll, sentence/paragraph, gv
Closes the implementation gap for the comprehensive test spec. Every test that the previous 'extend test spec' commit added as failing TDD now passes — text objects (iw/aw/iW/aW/i"/a"/i'/a'/i(/a(/i{/a{/i[/a[/i</a</it/at/ip/ap), search (/, ?, n, N, *, # with whole-word matching and wrap-around), insert-mode editing (Ctrl+W/U/H/T/D), number adjust (Ctrl+A/X) including signed, hex, count, multi-digit, cursor placement, visual reselection (gv, `<, `>), sentence/paragraph/section motions ( ) { } [[ ]], macros (q/@/@@ with count and multiple registers), and scroll commands (Ctrl+d/u/f/b/e/y, zt/zz/zb, gj/gk).
Also fixed a stale macro-recording removeLast() that dropped the wrong key and routed Esc routing through the test-friendly handleEscapeFromExternalSource hook so menu shortcut preemption no longer leaves Vim stuck in insert. Operator + search motion (d/pattern) remains skipped with an XCTSkip note for the next pass.
…d surface
The vim-mode page only listed ~25 commands. This branch ships ~100, so the docs now cover every command family: motions (h/j/k/l, word/WORD, sentence/paragraph, find-char, H/M/L, %), operators (d/c/y plus D/C/Y/S/s/r/R/~/gu/gU/J/gJ/>>/<<), text objects (iw/aw, i"/a", i(/a(, i{/a{, it/at, ip/ap), search (/?/n/N/*/#), marks, named and numbered registers, macros, repeat, insert-mode Ctrl+W/U/H/T/D, number adjust Ctrl+A/X, and scroll (Ctrl+d/u/f/b, zt/zz/zb, gj/gk).
keyboard-shortcuts.mdx now defers to the dedicated vim-mode page instead of duplicating an abbreviated subset that drifts as features land.
Rewrite tables with shorter headers (How to enter / Goes to / Holds / Action) and trim the prose between them. No em dashes, no filler words. Replace the keyboard-shortcuts vim section's catalog of commands with a one-line pointer to the dedicated page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
shift+jcommand to join lines #1222 —JandgJjoin lines in Normal and Visual modes, with count, whitespace stripping, and the paren-suppression rule./,?,n,N,*,#), insert-mode editing (Ctrl+W/U/H/T/D), number adjust (Ctrl+A/X),gvreselect, macros (q/@/@@), scroll (Ctrl+d/u/f/b, zt/zz/zb, gj/gk), sentence/paragraph/section motions, marks, named/numbered registers,.repeat,Uline undo, and full visual-mode parity.Clear Selection) preempting Esc before the local event monitor, both of which left Vim stuck in insert mode.What changed
Engine (
TablePro/Core/Vim/):VimEnginenow handles the full standard command surface.VimModegains.replace.VimOperatorgains.lowercase,.uppercase,.toggleCase,.indent,.outdent.VimTextBuffergains protocol methods for big-word motions,wordEndBackward, bracket matching, viewport-visible-line range, and indent string/width. The adapter wires them toCodeEditTextView; the mock has parallel pure-Swift implementations.Interceptor (
VimKeyInterceptor): Esc fast-path no longer gates onevent.window; the popup-close observer capturesNSApp.currentEventsynchronously; a newhandleEscapeFromExternalSource()lets the SwiftUI menu route through the engine first viaEditorEventRouter.handleVimEscapeFromMenu().Tests (
TableProTests/Core/Vim/): 25 files covering every command family. 504 pass, 1 skipped (d/patternoperator-+-search, documented as the next-pass TODO).Test plan
xcodebuild -project TablePro.xcodeproj -scheme TablePro test -only-testing:TableProTests/VimEngineJoinTests— all 30 J/gJ tests passSELECT * FROM users;, press Esc — block cursor lands on;, mode indicator switches to NORMALusers, pressciw— word disappears, mode enters Insert*on a word — cursor jumps to next occurrence;ncontinues,Nreversesqa{commands}q, replay with@aand5@aCtrl-Aon a number — value increments>>and<<indent / outdent at the configured tab width