Earlier this month I found jupyter-vim-binding, a Jupyter Notebook extension which “enables a Vim-like environment powered by Code Mirror’s Vim“. This extension provides some pretty fun capabilities. After installing, though, I noticed some limitations:
- Key repeats didn’t work. Whenever I held down
j
ork
to move lines, I moved one line then stop. Holding some keys, likel
, would make a box with an accent mark show up. - Using Shift–Escape to exit Vim command mode into Jupyter command mode didn’t work well. I use Karabiner-Elements to let my Caps Lock key be mapped to press as Escape when pressed alone and Control otherwise. This means when I try to type Shift-Escape it would register as Shift-Control, unless I let go of Escape before Shift, which is not natural.
- None of my mappings/configurations I usually use in Vim from my
vimrc
were there. Notably- Relative line numbers were not shown
s
andS
were not mapped to^
and$
- No line wrapping
The above listed are those issues which I managed to fix in this extension—scraping by with my minimal JavaScript knowledge! I thought I would share how.
Key Repeats in jupyter-vim-bindings
My issue with key repeats was not the issue of the extension but rather my Mac, which didn’t enable key repeats at all in Chrome and other applications. I followed this website’s advice, entering
$ defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
into the terminal and restarting Chrome.
Editing and reloading the jupyter-vim-bindings extension
Now I’d like to make Shift-Escape work, or add another key binding to exit Vim command mode. To modify the jupyter-vim-binding
extension, cd
into the vim-binding
directory of the extension and edit vim-binding.js
. I’ll show the edits I made below. After you’re done editing, you can reload the extension with the following commands:
jupyter nbextension disable vim_binding/vim_binding
jupyter nbextension enable vim_binding/vim_binding
Then run a new Jupyter notebook, and the extension should be updated.
Defining new mappings in jupyter-vim-bindings
Now let’s get back to editing. At line 70 of vim_bindings.js
, the variable cm_config.extraKeys
is set. You can see where Esc
and Shift-Esc
are mapped to leave Insert and Normal mode. Adding a new mapping is as easy as adding a new line such as (what worked in my case)
'Shift-Ctrl': CodeMirror.prototype.leaveNormalMode,
You could also remap a key not used in this extension, like Z
:
'Z': CodeMirror.prototype.leaveNormalMode,
This lets us remap s
and S
too:
'S': function test(cm) {
cm.execCommand("goLineStartSmart");
},
'Shift-S': function test(cm) {
cm.execCommand("goLineEnd");
},
To get this I used the CodeMirror documentation. What’s really going on here is that CodeMirror lets us define extra key mappings with the extraKeys
configuration mapping. Each entry in the list maps a key string to a function which takes the editor instance cm
(also called editor
by many other sites) and then uses the CodeMirror programming API to do something.
When we previously mapped Shift-Escape
to CodeMirror.prototype.leaveNormalMode, we really mapped to this function found at line 197 of lib/codemirror.js
:
CodeMirror.prototype.leaveNormalMode = function leaveNormalMode(cm) {
ns.notebook.command_mode();
ns.notebook.focus_cell();
};
So now for s
and S
we create a function which calls cm.execCommand
. Details of how this works can be found in the CodeMirror documentation.
Showing Relative Numbers in jupyter-vim-bindings
The CodeMirror documentation tells us that lineNumbers
is a Configuration option. Configuration options get set when we create a CodeMirror editor instance via CodeMirror
and fromTextArea
functions, and can be changed via the setOption
method. But at first glance such code is hard to find in jupyter-vim-bindings
. The correct place is line 79 of vim_bindings.js
, after which we call cm.setOption
several times to set options like keyMap
. To add line numbers, we can add our own:
cm.setOption('lineNumbers', true)
Getting line wrapping is as easy as:
cm.setOption('lineWrapping', true)
What about relative numbers? This is a bit more tricky. I found the answer here. Just as we added a cm.setOption
line above, we can add the following:
cm.on('cursorActivity', function(cm) {
const lineNum = cm.getCursor().line + 1;
if (cm.state.curLineNum === lineNum) {
return;
}
cm.state.curLineNum = lineNum;
cm.setOption('lineNumberFormatter', l =>
l === lineNum ? lineNum.toString().concat('-') : Math.abs(lineNum - l));
});
I edited the original solution a bit so that the current line would have a dash next to it, as I felt otherwise the numbers looked a bit confusing. The final code block looks like this for me:
ns.notebook.get_cells().map(function(cell) {
var cm = cell.code_mirror;
if (cm) {
cm.setOption('lineNumbers', true)
cm.setOption('lineWrapping', true)
cm.on('cursorActivity', function(cm) {
const lineNum = cm.getCursor().line + 1;
if (cm.state.curLineNum === lineNum) {
return;
}
cm.state.curLineNum = lineNum;
cm.setOption('lineNumberFormatter', l =>
l === lineNum ? lineNum.toString().concat('-') : Math.abs(lineNum - l));
});
cm.setOption('keyMap', cm_config.keyMap);
cm.setOption('extraKeys', $.extend(
cm.getOption('extraKeys') || {},
cm_config.extraKeys
));
}
});
Other features?
There were several tweaks I haven’t figured out how to do yet which would be nice to have:
- let
s
andS
work with other commands, such asds
. - Remap
;
and:
- Show a vertical line at 80 characters. Suggestions are here but I couldn’t get them to work.
- Map
Y
toy$
- many more small but unimportant things from my
vimrc
Let me know if you or anybody has found solutions to these!
One thought on “Configuring Vim bindings in your Jupyter Notebooks”