Around a year ago, I published the article “10 VS Code Vim Tricks to Boost Your Productivity ⚡”, which I shared various tips for the VSCodeVim extension. Turns out that the article was a hit, so allow me to share a few more VS Code Vim tips that I have up my sleeves 😆.
Prerequisites:
- You’ve installed the VSCodeVim extension
- Basic understanding of Vim keybindings. This guide is NOT for total beginners!
Table of Contents
- Navigation
- Vim Features
- Git
(Full Settings: settings.json
| keybindings.json
)
Navigation
#1 - Tabs Management
I prefix all my custom keybindings for managing editor tabs with the Shift
key so that it’s easy to remember. For tab navigation:
-
Shift + h
- Focus the left tab -
Shift + l
- Focus the right tab -
Shift + q
- Close current tab
Notice how intuitive they are, since h
and l
keys in Vim means left and right, and q
means “quit”.
To define the custom keybindings, open settings.json
by pressing "Ctrl+Shift+P" and search for “Preferences: Open User Settings (JSON)”. Then, add the following item:
{
"vim.normalModeKeyBindings": [
{
"before": ["H"], // Focus previous tab at the left
"commands": ["workbench.action.previousEditor"]
},
{
"before": ["L"], // Focus next tab at the right
"commands": ["workbench.action.nextEditor"]
},
{
"before": ["Q"], // Close current tab
"after": ["<C-w>", "q"]
}
]
}
As for reorganizing the order of tabs:
-
Shift + LeftArrow
- Move tab leftwards -
Shift + RightArrow
- Move tab rightwards
Setting up this keybinding is a bit tricky. VS Code has default keybindings where “Shift + LeftArrow” will select one character at the cursor’s left side (similar for “Shift + RightArrow”).
Actually we don’t need that since Vim’s Visual Mode can achieve the same thing via vh
and vl
keybindings. Thus, we need to disable VS Code’s default keybindings for Shift + Left/Right
.
Open keybindings.json
via “Ctrl + Shift + P” → “Preferences: Open Keyboard Shortcuts (JSON)” and append the following items:
[
{
"key": "shift+left",
"command": "-cursorColumnSelectLeft",
"when": "editorColumnSelection && textInputFocus"
},
{
"key": "shift+left",
"command": "-cursorLeftSelect",
"when": "textInputFocus && vim.mode != 'Visual'"
},
{
"key": "shift+left",
"command": "workbench.action.moveEditorLeftInGroup",
"when": "vim.mode == 'Normal'"
},
{
"key": "shift+right",
"command": "-cursorColumnSelectRight",
"when": "editorColumnSelection && textInputFocus"
},
{
"key": "shift+right",
"command": "-cursorRightSelect",
"when": "textInputFocus && vim.mode != 'Visual'"
},
{
"key": "shift+right",
"command": "workbench.action.moveEditorRightInGroup",
"when": "vim.mode == 'Normal'"
}
]
#2 - Splits Management
I prefix all splits-related keybindings with Ctrl
key, similar to Tip #1 where I prefix all tabs-related keybindings with Shift
key.
To create splits easily, I’ve defined 2 custom keybindings:
-
|
(Shift + \
) - Split tab vertically -
_
- Split tab horizontally
I picked these two symbols because it looks like the split direction (|
is a vertical stroke, _
is a horizontal stroke).
{
"vim.normalModeKeyBindings": [
{
"before": ["|"], // Split tab vertically
"after": ["<C-w>", "v"]
},
{
"before": ["_"], // Split tab horizontally
"after": ["<C-w>", "s"]
}
]
}
(💡 <C-w>
is a Vim keybinding syntax that means Ctrl + w
)
The Shift + q
keybinding we saw from Tip #1 can also close a split if it only has one tab 👇
To navigate around splits, I don’t like Vim’s <C-w> h/j/k/l
keybindings because I have to press Ctrl + w
every time. To reduce the number of keystrokes, I use Ctrl + h/j/k/l
instead:
{
"vim.normalModeKeyBindings": [
{
"before": ["<C-h>"], // Focus split window at left
"commands": ["workbench.action.focusLeftGroup"]
},
{
"before": ["<C-j>"], // Focus split window at right
"commands": ["workbench.action.focusBelowGroup"]
},
{
"before": ["<C-k>"], // Focus split window at above
"commands": ["workbench.action.focusAboveGroup"]
},
{
"before": ["<C-l>"], // Focus split window at below
"commands": ["workbench.action.focusRightGroup"]
}
]
}
⚠️ For Windows/Linux users, the Ctrl + k
keybinding may not work properly. You may see this message at the bottom bar:
(Ctrl + K) was pressed. Waiting for second key of chord…
This is because VS Code has a lot of default keyboard shortcuts that starts with Ctrl + k
(e.g. Ctrl+K Ctrl+S
to open keyboard shortcuts). As a fallback, you can use Vim’s original keybinding of Ctrl+w k
.
#3 - Multi-Cursor
VS Code’s multi-cursor editing is also supported in VS Code Vim.
Below are the built-in VS Code keybindings to enter multi-cursor mode:
- MacOS -
Cmd + d
- Windows/Linux -
Ctrl + d
Unfortunately for Windows/Linux users, pressing Ctrl + d
will instead scroll down the file, because it clashes with Vim’s keybinding of <C-d>
for scrolling down a file. As a workaround, I remapped Ctrl + d
to Alt + d
to enter multi-cursor mode. To do that, append these to keybindings.json
:
[
{
"key": "ctrl+d",
"command": "-editor.action.addSelectionToNextFindMatch",
"when": "editorFocus"
},
{
"key": "alt+d",
"command": "editor.action.addSelectionToNextFindMatch",
"when": "editorFocus"
}
]
Alternative to Cmd/Alt + d
, VS Code Vim also has a built-in keybinding of gb
to enter multi-cursor mode. Here’s a demo in MacOS for both the Cmd + d
and gb
keybindings:
VS Code Vim treats multi-cursor mode as a variant of Visual Mode, so you need to use Visual Mode keybindings to perform multi-cursor actions, such as:
-
Shift + i
- Move to beginning of selection and enter Insert Mode -
Shift + a
- Move to end of selection and enter Insert Mode -
s
orc
- Delete word and enter Insert Mode
For example, I need to press Shift + a
in order to start typing at the end of the word (unlike in Normal Mode where I only need to type a
):
#4 - Quick Search
To quickly search text from files, I’ve created the <leader>f
keybinding:
💡 <leader>
stands for Leader Key, which is commonly used as a prefix for user-defined shortcuts. By default it's the backslash key (\
), but we commonly map it to Space key (see vim.leader
option below)
{
"vim.leader": "<space>",
"vim.normalModeKeyBindings": [
{
"before": ["<leader>", "f"], // Search text
"commands": ["workbench.action.findInFiles"]
}
]
}
I also created a keybinding of <leader>s
to search all symbols in a workspace. This will save you a lot of time from locating the definition of a symbol.
{
"vim.normalModeKeyBindings": [
{
"before": ["<leader>", "s"], // Search symbol
"commands": ["workbench.action.showAllSymbols"]
}
]
}
For example, I want to directly jump to a React component called SaveButton
. To do that, I launch the symbol search via <leader>s
and enter the component name:
💡 Tip: If you place your cursor under a word (e.g. SaveButton
) and press <leader>s
, VS Code will automatically fill that word in the search bar 👇
Vim Features
#5 - Emulated Plugins
VS Code Vim emulates quite a number of Vim plugins. While I’m not going to walkthrough every plugin here (see docs for the full list), I want to highlight a plugin called vim-surround.
This plugin will save you tons of time when working with surrounding characters like paranthesis, brackets, quotes, and XML tags. Here’s a quick cheatsheet:
Surround Command | Description |
---|---|
y s <motion> <desired> |
Add desired surround around text defined by |
d s <existing> |
Delete existing surround |
c s <existing> <desired> |
Change existing surround to desired |
S <desired> |
Surround when in visual modes (surrounds full selection) |
For a full guide, please refer to vim-surround’s GitHub README.
Examples:
-
ysw)
- Add()
surround around a word -
ds]
- Delete the[]
that surrounds the cursor -
cs)]
- Changes the()
surrounding to[]
-
S}
- Surrounds the selected text with{}
block
#6 - Macros
Macro is a powerful feature in Vim that lets you record a series of commands and replay them. It’s useful to perform similar code changes for many times.
For example, let’s say I have a JavaScript object and I want to turn each value from a string to an array of strings. It’d be very tedious to manually edit line-by-line:
const data = {
item1: "one", // -> item1: ["one"]
item2: "two", // -> item2: ["two"]
...
}
Here’s the anatomy of macros:
- Record a macro:
q<register><commands>q
.- Begin a macro recording with
q
followed by<register>
(i.e. a single letter, which is like the name of a save slot). For example,qa
means I record a macro to a slot nameda
. -
<commands>
is the series of Vim keybindings you wish to perform. - End the recording by pressing
q
.
- Begin a macro recording with
- To play a macro:
@<register>
.-
<register>
is the single letter that you just recorded your macro to. For example,@a
will play the commands that I’ve previously recorded viaqa<commands>q
. - You can play the macro multiple times. For example,
4@a
plays the macro 4 times.
-
Back to our example. We basically want to repeat the action of adding a []
bracket around the string value. Let’s record our macro:
- Place our cursor at start of the object key name.
-
qa
- Begin recording the macro to slota
. -
f"
- Move the cursor to the first double quote. -
yst,]
- A vim-surround keybinding that we saw from Tip #5. It means adding a[]
bracket for the text from the current cursor’s position (first double quote) till the first comma (i.e.t,
). -
j0w
- Places the cursor in an appropriate location before the macro ends. We move one line down withj
, then move to beginning of first word with0w
.- 💡 The cursor’s end position matters because it affects the repeatability of the macro.
-
q
- Stops the macro recording.
To confirm the macro works, let’s replay it once via @a
. Note how the cursor perfectly lands at the start of next line (item3
), so that we can replay the macro consecutively via 4@a
(i.e. play 4 times):
Git
All my custom Git keybindings are prefixed with <leader>g
, where g
stands for git. I highly recommend grouping related keybindings with the same prefix so that it’s easier to remember 👍.
#7 - Peek Diff
If your cursor is located inside a Git hunk, I can press <leader>gp
to peek the diff changes (gp
= git peek).
{
"vim.normalModeKeyBindings": [
{
"before": ["<leader>", "g", "p"], // Peek Git diff for the changed line
"commands": ["editor.action.dirtydiff.next"]
}
]
}
#8 - Revert Hunk or Selected Lines
To revert a Git hunk (i.e. discard unstaged changes) in Normal Mode:
- Place my cursor inside the Git hunk
- Press
<leader>gr
(gr
= git revert)
{
"vim.normalModeKeyBindings": [
{
"before": ["<leader>", "g", "r"], // Revert hunk
"commands": ["git.revertSelectedRanges"]
}
]
}
To discard multiple hunks at the same time, I’ve also defined the same keybinding for Visual Mode:
{
"vim.visualModeKeyBindings": [
{
"before": ["<leader>", "g", "r"], // Revert hunk
"commands": ["git.revertSelectedRanges"]
}
]
}
Then, I can first select multiple hunks in Visual Mode, then use <leader>gr
to revert them:
#9 - Jump to Previous/Next Changes
Sometimes I have many Git changes across a long file. To quickly jump between changes:
-
<leader>gj
- Jump to next Git change (j
= down) -
<leader>gk
- Jump to previous Git change (k
= up)
{
"vim.normalModeKeyBindings": [
{
"before": ["<leader>", "g", "j"], // Jump to next Git change
"commands": ["workbench.action.editor.nextChange"]
},
{
"before": ["<leader>", "g", "k"], // Jump to previous Git change
"commands": ["workbench.action.editor.previousChange"]
}
]
}
#10 - Open File on Remote
If your Git repo has a remote upstream (e.g. GitHub, GitLab), you can easily get a sharable URL to the file you’re currently opening.
- Prerequisite: Install GitLens extension
- Add these keybindings to
settings.json
{
"vim.normalModeKeyBindings": [
{
"before": ["<leader>", "g", "g"], // Open file in GitHub
"commands": ["gitlens.openFileOnRemote"]
}
],
"vim.visualModeKeyBindings": [
{
"before": ["<leader>", "g", "g"], // Open file in GitHub
"commands": ["gitlens.openFileOnRemote"]
}
]
}
- Move your cursor on a line you wish to open on remote, or enter Visual Mode to select multiple lines
- Press
<leader>gg
(mnemonic: lastg
stands for GitHub)
To jump to a specific line on remote, use the keybinding in Normal Mode 👇
This also works for Visual Mode too. Try it yourself!
For the full settings code, they can be found below 👇
(I used .jsonc
instead of .json
in the Gists. Otherwise, the syntax highlighting will add an ugly red background to all comments since theoretically comments are not allowed in JSON 😢)
Thanks for reading. Don’t forget to like and share this post if you found it useful, cheers! 🙌
Top comments (4)
I can't believe there is no comment!!! You are my hero!!! Thanks a lot!
Thank you Buddy Soo Much
Amazing work. Thanks for sharing.
help me a lot!!