Skip to content

An attempt to emulate as much of vim as possible in QMK.

License

Notifications You must be signed in to change notification settings

andrewjrae/qmk-vim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

95 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QMK Vim

images/demo.gif

Table of Contents

About

This project aims to emulate as much of vim as possible in QMK userspace. The idea is to use clever combinations of shift, home, end, and control + arrow keys to emulate vim’s modal editing and motions.

The other goal is to make it relatively plug and play in user keymaps, while still providing customization options.

Features

Core Features

The core features are in in the tables below and takes up roughly 5% of at Atmega32u4.

Motions

Motions are available in normal and visual mode, and can be composed with actions. Note that in the table below the mods shown are for Linux/Windows, however if VIM_FOR_MAC is defined then these should change to LOPT.

Vim BindingActionNotes
hLEFT
jDOWN
kUP
lRIGHT
e / ELCTL + RIGHTThis may act more like a w command depending on the text environment
w / WLCTL + RIGHTBy default, this may act more like an e command depending on the text environment. Alternatively, see W motions
b / BLCTL + LEFT
0 / ^HOMEIn some text environments this goes to the true start, or the first piece of text
$END

Actions

Change, delete, and yank actions are all supported and can be combined with motions and text objects in normal mode as you would expect. For example cw will change until the next word (or end of word depending on your text environment). Note that by default the d action will copy the deleted text to the clipboard.

In visual mode each action can be accessed with c, d, and y respectively and they will act on the currently selected visual area.

Normal mode also supports these commands:

Vim BindingActionNotes
cc / SChange lineThis doesn’t copy the old line to clipboard
CChange to end of lineThis doesn’t copy the changed content to clipboard
sChange character to right of cursorThis doesn’t copy the changed content to clipboard
ddDelete lineThis copies the deleted line to clipboard
DDelete to end of lineThis copies the deleted text to clipboard
yyYank line
YYank to end of line

Paste Action

The paste_action() handles pasting, which is simple for non lines, but most of the time, I’m pasting lines so this function attempts to consistently handle pasting lines. Yanks and deletes of lines are tracked through the yanked_line global, the paste action then pastes accordingly. For copying and pasting lines the line(s) the selection is made from the very start of the line below, up to start of the first line. See the visual line mode section for more info on how lines are selected. There is also an optional paste_before_action(), enabled with the VIM_PASTE_BEFORE macro.

Normal Mode

This below tables lists all of the commands not covered by the motions and actions sections . Note that in the table below the mods shown are for Linux/Windows, however if VIM_FOR_MAC is defined then these should change to LCMD.

Vim BindingActionNotes
iinsert_mode()
IHOME, insert_mode()
aRIGHT, insert_mode()
AEND, insert_mode()
oEND, ENTER, insert_mode()
OHOME, ENTER, UP insert_mode()
vvisual_mode()
Vvisual_line_mode()
ppaste_action()
uLCTL + zThis works most places
CTRL + rLCTL + yThis may or may not work everywhere
xDELETE
XBACKSPACE

Note that all keycodes chorded with CTRL, GUI, or ALT, that aren’t bound to anything are let through. In other words, you can still alt tab and use shortcuts for whatever editor you’re in.

Insert Mode

Insert mode is rather straight forward, all keystrokes are passed through as normal with the exception of escape, which brings you back to normal mode.

Visual Mode

Visual mode behaves largely as one would expect, all motions and actions are supported. Escape of course returns you to normal mode. Note that hitting escape may move your cursor unexpectedly, especially if you don’t have BETTER_VISUAL_MODE enabled. This is because there isn’t a good way to just deselect text in “standard” editing, the best way is to move the text cursor with the arrow keys. The trouble for us is choosing which way to move, by default we always move right. However, with BETTER_VISUAL_MODE enabled the first direction moved in visual mode is recorded so that we can move the cursor to either the left or right or the selection as required. Of course this approach breaks down if you double back on the cursor, but I find I don’t do that all that often.

Visual Line Mode

