DocumentationDecision recordsADR-0042

Modal Vim editing

Writers who live in Vim asked for modal editing in both editors. ProseMirror has no Vim mode and the community packages cover a handful of motions at most,…

accepted

Context

Writers who live in Vim asked for modal editing in both editors. ProseMirror has no Vim mode and the community packages cover a handful of motions at most, while CodeMirror 6 ships a mature one. ADR-0013 deferred the vimMode setting until the editor was chosen; the editor is now a persistent direct ProseMirror view over the rendered document and a source view for raw Markdown.

Decision

One workspace setting, vimMode (a settings extension defaulting to off), switches both editors at once. It is toggled from settings, the command palette, or mod+alt+i.

The raw Markdown editor is a CodeMirror 6 view. Its Vim mode is the @replit/codemirror-vim extension, installed through a compartment so the setting can flip it without rebuilding the editor. :w flushes the pending save; :q, :wq, and :x return the note to the rendered editor. Syntax color comes from the Markdown grammar plus a small match decorator for Skriuw's own tokens, all class-based so the theme sheet owns every color. The editor grows with the note and the pane scrolls, matching the rendered editor.

Source lines wrap, and a Markdown paragraph is one source line, so the raw view counts screen rows rather than source lines wherever a number is shown: the gutter numbers every wrapped row, the status row and the jump-to-line field use the same numbering, and plain j and k are non-recursive maps to gj and gk in the normal and visual contexts only, so dj and other operator motions keep Vim's source-line meaning. Row counts come from CodeMirror's height map; which row of its line the cursor occupies needs layout, so it is measured after each update and written back through a state effect. Vim's own :N still targets source line N.

The rendered editor gets a ProseMirror plugin written for this document model. A Vim line is one hard-break or newline separated segment of a textblock, so dd on a bullet removes the bullet and $ reaches the end of a paragraph. Plain j and k move by the rows the view wrapped a line into, measured through coordsAtPos, because a paragraph is one line and a logical step would skip its whole body; they remember the screen x like gj/gk. Operator motions such as dj and views without layout use logical lines. The jump-to-line panel counts those same wrapped rows, read from the character boxes coordsAtPos reports rather than from a line height, so inline media taller than the text adds no rows, and :N and NG land at the start of row N. A paragraph therefore numbers the same in both editors; the views still differ where the raw view shows a source line the rendered document has no textblock for (blank lines, rules, media), and counting rows means 999G reaches the last row of a wrapped final paragraph where G reaches its first. The layout is measured once per document and editor width and reused until either changes. Only a bounded (virtualized) document, whose blocks outside the window have no layout, counts Markdown lines. The plugin owns only modal state and a per-view session (pending keys, prompt, macro recording); every edit is an ordinary transaction, so history, remote merges, bounded windows, and the save pipeline stay authoritative. Normal mode swallows keys ahead of the product keymaps and blocks text input; insert mode leaves every product behavior untouched, including slash menus and input rules. Modifier combinations Vim does not own pass through to application shortcuts.

The host lends the plugin narrow hooks: enablement, note identity, bounded undo/redo and window shifts, :N row jumps through the jump-to-line target, save, close-tab, the scroll container, and the clipboard. Mode survives bounded window rebuilds. A mouse selection enters visual mode; the bubble menu stays closed while Vim owns the selection.

Consequences

Vim in the rendered editor covers normal, insert, visual, and visual-line modes; counts; operators d c y > < g~ gu gU with motions, text objects, and doubled forms; find and search motions including *, #, n, and N through the note's search plugin; registers including named, append, black hole, and system clipboard; dot repeat with inserted text; macros; and :w, :q, :N, :noh, and :s with % and '<,'> ranges. Visual block mode, marks, and folds are not implemented. Motions skip blocks that hold no text, such as images and diagrams, and search in a bounded note sees the current window.

Raw Markdown highlighting no longer walks the whole source per keystroke; the old character limit is gone. The raw editor's textarea, overlay, and scroll synchronization are removed. The new dependency is CodeMirror 6 with its Markdown language and the Vim extension, bundled in the main chunk so no editor chunk loads after startup.