If you are here just for the destination without the
journey, I created a plugin, that you can find
here.

If you are like me, you like efficiency, and when it comes
to text editors Vim is one of the most efficient there is.
Why? Vim motions. Vim makes it stupidly easy to move around
once you get used to it. You move left, down, right and up
via hjkl, you can jump a word forward using w, backward
with b and a lot more but that’s not what this post
is about.

Despite this supposed greatness of vim, I used VSCode for
several years before switching, because one thing was
holding me back – colemak-dh, an alternative English
layout, that tries to solve the inefficiencies of QWERTY.
Sadly it is not as popular and most software, vim included,
does not expect you to be using it.

ANSI variant colemak-dh
0. ANSI variant colemak-dh

Just imagine trying to move via hjkl while using the above
layout, not ideal. My initial solution was to just remap
the most important keys and learn to live with the rest
being a bit messed up, remapping things as I go whenever I
deemed it necessary.

1
2
3
4
noremap m h
noremap n j
noremap e k
noremap i l

But then I found the langmap option:

    This option allows switching your keyboard into a special language
    mode.  When you are typing text in Insert mode the characters are
    inserted directly.  When in Normal mode the 'langmap' option takes
    care of translating these special characters to the original meaning
    of the key.

Which sounds exactly like what I wanted, so I removed my
previous mappings, set the langmap option in
my config and was happy for maybe like 10 minutes before
trying to use a mapping containing CTRL. Turns out
langmap does not work with alt or control, on
top of that plugins don’t expect you to use this
feature, that maybe like three people know about, therefore
mappings set by them sometimes stop working and sometimes
do completely different things.

I was so close to greatness and I didn’t intend to
give up. The only thing left to do was remap all of
vim’s default mappings, all of them.

Typing all the mappings by hand was out of question and
having a huge for loop run every time vim started also
didn’t feel right, thus I settled on generating the
mappings with python and then writing them into a .vim
file.

We define a dictionary containing all the keys, necessary
to translate between colemak-dh and qwerty, plus a small
helper function, so we can also translate uppercase
characters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
COLEMAK = {
    "p": ";",
    "t": "b",
    "x": "c",
    "c": "d",
    "k": "e",
    "e": "f",
    "m": "h",
    "l": "i",
    "y": "j",
    "n": "k",
    "u": "l",
    "h": "m",
    "j": "n",
    ";": "o",
    "r": "p",
    "s": "r",
    "d": "s",
    "f": "t",
    "i": "u",
    "z": "x",
    "o": "y",
    "b": "z",
}



def to_colemak(ch: str) -> str:
    if ch.isupper():
        return COLEMAK[ch.lower()].upper()
    else:
        return COLEMAK[ch]

Different modes have different mappings and the mappings
sometimes get more complex than just “j
goes down”. I created another dictionary with keys
being modes and elements lists of tuples containing mapping
and a list of characters to apply it to. For example
("g{}", ["f",
"F", "t", "T"])

will format the string
"g{}" with f, F, t and T,
resulting in:

1
2
3
4
noremap <C-W>gT <C-W>gF
noremap <C-W>gB <C-W>gT
noremap <C-W>gt <C-W>gf
noremap <C-W>gb <C-W>gt

As a convenience, if we omit the tuple and just have a
string, the string will get formatted for all the
characters, e.g.
"{}" remaps several of
the window commands.

For more complex mappings we can use a lambda like this:

1
2
3
4
lambda lhs, rhs: (
    " + lhs + ">",
    " + rhs + ">",
)

After around 45 lines of mappings we can run the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
outfile = Path("./autoload/colemak_dh.vim")
outfile.parent.mkdir(exist_ok=True)
contents = "function colemak_dh#setup()n"

for mode in modes:
    for mapping in modes[mode]:
        if type(mapping) == tuple:
            set = mapping[1]
            mapping = mapping[0]
        else:
            set = list(COLEMAK.keys())
        set.sort()
        contents += "".join(gen_mappings(mode, mapping, set))

contents += "endfunctionn"
outfile.write_text(contents)

gen_mappings checks if lhs is a
string or a lambda, loops through the set we supplied and
then passes the formatted mappings to this function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
REMAPPED = {}


def make_map(mode: str, lhs: str, rhs: str) -> str:
    if not mode in REMAPPED.keys():
        REMAPPED[mode] = []

    s = "    {}noremap {} {}n".format(mode, lhs, rhs)
    if not rhs in REMAPPED[mode]:
        s += "    {}noremap {} n".format(mode, rhs)

    REMAPPED[mode].append(lhs)

    return s

To avoid clashing mappings, mappings that take too long to
resolve because of timeout and similar issues,
we check if we already mapped the rhs and if
we didn’t, we map it to '',
disabling it.

In the big dictionary of mappings there are some, that
break things, most notably remapping
"" for insert mode turns
the Enter key into Backspace because of how terminal
emulators handle input, the i key,
that’s remapped to l will wait for
timeout in visual mode and some more shenanigans. To fix
this we simply append few more lines to the file before
writing it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
contents += """    inoremap  
    cnoremap  
    inoremap  
    cnoremap  
    nnoremap XX ZZ
    vnoremap  i l
    nnoremap  z b
    vnoremap  z b
    noremap  z b
endfunction
"""

The final result is a .vim file with around 800 lines of
noremap commands. To use this in our config we
can do call colemak_dh#setup() or
vim.fn['colemak_dh#setup']() if using Lua.

I also tried doing this in pure Lua, but I did some quick
non-scientific benchmarks and sourcing an empty Lua file
takes around 3.5ms on my machine, while sourcing the 800
noremap calls takes 1ms.

Have something to say about one of the posts or the site itself?
Feel free to leave your thoughts in my
public inbox
or
contact
me personally.

Read More