Visual line mode is very similar to visual mode as you would expect however only the j and k motions are supported and of course the entire line is selected. However, there is no perfect way (that I know of) to select lines the way vim does easily. The way I used do it before I used vim, was to get myself to the start of the line then hit shift and up or down. Going down works almost as you’d expect in vim, but you’ll always be a line behind since it doesn’t highlight the line the cursor is currently on. Going up on the other hand will select the line the cursor is on, but it will always be missing the first line. So neither solution quite works on it’s own, BETTER_VISUAL_MODE does mostly fix these issues, but at the price of a larger compile size, hence why it’s not on by default.

A note on the default implementation, since most programming environments make the home key go to the start of the indent or the actual start of the line dynamically, consistently getting to the start of a line isn’t as easy as hitting home. The most consistent way I’ve found is to hit end on the line above, and then right arrow your way to the start of the next line. This works as long as there is no line wrapping, so in the default implementation, entering visual line mode sends KC_END, KC_RIGHT, LSFT(KC_UP). Not only is this quite consistent, it also immediately highlights the current line just as you would expect. The only downside with the default implementation is that if you then try to go down that first line will be deselected, so you have to start your visual selection a line above when moving downwards. Of course BETTER_VISUAL_MODE fixes this as long as you don’t double back on the cursor.

Extra Features

In an effort to reduce the size overhead of the project, any extra features can be enabled and disabled using macros in your config.h.

MacroFeatures Enabled/DisabledBytes (gcc 8.3.0)
NO_VISUAL_MODEDisables the normal visual mode.+256 B
NO_VISUAL_LINE_MODEDisables the normal visual line mode.+336 B
BETTER_VISUAL_MODEMakes the visual modes much more vim like, see visual_line_mode() for details.-172 B
VIM_I_TEXT_OBJECTSAdds the i text objects, which adds the iw and ig text objects, see text objects for details.-122 B
VIM_A_TEXT_OBJECTSAdds the a text objects, which adds the aw and ag text objects.-138 B
VIM_G_MOTIONSAdds gg and G motions, which only work in some programs.-116 B
VIM_COLON_CMDSAdds the colon command state, but only the w and q commands are supported (can be in combination).-72 B
VIM_PASTE_BEFOREAdds the P command.-60 B
VIM_REPLACEAdds the r command.-76 B
VIM_DOT_REPEATAdds the . command, allowing you to repeat actions, see dot repeat for details.-232 B
VIM_W_BEGINNING_OF_WORDMakes the w and W motions skip to the beginning of the next word, see W motions for details.-104 B
VIM_NUMBERED_JUMPSAdds the ability to do numbered motions, ie 10j or 5w, be wary of large numbers however, as they can freeze up your keyboard.-544 B
ONESHOT_VIMEnables running vim in “oneshot” mode, see oneshot vim for details.-76 B
VIM_FOR_ALLAdds the ability to toggle Mac support on and off at runtime, rather than only at compile time.-456 B

Text Objects

Unfortunately there is really no way to implement text objects properly, especially things like brackets. However, word objects in some form are quite possible. The tricky part is distinguishing between an inner and outer word, some editors will have a forward word jump go to the end of a word like vim”s w.

It’s easy to get an inner word if word jump acts like e, since you can go to the end of the word, then hold shift and jump to the start. And similarly it’s easy to get an outer word if word jump acts like w, since you can go to the start of the next word then hold shift and jump back to the start of your word. However, getting an inner word with just w and b at your disposal isn’t possible without using arrow keys which won’t be consistent in scenarios where the word punctuated in some way. But, it is possible to get an outer word with b and e. In vim terms, the sequence looks like eebvb, now in vim that doesn’t do exactly what we want, but with word jumps it does result in an outer word selection.

It should be noted that this always selects extra space to the right of the word, and if the cursor is at the end of a word it will get the wrong word. So it isn’t ideal, but it works okay in general.

There is also a the g object, which isn’t even a default vim object, but CTRL+A provides such a nice way to select the entire document that I couldn’t help it. I find it especially nice if I’m sending a message and I want to delete what I wrote or change the whole thing, with dig or cig.

Dot Repeat

