Have you ever wanted to navigate/edit text from the home row, without needing to use the keyboard arrow keys or the mouse? Well, if you’re on macOS, you’re in luck!
As a coder obsessed with efficiency, I’m particular about my keyboard shortcuts. Surprisingly, I’ve yet to encounter anybody in person who also loves macOS’s out-of-box emacs style text navigation shortcuts (here’s someone who agrees with me on the web).
Windows and Linux use the control (⌃) key as a main modifier, e.g. ⌃f for find, ⌃c for copy. This is unfortunately overloaded with unix/emacs-style navigation, e.g. ⌃f to move forward one character, ⌃b to move back one character, etc.
In contrast, macOS uses the command (⌘) key for main modifiers, e.g. ⌘f for find, ⌘c for copy. This frees up the control key, and therefore macOS natively out-of-box uses the control key for emacs style navigation!
So when interacting with text input on macOS (for example writing this blog post), if I need to make edits, I can move forwards/backwards/beginning_of_line etc. by just typing ⌃f/⌃b/⌃a/etc. which is far faster and more ergonomic than shifting my hands over to the arrow keys, or even worse, clicking the spot with my mouse.
That’s amazing, but emacs has more in store: word-based navigation. If we’re already hitting ⌃f to move forward a character, what if we want to move forward by a whole word instead? Intuitively, emacs has you hit meta-f to move forward by a word, where meta is typically the modifier key to the right of control, e.g. the alt or the option (⌥) key.
Unfortunately, macOS doesn’t support word-based navigation out-of-box, but luckily supports custom keyboard shortcuts for this. Apple’s documentation describes how you can create a configuration file with the following contents, to recreate the emacs style keybindings.
/* ~/Library/KeyBindings/DefaultKeyBinding.dict */
{
/* tilde represents the meta/option key */
"~f" = "moveWordForward:";
"~b" = "moveWordBackward:";
"~d" = "deleteWordForward:";
"~\010" = "deleteWordBackward:"; /* Meta-backspace */
}
This is great, you just create that file at the specified location, and meta word-based navigation now works for you! I used a similar configuration for years, but one thing always bothered me:
Although I could hit meta-f for forward-by-word and meta-b for backward-by-word, I also wanted to do meta-shift-f for select-forward-by-word, and meta-shift-b for select-backward-by-word, another really useful emacs feature.
Apples API explicitly lists the capability for this, with moveWordForwardAndModifySelection and moveWordBackwardAndModifySelection. Both these methods do what I want them to do, but I was unable to get the desired shortcut to trigger them.
My DefaultKeyBindings.dict would look like this:
{
/* $ represents the shift key */
"~f" = "moveWordForward:";
"~$f" = "moveWordForwardAndModifySelection:";
"~b" = "moveWordBackward:";
"~$b" = "moveWordBackwardAndModifySelection";
"~d" = "deleteWordForward:";
"~\010" = "deleteWordBackward:"; /* Meta-backspace */
}
But for some reason, pressing ⌥⇧f would always result in typing the character Ï and ⌥⇧b would always result in typing the character ı.
Perhaps this could be solved with some third party app like Karabiner-Elements, but I didn’t want to install some third party tool just for this. Over the past few years, I spent countless hours searching StackOverflow and trying every permutation of DefaultKeyBindings.dict to try to get this to work, but to no avail. And the other day, while setting up DefaultKeyBindings.dict on a new macbook, I decided to give it another attempt.
Hours later, I was about to give up, when I stumbled upon a related answer that gave me a clue. The author explains that binding “shift + 6” is impossible, because shift+6 would result in the ^ character, so you need to bind “shift + ^”
I quickly ran back to my DefaultKeyBinding.dict, and tried the following (notice the capital F and capital B):
{
"~f" = "moveWordForward:";
"~$F" = "moveWordForwardAndModifySelection:";
"~b" = "moveWordBackward:";
"~$B" = "moveWordBackwardAndModifySelection";
"~d" = "deleteWordForward:";
"~\010" = "deleteWordBackward:"; /* Meta-backspace */
}
THIS WORKS!!! All I needed to do was change the binding to a capital F and capital B! So now I finally have fully working by-word key navigation with selection.
You can find the complete set of my DefaultKeyBinding.dict in my GitHub codex repository (along with all my other system configuration settings!).