Providing full Vim emulation was not originally a goal of ModalEdit. The idea of the extension is to provide an engine that allows the user to map any key combination to any command provided by VS Code. However, most users equate modal editing with Vim and are familiar with its default keybindings. Vim users really love the powerful key sequences that combine editing operations with cursor motions or text ranges.
ModalEdit has also evolved by taking inspiration from Vim. Many capabilities were added with the motive to enable some Vim feature that was previously not possible to implement. With version 2.0 ModalEdit's functionality is now extensive enough to build a semi-complete Vim emulation. So, here we go...
Adding Vim keybindings as optional presets serves two purposes: it lowers the barrier to entry for Vim users who don't want to spend the time defining bindings from ground up. Secondly, Vim presets serve as an example to show how you can build sophisticated command sequences using the machinery provided by ModalEdit.
If you are not interested on how the Vim keybindings are implemented and just
want to use them, you can skip this discussion. Just import the presets by
pressing
We start with basic motion commands which are mostly straightforward to implement. Motions have two modes of operation: normal mode (moving cursor), and visual mode (extending selection). We make sure all motions work correctly in both modes. This allows us to reuse these keybindings when implementing more advanced operations. Our goal is to avoid repetition by building complex sequences from primitive commands.
In Vim, there are multiple key sequences for a same operation. For example,
you can convert a paragraph upper case by typing
Many ways to skin a cat...
The list of available cursor motion commands is shown below.
Keys | Cursor Motion |
---|---|
Enter |
Beginning of next line |
Space |
Next character on right |
h |
Left |
j |
Down |
k |
Up |
l |
Right |
0 |
First character on line |
$ |
Last character on line |
^ |
First non-blank character on line |
g_ |
Last non-blank character on line |
gg |
First charater in file |
G |
Last character in file |
w |
Beginning of next word |
e |
End of next word |
b |
Beginning of previous word |
W |
Beginning of next alphanumeric word |
B |
Beginning of previous alphanumeric word |
H |
Top of the screen |
M |
Middle of the screen |
L |
Bottom of the screen |
% |
Matching bracket |
Now, lets implement all the keybindings listed above.
{
"keybindings": {
Cursor can be advanced in a file with with enter and space. These are not technically motion commands but included for compatibility.
"\n": [
"cursorDown",
{
"command": "cursorMove",
"args": {
"to": "wrappedLineFirstNonWhitespaceCharacter"
}
}
],
" ": "cursorRight",
Move cursor up/down/left/right.
"h": "cursorLeft",
"j": "cursorDown",
"k": "cursorUp",
"l": "cursorRight",
Move to first/last character on line. These work also in visual mode.
"0": {
"command": "cursorMove",
"args": "{ to: 'wrappedLineStart', select: __selecting }"
},
"$": {
"command": "cursorMove",
"args": "{ to: 'wrappedLineEnd', select: __selecting }"
},
Move to first/last non-blank character on line. Also these ones use the
__selecting
flag to check whether we are in visual mode.
"^": {
"command": "cursorMove",
"args": "{ to: 'wrappedLineFirstNonWhitespaceCharacter', select: __selecting }"
},
"g": {
"_": {
"command": "cursorMove",
"args": "{ to: 'wrappedLineLastNonWhitespaceCharacter', select: __selecting }"
},
Moving to the beginning of file is defined as a conditional command to make it work in visual mode.
"g": {
"condition": "__selecting",
"true": "cursorTopSelect",
"false": "cursorTop"
},
Commands starting with
Keys | Command |
---|---|
gJ |
Join lines without space in between |
gu <motion> |
Convert text specified by <motion> to lowercase |
gU <motion> |
Convert text specified by <motion> to uppercase |
gt |
Go to next tab |
gT |
Go to previous tab |
Joining lines without space is done by deleting a character after the join command.
"J": [
"editor.action.joinLines",
"deleteRight"
],
The lower/uppercase transition works with any motion, but since we have not
defined all of them yet, we explain the command structure
later in the document.
The structure we use here is exactly the same as with
"u,U": {
"id": 1,
"help": "Change case with motion",
"u,U": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: __cmd.slice(0, -3) + 'V' + __rcmd[0] }"
},
"h,j,k,l,w,e,b,W,B,%": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __cmd.slice(0, -3) + __rcmd[0] + __rcmd[1] }"
},
"^,$,0,G,H,M,L": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[0] + __rcmd[1] }"
},
"g": {
"g,_": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
},
"f,t,F,T": {
"help": "Do until _",
" -~": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
},
"a,i": {
"help": "Do around/inside _",
"w,p,b,B,t, -/,:-@,[-`,{-~": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
},
"`,'": {
"help": "Do until mark _",
"a-z": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
}
},
"t": "workbench.action.nextEditor",
"T": "workbench.action.previousEditor"
},
Now we can complete the list of basic motion commands. This one movest the cursor at the end of the file and selects the range, if visual mode is on.
"G": {
"condition": "__selecting",
"true": "cursorBottomSelect",
"false": "cursorBottom"
},
The logic of separating words is bit different in VS Code and Vim, so we will
not try to imitate Vim behavior here. These keys are mapped to the most similar
motion available. The
"w": "cursorWordStartRight",
"e": "cursorWordEndRight",
"b": "cursorWordStartLeft",
"W": {
"command": "cursorWordStartRight",
"repeat": "__char.match(/\\W/)"
},
"B": {
"command": "cursorWordStartLeft",
"repeat": "__char.match(/\\W/)"
},
Moving cursor to the top, middle, and bottom of the screen is mapped to
"H": {
"command": "cursorMove",
"args": "{ to: 'viewPortTop', select: __selecting }"
},
"M": {
"command": "cursorMove",
"args": "{ to: 'viewPortCenter', select: __selecting }"
},
"L": {
"command": "cursorMove",
"args": "{ to: 'viewPortBottom', select: __selecting }"
},
Move to matching bracket command is somewhat challenging to implement
consistently in VS Code. This is due to the problem that there are no commands
that do exactly what Vim's motions do. In normal mode we call the
jumpToBracket
command which works if the cursor is on top of a bracket, but
does not allow for the selection to be extended. In visual mode we use the
smartSelect.expand
command instead to extend the selection to whatever
syntactic scope is above the current selection. In many cases, it is more useful
motion than jumping to a matching bracket, but using it means that we are
diverging from Vim's functionality.
"%": {
"condition": "__selecting",
"true": "editor.action.smartSelect.expand",
"false": "editor.action.jumpToBracket"
},
Advanced cursor motions in Vim include jump to character, which is especially
powerful in connection with editing commands. With this motion, we can apply
edits upto or including a specified character. The same motions work also as
jump commands in normal mode. We have to provide separate implementations for
normal and visual mode, since we need to provide different parameters to the
modaledit.search
command we are utilizing.
Keys | Cursor Motion |
---|---|
f <char> |
Jump to next occurrence of <char> |
F <char> |
Jump to previous occurrence of <char> |
t <char> |
Jump to character before the next occurrence of <char> |
T <char> |
Jump to character after the previous occurrence of <char> |
; |
Repeat previous f, t, F or T motion |
, |
Repeat previous f, t, F or T motion in opposite direction |
All of these keybindings are implemented using the incremental search command, just the parameters are different for each case. Basically we just perform either a forward or backward search and adjust the cursor position after the character has been selected. We also need to adjust cursor position before repeating the search.
The adjustment is done by invoking key bindings
"f": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"typeAfterAccept": "hv",
"typeBeforeNextMatch": "l",
"typeAfterNextMatch": "hv",
"typeAfterPreviousMatch": "v"
}
},
"F": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"backwards": true,
"typeAfterAccept": "v",
"typeAfterNextMatch": "v",
"typeBeforePreviousMatch": "l",
"typeAfterPreviousMatch": "hv"
}
},
"t": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"typeAfterAccept": "hhv",
"typeBeforeNextMatch": "ll",
"typeAfterNextMatch": "hhv",
"typeBeforePreviousMatch": "h",
"typeAfterPreviousMatch": "lv"
}
},
"T": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"backwards": true,
"typeAfterAccept": "lv",
"typeBeforeNextMatch": "h",
"typeAfterNextMatch": "lv",
"typeBeforePreviousMatch": "ll",
"typeAfterPreviousMatch": "hhv"
}
},
Repeating the motions can be done simply by calling nextMatch
or
previousMatch
.
";": "modaledit.nextMatch",
",": "modaledit.previousMatch",
You can also combine jump to bookmark motions with editing commands in Vim. Therefore, we define them along with the other motions. We use the bookmark commands provided by ModalEdit to implement these keybindings:
Keys | Cursor Motion |
---|---|
m <char> |
Define a bookmark and bind it to key <char> |
` <char> |
Jump to bookmark bound to key <char> |
' <char> |
Jump to the first non-blank character of the line where bookmark <char> resides |
Jump commands also work in visual mode.
"m": {
"help": "Define mark _",
"a-z": {
"command": "modaledit.defineBookmark",
"args": "{ bookmark: __rcmd[0] }"
}
},
"`": {
"a-z": {
"command": "modaledit.goToBookmark",
"args": "{ bookmark: __rcmd[0], select: __selecting }"
}
},
"'": {
"a-z": [
{
"command": "modaledit.goToBookmark",
"args": "{ bookmark: __rcmd[0], select: __selecting }"
},
{
"command": "cursorMove",
"args": "{ to: 'wrappedLineFirstNonWhitespaceCharacter', select: __selecting }"
}
]
},
Next, we define keybindings that switch between normal, insert, and visual mode:
Keys | Command |
---|---|
i |
Switch to insert mode |
I |
Move to start of line and switch to insert mode |
a |
Move to next character and switch to insert mode |
A |
Move to end of line and switch to insert mode |
o |
Insert line below current line, move on it, and switch to insert mode |
O |
Insert line above current line, move on it, and switch to insert mode |
v |
Switch to visual mode |
V |
Select current line and switch to visual mode |
These commands have more memorable names such as i
= insert, a
= append,
and o
= open, but above we describe what the commands do exactly instead
of using these names.
"i": "modaledit.enterInsert",
"I": [
"cursorHome",
"modaledit.enterInsert"
],
The a
has to check if the cursor is at the end of line. If so, we don't move
right because that would move to next line.
"a": [
{
"condition": "__char == ''",
"false": "cursorRight"
},
"modaledit.enterInsert"
],
"A": [
"cursorEnd",
"modaledit.enterInsert"
],
"o": [
"editor.action.insertLineAfter",
"modaledit.enterInsert"
],
"O": [
"editor.action.insertLineBefore",
"modaledit.enterInsert"
],
Note that visual mode is not really a mode. Basically we just set the
__selecting
flag that changes the behavior of normal mode commands. Nor is
there a separate line selection mode. We just mimic Vim's behavior using
VS Code's builtin commands that select ranges of text, when the __selecting
flag is on.
"v": "modaledit.toggleSelection",
"V": [
{
"command": "cursorMove",
"args": {
"to": "wrappedLineStart"
}
},
"modaledit.toggleSelection",
"cursorDownSelect"
],
Editing commands in normal mode typically either affect current character or line, or expect a motion key sequence at the end which specifies the scope of the edit. Let's first define simple commands that do not require a motion annex:
Keys | Command |
---|---|
x |
Delete character under cursor |
X |
Delete character left of cursor (backspace) |
r |
Replace character under cursor (delete and switch to insert mode) |
s |
Substitute character under cursor (same as r ) |
S |
Substitute current line (delete and switch to insert mode) |
D |
Delete rest of line |
C |
Change rest of line (delete and switch to insert mode) |
Y |
Yank (copy) rest of line |
p |
Paste contents of clipboard after cursor |
P |
Paste contents of clipboard at cursor |
J |
Join current and next line. Add space in between |
u |
Undo last change |
. |
Repeat last change |
"x": "deleteRight",
"X": "deleteLeft",
"r,s": [
"deleteRight",
"modaledit.enterInsert"
],
"S": {
"command": "modaledit.typeNormalKeys",
"args": {
"keys": "cc"
}
},
Deleting in Vim always copies the deleted text into clipboard, so we do that as well. If you are wondering why we don't use VS Code's cut command, it has a synchronization issue that sometimes causes the execution to advance to the next command in the sequence before cutting is done. This leads to strange random behavior that usually causes the whole line to disappear instead of the rest of line.
"D": [
"modaledit.cancelSelection",
"cursorEndSelect",
"editor.action.clipboardCopyAction",
"deleteRight",
"modaledit.cancelSelection"
],
Again, we utilize existing mappings to implement the
"C": {
"command": "modaledit.typeNormalKeys",
"args": {
"keys": "Di"
}
},
Yanking or copying is always done on selected range. So, we make sure that only rest of line is selected before copying the range to clipboard. Afterwards we clear the selection again.
"Y": [
"modaledit.cancelSelection",
"cursorEndSelect",
"editor.action.clipboardCopyAction",
"modaledit.cancelSelection"
],
Pasting text at cursor is done with
"p": [
"cursorRight",
"editor.action.clipboardPasteAction",
"modaledit.cancelSelection"
],
"P": [
"editor.action.clipboardPasteAction",
"modaledit.cancelSelection"
],
"J": "editor.action.joinLines",
Undoing last change is also a matter of calling built-in command. We clear the selection afterwards.
"u": [
"undo",
"modaledit.cancelSelection"
],
The last "simple" keybinding we define is
".": "modaledit.repeatLastChange",
So, far we have kept the structure of keybindings quite simple. Now we tackle the types of keybinding that work in tandem with motion commands. Examples of such commands include:
{}
a
We can combine any editing command with any motion, which gives us thousands of possible combinations. First type the command key and then motion which specifies the position or range you want to apply the command to.
Keys | Command |
---|---|
d <motion> |
Delete range specified by <motion> |
c <motion> |
Delete range specified by <motion> and switch to insert mode |
y <motion> |
Yank range specified by <motion> to clipboard |
> <motion> |
Indent range specified by <motion> |
< <motion> |
Outdent range specified by <motion> |
= <motion> |
Reindent (reformat) range specified by <motion> |
We can define all commands listed above in a single keybinding block. Remember that our strategy is just to map the key sequences of the edit commands that use motions to equivalent commands that work in visual mode. We do the specified motion in visual mode selecting a range of text, and then running the command on the selection. It does not matter which editing command we run, all of them can be mapped the same way.
"d,y,c,<,>,=": {
"id": 2,
"help": "Edit with motion",
The motions can be also divided to two categories: repeatable and non-repeatable. Some motions we can repeat, such as move to next character/word/line, but some we can only do once, such as move to end of line, beginning of file, or to a bookmark. Later on we make it possible to run repeatable motions n times by typing number n before a motion command or an editing command.
We can run all editing commands on the current line by repeating the command
key. For example V
command
to it and lastly the actual editing command. The whole logic resides in the JS
expression in the args
property below.
If you are wondering where the number prefix comes from as we don't have any
numbers in the path to our keybinding block, notice that we defined an id
for
our block above. We can use this id
to jump to the block from other keybinding
blocks. The entered key sequence __cmd
contains the full sequence entered by
the user, not just immediate sequence that lead to the block. So, we can extract
the number from the start of the __cmd
string.
"d,y,c,<,>,=": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: __cmd.slice(0, -2) + 'V' + (__rcmd[0] =='c' ? 'dO' : __rcmd[0]) }"
},
Another thing to note is that we actually have some logic when choosing what
command to run; we transform the
All the other repeatable commands can be defined in almost identical way. For
example the command to yank three words
"h,j,k,l,w,e,b,W,B,%": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __cmd.slice(0, -2) + __rcmd[0] + __rcmd[1] }"
},
Non-repeatable motions are even easier. We just basically rearrange the key
sequence and add
"^,$,0,G,H,M,L": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[0] + __rcmd[1] }"
},
"g": {
"g,_": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
},
Next motions jump to a character. They are handy when you want to edit text
until a specified character. For example the command
"f,t,F,T": {
"help": "Do until _",
" -~": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
},
Doing an edit inside set of delimiters like braces, parenthesis or quotation
marks can be done with the
"a,i": {
"help": "Do around/inside _",
"w,p,b,B,t, -/,:-@,[-`,{-~": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
},
The last motion you can combine with editing commands is jump to tag. It is
convenient when you want to edit long ranges of text that can't fit on screen.
We have two variants of the motion:
"`,'": {
"help": "Do until mark _",
"a-z": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: 'v' + __rcmd[1] + __rcmd[0] + __rcmd[2] }"
}
}
},
As stated above, you can repeat many motions and edit commands by prefixing them with number(s). All of the repeatable commands are listed below. We use a recursive keymap that loops in the same mapping while you type number keys. After you type letter(s), we invoke the command designated by the letters <num> times, or perform a jump command to line <num>.
Keys | Command |
---|---|
<num>h|j|k|l|w|e|b|W|B|% |
Repeat motion <num> times |
<num>u |
Undo <num> times |
<num>v|V |
Select <num> characters/lines |
<num>s|S |
Substitute (replace) <num> characters/lines |
<num>J |
Join <num> lines |
<num>gJ |
Join <num> lines without space in between |
<num>G|gg |
Jump to line <num> |
<num>gu <motion> |
Convert the range specified by <motion> repeated <num> times to lowercase |
<num>gU <motion> |
Convert the range specified by <motion> repeated <num> times to uppercase |
<num>d <motion> |
Delete the range specified by <motion> repeated <num> times |
<num>c <motion> |
Change the range specified by <motion> repeated <num> times |
<num>y <motion> |
Yank the range specified by <motion> repeated <num> times |
<num>> <motion> |
Indent the range specified by <motion> repeated <num> times |
<num>< <motion> |
Outdent the range specified by <motion> repeated <num> times |
<num>= <motion> |
Reformat the range specified by <motion> repeated <num> times |
The recursive part of the keymap defined below.
"1-9": {
"id": 3,
"help": "Repeat / go to line",
"0-9": 3,
If any of the repeatable motions is typed after a number, we just do that motion
<num> times. The parseInt
function extracts the number from the beginning of
the key sequence.
"h,j,k,l,w,e,b,W,B,u,%": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: __rcmd[0] }",
"repeat": "parseInt(__cmd)"
},
Repeating
"v": {
"command": "cursorRightSelect",
"repeat": "parseInt(__cmd)"
},
"V": {
"command": "expandLineSelection",
"repeat": "parseInt(__cmd)"
},
Also substitution commands can be repeated. In this case we just remap the key sequence with the parsed number in front.
"s": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: parseInt(__cmd) + 'vc' }"
},
"S": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: parseInt(__cmd) + 'cc' }"
},
We can join multiple lines at once, too. This works because the
editor.action.joinLines
joins all selected lines. We just have to clear the
selection afterwards.
"J": [
{
"command": "modaledit.typeNormalKeys",
"args": "{ keys: parseInt(__cmd) + 'VJ' }"
},
"modaledit.cancelSelection"
],
Jumping to a line in Vim is also done by entering first a number and then eiter
"G": [
{
"command": "revealLine",
"args": "{ lineNumber: parseInt(__cmd) - 1, at: 'top' }"
},
{
"command": "cursorMove",
"args": {
"to": "viewPortTop"
}
}
],
"g": {
"g": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: parseInt(__cmd) + 'G' }"
},
Joining lines without space in between is implemented by repeating the command.
"J": {
"command": "modaledit.typeNormalKeys",
"args": "{ keys: __cmd.slice(-2) }",
"repeat": "parseInt(__cmd)"
},
Repeating the complex editing commands is just a matter of jumping to their
keymap blocks. If 1
.
The rest of the editing commands are implemented in block 2
.
"u,U": 1
},
"d,c,y,<,>,=": 2
},
Searching introduces a pseudo-mode that captures the keyboard and suspends other commands as long as search is on. Searching commands are shown below.
Keys | Command |
---|---|
/ |
Start case-sensitive search forwards |
? |
Start case-sensitive search backwards |
n |
Select the next match |
p |
Select the previous match |
Note: Searching commands work also with multiple cursors. As in Vim, search wraps around if top or bottom of file is encountered.
"/": [
{
"command": "modaledit.search",
"args": {
"caseSensitive": true,
"wrapAround": true
}
}
],
"?": {
"command": "modaledit.search",
"args": {
"backwards": true,
"caseSensitive": true,
"wrapAround": true
}
},
"n": "modaledit.nextMatch",
"N": "modaledit.previousMatch",
Rest of the normal mode commands are not motion or editing commands, but do miscellaenous things.
Keys | Command |
---|---|
: |
Show command menu (same as |
zz |
Center cursor on screen |
ZZ |
Save file and close the current editor (tab) |
ZQ |
Close the current editor without saving |
Note that
":": "workbench.action.showCommands",
"z": {
"z": {
"command": "revealLine",
"args": "{ lineNumber: __line, at: 'center' }"
}
},
"Z": {
"help": "Z - Close and save, Q - Close without saving",
"Z": [
"workbench.action.files.save",
"workbench.action.closeActiveEditor"
],
"Q": "workbench.action.closeActiveEditor"
}
},
ModalEdit 2.0 adds a new configuration section called selectbidings
that has
the same structure as the keybindings
section. With it you can now map keys
that act as the lead key of a normal mode sequence to run a commands when
pressed in visual mode. For example keys
selectbindings
section is always checked first when ModalEdit looks for a
mapping for a keypress. If there is no binding defined in selectbindings
then it checks the keybindings
section. Note that you can still define normal
mode commands that work differently when selection is active. You can use either
a conditional or parameterized command to check the __selecting
flag, and
perform a different action based on that.
We define all the motions that do not yet work correctly in visual mode. The full list is below:
Keys | Command |
---|---|
h|j|k|l |
Select text to left/down/up/right |
w |
Select until beginning of next word |
e |
Select until end of word |
b |
Select until beginning of previous word |
W |
Select until beginning of next alphanumeric word |
B |
Select unting beginning of previous alphanumeric word |
f <char> |
Select until next occurrence of <char> including it |
F <char> |
Select until previous occurrence of <char> including it |
t <char> |
Select until next occurrence of <char> but not including it |
T <char> |
Select until previous occurrence of <char> but not including it |
a <char> |
Select text inside <char> including it |
i <char> |
Select text inside <char> but not including it |
aw |
Select current word including the whitespace around it |
iw |
Select current word not including the whitespace around it |
ap |
Select current paragraph including the whitespace around it |
ip |
Select current paragraph not including the whitespace around it |
a( | a) | ab |
Select text inside parenthesis including them |
i( | i) | ib |
Select text inside parenthesis not including them |
a{ | a} | aB |
Select text inside curly braces including them |
i{ | i} | iB |
Select text inside curly braces not including them |
a[ | a] |
Select text inside brackets including them |
i[ | i] |
Select text inside brackets not including them |
a< | a> | at |
Select text inside ange brackets (tag) including them |
i[ | i] | at |
Select text inside angle brackets (tag) not including them |
The basic movement commands are otherwise identical to normal mode defintions,
but the actual commands invoked are different. Roughly speaking, we just add
Select
at the end of each command.
"selectbindings": {
"l": "cursorRightSelect",
"h": "cursorLeftSelect",
"j": "cursorDownSelect",
"k": "cursorUpSelect",
"w": "cursorWordStartRightSelect",
"e": "cursorWordEndRightSelect",
"b": "cursorWordStartLeftSelect",
"W": {
"command": "cursorWordStartRightSelect",
"repeat": "__char.match(/\\W/)"
},
"B": {
"command": "cursorWordStartLeftSelect",
"repeat": "__char.match(/\\W/)"
},
Selecting forwards/backwards until a character is found can be implemented with
the modaledit.search
command as in normal mode. The difference is in what
parameters we use; we include the selectTillMatch
flag, and provide different
typeBefore...
and typeAfter...
key sequences.
"f": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"selectTillMatch": true
}
},
"F": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"backwards": true,
"selectTillMatch": true
}
},
"t": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"typeAfterAccept": "h",
"typeBeforeNextMatch": "l",
"typeAfterNextMatch": "h",
"typeBeforePreviousMatch": "h",
"typeAfterPreviousMatch": "l",
"selectTillMatch": true
}
},
"T": {
"command": "modaledit.search",
"args": {
"acceptAfter": 1,
"backwards": true,
"typeAfterAccept": "l",
"typeBeforeNextMatch": "h",
"typeAfterNextMatch": "l",
"typeBeforePreviousMatch": "l",
"typeAfterPreviousMatch": "h",
"selectTillMatch": true
}
},
Selecting text inside/around delimiters are motions that are only defined in visual mode. The motions can be used along with editing commands in normal mode, but obviously cannot be performed by themselves as they select ranges of text thus entering visual mode automatically.
All variants of these motions are implemented with the
modaledit.selectBetween
command.
The command takes start and end delimiters, which can be also regular
expressions, and selects the range between these delimiters. The scope of the
search is by default the current line, but for some variants we specify the
docScope
parameter which causes the search to consider the whole file.
"a,i": {
"help": "Select around/inside _",
"w": [
{
"command": "modaledit.selectBetween",
"args": "{ from: '\\\\W', to: '\\\\W', regex: true, inclusive: __rcmd[1] == 'a' }"
}
],
"p": [
{
"command": "modaledit.selectBetween",
"args": "{ from: '(?<=\\\\r?\\\\n)\\\\s*\\\\r?\\\\n', to: '(?<=\\\\r?\\\\n)\\\\s*\\\\r?\\\\n', regex: true, inclusive: __rcmd[1] == 'a', docScope: true }"
}
],
" -/,:-@,[-`,{-~": [
{
"command": "modaledit.selectBetween",
"args": "{ from: __rcmd[0], to: __rcmd[0], inclusive: __rcmd[1] == 'a' }"
}
],
"(,),b": [
{
"command": "modaledit.selectBetween",
"args": "{ from: '(', to: ')', inclusive: __rcmd[1] == 'a', docScope: true }"
}
],
"{,},B": [
{
"command": "modaledit.selectBetween",
"args": "{ from: '{', to: '}', inclusive: __rcmd[1] == 'a', docScope: true }"
}
],
"[,]": [
{
"command": "modaledit.selectBetween",
"args": "{ from: '[', to: ']', inclusive: __rcmd[1] == 'a', docScope: true }"
}
],
"<,>,t": [
{
"command": "modaledit.selectBetween",
"args": "{ from: '<', to: '>', inclusive: __rcmd[1] == 'a' }"
}
]
},
The last pieces of puzzle are editing commands that operate on selected text in visual mode. They are the same editing operations we already defined in normal mode, but remarkable simpler in this context. Since VS Code's operations already work on selected text, we only need to call the built-in commands and clear the selection afterwards.
Keys | Command |
---|---|
> |
Indent selection |
< |
Outdent selection |
= |
Reindent (reformat) selection |
d | x |
Delete (cut) selection |
c |
Change selection (cut and enter insert mode) |
y |
Yank (copy) selection |
u |
Transorm selection to lowercase |
U |
Transorm selection to upppercase |
Here are the implementations.
">": [
"editor.action.indentLines",
"modaledit.cancelSelection"
],
"<": [
"editor.action.outdentLines",
"modaledit.cancelSelection"
],
"=": [
"editor.action.formatSelection",
"modaledit.cancelSelection"
],
"d,x": [
"editor.action.clipboardCopyAction",
"deleteRight",
"modaledit.cancelSelection"
],
"c": [
"editor.action.clipboardCopyAction",
"deleteRight",
"modaledit.enterInsert"
],
"y": [
"editor.action.clipboardCopyAction",
"modaledit.cancelSelection"
],
"u": [
"editor.action.transformToLowercase",
"modaledit.cancelSelection"
],
"U": [
"editor.action.transformToUppercase",
"modaledit.cancelSelection"
]
}
}
The list of commands we provided is by no means exhaustive but still contains literally thousands of key combinations that cover the most commonly used Vim operations. This is quite a nice achievement considering that we only wrote about 600 lines of configuration, and most of it is pretty trivial. This demonstrates that ModalEdit's functionality is powerful enough to build all kinds of operations that make modal editors like Vim popular.