The dot repeat feature can be enabled with the VIM_DOT_REPEAT macro. This lets the user hit the . key in normal mode to repeat the last normal mode command. For example, typing ciw, hello!, will replace the underlying word with hello!, now going over another word hitting . will repeat the action, just like vim does. The way this works is that once an action starts, like c or D, or even A all keycodes are recorded until we return to the normal mode state. Once you hit . it goes through the recorded keys until it hits normal mode again. The default size of the recorded keys buffer is 64, but can be modified with the VIM_REPEAT_BUF_SIZE macro.

W Motions

If the VIM_W_BEGINNING_OF_WORD macro is defined, the w and W motions (which are synonymous) will skip to the beginning of the next word by sending LCTL + RIGHT and then tapping LCTL + RIGHT, LCTL + LEFT on release. Otherwise, their default behavior is to imitate the e and E motions by sending LCTL + RIGHT. Note that enabling this feature currently causes unexpected side effects with actions such as cw and dw, where the w motion acts like an e motion (context).

Numbered Jumps

The numbered jumps feature allows users to do repeat motions a specified number of times just like in vim. By enabling VIM_NUMBERED_JUMPS, you can now type 10j to jump down 10 lines, or you can type c3w to change the next three words.

Oneshot Vim

“Oneshot” vim is not a normal vim feature, rather it’s a way to use this enable this vim mode for a brief moment. Inspired by oneshot keys and other features like caps word, this mode tries to intelligently leave vim mode automatically. This was added because sometimes I only want to make a small edit, or quickly navigate and yank some text, and I don’t like having to toggle the mode on and off. Especially as sometimes I forget to turn it off and get briefly confused why the real vim isn’t working quite right.

In essence, this mode works as normal except that any time you press Esc you exit the mode, same goes if you call any complex action like daw, ciw and the like. Note that currently simple things like x and Y do not cause oneshot vim to exit.

To enable this mode simply add the ONESHOT_VIM macro to your config.h. Then add some way to call the start_oneshot_vim() function.

Configuration

