Build your own Vim emulation for VS Code

栏目: IT技术 · 发布时间: 4年前

内容简介:This might sound difficult, and granted, the learning curve is steep. But after you are accustomed to this way of editing, it is very difficult to go back. For example, having to use arrow keys to move cursor feels uncomfortable. The proof of this is that

Build Your Own Vim Emulation for VS Code

Build your own Vim emulation for VS Code Vim is the great white shark of text editors. It has survived among modern IDEs because of its unique philosophy. It supports modal editing which is almost an opposite approach to how other editors work. You start in "normal" mode where you can navigate and edit text but not insert it. When you want to enter text, you switch to "insert" mode where you can type new text but do nothing else. So, you end up bouncing between modes as you edit your file.

This might sound difficult, and granted, the learning curve is steep. But after you are accustomed to this way of editing, it is very difficult to go back. For example, having to use arrow keys to move cursor feels uncomfortable. The proof of this is that nearly all popular text editors have some kind of add-in that provides Vim emulation. VS Code has several of them.

So, why write yet another extension that provides Vim emulation? Well, the problem is that most emulators try to make VS Code behave exactly like Vim which quickly becomes an exercise in futility. Trying to mimick Vim's functionality too closely results in disjointed user experience as it has a lot of features that do not have counterparts in VS Code.

ModalEdit's approach is to utilize VS Code's existing features and just add the concept of modal editing to the mix. Turns out implementing modal editing is extremely simple. We basically provide pair of commands that switch between normal mode and insert mode.

To complete the feature set, we need an ability to bind normal mode key sequences to VS Code's commands. ModalEdit accomplishes this by defining the key bindings in the VS Code's standard configuration file settings.json . It allows binding any key to any command, or a sequence of commands. It supports also conditional commands that do different things based on editor state. Refer to theREADME page for the details of the configuration options.

With these capabilities we can start building our Vim emulation. We don't have to use Vim's standard key bindings, if we prefer not to. You can map any key (sequence) to any command. But to keep things familiar, we'll follow Vim's conventions in this tutorial.

This document is actually a literate settings.json file . That is, an actual configuration file that contains documentation in comments. The upshot of this is that I can test the key bindings right away while writing this document. Saving the file makes ModalEdit apply the bindings. The web page you are reading is generated from the file using LiTScript. SeeLiTScript home page, if you want to learn more about that.

Let's start configuring our Vim emulator by adding the ModalEdit's configuration block for key bindings. We'll use the Vim Cheat Sheet as our specification for key bindings to be added.

{
    "modaledit.keybindings": {

Switching Between Modes

First things first: we need to be able to enter the normal mode somehow. The Esc key is mapped to the modaledit.enterNormal command by default, so we dont't need to do anything for that. If you like, you can map other keys to this command using VS Code's standard keymappings pressing Ctrl+K Ctrl+S .

Insert Text

There are multiple ways to enter insert mode. If you want to insert text in the current cursor position, you press i .

"i": "modaledit.enterInsert",

To insert text at the beginning of line, you press I . For this operation, we need a command sequence, i.e. an array of commands.

"I": [
            "cursorHome",
            "modaledit.enterInsert"
        ],

Append Text

Appending text works analogously; a appends text after current character and A at the end of the line. There is a special case, though. If cursor is already at the last character of the line, it should not move. This is why we use a conditional command to move the cursor only, if the current character is not 0 which marks the end of the line. A conditional command is an object that contains the condition property. The value of the property is a JS expression which ModalEdit evaluates. It selects the command based on the result. In this case, the result false will execute the cursorRight command.

"a": [
            {
                "condition": "__char == 0",
                "false": "cursorRight"
            },
            "modaledit.enterInsert"
        ],
        "A": [
            "cursorEnd",
            "modaledit.enterInsert"
        ],

Open a New Line

The third way to enter insert mode is to open a line. This means creating an empty line, and putting the cursor on it. There are two variants of this command as well: o opens a new line below the current line whereas O opens it on the current line.

"o": [
            "editor.action.insertLineAfter",
            "modaledit.enterInsert"
        ],
        "O": [
            "editor.action.insertLineBefore",
            "modaledit.enterInsert"
        ],

Now we can test the commands we just created.

Build your own Vim emulation for VS Code

Cursor Movement

The next task in hand is to add commands for moving the cursor. As all Vim users know, instead of arrow keys, we move the cursor with h , j , k , and l keys. Before implementing these, let's talk a bit about text selection.

Selecting Text

In Vim, there is a separate "visual" mode that you activate when you want to select text. Visual mode can be characterwise or linewise. VS Code has no simillar concept. By contrast, to select text you move the cursor with Shift key depressed.

ModalEdit bridges this gap by providing the special command modaledit.toggleSelection which toggles selection mode on and off. Selection mode is not really a mode in the same sense as normal and insert mode are; you can select text both in normal mode and insert mode. Rather it is an additional flag that you can set when you want to select text as you move the cursor.

Selection mode is also implicitly on whenever there is text selected. If you select text with mouse, for example, the selection mode is turned on (Vim behaves like this too). To turn off the selection mode, call modaledit.toggleSelection again (or modaledit.clearSelection ).

The end result is that selection mode works almost like visual mode in Vim, the main difference being that selection mode is not automatically turned off when you enter insert mode.

So, let's add a binding to toggle selection mode on or off. We use the familiar v key for this.

"v": "modaledit.toggleSelection",

Now we can add commands for cursor movement. These commands use the generic cursorMove command which takes arguments. The arguments we use are partly constant and partly dynamic. Therefore, we use ModalEdit's feature which allows us to define parameters as a JS expression. The __selecting flag in the expression indicates whether selection mode is on. The same effect could be achieved also with a conditional command, but this way is a bit simpler.

"h": {
            "command": "cursorMove",
            "args": "{ to: 'left', select: __selecting }"
        },
        "j": {
            "command": "cursorMove",
            "args": "{ to: 'down', select: __selecting }"
        },
        "k": {
            "command": "cursorMove",
            "args": "{ to: 'up', select: __selecting }"
        },
        "l": {
            "command": "cursorMove",
            "args": "{ to: 'right', select: __selecting }"
        },

We can also simulate linewise visual mode using VS Code's expandLineSelection command. Note that we don't need to call modaledit.toggleSelection this time as selection mode is turned on automatically.

"V": "expandLineSelection",

Moving Inside Screen

To move cursor quickly to the top, middle, or bottom of the screen we use keys H , M , and L . Again, we need to use the cursorMove command .

"H": {
            "command": "cursorMove",
            "args": "{ to: 'viewPortTop', select: __selecting }"
        },
        "M": {
            "command": "cursorMove",
            "args": "{ to: 'viewPortCenter', select: __selecting }"
        },
        "L": {
            "command": "cursorMove",
            "args": "{ to: 'viewPortBottom', select: __selecting }"
        },

Jumping to Previous/Next Word

Other commonly used navigation commands in Vim include w and b which move the cursor to the start of the next and previous word. For these we need to use conditional commands because cursorMove falls short in this use case.

"w":  {
            "condition": "__selecting",
            "true": "cursorWordStartRightSelect",
            "false": "cursorWordStartRight"
        },
        "b": {
            "condition": "__selecting",
            "true": "cursorWordStartLeftSelect",
            "false": "cursorWordStartLeft"
        },

e jumps to the end of the next word.

"e": {
            "condition": "__selecting",
            "true": "cursorWordEndRightSelect", 
            "false": "cursorWordEndRight"
        },

Note: We omit variants of these commands W , B , and E which skip the punctuation characters. There are no built-in commands in VS Code that work exactly like those in Vim. This is one of the subtle differences between the editors.

Jumping to Start/End of Line

In the similar vein, we'll throw in commands for jumping to the beginning 0 , to the first non-blank character ^ , and to the end of line $ .

"0": {
            "command": "cursorMove",
            "args": "{ to: 'wrappedLineStart', select: __selecting }"
        },
        "^": {
            "command": "cursorMove",
            "args": "{ to: 'wrappedLineFirstNonWhitespaceCharacter', select: __selecting }"
        },
        "$": {
            "command": "cursorMove",
            "args": "{ to: 'wrappedLineEnd', select: __selecting }"
        },

A lesser known variant of above commands is g_ that jumps to the last non-blank character of the line. Since it is a two key sequence we need to open a block for all commands beginning with g .

"g": {
            "_": {
                "command": "cursorMove",
                "args": "{ to: 'wrappedLineLastNonWhitespaceCharacter', select: __selecting }"
            },

Jumping to Start/End of Document

Another command beginning with g is gg which jumps to the beginning of the file.

"g": {
                "condition": "__selecting",
                "true": "cursorTopSelect", 
                "false": "cursorTop"
            },
        },

The opposite of that is G wich jumps to the end of file.

"G": {
            "condition": "__selecting",
            "true": "cursorBottomSelect", 
            "false": "cursorBottom"
        },

Jump to Character

We have the basic movement commands covered, so let's move on to more sophisticated ones. Seasoned Vim users avoid hitting movement commands repeatedly by using f and F keys which move directly to a given character. VS Code provides no built-in command for this, but ModalEdit includes an incremental search command which can be customized to this purpose.

"f": {
            "condition": "__selecting",
            "true": {
                "command": "modaledit.search",
                "args": { 
                    "caseSensitive": true, 
                    "acceptAfter": 1, 
                    "selectTillMatch": true, 
                }
            },
            "false": {
                "command": "modaledit.search",
                "args": { 
                    "caseSensitive": true, 
                    "acceptAfter": 1, 
                    "typeAfterAccept": "h", 
                }
            }, 
        },

The command is a bit involved, so let's explain what each argument does.

  • caseSensitive sets the search mode to case sensitive (as in Vim).
  • acceptAfter ends the incremental search as soon as first entered character is found. Normally the user needs to press Enter to accept the search or Esc to cancel it.
  • selectTillMatch argument controls whether selection is extended until the searched character. This depends on whether we have selection mode on or not.
  • typeAfterAccept argument allows you to run other commands (using their key bindings) after the search is done. By default, modalEdit.search command selects the found character(s). With h command we move the cursor over the searched character.

Now we can implement the opposite F command which searches for the previous character. The backwards parameter switches the search direction.

"F": {
            "condition": "__selecting",
            "true": {
                "command": "modaledit.search",
                "args": { 
                    "caseSensitive": true, 
                    "acceptAfter": 1, 
                    "selectTillMatch": true, 
                    "backwards": true,
                }
            },
            "false": {
                "command": "modaledit.search",
                "args": { 
                    "caseSensitive": true, 
                    "acceptAfter": 1, 
                    "typeAfterAccept": "h", 
                    "backwards": true
                }
            }, 
        },

With ; and , keys you can repeat the previous f or F commands either forwards or backwards.

";": "modaledit.nextMatch",
        ",": "modaledit.previousMatch",

We omitted few useful jump commands, like t , T , { , and } as there are no corresponding commands in available in VS Code. You can always look for other extensions that provide similar functionality.

Center Cursor on Screen

The last movement command we add is zz that scrolls the screen so that cursor is at the center. Again, the ability to use JS expression in arguments comes in handy. We use the __line parameter to get the line where the cursor is.

"z": {
            "z": {
                "command": "revealLine",
                "args": "{ lineNumber: __line, at: 'center' }"
            }
        },

Let's test some of the movement commands. We should be able to navigate now without using arrow keys or Home and End keys.

Build your own Vim emulation for VS Code

We skipped commands that move cursor up and down on page at the time. The reason for this is that these commands are bound to Ctrl+b and Ctrl+f in Vim. Since these are "normal" VS Code shortcuts we cannot remap them in ModalEdit. If you want to use these shortcuts, you need to add the bindings to the VS Code's keybindings.json file. Below is an example that uses the modaledit.normal context to make the shortcuts work only in normal mode. Most of the Vim's standard Ctrl +key combinations are already in use, so you need to decide whether you want to remap the existing commands first.

// keybindings.json
{
    {
        "key": "ctrl+b",
        "command": "cursorPageUp",
        "when": "editorTextFocus && modaledit.normal"
    },
    {
        "key": "ctrl+f",
        "command": "cursorPageDown",
        "when": "editorTextFocus && modaledit.normal"
    }
}

Editing

Now we'll implement Vim's common editing commands. We only add the ones that have counterparts in VS Code.

Joining Lines

J joins current and next line together.

"J": "editor.action.joinLines",

Changing Text

Change commands delete some text and then enter insert mode. cc changes the current line (or all selected lines), c$ changes the text from the cursor to the end of line, and cw changes the end of the word. Three key sequnce ciw changes the whole word under the cursor.

"c": {
            "c": [
                "deleteAllLeft",
                "deleteAllRight",
                "modaledit.enterInsert"
            ],
            "$": [
                "deleteAllRight",
                "modaledit.enterInsert"
            ],
            "w": [
                "deleteWordEndRight",
                "modaledit.enterInsert"
            ],
            "i": {
                "w": [
                    "deleteWordStartLeft",
                    "deleteWordEndRight",
                    "modaledit.enterInsert"
                ]
            }
        },

A shorthand for c$ command is C .

"C": [
            "deleteAllRight",
            "modaledit.enterInsert"
        ],

Substition commands do basically same things as change commands; s changes the character under cursor, and S is same as cc .

"s": [
            "deleteRight",
            "modaledit.enterInsert"
        ],
        "S": [
            "deleteAllLeft",
            "deleteAllRight",
            "modaledit.enterInsert"
        ],

Undo & Redo

You can undo the last change with u . We also clear the selection to copy Vim's operation.

"u": [
            "undo",
            "modaledit.cancelSelection"
        ],

Since redo is mapped to `Ctrl+r" by default, we leave this binding as an exercise to the reader.

Visual (Selection) Commands

Visual commands operate on the selected text. < and > shift selected text left or right (indent/outdent).

"<": "editor.action.outdentLines",
        ">": "editor.action.indentLines",

Clipboard Commands

y yanks, i.e. copies, selected text to clipboard. Following Vim convention, we also clear the selection.

"y": [
            "editor.action.clipboardCopyAction",
            "modaledit.cancelSelection"
        ],

d deletes (cuts) the selected text and puts it to clipboard. Capital D deletes the rest of the line. x deletes just the character under the cursor.

"d": "editor.action.clipboardCutAction",
        "D": [
            "cursorEndSelect",
            "editor.action.clipboardCutAction"
        ],
        "x": [
            "cursorRightSelect",
            "editor.action.clipboardCutAction"
        ],

Note: If there is no text selected, y and d commands perform exactly the same actions as yy and dd in Vim. That is, they yank or delete the current line. Again, one of the subtle differences that is futile to try to unify.

For pasting (or putting in Vim parlance) the text in clipboard you have two commands: p puts the text after the cursor, and P puts it before.

"p": [
            "cursorRight",
            "editor.action.clipboardPasteAction"
        ],
        "P": "editor.action.clipboardPasteAction",

Switching Case

Switching selected text to upper or lower case is done with a nifty trick. We can examine the selection in a conditional command that calls different VS Code commands based on the expression. The command is bound to the tilde ~ character.

"~": {
            "condition": "__selection == __selection.toUpperCase()",
            "true": "editor.action.transformToLowercase",
            "false": "editor.action.transformToUppercase"
        },

Marks

Marks or bookmarks, as they are more commonly known, provide a handy way to jump quickly inside documents. Surprisingly, VS Code does not contain this feature out-of-the-box. Since it is easy to implement, ModalEdit fills the gap and adds two simple commands: modaledit.defineBookmark and modaledit.goToBookmark . Using these we can implement Vim's mark commands.

You can potentially define dozens of bookmarks but to keep things simple, we support only four of them: from a to d . To define a bookmark, you type ma , for example, and to jump to that mark, type a .

"m": {
            "a" : {
                "command": "modaledit.defineBookmark",
                "args": {
                    "bookmark": 0
                }
            },
            "b" : {
                "command": "modaledit.defineBookmark",
                "args": {
                    "bookmark": 1
                }
            },
            "c" : {
                "command": "modaledit.defineBookmark",
                "args": {
                    "bookmark": 2
                }
            },
            "d" : {
                "command": "modaledit.defineBookmark",
                "args": {
                    "bookmark": 3
                }
            }
        },
        "`": {
            "a" : {
                "command": "modaledit.goToBookmark",
                "args": {
                    "bookmark": 0
                }
            },
            "b" : {
                "command": "modaledit.goToBookmark",
                "args": {
                    "bookmark": 1
                }
            },
            "c" : {
                "command": "modaledit.goToBookmark",
                "args": {
                    "bookmark": 2
                }
            },
            "d" : {
                "command": "modaledit.goToBookmark",
                "args": {
                    "bookmark": 3
                }
            }
        },

Searching

The last category of commands we implement is searching. We use the incremental search command provided by ModalEdit for this. As in Vim, typing / starts an incremental search. ? starts a search backwards.

"/": {
            "command": "modaledit.search",
            "args": { 
                "caseSensitive": true 
            }
        },
        "?": {
            "command": "modaledit.search",
            "args": { 
                "caseSensitive": true,
                "backwards": true 
            }
        },

Jumping to next previous match is done with keys n and N .

"n": "modaledit.nextMatch",
        "N": "modaledit.previousMatch",

There are some subtle differences in the search functionality as well. Instead of just highlighting matches ModalEdit selects them. This is preferable anyway, as replacing needs to be done manually with selection commands. Finally, modaledit.search does not support regexes. Use VS Code's built-in find command, if you need regex support.

Conclusion

We have built far from complete but nevertheless usable Vim emulation which you can tweak in various ways to make it better. The point of this exercise was to show that you can significantly enhance VS Code's editing experience using just a simple extension and built-in commands.

When writing ModalEdit my goal was not to provide a new Vim emulation. In fact, I don't use Vim-style key bindings myself. I find the countless keyboard combinations in Vim superfuous and inconsistent. I doubt that anybody knows, let alone uses all the available commands. Using Vim is like working in a workshop where you have shelves full of bench planes but the only tool you really need is your trusty No 4 Stanley.

Build your own Vim emulation for VS Code

What I am trying to demonstrate is that you don't need to learn all the magical Vim facilities to become better at editing. Just keep an eye on what operations you repeat, and think how you could make them more efficient. Then add commands that will speed up those operations. Try to make the new commands as general as possible, and as easy as possible to use. Your text editor should adapt to your way of working, not the other way around.

The thing that Vim got right, though, is modal editing. It is the laser beam on the great white shark's head that really makes it cool. Modal editing puts all the commands at your fingertips, and makes you faster. That is why Vim is still popular, even though there is lot of competition around. So, the obvious conclusion is to equip modern editors like VS Code with the same frickin' laser beam. It requires some training to operate, but when properly configured will cut through text at the speed of light.

Happy Editing! :sunglasses:


以上所述就是小编给大家介绍的《Build your own Vim emulation for VS Code》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

降维打击

降维打击

杨 健 / 北京时代华文书局 / 2016-10 / 68

“降维打击”出自中国科幻作家刘慈欣的小说《三体》,而笔者在这本书中试图把“降维打击”的思维引入到企业经营管理的实战中,总结出一套“降维打击”的商业理论。 按照笔者的理解,企业竞争力可以体现在若干个维度的累加上,具有高维度思维的企业,主动将竞争对手的某一核心维度的竞争力降为零,并跟对手在自己更具竞争优势的维度内进行竞争,从而实现以小博大、以弱灭强的商业竞争结果,这就是企业竞争中的“降维打击”。......一起来看看 《降维打击》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具