Modal Editing in VS CodeModalEdit is a simple but powerful extension that adds configurable "normal" mode to VS Code. The most prominent modal editor is Vim, which also inspired the development of ModalEdit. It includes Vim commands as presets you can import, but ModalEdit's true power comes with its configurability. You can emulate existing editors like Vim or Kakoune or build your keyboard layout from ground up and add exactly the features you need. As in Vim, the goal of the extension is to save your keystrokes and make editing as efficient as possible. Unlike most Vim emulators, ModalEdit leverages the built-in features of VS Code. You define your keybindings using commands provided by VS Code and other extensions. You can build complex operations by arranging commands into sequences. You can define conditional commands that do different things based on editor state. Also, you can map these commands to arbitrarily long keyboard sequences.
Getting StartedWhen extension is installed text documents will open in normal mode. The current mode is shown in the status bar. You can switch between modes by clicking the pane in the status bar. In normal mode keys don't output characters but invoke commands. You can
specify these commands in the
To define the key mappings used in normal mode, add a property named
When you save the ModalEdit adds a regular VS Code keyboard shortcut for Selections/Visual ModeModalEdit does not have a separate selection/visual mode as Vim has. It is possible to select text both in normal mode and insert mode. However, since it is typical that commands in normal have different behavior when selection is active, the status bar text changes to indicate that. You can change the text shown in status bar using configuration parameters ModalEdit defines a new command ConfigurationYou can define the normal mode commands in four different ways. It is also possible to combine them freely. Single CommandThe simplest way is to map a key to a single command. This has the format:
The The example in the previous section maps the Commands with ArgumentsSome commands take arguments. For example
The
The If returned value is not a number, the expression is treated as a condition that is evaluated after the command has run. The command is repeated as long as the expression returns a truthy value. Below is an example that maps key
Sequence of CommandsTo construct more complex operations consisting of multiple steps, you can define command sequences. Commands in a sequence will be run one after another. A sequence is defined as an array.
In above, The next example maps the
Conditional CommandsFor even more complex scenarios, you can define commands that run different commands depending on a specified condition. The most common use case for this is to run a different command when selection is active. The format of a conditional commands is:
Here Below is an example that moves cursor one word forward with
Binding Key SequencesWhen you want to define a multi-key sequence, nest the key bindings. You can define a two key command using the following format.
Again, the The example below defines two commands that are bound to key sequences
Defining Recursive KeymapsVersion 1.5 of ModalEdit introduced the possibility to create recursive keymaps. With this feature you can define arbitrarily long keyboard sequences. This is useful, for example, for creating commands that you can repeat by entering first a number followed by a command key. Keymaps got two new features to enable this functionality. Key RangesYou can add multiple characters to a keybinding comma Keymap IDsBy giving keymap a numeric ID, you can refer to it in another (or same) keymap.
With key ranges, this allows you to create a binding that can take theoretically
infinitely long key sequence. The example below shows how you can define
commands like
We give the keymap attached to key range The picture below illustrates how keymap and command objects are stored in memory. It is also possible to jump to another keymap, which enables even more complicated keyboard sequences. The only restriction is that you can only jump to a key binding which is already defined. I.e. you cannot refer to an ID of a keymap that appears later in the configuration.
Another new feature used in the example above is the optional Keybindings in Selection/Visual ModeModalEdit 2.0 adds a new configuration section called For example, you might want the
Debugging KeybindingsIf you are not sure that your bindings are correct, check the ModalEdit's output log. You can find it by opening View - Output and then choosing the ModalEdit from the drop-down menu. Errors in configuration will be reported there. If your configuration is ok, you should see the following message. Changing CursorsYou can set the cursor shape shown in each mode by changing the following settings.
The possible values are:
Changing Status BarWith version 2.0, you can also change the text shown in status bar in each mode
along with the text color. Note that you can add icons in the text by using
syntax The color of the status text is specified in HTML format, such as
Start in Normal ModeIf you want VS Code to be in insert mode when it starts, set the
Example ConfigurationsYou can find example key bindings here. These are my own settings. The cheat sheet for my keyboard layout is shown below. I have created it in http://www.keyboard-layout-editor.com/. Please note that my keyboard layout is Finnish, so the non-alphanumeric keys might be in strange places. As you can see, I haven't followed Vim conventions but rather tailored the keyboard layout according to my own preferences. I encourage you to do the same. In general, you should not try to convert VS Code into a Vim clone. The editing
philosophies of Vim and VS Code are quite dissimilar. Targets of Vim operations
are defined with special range commands, whereas VS Code's commands operate on
selected text. For example, to delete a word in Vim, you first press To better understand the difference, check out Kakoune editor's documentation. ModalEdit extends VS Code with normal mode editing, so you have more or less the same capabilities as in Kakoune. Additional VS Code CommandsModalEdit adds few useful commands to VS Code's repertoire. They help you create more Vim-like workflow for searching and navigation. Switching between ModesUse the following commands to change the current editor mode. None of the commands require any arguments.
Incremental SearchThe standard search functionality in VS Code is quite clunky as it opens a dialog which takes you out of the editor. To achieve more fluid searching experience ModalEdit provides incremental search commands that mimic Vim's corresponding operations.
|
Argument | Type | Default | Description |
---|---|---|---|
backwards |
boolean |
false |
Search backwards. Default is forwards |
caseSensitive |
boolean |
false |
Search is case-sensitive. Default is case-insensitive |
wrapAround |
boolean |
false |
Search wraps around to top/bottom depending on search direction. Default is off. |
acceptAfter |
number |
undefined |
Accept search automatically after x characters has been entered. This helps implementing quick one or two character search operations. |
selectTillMatch |
boolean |
false |
Select the range from current position till the match instead of just the match. Useful with acceptAfter to quickly extend selection till the specified character(s). |
typeAfterAccept |
string |
undefined |
Allows to run normal mode commands through key bindings (see modaledit.typeNormalKeys command) after successful search. The argument can be used to enter insert mode, or clear selection after search, for example. |
typeBeforeNextMatch |
string |
undefined |
Run the specified key commands before searhing for the next match. |
typeAfterNextMatch |
string |
undefined |
Run the specified key commands after the next match command is executed. |
typeBeforePreviousMatch |
string |
undefined |
Run the specified key commands before searhing for the previous match. |
typeAfterPreviousMatch |
string |
undefined |
Run the specified key commands after the previous match command executed. |
modaledit.cancelSearch
Cancels the incremental search, returns the cursor to the starting position, and switches back to normal mode.
modaledit.deleteCharFromSearch
Deletes the last character of the search string. By default the backspace key is bound to this command when ModalEdit is active and in search mode.
modaledit.nextMatch
Moves to the next match and selectes it. Which way to search depends on the search direction.
modaledit.previousMatch
Moves to the previous match and selectes it. Which way to search depends on the search direction.
Bookmarks
To quickly jump inside documents ModalEdit provides two bookmark commands:
modaledit.defineBookmark
stores the current position in a bookmark, andmodaledit.goToBookmark
jumps to the given bookmark.modaledit.showBookmarks
shows the defined bookmarks in the command bar and allows jumping to them by selecting one.
The first two commands take one argument which contains the bookmark name. It
can be any string (or number), so you can define unlimited number of bookmarks.
If the argument is omitted, default value 0
is assumed.
{
"command": "modaledit.defineBookmark",
"args": {
"bookmark": "0"
}
}
Quick Snippets
Snippets come in handy when you need to insert boilerplate text. However, the problem with snippets is that very seldom one bothers to create a new one. If a snippet is used only a couple of times in a specific situation, the effort of defining it nullifies the advantage.
With ModalEdit, you can create snippets quickly by selecting a region of text
and invoking command modaledit.defineQuickSnippet
. You can assign the snippet
to a register by specifying its index as an argument.
{
"command": "modaledit.defineQuickSnippet",
"args": {
"snippet": 1
}
}
Use the modaledit.insertQuickSnippet
command to insert the defined snippet at
the cursor position. It takes the same argument as modaledit.defineQuickSnippet
.
A snippet can have arguments or placeholders which you can fill in after
inserting it. These are written as $1
, $2
, ... inside the snippet. You can
quickly define the arguments with the modaledit.fillSnippetArgs
command. First
multi-select all the arguments (by pressing Alt
while selecting with a mouse),
then run the command. After that, select the snippet itself and run the
modaledit.defineQuickSnippet
command.
In the following example key sequence q - a
fills snippet arguments,
q - w - 1
defines snippet in register 1, and q - 1
inserts it.
It is usually a good idea to run editor.action.formatDocument
after inserting
a snippet to clean up whitespace. You can do this automatically adding it
to the command sequence.
"q": {
"1": [
{
"command": "modaledit.insertQuickSnippet",
"args": {
"snippet": 1
}
},
"editor.action.formatDocument"
],
}
Invoking Key Bindings
The new command modaledit.typeNormalKeys
invokes commands through key
bindings. Calling this command with a key sequence has the same effect as
pressing the keys in normal mode. This allows you to treat key bindings as
subroutines that can be called using this command.
The command has one argument keys
which contains the key sequence as string.
Assuming that keys k
and u
are bound to some commands, the following
example runs them both one after another.
{
"command": "modaledit.typeNormalKeys",
"args": {
"keys": "ku"
}
}
Selecting Text Between Delimiters
The modaledit.selectBetween
command helps implement advanced selection
operations. The command takes as arguments two strings/regular expressions that
delimit the text to be selected. Both of them are optional, but in order for the
command to do anything one of them needs to be defined. If the from
argument
is missing, the selection goes from the cursor position forwards to the to
string. If the to
is missing the selection goes backwards till the from
string. In addition to these parameters, the command has four flags:
- If the
regex
flag is on,from
andto
strings are treated as regular expressions in the search. - The
inclusive
flag tells if the delimiter strings are included in the selection or not. By default the delimiter strings are not part of the selection. - The
caseSensitive
flag makes the search case-sensitive. When this flag is missing or false the search is case-insensitive. - By default the search scope is the current line. If you want search inside
the whole document, set the
docScope
flag.
Below is an example that selects all text inside quotation marks. For more advanced examples check the tutorial.
{
"command": "modaledit.selectBetween",
"args": {
"from": "(",
"to": ")"
}
}
Repeat Last Change
modaledit.repeatLastChange
command repeats the last command (sequence) that
caused text in the editor to change. It corresponds to the dot .
command
in Vim. The command takes no arguments.
Importing Presets
In version 2.0, new command modaledit.importPresets
was introduced. It reads
keybindings from a file and copies them to the global settings.json
file. It
overrides existing keybindings, so back them up somewhere before running the
command, if you want to preserve them.
In 2.0, also Vim keybindings were added as built-in presets. You can learn more
about Vim bindings here. Built-in presets are located under the presets
folder under the extension installation folder. The command scans and lists all
the files there. It also provides an option to browse for any other file you
want to import.
Presets are stored either in a JSON or JavaScript file. In either case, the file to be imported should evaluate to an object which should have at least one of the following properties:
{
"keybindings": { ... },
"selectbindings": { ... }
}
Both of the properties must follow the configuration structure defined above. It is also possible to define the object in JS. In that case the object should be the expression that the whole script evaluates to.
Acknowledgements
I was using the Simple Vim extension for a long time, but was never fully happy with it. It shares the idea of being a simple extension reusing VS Code's functionality, but it is sorely lacking in configurability. If you don't like its default key mappings, you are out of luck.
Then I found extension called Vimspired which has a really great idea for
implementing modal editing: just add a section in the settings.json
which
contains the keymap for normal mode. This allows you to mimic Vim behavior, if
you wish to do so, or take a completely different approach. For example, don't
use h
, j
, k
, l
keys to move the cursor but w
, a
, s
, d
keys
instead.
I really like Vimspired, but still wanted to change some of its core behavior and add many additional features. I didn't want to harass the author with extensive pull requests, so I decided to implement my own take of the theme. I shameleslly copied the core parts of Vimspired and then changed them beyond recognition. Anyway, credit goes to Brian Malehorn for coming up with the great idea and helping me jump start my project.