Setup

  • First add the repo as a submodule to your keymap or userspace.
    git submodule add https://github.com/andrewjrae/qmk-vim.git
        
  • Next, you need to source the files in the make file, the easy way to do this is to just add this line to your keymap’s rules.mk file:
    include $(KEYBOARD_PATH_2)/keymaps/$(KEYMAP)/qmk-vim/rules.mk
        

    …or to your userspace’s rules.mk file:

    include $(USER_PATH)/qmk-vim/rules.mk
        

    If this doesn’t work, you can either try changing the number in the KEYBOARD_PATH_2 variable (values 1-5), or simply copy the contents from qmk-vim/rules.mk.

  • Now add the header file so you can add process_vim_mode() to your process_record_user(), it can either go at the top or the bottom, it depends on how you want it to interact with your keycodes.

    If you process at the beginning it will look something like this, make sure that you return false when process_vim_mode() returns false.

    #include "qmk-vim/src/vim.h"
    
    bool process_record_user(uint16_t keycode, keyrecord_t *record) {
        // Process case modes
        if (!process_vim_mode(keycode, record)) {
            return false;
        }
        ...
        
  • The last step is to add a way to enter into vim mode. There are many ways to do this, personally I use leader sequences, but using combos or just a macro on a layer are all viable ways to do this. The important part here is ensure that you also have a way to get out of vim mode, since by default there is no way out. Enabling VIM_COLON_CMDS will allow you to also use :q or :wq in order to get out of vim, but in general I would recommend using the toggle_vim_mode() function.

    As a simple example, here is the setup for a simple custom keycode macro:

    enum custom_keycodes {
        TOG_VIM = SAFE_RANGE,
    };
    
    bool process_record_user(uint16_t keycode, keyrecord_t *record) {
        // Process case modes
        if (!process_vim_mode(keycode, record)) {
            return false;
        }
    
        // Regular user keycode case statement
        switch (keycode) {
            case CAPSWORD:
                if (record->event.pressed) {
                    toggle_vim_mode();
                }
                return false;
            default:
                return true;
        }
    }
        

Adding Keybinds

Since most vim users remap a key here or there, I’ve added hooks for the normal, visual, visual line, and insert modes. These hooks act in the exact same way that process_record_user() does, except that keycodes come in with any active modifiers applied to them. And not all keycodes will be passed down to vim, vim mode only intercepts keycodes alphanumeric, and symbolic keycodes (and escape).

For example pressing KC_LSHIFT and then KC_A will have LSFT(KC_A) sent down to vim mode. It should also be noted that all modifiers will be added to the keycode as the left mod, ie you can always use LSFT(KC_A) for catching A.

The hooks that you can use are:

bool process_insert_mode_user(uint16_t keycode, const keyrecord_t *record);
bool process_normal_mode_user(uint16_t keycode, const keyrecord_t *record);
bool process_visual_mode_user(uint16_t keycode, const keyrecord_t *record);
bool process_visual_line_mode_user(uint16_t keycode, const keyrecord_t *record);

As an example, I have the bad habit of hitting CTRL+S all the time. And for a long time I’ve had it so that in insert mode, CTRL+S saves and enters normal_mode(). So in my keymap.c file I have this binding added:

bool process_insert_mode_user(uint16_t keycode, const keyrecord_t *record) {
    if (record->event.pressed && keycode == LCTL(KC_S)) {
        normal_mode();
        tap_code16(keycode);
        return false;
    }
    return true;
}

Setting Custom State

The following user hooks are also called whenever the active mode is changed:

void insert_mode_user(void);
void normal_mode_user(void);
void visual_mode_user(void);
void visual_line_mode_user(void);

These can optionally be used to set custom state in your keymap.c file; for example, changing the RGB lighting layer to indicate the current mode:

void insert_mode_user(void) {
  rgblight_set_layer_state(VIM_LIGHTING_LAYER, false);
}
void normal_mode_user(void) {
  rgblight_set_layer_state(VIM_LIGHTING_LAYER, true);
}

Mac Support

Since Macs have different shortcuts, you need to set the VIM_FOR_MAC macro in your config.h. That being said I’m not a Mac user so it’s all untested and I’d guess there are some issues.

If you are a Mac user and do encounter issues, feel free to put up a PR or an issue.

If you intend to use your keyboard with both Mac and Windows or Linux computers, you can set the VIM_FOR_ALL macro in your config.h. This will allow you to use the following functions in your keymap.c to switch between Mac support mode and non-Mac support mode:

void enable_vim_for_mac(void);
void disable_vim_for_mac(void);
void toggle_vim_for_mac(void);
bool vim_for_mac_enabled(void);

VIM_FOR_ALL overrides the behavior of VIM_FOR_MAC.

By default the keyboard will start in non-Mac support mode, but if VIM_FOR_ALL and VIM_FOR_MAC are both defined the keyboard will start in Mac support mode.

Displaying Modes

To help remind you that you have vim mode enabled, there are two functions available. The vim_mode_enabled() function which returns true is vim mode is active, and the get_vim_mode() function which returns the current vim mode.

In my keymap I use these to display the current mode on my OLED.

if (vim_mode_enabled()) {
    switch (get_vim_mode()) {
        case NORMAL_MODE:
            oled_write_P(PSTR("-- NORMAL --\n"), false);
            break;
        case INSERT_MODE:
            oled_write_P(PSTR("-- INSERT --\n"), false);
            break;
        case VISUAL_MODE:
            oled_write_P(PSTR("-- VISUAL --\n"), false);
            break;
        case VISUAL_LINE_MODE:
            oled_write_P(PSTR("-- VISUAL LINE --\n"), false);
            break;
        default:
            oled_write_P(PSTR("?????\n"), false);
            break;
    }

Contributing

Updating Readme Firmware Sizes

If you’d like to submit a pull request, please update the table with the firmware sizes for each feature. This can be done automatically by running the following commands in the root directory of this repository (it may take a few minutes, since it recompiles for each feature in the table):

docker build -t qmk-vim-update-readme update-readme
docker run -v $PWD:/qmk_firmware/keyboards/uno/keymaps/qmk-vim-update-readme/qmk-vim qmk-vim-update-readme

About

An attempt to emulate as much of vim as possible in QMK.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages