Grow shortcut trees in Visual Studio Code with Which Key

Which Key is an editor feature to help you navigate a tree of customisable keyboard shortcuts with a lens on accessible sub-branches:

Which Key for Emacs and Which Key for Neovim are both popular, and the Helix editor ships with the feature by default, where it’s bound to the space key:

Helix editor showing Which Key feature to narrow shortcut choices through a tree of options.

It works well to orient new users but it’s great for power users too, who can grow a personal tree of shortcuts instead of putting up with default keybindings.

Shortcut chords in VS Code

VS Code already supports shortcut “chords” — a chain of keypress actions that trigger a command when you complete the sequence. By default on macOS ⌘K followed by ⌘C will comment out the current line in the editor, and like Emacs you keep ⌘ held down; no need to lift command between pressing k and c, hence “chord”.

There are 46 chorded options after ⌘K by default. But VS Code won’t give you hints about what they are (despite a 2016 feature request). You have to discover and remember the sequence you care about, like learning the church organ or studying Tekken combos.

When you press ⌘K in VS Code it assumes you know what to press next, which — speaking for myself — is far too generous:

Chord prompt from VS Code showing the less than helpful text, “Command K was pressed. Waiting for second key of chord…”.

Which Key for VS Code

With Which Key for VS Code it doesn’t have to be this way. The extension helps VS Code users grow their own shortcut trees of mini menus with built-in hints.

I press ⌘L (for ‘list’) in VS Code to summon my root Which Key menu:

Which Key prompt from VS Code showing a list of helpful submenu options together with their shortcut keys.

I release ⌘L and press “g” to show my git commands submenu:

Custom Which Key git submenu in VS Code showing a list of possible git operations and their shortcut keys.

Helpful git commands are one more keypress away. They use mnemonic sequences that make sense to me, because it was me who chose them and not Microsoft (‘git init’ → ⌘L g i, ‘GitHub’ → ⌘L g h, and so on).

An option for the impatient

If you like the look of this stuff but don’t want to spend time configuring custom commands and shortcuts, you could install the VSpaceCode extension. It comes with a bunch of shortcuts that someone who probably isn’t you configured.

The VSpaceCode defaults are fine and they’ll be faster to start using. But you’ll end up with menu items you don’t care about, missing commands from your daily workflow, as well as mnemonics that are pretty good but perhaps not what you would have picked, simply through the gift and curse of not sharing a brain with the plugin’s authors.

An option for the dedicated

For me it’s better to grow your own tree of shortcuts from scratch with the commands you really want. To build custom trees like this you don’t need VSpaceCode, just Which Key (from the same lovely authors):

  1. Install the Which Key extension. (Press ⌘P and type ext install VSpaceCode.whichkey.)

  2. Open VS Code’s Command Palette (⇧⌘P) and type enough of “Preferences: Open User Settings (JSON)” that the option appears, then select it and press enter.

    The file that opens should be named settings.json. (If it’s named keybindings.json or defaultSettings.json or Settings without the .json or anything else, close it and look for “User Settings (JSON)” from the menu again.)

    Configure your Which Key bindings tree with JSON. Some pointers:

    • For the ‘name’ values, I try to use the convention of appending ‘…’ if the action will open another prompt or submenu. The absence of the ellipsis tells me I’m about to run a command straight away instead of triggering another prompt or menu.

    • For the ‘key’ values, it’s best to use a bare key like ‘f’ over a combo like ‘cmd+f’. The latter won’t work because it conflicts with “find in file”.

    • I’ve omitted my full config to encourage you to make your own instead of copying mine.

    "whichkey.bindings": [
        {
           "key": "g",
           "name": "git…",
           "type": "bindings",
           "bindings": [
               {
               "key": "i",
               "name": "init",
               "type": "command",
               "command": "git.init"
               },
               {
               "key": "g",
               "name": "status",
               "type": "command",
               "command": "workbench.view.scm"
               },
               {
               "key": "c",
               "name": "checkout…",
               "type": "command",
               "command": "git.checkout"
               },
               {
               "key": "P",
               "name": "push",
               "type": "command",
               "command": "git.push"
               },
               {
               "key": "F",
               "name": "pull",
               "type": "command",
               "command": "git.pull"
               },
               {
               "key": "f",
               "name": "fetch",
               "type": "command",
               "command": "git.fetch"
               },
               {
               "key": "s",
               "name": "stage",
               "type": "command",
               "command": "git.stage"
               },
               {
               "key": "S",
               "name": "stage selected",
               "type": "command",
               "command": "git.stageSelectedRanges"
               },
               {
               "key": "u",
               "name": "unstage",
               "type": "command",
               "command": "git.unstage"
               },
               {
               "key": "U",
               "name": "unstage selected",
               "type": "command",
               "command": "git.unstageSelectedRanges"
               },
           ]
        },
        {
           "key": "p",
           "name": "project…",
           "type": "bindings",
           "bindings": [
               {
               "key": "o",
               "name": "open",
               "type": "command",
               "command": "workbench.action.openRecent"
               }
           ]
        },
    ],
    

    To get the command ID strings like workbench.action.openRecent:

    i. Find the command in the command palette (⇧⌘P) and click the Configure Keybinding icon:

    Visual Studio Code command palette showing the Open User Settings command.'

    ii. Copy the command ID by second-clicking the command name (or copying the text after @command: in the search filter at the top):

    Visual Studio Code keybindings list showing the Copy Command ID contextual option.'

    This works for commands from installed extensions too.

  3. Finally, bind a key to open the root Which Key menu in VS Code’s keybindings.json.

    This is separate to the settings.json config you’ve just been editing. Open it with ⇧⌘P via the command called “Preferences: Open Keyboard Shortcuts (JSON)”.

    I use ⌘L to show Which Key:

      {
        "key": "cmd+l",
        "command": "whichkey.show"
      },
    

    The shortcut works even if focus is in a non-editor pane. You get quick access to your Which Key menu throughout VS Code.