Skip to content
| Marketplace
Sign in
Visual Studio Code>Keymaps>Vim UltraNew to Visual Studio Code? Get it now.
Vim Ultra

Vim Ultra

weixu

|
14 installs
| (0) | Free
Modal Vim editing on a Rust/WASM engine — motions, operators, text objects, counts, named, numbered and clipboard registers, marks and the jumplist (ctrl-o / ctrl-i), visual modes, multiple cursors, dot-repeat (.), macros (q, @, @@, @:), counted inserts, EasyMotion jumps, regex search (/ ? n N * #),
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Vim Ultra

Modal Vim editing for VSCode, powered by a from-scratch Vim engine written in Rust (crates/vim-engine) and compiled to WASM. The TypeScript side is a thin host: it forwards keys, applies the engine's effects (edits, selections, commands), and mirrors external document changes back into the engine.

Supported today

  • Modes: normal, insert, visual, visual line. Status bar shows the mode and pending keys; the cursor is a block outside insert mode.
  • Motions: h j k l, 0 ^ $, _ (first non-blank, count - 1 lines down), g_ (last non-blank), | (to a column: 12|), w W b B e E, ge gE (back to the end of a word), gg G, H M L (top, middle and bottom of the screen; 3H is three lines in), { }, ( ), %, [{ [( ]} ]) to the brace or paren that opens or closes the block the cursor is in, f F t T with ;/,, enter + -, all with counts (3w, 2f,, 2]} for two blocks out), all usable after an operator (d_ is dd, dge, yL) and in visual mode. Bracket matching follows vim's: a brace inside a "…" string, a '}' literal or an escaped \} is not the block's edge. ( and ) go by vim's sentences: one ends at a ., ! or ? (closing ), ], " or ' may follow) before a space, a tab or the end of the line, and an empty line is a sentence by itself. With an operator they stop short of the line break, so d) on a line's last sentence leaves the next line where it is.
  • Search: / and ? (the pattern replaces the mode text in the status bar while you type it; backspace edits it, escape aborts, enter runs it), n/N to repeat, */# for the word under the cursor. Searches wrap around the buffer, take counts (3n), an empty pattern repeats the last one, and — being exclusive motions — they combine with operators (d/foo⏎, dn, y*). Typing a pattern highlights its matches and peeks at the one enter would land on (vim's incsearch): the view scrolls there without moving the cursor, comes back if you escape, and stays if you commit. A landed search reports match 3 of 11 in the status bar.
  • Regular expressions in every pattern: . * \+ \? \{n,m} (and the non-greedy \{-}), \(…\) groups with \%(…\) non-capturing, \| alternation, [a-z] and [^…] collections with [:alpha:] names, the classes \w \W \s \S \d \D \a \l \u \h \x \o (and negations), ^ $ \< \> anchors, \zs/\ze to trim the match, \c/\C for case, and the magic levels \v \m \M \V.
  • Search and replace: :[range]s/pattern/replacement/flags, with ranges %, 5, 1,$, .,+3, '<,'> (typing : in visual mode fills that in) and flags g i I n e. Replacements take & / \0 for the whole match, \1…\9 for groups, ~ for the previous replacement, \r for a line break, and \u \l \U \L \E for case. & repeats the last substitution on the current line, as does a bare :s, and :12 jumps to a line. The status bar reports the result (3 substitutions on 2 lines) and any error.
  • Line commands, on the same ranges (plus ;, which counts the second address from the first: :5;+2, and marks: :'a,'bd): :d[elete] [x] and :y[ank] [x] into the registers dd and yy use, or register x too (:d _ throws the lines away; :y A appends; :d 3 takes a count); :pu[t] [x] puts a register below the line as lines of its own, :pu! above it and :0pu above the first line; :m[ove] {address} and :t / :co[py] {address} — :m0 to the top, :m$ to the bottom, :m+1 down a line, :t. to duplicate; :j[oin], with :j! keeping the white space; :> and :< through VSCode's own indent, so your tab settings apply (:>> shifts twice); and :sor[t], with ! to reverse, i to ignore case, n to sort by the first number, u to drop duplicates, and /pattern/ to sort on what follows a match (with r, on the match). Like Vim, a command over more than two lines says how many (5 fewer lines).
  • :g[lobal]/pattern/cmd and :v[global] (or :g!) run any of the line commands — :s too — on every line that matches, or every line that doesn't: :g/TODO/d, :v/\S/d, :g/^/m0 to reverse the buffer, :g/log/s/foo/bar/g, nested as :g/a/v/b/d. They work the way Vim's do, so lines that move or merge along the way are still visited exactly once, and the whole run is a single undo step.
  • Files: :w, :wq, :x and ZZ (write only if modified, then close), :q, :q! and ZQ, :up[date], :wa, :qa, :qa!, :wqa / :xa, and :e! to read the file back from disk. Writing is VSCode's own save, so format-on-save and the other save hooks run, and the status bar answers the way Vim does: "src/a.ts" 12L, 340B written. :q won't throw away unsaved changes (E37: No write since last change (add ! to override)) unless another editor still shows the document, and :qa won't close anything while any document has them: it switches to the first one (E162). :qa closes every editor rather than quitting VSCode.
  • Command names abbreviate the way Vim's do — :wr, :sor, :co, :noh — and anything else says E492: Not an editor command. :noh is accepted and does nothing, since match highlights never outlive the search prompt.
  • Operators: d c y > < with any motion (d2w, ci", y$, >j), doubled for lines (dd, 3yy, cc, >>), and on visual selections. The case operators gu (lower), gU (upper) and g~ (toggle) work the same way — gUiw, gu$, g~j — doubled as guu/gugu, gUU/gUgU and g~~/g~g~; in visual mode u and U lower- and uppercase the selection, beside ~.
  • Text objects: iw aw iW aW, is as (sentences), ip ap (paragraphs), i" a" i' a' i\ a``, i( a( i[ a[ i{ a{ i< a< (aliases b/B) and it at (HTML/XML tags), all chosen the way vim chooses them. A count takes more of them — d2aw two words, 2i( the block around the one you're in, 2it the enclosing tag. a objects bring the blanks after them, or the ones before when there are none (daw on a line's last word, das on its last sentence). ip/ap work on whole lines: dap deletes a paragraph and the blank lines after it, and lines of nothing but spaces count as blank. i{ on a block whose braces sit on their own lines takes just the lines between, so di{ leaves { and } touching. Brackets work from either bracket, and outside any block i( uses the next one ahead. Quotes stay on the line, pair up from its start when the cursor is on one, skip \", and 2i" takes the quotes too. Tags ignore case, skip <br/> and a <br> with no end tag, and work from the tag itself. In visual mode an object selects (vip turns it linewise), and repeating it grows the selection — vawaw, vipip, vi(i( one block out, vitit the tags and then the enclosing block.
  • Actions: x X s S D C Y r ~ J p P o O, u / ctrl+r (delegates to VSCode undo/redo), zz zt zb, ctrl+d/u/f/b scrolling. r<CR> is vim's line-splitting special case: the characters go away and a single line break takes their place, however many the count asked for. o/O open the new line through VSCode's own line-insert — the same thing enter in insert mode runs — so it arrives indented by the language's rules rather than at column 0.
  • More actions: gJ joins lines as they are, without adding or removing spaces. gv brings back the last visual selection (and, from visual mode, swaps it with the current one). gi resumes inserting where you last left insert mode, gI inserts at column 0. gp/gP paste like p/P but leave the cursor just after the pasted text. ctrl+a/ctrl+x add to or subtract from the number under or after the cursor — with a count, negative decimals, 0x1f hex and 0b101 binary kept in their own base, case and width (007 becomes 008); in visual mode they change the first number on every selected line, and g ctrl+a counts up line by line (1 1 1 becomes 2 3 4). ctrl+e/ctrl+y scroll the view a line (or a count of lines) without moving the text under the cursor.
  • Repeat: . does the last change again at the cursor — an operator with its motion or text object (dw, ciw and the word typed in its place, >j, gUiw), the edits that stand alone (x X s S D C r J gJ p P ~ & ctrl+a ctrl+x), and the inserts with what they typed (i a I A o O gi gI, A; then j. down a column of lines). A count given to . replaces the first one, for the repeats after it too (d2w then 3. deletes three words); the register stays the one named, except that "1–"8 step on to the next, so "1p.. puts the last three deletes. A change made in visual mode repeats over as much text from the cursor: as many lines, or as many characters within a line, or up to the same column on the last of several lines. Motions, yanks, searches, undo and : commands are not changes and leave what . repeats alone. With several cursors . repeats at every one. The change and the text typed after it reach the document as one edit, so one u undoes the repeat. o/O are repeated through VSCode's line-insert again, so the repeated line is indented as the first one was.
  • Counted inserts: 3ix then escape puts xxx; 3o opens three lines with the text typed on each, indented like the first.
  • Macros: q{a-z} starts recording into a register (q{A-Z} appends to it, q{0-9} and q" work too), and the status bar says recording @a until q stops it. @a runs it, 3@a three times, @@ runs the last register run again and @: the last : command line. What you type in insert mode goes into the recording as the text it left in the document — a backspace, an accepted completion or a bracket VSCode closed for you is recorded as what it did — and comes back as that text when the macro runs, without auto-indent or auto-closing having another go at it. The register reads in Vim's key notation (ciwfoo<esc>j), so :reg a shows what was recorded and "ap puts it as text you can edit and yank back. A key that fails stops the macro and anything still queued behind it, as in Vim — a motion with nowhere to go (j on the last line, h in the first column, f finding nothing), a search with no match, an error — which is what ends a recursive macro (qaq, then qa…@aq). A macro runs at every cursor when there are several. escape stops one that is running, and keys typed meanwhile wait until it has.
  • Visual I and A insert once, at the selection's edge, as Vim's do outside block mode: I at the start of the selection's first line (at the cursor, if you selected upward), A after the last selected character. Typing at every line is block visual's job, which multiple cursors do here.
  • EasyMotion jumps: <space><space> — the leader twice — labels every place the motion after it could land, anywhere on screen, and one or two keystrokes go there. w W b B for word starts, e E ge gE for word ends, j k for lines, f{char} F{char} t{char} T{char} the way the motions themselves work, s{char} looking both ways at once, 2s{char}{char} (any count) for that many characters, and /{chars}⏎ for as many as you care to type. The nearest targets get one-key labels and the rest two, the screen behind them dims, and the label you have typed narrows what is left. It is a motion like any other, so d<space><space>w deletes up to the target you pick and a jump in visual mode extends the selection. Escape, or any key that spells no label, calls it off. With the leader on the space bar a lone <space> still moves one character right — it runs as soon as the next key turns out not to open a jump, which is what Vim does with an ambiguous mapping.
  • Editor integration: gh pops the hover for the symbol under the cursor — its type and docs — and gd goes to its definition, like vscode-vim's bindings of the same names.
  • Registers: "x in front of any yank, delete, change or put — before or after a count ("a3yy, 3"ayy), in normal and visual mode, for x X s S D C Y as much as d c y p P gp gP:
    • "a–"z, and "A–"Z to append to them — lines after characters make the register linewise, as in Vim.
    • "0 holds the last yank; "1–"9 the last nine deletes of whole lines or across lines, shifting down with each one; "- the last delete within a line. Deletes with %, (, ), `, /, ?, n, N, { and } always go to "1 too, as Vim's do. "" is whichever register was written last.
    • "_ is the black hole: "_dd deletes without touching any register. Visual p puts the register over the selection and files the selection the way d would; visual P leaves every register as it was.
    • Read-only ". (the text the last insert typed), ": (the last command line) and "/ (the last search pattern): put them, or name them for :pu. A yank or delete into one does nothing.
    • "+ and "* are the system clipboard: a yank or delete into them copies to it (lines with a final line break, so other programs — and Vim — see lines), and a put reads whatever is on it right then, from this or any other program. The unnamed register stays separate from the clipboard, as in Vim without clipboard=unnamed.
    • The registers belong to the editor, not the document: "ayy in one file, "ap in another.
    • :reg[isters] and :di[splay] (optionally followed by the names to show, :reg a0) list what each register holds in a list over the editor — the status bar has room for one line — with a line break shown as ^J. escape closes it.
  • Marks: m{a-z} sets a mark in the document; 'a goes to its line (first non-blank, linewise) and `a to the very spot (charwise, exclusive). Both are motions, so d'a, y`a and vawork. Marks follow the text they were set on: lines added or deleted above move them,Jand:mcarry them along, and deleting a mark's line deletes the mark, as in Vim. Special marks:''/ ``` `` ``` where the last jump left from,'[/']the first and last character of the text last changed, yanked or put,'.the last change,'^where insert mode was left (wheregipicks up), and'</'>the last visual selection.m', m[, m], m<andm>set those by hand.m{A-Z}sets a file mark:'A` from any other document opens the one it was set in and lands on it.
    • Ex commands: marks as addresses (:'a,'bd, :'a to go there), :marks [names] lists them, :k{a-z} and :[range]ma[rk] {a-z} set one on a line, and :delm[arks] {marks} deletes some (:delm a-d x), :delm! every lowercase one.
  • Jumplist: G, gg, /, ?, n, N, *, #, %, (, ), {, }, H, M, L, mark jumps, :{line} and EasyMotion jumps leave '' behind and go on the jumplist; ctrl+o steps back through it and ctrl+i forward again, with counts. As in Vim it keeps one entry per line — the latest visit — and the last hundred jumps; a jump that went nowhere (G on the last line) leaves '' as it was. :ju[mps] lists it.
  • Multiple cursors: make them the VSCode way — cmd+alt+up/down for a column, cmd+d for the next occurrence, alt-click — and every key runs at all of them. I opens insert mode at each line's first non-blank, A at each end, i a o O where you'd expect; motions, operators, text objects, x, p, r and ctrl+a all work per cursor, and the status bar counts them; gv and gi remember a selection and an insert spot per cursor. Escape leaves insert mode with the cursors intact; escape again drops back to one. Selections made with cmd+d come in as visual mode at every cursor, so c changes each occurrence. What is shared is the mode, the registers, the marks and the search history; what runs once is : commands, undo, scrolling and gd, which already cover the document. Registers work like VSCode's own copy and paste: a yank or delete at several cursors fills the register with one piece per cursor, and a put at as many cursors gives each cursor its own piece — yiw then P doubles every cursor's word. A put at any other number of cursors (one included) puts every piece, top to bottom, one per line. The key counts as one register write however many cursors ran it: one "1 shift for a dd at three cursors, one "A append, one copy to the clipboard. Marks are set from the primary cursor (ma at every cursor is one mark a), ctrl+o and ctrl+i drop back to the primary cursor first, and the status bar reports what the primary cursor did.
  • Mouse clicks and drags work: a drag enters visual mode, and so does shift+arrow from normal mode. A selection a command leaves behind does not: the match still highlighted after cmd+f, or the placeholder a completion lands on, leaves the mode alone — the next key moves the cursor or types over the text, rather than dragging a visual selection along. Insert mode is left by escape and nothing else.

Settings

  • vimUltra.enabled (default true) — modal editing on at startup; Vim Ultra: Toggle Vim Mode flips it for the session.
  • vimUltra.escapeAlwaysExitsInsert (default true) — escape leaves insert mode even when a popup would otherwise eat it: with the suggestion widget, the parameter hints tooltip or an inline suggestion (Copilot ghost text) showing, it dismisses them and drops you into normal mode, rather than spending the key on the popup and leaving you still typing. Turn it off for VSCode's two-press behaviour.
  • vimUltra.leader (default <space>) — the leader key, written the way a Vim mapping is (<space>, ,, \).
  • vimUltra.easyMotion.enabled (default true), .trigger (default <leader><leader>), .markerKeys (default asdghklqwertyuiopzxcvbnmfj; — vim-easymotion's own, home row first; more keys means fewer two-key labels) and .dimBackground (default true).

Not yet implemented

Replace mode (R) and block visual (ctrl+v — the column editing it is for is what multiple cursors do here).

Repeating and macros stop short of Vim's in a few places. What . repeats is remembered per document, where Vim has one for the whole editor. Moving the cursor while inserting (an arrow key, a click) starts the insert over, as in Vim, so . then repeats an i of what was typed since; a macro cannot record the move, and keeps the text typed before it as though it had all been typed in one place. ctrl+d/u/f/b, mouse clicks and the editor's own commands are not recorded (the scroll keys do run when a register spells them out), there is no q: / q/ command-line window, no @=, and no :normal. Some keys that make Vim beep do not stop a macro here: u with nothing left to undo is VSCode's to report. A macro is cut off after 100,000 keys, or 1,000 registers deep (E169), rather than running until interrupted.

Registers stop short of Vim's in a few places. There is no "% / "# (file names), "= (expressions), :let @a, or ctrl+r in insert mode. ". is the text between where the insert began and where it ended (and a bracket the editor closed after the cursor), so backspacing past the start of an insert, or a completion that rewrites the word before it, gives what is left rather than Vim's record of the keys. Moving the cursor while inserting starts it over, as in Vim. A yank or delete into a read-only register does nothing, where Vim also beeps; a visual p with nothing in the register leaves the selection alone, where Vim deletes it first. "+ and "* are the same clipboard.

Marks are kept for as long as VSCode runs — nothing is saved between sessions, as Vim's viminfo does — and the numbered marks '0–'9 and '" are not there. The jumplist is the document's own: ctrl+o does not go back to the previous file, and 'A into another document is recorded in the document it left, not the one it lands in. :marks and :jumps count columns in UTF-16 units, where Vim counts bytes. '< and '> are the two ends of one remembered selection, so :delm < forgets both. '[ '] are not set by > and <, which VSCode's indent does.

Ex commands stop at the ones listed above: no :normal, :r, buffers, windows or tabs, and no | to chain commands. An unknown one says so in the status bar rather than doing something surprising. File names are not taken — :w other.ts, :e other.ts and :sav say they are not supported; VSCode's Save As and Open do those — and nor is writing part of a buffer (:'<,'>w). Saving a new, untitled document is VSCode's Save As, so :wq there leaves the newly saved file open. :sort has no b o x f (binary, octal, hex and float numbers) or l (locale). :> leaves the cursor on the first line it shifted where Vim leaves it on the last, and each > of :>> is an undo step of its own. Under :g, an empty command — Vim's print — reports how many lines match instead, and file commands and the listings (:reg, :marks, :jumps) are refused.

A visual selection stops at a line's last character and never takes the line break after it, so vi{ on braces that sit on their own lines selects the lines between without their final line break: d then leaves an empty line where vim leaves none (di{ without visual mode removes the lines). { } stop at empty lines only, where ( ), is/as and ip/ap also take nroff paragraph and section macros (.PP, .SH, …) and form feeds as boundaries, as vim does.

Some of the motions and actions stop short of Vim's details. | counts characters, not screen columns, so a tab is one. H and L land on the very first and last lines on screen: scrolloff (VSCode's cursorSurroundingLines) is not applied. ctrl+e/ctrl+y in visual mode, or with several cursors, let the cursor scroll off screen rather than drag it along, since VSCode would drop the selections to do that. A count before visual I or A does not repeat the typed text, a count given to . after a visual change is not used, and ctrl+a reads only decimal, hex and binary (Vim's default nrformats): no octal, no letters.

EasyMotion labels only the lines on screen, matches the characters of a f/s// jump literally and case-sensitively (no regexp, no smartcase), and stays out of the way while several cursors are up — a jump is one cursor's choice of one landing place. Targets past what two label keys can spell are dropped rather than left unreachable; with the default 27 keys that is 729 of them, well past a screenful.

Multiple cursors edit one document, so two of them reaching for the same text (dd on adjoining lines, d/foo⏎ across a neighbour) is a conflict: the editor rejects the whole transaction, the key does nothing, and the engine rebuilds its mirror from the document rather than guessing. Each cursor also reads the buffer as the cursors below it have already left it, so a motion that runs down past another cursor's edit sees the new text.

Pattern support stops at look-around (\@= and friends), \&, ~ for the previous substitute pattern, and multi-line matching: a match never spans a line break, so \n in a pattern matches nothing. Matching is case-sensitive unless the pattern says otherwise (ignorecase and smartcase are not implemented). Match highlights show while a pattern is being typed (incsearch) but clear once it runs — there is no persistent hlsearch. The c (confirm) flag on :s is rejected instead of silently replacing without asking. In a replacement, \n breaks the line like \r does, rather than inserting Vim's NUL.

Searching is bounded, not linear-time: patterns run on a backtracking VM, so a catastrophic one (\%(\w*\)*\d\{9} and relatives) is stopped by a step budget sized to the buffer — a tenth of a second or so — and reported as gave up: pattern is too slow to run here rather than passed off as "not found" or allowed to freeze the editor. Ordinary patterns are nowhere near it: most lines are rejected by a required-literal test before matching starts, which puts a whole-buffer search over a 1 MB file at well under a millisecond.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft