├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── README.md ├── lib ├── codemirror.js └── jupyter │ ├── actions.js │ ├── codecell.js │ ├── completer.js │ ├── keyboard.js │ ├── notebook.js │ ├── quickhelp.js │ └── shortcuts.js ├── resource ├── completion.png ├── screencast.gif └── tooltip.png ├── vim_binding.css ├── vim_binding.js └── vim_binding.yaml /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | =============================================================================== 3 | 4 | Please read the rules listed below before you contribute this extension. 5 | 6 | 7 | Correcting document 8 | ------------------------------------------------------------------------------- 9 | 10 | **YOU ARE VERY WELCOME !!!** 11 | 12 | While I'm not native English speaker, I would ask you to correct documents. 13 | 14 | 15 | Reporting an issue 16 | ------------------------------------------------------------------------------- 17 | 18 | I'm welcome to hear issues to improve jupyter-vim-binding, but please read the 19 | followings to save time of yours and mines ;-) 20 | 21 | ### Before reporting an issue 22 | 23 | Please confirm the following before reporting an issue: 24 | 25 | 1. Confirm the version of your jupyter-vim-binding and Jupyter. Only a latest 26 | version (and maybe one or two below) is supported 27 | 2. Confirm the behavior in [CodeMirror.Vim][]. If the issue is reproducible 28 | even in [CodeMirror.Vim][] then the issue belongs to the [CodeMirror.Vim][] 29 | 3. Confirm if the behavior is listed in [Limitation](README.md#Limitation). 30 | 31 | [CodeMirror.Vim]: https://codemirror.net/demo/vim.html 32 | 33 | ### Required information in an issue 34 | 35 | Please write the following information in your issue: 36 | 37 | - Operating system and version (e.g. Ubuntu Gnome 15.04 64bit) 38 | - Browser and version (e.g. Firefox 44.0.2) 39 | - Version (revision of the repository) of your Jupyter notebook 40 | - Version (revision of the repository) of your jupyter-vim-binding 41 | - Step by step procedure to reproduce 42 | - Expected behavior and actual behavior 43 | - What you have done to solve the issue if you did (e.g. Confirmed with Firefox 44 | xxxx and Chrome xxxx) 45 | 46 | Less information makes difficult to debug, so please write as much information 47 | as you can. 48 | 49 | 50 | Adding extra mappings for Jupyter manipulation 51 | ------------------------------------------------------------------------------- 52 | 53 | If you feel that jupyter-vim-binding should provide extra default mappings for 54 | manipulating Jupyter, please read the followings ;-) 55 | 56 | 57 | ### Read and follow the basic concept 58 | 59 | Read and follow the basic concept of jupyter-vim-binding described at 60 | [Concept][] page. 61 | For example, I probably reject your PR if you provide *One-time mappings* 62 | without `` prefix in *Command mode* without reason. 63 | 64 | [Concept]: https://github.com/lambdalisue/jupyter-vim-binding/wiki/Concept 65 | 66 | ### Read and try to avoid conflicts with native Vim 67 | 68 | Read and try to avoid conflicts with native Vim key mappings as much as 69 | possible. 70 | 71 | I understand that Vim provides a lot of native mappings which seems not useful 72 | in Jupyter (such as ``/`` jumps which does not work.) 73 | So conflicted key mappings are OK if you can provide any reason or evidence to 74 | avoid the native mappings. But otherwise, you should try to avoid conflicts. 75 | 76 | Note that most of key mappings can be confirmed by executing `:h CTRL-O` or 77 | whatever in a native Vim. 78 | 79 | 80 | ### Follow coding style 81 | 82 | Not fully and critical but I prefer to follow [Google JavaScript Style Guide][]. 83 | So I may ask you to modify your PR code a bit to follow the style. 84 | 85 | [Google JavaScript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 86 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | 4 | ### Environment 5 | 6 | - [ ] Operating system : (e.g. Ubuntu Gnome 15.04 64bit) 7 | - [ ] Web browser : (e.g. Firefox 44.0.2) 8 | - [ ] Version or revision of Jupyter Notebook : (e.g. 4.1 or 620fb29) 9 | - [ ] Revision of jupyter-vim-binding : (e.g. 5a057d6) 10 | 11 | 12 | ### Behavior 13 | 14 | #### Expected 15 | 16 | #### Actual 17 | 18 | 19 | ### Step by step procedure 20 | 21 | 1. Start local Jupyter Notebook by `jupyter notebook` 22 | 2. Access http://localhost:8888/ 23 | 3. etc. 24 | 25 | 26 | ### What you have done to solve the issue 27 | 28 | For example, the behavior is confirmed with Firefox 44.0.2 and Google Chrome xx.xx.xx and could not be reproduced in Firefox but Chrome or what ever. 29 | 30 | 31 | ### Remarks 32 | 33 | If any 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### This PR is for 2 | 3 | - [ ] Fix a reported issue : #XXX 4 | - [ ] Fix a unreported issue : Write summary in a new section 5 | - [ ] Add a reported feature implement : #XXX 6 | - [ ] Add a unreported feature implement : Write summary in a new section 7 | - [ ] Other : Write summary in a new section 8 | 9 | ### Summary 10 | 11 | If any 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jupyter-vim-binding 2 | =============================================================================== 3 | ![Version 2.1.0](https://img.shields.io/badge/version-2.1.0-yellow.svg?style=flat-square) ![Support Jupyter 4.1 or above](https://img.shields.io/badge/support-Jupyter%204.1%20or%20above-yellowgreen.svg?style=flat-square) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) ![Doc](https://img.shields.io/badge/doc-%3Ah%20Press%20F1%20on%20Jupyter-orange.svg?style=flat-square) 4 | 5 | --- 6 | 7 | **See https://github.com/jupyterlab-contrib/jupyterlab-vim for an alternative of this plugin.** 8 | 9 | --- 10 | 11 | Do you use Vim? And you need to use [Jupyter Notebook]? 12 | This is a [Jupyter Notebook][] (formerly known as [IPython Notebook][]) extension to enable Vim like environment powered by [CodeMirror's Vim][]. 13 | I'm sure that this plugin helps to improve your QOL. 14 | 15 | [Jupyter Notebook]: https://jupyter.org/ 16 | [IPython Notebook]: http://ipython.org/notebook.html 17 | [CodeMirror's Vim]: https://codemirror.net/demo/vim.html 18 | [IPython-notebook-extensions]: https://github.com/ipython-contrib/IPython-notebook-extensions 19 | 20 |
21 | Screencast 22 |
23 | 24 | This extension stands for providing a Vim like environment, so it would drastically overwrite the default mappings and introduce new behaviors. 25 | For example 26 | 27 | - Jupyter has two modes, *Command mode* and *Edit mode* but this extension has three modes, *Jupyter mode*, *Vim command mode*, and *Insert mode* 28 | - Jupyter provides `C` (`Shift-c`) and `V` (`Shift-v`) to perform copy and paste cells, but this extension provides `yy` and `p` to perform copy and paste cells 29 | - Jupyter provides `` (`Ctrl-s`) to save a checkpoint, but this extension eliminates that mapping while `:w` works same 30 | - A lot more. 31 | 32 | 33 | Need contributors 34 | ------------------------------------------------------------------------------- 35 | 36 | While I changed my job, I don't use jupyter notebook, and I can't make enough time to maintain this plugin. 37 | 38 | **So if you like this plugin, please consider being a contributor.** 39 | 40 | https://github.com/lambdalisue/jupyter-vim-binding/issues/89 41 | 42 | 43 | Installation 44 | ------------------------------------------------------------------------------- 45 | 46 | There are several ways to install the extension, see [Installation](https://github.com/lambdalisue/jupyter-vim-binding/wiki/Installation) for detail. 47 | The procedure below is the most simple one for quick use (**A recommended way is different from this. See the link above if you are a beginner.**) 48 | 49 | ```bash 50 | # Create required directory in case (optional) 51 | mkdir -p $(jupyter --data-dir)/nbextensions 52 | # Clone the repository 53 | cd $(jupyter --data-dir)/nbextensions 54 | git clone https://github.com/lambdalisue/jupyter-vim-binding vim_binding 55 | # Activate the extension 56 | jupyter nbextension enable vim_binding/vim_binding 57 | ``` 58 | 59 | 60 | Usage 61 | ------------------------------------------------------------------------------- 62 | 63 | This extension provides *Jupyter mode* (For manipulating Jupyter) and *Vim mode* (For manipulating text). 64 | In *Vim mode*, there is *Command mode* and *Insert mode* like native Vim. 65 | Users can distinguish these modes by the background color of the cell. 66 | 67 | Key mappings are designed for Vimmer so probably you don't need to know much about the mapping but remember the followings to survive: 68 | 69 | - All mappings are shown by hitting `` 70 | - Enter *Vim mode*; a super mode of *Vim command mode* and *Insert mode*; by 1) Double clicking a cell, 2) Hit `` on a cell, or 3) Hit `i` on a cell 71 | - Leave *Vim mode* and re-enter *Jupyter mode* by `:q` or `` (`Shift-Escape`) 72 | - Enter *Insert mode* or leave *Insert mode* as like Vim (`i`, `a`, etc.) 73 | 74 | You can find detailed information about the mappings or concept in [Concept](https://github.com/lambdalisue/jupyter-vim-binding/wiki/Concept) page. 75 | 76 | 77 | Completion and Tooltip 78 | ------------------------------------------------------------------------------- 79 | 80 | jupyter-vim-binding supports ``/`` completion and `` tooltip in a code cell (not in markdown / raw cell). 81 | These mappings are not listed in a help panel, due to a technical limitation. 82 | 83 | When the user hits `` or ``, a completion panel like below will be shown. 84 | Once the completion panel is shown, users can select a candidate by ``/`` and apply by `` or cancel by ``. 85 | 86 | ![Completion](resource/completion.png) 87 | 88 | When user hit ``, a tooltip panel like below will be shown. 89 | The tooltip will disappear when users perform some actions like hitting a key. 90 | 91 | ![Tooltip](resource/tooltip.png) 92 | 93 | Note that you can repeat `` to make the tooltip larger (more information). 94 | 95 | 96 | Plug mappings 97 | ------------------------------------------------------------------------------- 98 | 99 | jupyter-vim-binding provides the following `` mappings for CodeMirror. 100 | 101 | - `(vim-binding-j)` : `j` which move to the next cell at the cell side 102 | - `(vim-binding-k)` : `k` which move to the previous cell at the cell side 103 | - `(vim-binding-gj)` : `gj` which move to the next cell at the cell side 104 | - `(vim-binding-gk)` : `gk` which move to the previous cell at the cell side 105 | - `(vim-binding-+)` : `+` which move to the next cell at the cell side 106 | - `(vim-binding--)` : `-` which move to the previous cell at the cell side 107 | - `(vim-binding-_)` : `_` which move to the next cell at the cell side 108 | 109 | While CodeMirror's Vim does not provide `noremap` type of mappings. 110 | You need to use these `` mappings to prevent an infinite loop (See samples in Customization section). 111 | 112 | 113 | Customization 114 | ------------------------------------------------------------------------------- 115 | 116 | To customize key mappings in *Vim mode*, you need to understand that there are two kinds of mappings in this extension: 117 | 118 | 1. Mappings provided by [Jupyter Notebook][], users can customize this type of mappings with [Keyboard shortcut editor][] provided in [IPython-notebook-extensions][] 119 | 2. Mappings provided by [CodeMirror's Vim][], users can customize this type of mappings with [`custom.js`][] as described below 120 | 121 | To customize mappings provided by [CodeMirror's Vim][], create a [`custom.js`][] at `~/.jupyter/custom/custom.js` (at least in Linux) and use [CodeMirror's Vim API][] to manipulate like: 122 | 123 | ```javascript 124 | // Configure CodeMirror Keymap 125 | require([ 126 | 'nbextensions/vim_binding/vim_binding', // depends your installation 127 | ], function() { 128 | // Map jj to 129 | CodeMirror.Vim.map("jj", "", "insert"); 130 | // Swap j/k and gj/gk (Note that mappings) 131 | CodeMirror.Vim.map("j", "(vim-binding-gj)", "normal"); 132 | CodeMirror.Vim.map("k", "(vim-binding-gk)", "normal"); 133 | CodeMirror.Vim.map("gj", "(vim-binding-j)", "normal"); 134 | CodeMirror.Vim.map("gk", "(vim-binding-k)", "normal"); 135 | }); 136 | 137 | // Configure Jupyter Keymap 138 | require([ 139 | 'nbextensions/vim_binding/vim_binding', 140 | 'base/js/namespace', 141 | ], function(vim_binding, ns) { 142 | // Add post callback 143 | vim_binding.on_ready_callbacks.push(function(){ 144 | var km = ns.keyboard_manager; 145 | // Allow Ctrl-2 to change the cell mode into Markdown in Vim normal mode 146 | km.edit_shortcuts.add_shortcut('ctrl-2', 'vim-binding:change-cell-to-markdown', true); 147 | // Update Help 148 | km.edit_shortcuts.events.trigger('rebuild.QuickHelp'); 149 | }); 150 | }); 151 | ``` 152 | 153 | If you would like to customize the design, create a your `custom.css` at `~/.jupyter/custom/custom.css` (at least in Linux) like: 154 | 155 | ```css 156 | /* Jupyter cell is in normal mode when code mirror */ 157 | .edit_mode .cell.selected .CodeMirror-focused.cm-fat-cursor { 158 | background-color: #F5F6EB !important; 159 | } 160 | /* Jupyter cell is in insert mode when code mirror */ 161 | .edit_mode .cell.selected .CodeMirror-focused:not(.cm-fat-cursor) { 162 | background-color: #F6EBF1 !important; 163 | } 164 | ``` 165 | 166 | See [Customization](https://github.com/lambdalisue/jupyter-vim-binding/wiki/Customization) to find useful snippets. Don't be afraid to share your snippets at that page ;-) 167 | 168 | [Keyboard shortcut editor]: https://github.com/ipython-contrib/IPython-notebook-extensions/tree/master/nbextensions/usability/keyboard_shortcut_editor 169 | [`custom.js`]: http://jdfreder-notebook.readthedocs.org/en/docs/examples/Notebook/JavaScript%20Notebook%20Extensions.html 170 | [CodeMirror's Vim API]: https://codemirror.net/doc/manual.html#vimapi 171 | 172 | 173 | Limitation 174 | ------------------------------------------------------------------------------- 175 | 176 | jupyter-vim-binding has the following technical limitation. 177 | If anybody knows about a confirmed workaround for these limitations, let me know. 178 | 179 | ### Google Chrome 180 | 181 | Google Chrome prohibits javascript from overriding several key mappings such as `Ctrl-N`, `Ctrl-T`, etc. 182 | Because of this policy, users have no chance to use default key mappings of jupyter-vim-binding, such as `` completion. 183 | 184 | - https://code.google.com/p/chromium/issues/detail?id=33056 185 | - http://stackoverflow.com/questions/15911785/overriding-shortcut-keys-in-firefox-and-chrome 186 | - https://github.com/liftoff/GateOne/issues/290 187 | 188 | ### Vivaldi 189 | 190 | The chromium-based [Vivaldi][vivaldi] browser provides more flexibility in key mapping customizations and might be a viable alternative to Google Chrome for power users. 191 | In contrast to Google Chrome or Chromium, (almost) all keyboard shortcuts in Vivaldi can be [changed or disabled][vivaldi-keyboard], including (but not limited to) `Ctrl-N`, `Ctrl-T`, `Ctrl-J`, etc. 192 | 193 | Furthermore, Vivaldi allows assigning a keyboard shortcut to temporarily [disable all other browser keyboard shortcuts][vivaldi-disable], making all key mappings available for other uses. 194 | Note that this temporary change applies globally to *all* tabs and windows of the browser instance (or "Profile") under consideration. To confine it to a subset of tabs, use a separate profile via [the `--user-data-dir=...` option][user-data-dir]. 195 | 196 | [vivaldi]: https://vivaldi.com/ 197 | [vivaldi-keyboard]: https://vivalditips.com/customization/shortcuts/en 198 | [vivaldi-disable]: https://www.ghacks.net/2017/02/07/vivaldi-tip-block-all-keyboard-shortcuts/ 199 | [user-data-dir]: https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md 200 | 201 | ### Clipboard 202 | 203 | Most modern browsers prohibit javascript from accessing a system clipboard without user action, such as clicking a button. 204 | Because of this, there is no chance to enable copy and paste through `yy`, `dd`, or `p` while HTML5 clipboard object cannot be retrieved in a `keydown` event. 205 | So Users need to use browser default mappings such as `Ctrl-C`, `Ctrl-V` if they want to copy and paste through a system clipboard. 206 | 207 | The followings are clipboard library for javascript, but all of them require `click` event or no paste support. 208 | 209 | - https://github.com/zeroclipboard/zeroclipboard 210 | - https://clipboardjs.com/ 211 | 212 | What we need is a `clipboard` object which can be used for copy and paste in a `keydown` event rather than `click` event. 213 | However, I don't know any workaround for this, so it is impossible to perform copy and paste in `yy` or `p` for now. 214 | 215 | 216 | License 217 | ------------------------------------------------------------------------------- 218 | 219 | The MIT License (MIT) 220 | 221 | Copyright (c) 2015-2016 Alisue, hashnote.net 222 | 223 | Permission is hereby granted, free of charge, to any person obtaining a copy 224 | of this software and associated documentation files (the "Software"), to deal 225 | in the Software without restriction, including without limitation the rights 226 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 227 | copies of the Software, and to permit persons to whom the Software is 228 | furnished to do so, subject to the following conditions: 229 | 230 | The above copyright notice and this permission notice shall be included in 231 | all copies or substantial portions of the Software. 232 | 233 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 234 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 235 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 236 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 237 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 238 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 239 | THE SOFTWARE. 240 | -------------------------------------------------------------------------------- /lib/codemirror.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery', 3 | 'base/js/namespace', 4 | 'codemirror/keymap/vim', 5 | ], function($, ns) { 6 | var undefined; 7 | var exports = {}; 8 | var Original = undefined; 9 | 10 | var moveByLinesOrCell = function moveByLinesOrCell(cm, head, motionArgs, vim) { 11 | var cur = head; 12 | var endCh = cur.ch; 13 | // TODO: these references will be undefined 14 | // Depending what our last motion was, we may want to do different 15 | // things. If our last motion was moving vertically, we want to 16 | // preserve the HPos from our last horizontal move. If our last motion 17 | // was going to the end of a line, moving vertically we should go to 18 | // the end of the line, etc. 19 | switch (vim.lastMotion) { 20 | case this.moveByLines: 21 | case this.moveByDisplayLines: 22 | case this.moveByScroll: 23 | case this.moveToColumn: 24 | case this.moveToEol: 25 | // JUPYTER PATCH: add our custom method to the motion cases 26 | case moveByLinesOrCell: 27 | endCh = vim.lastHPos; 28 | break; 29 | default: 30 | vim.lastHPos = endCh; 31 | } 32 | var repeat = motionArgs.repeat + (motionArgs.repeatOffset || 0); 33 | var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; 34 | var first = cm.firstLine(); 35 | var last = cm.lastLine(); 36 | // Vim cancels linewise motions that start on an edge and move beyond 37 | // that edge. It does not cancel motions that do not start on an edge. 38 | 39 | // JUPYTER PATCH BEGIN 40 | // here we insert the jumps to the next cells 41 | if(line < first || line > last){ 42 | var current_cell = ns.notebook.get_selected_cell(); 43 | var key = ''; 44 | if (current_cell.cell_type == 'markdown') { 45 | current_cell.execute(); 46 | } 47 | if (motionArgs.forward) { 48 | ns.notebook.select_next(); 49 | key = 'j'; 50 | } else { 51 | ns.notebook.select_prev(); 52 | key = 'k'; 53 | } 54 | ns.notebook.edit_mode(); 55 | var new_cell = ns.notebook.get_selected_cell(); 56 | if (current_cell !== new_cell && !!new_cell) { 57 | // The selected cell has moved. Move the cursor at very end 58 | var cm2 = new_cell.code_mirror; 59 | cm2.setCursor({ 60 | ch: cm2.getCursor().ch, 61 | line: motionArgs.forward ? cm2.firstLine() : cm2.lastLine() 62 | }); 63 | // Perform remaining repeats 64 | repeat = motionArgs.forward ? line - last : first - line; 65 | repeat -= 1; 66 | if (Math.abs(repeat) > 0) { 67 | CodeMirror.Vim.handleKey(cm2, repeat + key); // e.g. 4j, 6k, etc. 68 | } 69 | } 70 | return; 71 | } 72 | // JUPYTER PATCH END 73 | 74 | if (motionArgs.toFirstChar){ 75 | endCh = findFirstNonWhiteSpaceCharacter(cm.getLine(line)); 76 | vim.lastHPos = endCh; 77 | } 78 | vim.lastHSPos = cm.charCoords(CodeMirror.Pos(line, endCh), 'div').left; 79 | return CodeMirror.Pos(line, endCh); 80 | }; 81 | var moveByDisplayLinesOrCell = function moveByDisplayLinesOrCell(cm, head, motionArgs, vim) { 82 | var cur = head; 83 | switch (vim.lastMotion) { 84 | case this.moveByDisplayLines: 85 | case this.moveByScroll: 86 | case this.moveByLines: 87 | case this.moveToColumn: 88 | case this.moveToEol: 89 | // JUPYTER PATCH 90 | case moveByDisplayLinesOrCell: 91 | break; 92 | default: 93 | vim.lastHSPos = cm.charCoords(cur, 'div').left; 94 | } 95 | var repeat = motionArgs.repeat; 96 | var res = cm.findPosV( 97 | cur, (motionArgs.forward ? repeat : -repeat), 'line', vim.lastHSPos 98 | ); 99 | 100 | // JUPYTER PATCH BEGIN 101 | if (res.hitSide) { 102 | var current_cell = ns.notebook.get_selected_cell(); 103 | var key = ''; 104 | if (motionArgs.forward) { 105 | ns.notebook.select_next(); 106 | key = 'gj'; 107 | } else { 108 | ns.notebook.select_prev(); 109 | key = 'gk'; 110 | } 111 | ns.notebook.edit_mode(); 112 | var new_cell = ns.notebook.get_selected_cell(); 113 | if (current_cell !== new_cell && !!new_cell) { 114 | // The selected cell has moved. Move the cursor at very end 115 | var cm2 = new_cell.code_mirror; 116 | cm2.setCursor({ 117 | ch: cm2.getCursor().ch, 118 | line: motionArgs.forward ? cm2.firstLine() : cm2.lastLine() 119 | }); 120 | // Perform remaining repeats 121 | repeat = repeat - Math.abs(res.line - cur.line); 122 | repeat -= 1; 123 | if (repeat > 0) { 124 | CodeMirror.Vim.handleKey(cm2, repeat + key); // e.g. 4j, 6k, etc. 125 | } 126 | return; 127 | } 128 | } 129 | // JUPYTER PATCH END 130 | 131 | if (res.hitSide) { 132 | if (motionArgs.forward) { 133 | var lastCharCoords = cm.charCoords(res, 'div'); 134 | var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; 135 | res = cm.coordsChar(goalCoords, 'div'); 136 | } else { 137 | var resCoords = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), 'div'); 138 | resCoords.left = vim.lastHSPos; 139 | res = cm.coordsChar(resCoords, 'div'); 140 | } 141 | } 142 | vim.lastHPos = res.ch; 143 | return res; 144 | }; 145 | CodeMirror.Vim.defineMotion("moveByLinesOrCell", moveByLinesOrCell); 146 | CodeMirror.Vim.defineMotion("moveByDisplayLinesOrCell", moveByDisplayLinesOrCell); 147 | CodeMirror.Vim.mapCommand( 148 | "(vim-binding-k)", "motion", "moveByLinesOrCell", 149 | {forward: false, linewise: true }, 150 | {context: "normal"} 151 | ); 152 | CodeMirror.Vim.mapCommand( 153 | "(vim-binding-j)", "motion", "moveByLinesOrCell", 154 | {forward: true, linewise: true }, 155 | {context: "normal"} 156 | ); 157 | CodeMirror.Vim.mapCommand( 158 | "(vim-binding-gk)", "motion", "moveByDisplayLinesOrCell", 159 | {forward: false }, 160 | {context: "normal"} 161 | ); 162 | CodeMirror.Vim.mapCommand( 163 | "(vim-binding-gj)", "motion", "moveByDisplayLinesOrCell", 164 | {forward: true }, 165 | {context: "normal"} 166 | ); 167 | CodeMirror.Vim.mapCommand( 168 | "(vim-binding-+)", "motion", "moveByLinesOrCell", 169 | {forward: true, toFirstChar: true }, 170 | {context: "normal"} 171 | ); 172 | CodeMirror.Vim.mapCommand( 173 | "(vim-binding--)", "motion", "moveByLinesOrCell", 174 | {forward: false, toFirstChar: true }, 175 | {context: "normal"} 176 | ); 177 | CodeMirror.Vim.mapCommand( 178 | "(vim-binding-_)", "motion", "moveByLinesOrCell", 179 | {forward: true, toFirstChar: true, repeatOffset: -1 }, 180 | {context: "normal"} 181 | ); 182 | CodeMirror.Vim.defineEx("quit", "q", function(cm){ 183 | cm.leaveNormalMode(); 184 | }); 185 | 186 | exports.attach = function attach() { 187 | if (Original !== undefined) { 188 | return; 189 | } 190 | Original = $.extend(CodeMirror.prototype); 191 | CodeMirror.prototype.save = function() { 192 | ns.notebook.save_checkpoint(); 193 | }; 194 | CodeMirror.prototype.leaveInsertMode = function leaveInsertMode(cm) { 195 | CodeMirror.Vim.handleKey(cm || this, ''); 196 | }; 197 | CodeMirror.prototype.leaveNormalMode = function leaveNormalMode(cm) { 198 | ns.notebook.command_mode(); 199 | ns.notebook.focus_cell(); 200 | }; 201 | }; 202 | 203 | exports.detach = function detach() { 204 | if (Original === undefined) { 205 | return; 206 | } 207 | CodeMirror.prototype = Original; 208 | Original = undefined; 209 | CodeMirror.Vim.mapCommand( 210 | "k", "motion", "moveByLines", 211 | {forward: false, linewise: true }, 212 | {context: "normal"} 213 | ); 214 | CodeMirror.Vim.mapCommand( 215 | "j", "motion", "moveByLines", 216 | {forward: true, linewise: true }, 217 | {context: "normal"} 218 | ); 219 | CodeMirror.Vim.mapCommand( 220 | "gk", "motion", "moveByDisplayLines", 221 | {forward: false }, 222 | {context: "normal"} 223 | ); 224 | CodeMirror.Vim.mapCommand( 225 | "gj", "motion", "moveByDisplayLines", 226 | {forward: true }, 227 | {context: "normal"} 228 | ); 229 | CodeMirror.Vim.mapCommand( 230 | "+", "motion", "moveByLines", 231 | {forward: true, toFirstChar: true }, 232 | {context: "normal"} 233 | ); 234 | CodeMirror.Vim.mapCommand( 235 | "-", "motion", "moveByLines", 236 | {forward: false, toFirstChar: true }, 237 | {context: "normal"} 238 | ); 239 | CodeMirror.Vim.mapCommand( 240 | "_", "motion", "moveByLines", 241 | {forward: true, toFirstChar: true, repeatOffset: -1 }, 242 | {context: "normal"} 243 | ); 244 | }; 245 | 246 | // NOTE: 247 | // These default mapping requires to be out side of 'attach' to allow 248 | // users to customize in 'custom.js' 249 | CodeMirror.Vim.map('k', '(vim-binding-k)', 'normal'); 250 | CodeMirror.Vim.map('j', '(vim-binding-j)', 'normal'); 251 | CodeMirror.Vim.map('gk', '(vim-binding-gk)', 'normal'); 252 | CodeMirror.Vim.map('gj', '(vim-binding-gj)', 'normal'); 253 | CodeMirror.Vim.map('+', '(vim-binding-+)', 'normal'); 254 | CodeMirror.Vim.map('-', '(vim-binding--)', 'normal'); 255 | CodeMirror.Vim.map('_', '(vim-binding-_)', 'normal'); 256 | CodeMirror.Vim.map('', '', 'normal'); 257 | CodeMirror.Vim.map('', '', 'normal'); 258 | CodeMirror.Vim.map('', '', 'normal'); 259 | CodeMirror.Vim.map('', '', 'normal'); 260 | 261 | return exports; 262 | }); 263 | -------------------------------------------------------------------------------- /lib/jupyter/actions.js: -------------------------------------------------------------------------------- 1 | // NOTE: 2 | // The default 'codecell.js' does not support / completion and 3 | // tooltip so monkey-patch the module to support that keys 4 | define([ 5 | 'jquery', 6 | 'base/js/namespace', 7 | ], function($, ns) { 8 | "use strict"; 9 | var undefined; 10 | var exports = {}; 11 | var actions = ns.keyboard_manager.actions; 12 | var Original = undefined; 13 | 14 | var isInInsertMode = function isInInsertMode(cm) { 15 | return cm && cm.state.vim.insertMode; 16 | }; 17 | 18 | exports.attach = function(params) { 19 | if (Original !== undefined) { 20 | return; 21 | } 22 | Original = $.extend(actions._actions); 23 | // Register altrenative actions of jupyter-notebook 24 | for (var name in actions._actions) { 25 | if (name.match(/^jupyter-notebook:/)) { 26 | var action = (function() { 27 | var action = $.extend({}, actions._actions[name]); 28 | var handler = action.handler; 29 | action.handler = function(env, event) { 30 | var cell = env.notebook.get_selected_cell(); 31 | if (cell && (isInInsertMode(cell.code_mirror))) { 32 | // CodeMirror is in InsertMode, prevent any Jupyter action in 33 | // InsertMode and let CodeMirror to do things 34 | return; 35 | } 36 | var restore = env.notebook.mode === 'edit'; 37 | env.notebook.command_mode(); 38 | var result = handler(env, event); 39 | if (restore) env.notebook.edit_mode(); 40 | return result; 41 | }; 42 | return action; 43 | })(); 44 | actions.register( 45 | action, 46 | name.replace(/^jupyter-notebook:/, ''), 47 | 'vim-binding' 48 | ); 49 | } 50 | } 51 | 52 | // vim-binding original actions (Insert mode) 53 | actions.register({ 54 | 'help': 'run cell, select below', 55 | 'handler': function(env, event) { 56 | env.notebook.command_mode(); 57 | actions.call('jupyter-notebook:run-cell-and-select-next', event, env); 58 | env.notebook.edit_mode(); 59 | } 60 | }, 'run-cell-and-select-next', 'vim-binding'); 61 | 62 | actions.register({ 63 | 'help': 'run cell, select below', 64 | 'handler': function(env, event) { 65 | env.notebook.command_mode(); 66 | if (env.notebook.get_selected_cell().cell_type == 'markdown') { 67 | actions.call('jupyter-notebook:run-cell-and-select-next', event, env); 68 | } else { 69 | actions.call('jupyter-notebook:select-next-cell', event, env); 70 | } 71 | env.notebook.edit_mode(); 72 | } 73 | }, 'select-next-cell-eval-markdown', 'vim-binding'); 74 | 75 | actions.register({ 76 | 'help': 'run cell, select above', 77 | 'handler': function(env, event) { 78 | env.notebook.command_mode(); 79 | if (env.notebook.get_selected_cell().cell_type == 'markdown') { 80 | actions.call('jupyter-notebook:run-cell', event, env); 81 | } 82 | actions.call('jupyter-notebook:select-previous-cell', event, env); 83 | env.notebook.edit_mode(); 84 | } 85 | }, 'select-previous-cell-eval-markdown', 'vim-binding'); 86 | 87 | actions.register({ 88 | 'help': 'run selected cells', 89 | 'handler': function(env, event) { 90 | env.notebook.command_mode(); 91 | actions.call('jupyter-notebook:run-cell', event, env); 92 | env.notebook.edit_mode(); 93 | } 94 | }, 'run-cell', 'vim-binding'); 95 | 96 | actions.register({ 97 | 'help': 'run cell, insert below', 98 | 'handler': function(env, event) { 99 | env.notebook.command_mode(); 100 | actions.call('jupyter-notebook:run-cell-and-insert-below', event, env); 101 | env.notebook.edit_mode(); 102 | } 103 | }, 'run-cell-and-insert-below', 'vim-binding'); 104 | 105 | actions.register({ 106 | 'help': 'run all cells', 107 | 'handler': function(env, event) { 108 | env.notebook.command_mode(); 109 | actions.call('jupyter-notebook:run-all-cells', event, env); 110 | env.notebook.edit_mode(); 111 | } 112 | }, 'run-all-cells', 'vim-binding'); 113 | 114 | actions.register({ 115 | 'handler': function(env, event) { 116 | env.notebook.command_mode(); 117 | actions.call('jupyter-notebook:run-all-cells-above', event, env); 118 | env.notebook.edit_mode(); 119 | } 120 | }, 'run-all-cells-above', 'vim-binding'); 121 | 122 | actions.register({ 123 | 'handler': function(env, event) { 124 | env.notebook.command_mode(); 125 | actions.call('jupyter-notebook:run-all-cells-below', event, env); 126 | env.notebook.edit_mode(); 127 | } 128 | }, 'run-all-cells-below', 'vim-binding'); 129 | 130 | // vin-binding original actions (Command mode) 131 | actions.register({ 132 | 'help': 'scroll notebook up', 133 | 'handler': function(env, event) { 134 | // Page scroll is a bit buggy and slow so replace it to faster one. 135 | // If the scroll-speed is fast enough, I think we actually don't need 136 | // to scroll exactly one page 137 | var repeat = 20; 138 | while (repeat) { 139 | actions.call('vim-binding-normal:scroll-up', event, env); 140 | repeat--; 141 | } 142 | } 143 | }, 'scroll-notebook-up', 'vim-binding-normal'); 144 | 145 | actions.register({ 146 | 'help': 'scroll notebook down', 147 | 'handler': function(env, event) { 148 | // Page scroll is a bit buggy and slow so replace it to faster one. 149 | // If the scroll-speed is fast enough, I think we actually don't need 150 | // to scroll exactly one page 151 | var repeat = 20; 152 | while (repeat) { 153 | actions.call('vim-binding-normal:scroll-down', event, env); 154 | repeat--; 155 | } 156 | } 157 | }, 'scroll-notebook-down', 'vim-binding-normal'); 158 | 159 | actions.register({ 160 | 'help': 'scroll up', 161 | 'handler': function(env, event) { 162 | var scrollUnit = params.scroll_unit; 163 | var site = document.querySelector('#site'); 164 | var prev = site.scrollTop; 165 | site.scrollTop -= scrollUnit; 166 | } 167 | }, 'scroll-up', 'vim-binding-normal'); 168 | 169 | actions.register({ 170 | 'help': 'scroll down', 171 | 'handler': function(env, event) { 172 | var scrollUnit = params.scroll_unit; 173 | var site = document.querySelector('#site'); 174 | var prev = site.scrollTop; 175 | site.scrollTop += scrollUnit; 176 | } 177 | }, 'scroll-down', 'vim-binding-normal'); 178 | 179 | actions.register({ 180 | 'help': 'select the first cell', 181 | 'handler': function(env, event) { 182 | var cells = env.notebook.get_cells(); 183 | if (cells.length > 0) { 184 | cells[0].focus_cell(); 185 | } 186 | } 187 | }, 'select-first-cell', 'vim-binding-normal'); 188 | 189 | actions.register({ 190 | 'help': 'select the last cell', 191 | 'handler': function(env, event) { 192 | var cells = env.notebook.get_cells(); 193 | if (cells.length > 0) { 194 | cells[cells.length - 1].focus_cell(); 195 | } 196 | } 197 | }, 'select-last-cell', 'vim-binding-normal'); 198 | 199 | actions.register({ 200 | 'help': 'expand output', 201 | 'handler': function(env, event) { 202 | env.notebook.expand_output(); 203 | } 204 | }, 'expand-output', 'vim-binding-normal'); 205 | 206 | actions.register({ 207 | 'help': 'expand all output', 208 | 'handler': function(env, event) { 209 | env.notebook.expand_all_output(); 210 | } 211 | }, 'expand-all-output', 'vim-binding-normal'); 212 | 213 | actions.register({ 214 | 'help': 'collapse output', 215 | 'handler': function(env, event) { 216 | env.notebook.collapse_output(); 217 | } 218 | }, 'collapse-output', 'vim-binding-normal'); 219 | 220 | actions.register({ 221 | 'help': 'collapse all output', 222 | 'handler': function(env, event) { 223 | env.notebook.collapse_all_output(); 224 | } 225 | }, 'collapse-all-output', 'vim-binding-normal'); 226 | 227 | 228 | // vim-binding original actions (Edit mode) 229 | actions.register({ 230 | 'help': 'scroll notebook up', 231 | 'handler': function(env, event) { 232 | var cell = env.notebook.get_selected_cell(); 233 | if (cell && isInInsertMode(cell.code_mirror)) { 234 | return; 235 | } 236 | actions.call('vim-binding-normal:scroll-notebook-up', event, env); 237 | env.notebook.edit_mode(); 238 | } 239 | }, 'scroll-notebook-up', 'vim-binding'); 240 | 241 | actions.register({ 242 | 'help': 'scroll notebook down', 243 | 'handler': function(env, event) { 244 | var cell = env.notebook.get_selected_cell(); 245 | if (cell && isInInsertMode(cell.code_mirror)) { 246 | return; 247 | } 248 | actions.call('vim-binding-normal:scroll-notebook-down', event, env); 249 | env.notebook.edit_mode(); 250 | } 251 | }, 'scroll-notebook-down', 'vim-binding'); 252 | 253 | actions.register({ 254 | 'help': 'scroll up', 255 | 'handler': function(env, event) { 256 | var cell = env.notebook.get_selected_cell(); 257 | if (cell && isInInsertMode(cell.code_mirror)) { 258 | return; 259 | } 260 | actions.call('vim-binding-normal:scroll-up', event, env); 261 | env.notebook.edit_mode(); 262 | } 263 | }, 'scroll-up', 'vim-binding'); 264 | 265 | actions.register({ 266 | 'help': 'scroll down', 267 | 'handler': function(env, event) { 268 | var cell = env.notebook.get_selected_cell(); 269 | if (cell && isInInsertMode(cell.code_mirror)) { 270 | return; 271 | } 272 | actions.call('vim-binding-normal:scroll-down', event, env); 273 | env.notebook.edit_mode(); 274 | } 275 | }, 'scroll-down', 'vim-binding'); 276 | 277 | actions.register({ 278 | 'help': 'select the first cell', 279 | 'handler': function(env, event) { 280 | var cell = env.notebook.get_selected_cell(); 281 | if (cell && isInInsertMode(cell.code_mirror)) { 282 | return; 283 | } 284 | env.notebook.command_mode(); 285 | actions.call('vim-binding-normal:select-first-cell', event, env); 286 | env.notebook.edit_mode(); 287 | } 288 | }, 'select-first-cell', 'vim-binding'); 289 | 290 | actions.register({ 291 | 'help': 'select the last cell', 292 | 'handler': function(env, event) { 293 | var cell = env.notebook.get_selected_cell(); 294 | if (cell && isInInsertMode(cell.code_mirror)) { 295 | return; 296 | } 297 | env.notebook.command_mode(); 298 | actions.call('vim-binding-normal:select-last-cell', event, env); 299 | env.notebook.edit_mode(); 300 | } 301 | }, 'select-last-cell', 'vim-binding'); 302 | 303 | actions.register({ 304 | 'help': 'expand output', 305 | 'handler': function(env, event) { 306 | var cell = env.notebook.get_selected_cell(); 307 | if (cell && isInInsertMode(cell.code_mirror)) { 308 | return; 309 | } 310 | env.notebook.command_mode(); 311 | actions.call('vim-binding-normal:expand-output', event, env); 312 | env.notebook.edit_mode(); 313 | } 314 | }, 'expand-output', 'vim-binding'); 315 | 316 | actions.register({ 317 | 'help': 'expand all output', 318 | 'handler': function(env, event) { 319 | var cell = env.notebook.get_selected_cell(); 320 | if (cell && isInInsertMode(cell.code_mirror)) { 321 | return; 322 | } 323 | env.notebook.command_mode(); 324 | actions.call('vim-binding-normal:expand-all-output', event, env); 325 | env.notebook.edit_mode(); 326 | } 327 | }, 'expand-all-output', 'vim-binding'); 328 | 329 | actions.register({ 330 | 'help': 'collapse output', 331 | 'handler': function(env, event) { 332 | var cell = env.notebook.get_selected_cell(); 333 | if (cell && isInInsertMode(cell.code_mirror)) { 334 | return; 335 | } 336 | env.notebook.command_mode(); 337 | actions.call('vim-binding-normal:collapse-output', event, env); 338 | env.notebook.edit_mode(); 339 | } 340 | }, 'collapse-output', 'vim-binding'); 341 | 342 | actions.register({ 343 | 'help': 'collapse all output', 344 | 'handler': function(env, event) { 345 | var cell = env.notebook.get_selected_cell(); 346 | if (cell && isInInsertMode(cell.code_mirror)) { 347 | return; 348 | } 349 | env.notebook.command_mode(); 350 | actions.call('vim-binding-normal:collapse-all-output', event, env); 351 | env.notebook.edit_mode(); 352 | } 353 | }, 'collapse-all-output', 'vim-binding'); 354 | 355 | 356 | // jupyter-notebook actions which should call command_mode but edit_mode 357 | actions.register({ 358 | 'help': 'extend selected cells above', 359 | 'handler': function(env, event) { 360 | var cell = env.notebook.get_selected_cell(); 361 | if (cell && isInInsertMode(cell.code_mirror)) { 362 | return; 363 | } 364 | env.notebook.command_mode(); 365 | return actions.call( 366 | 'jupyter-notebook:extend-selection-above', event, env 367 | ); 368 | } 369 | }, 'extend-selection-above', 'vim-binding'); 370 | 371 | actions.register({ 372 | 'help': 'extend selected cells below', 373 | 'handler': function(env, event) { 374 | var cell = env.notebook.get_selected_cell(); 375 | if (cell && isInInsertMode(cell.code_mirror)) { 376 | return; 377 | } 378 | env.notebook.command_mode(); 379 | return actions.call( 380 | 'jupyter-notebook:extend-selection-below', event, env 381 | ); 382 | } 383 | }, 'extend-selection-below', 'vim-binding'); 384 | }; 385 | 386 | exports.detach = function() { 387 | if (Original === undefined) { 388 | return; 389 | } 390 | actions._actions = Original; 391 | Original = undefined; 392 | }; 393 | 394 | return exports; 395 | }); 396 | -------------------------------------------------------------------------------- /lib/jupyter/codecell.js: -------------------------------------------------------------------------------- 1 | // NOTE: 2 | // The default 'codecell.js' does not support / completion and 3 | // tooltip so monkey-patch the module to support that keys 4 | define([ 5 | 'jquery', 6 | 'base/js/keyboard', 7 | 'notebook/js/codecell', 8 | ], function($, keyboard, codecell) { 9 | "use strict"; 10 | var undefined; 11 | var exports = {}; 12 | var keycodes = keyboard.keycodes; 13 | var Original = undefined; 14 | var CodeCell = codecell.CodeCell; 15 | 16 | exports.attach = function attach() { 17 | if (Original !== undefined) { 18 | return; 19 | } 20 | Original = $.extend(CodeCell.prototype); 21 | CodeCell.prototype.handle_codemirror_keyevent = function handle_codemirror_keyevent(editor, event) { 22 | if (!this.completer.visible && event.type === 'keydown') { 23 | var code = event.keyCode; 24 | var ctrl = event.ctrlKey; 25 | 26 | if (ctrl && code === keycodes.g) { 27 | if (editor.somethingSelected() || editor.getSelections().length !== 1){ 28 | var anchor = editor.getCursor("anchor"); 29 | var head = editor.getCursor("head"); 30 | if( anchor.line !== head.line){ 31 | return false; 32 | } 33 | } 34 | this.tooltip.request(this); 35 | event.codemirrorIgnore = true; 36 | event.preventDefault(); 37 | return true; 38 | } else if (ctrl && (code === keycodes.n || code === keycodes.p)) { 39 | this.tooltip.remove_and_cancel_tooltip(); 40 | 41 | // completion does not work on multicursor, it might be possible though in some cases 42 | if (editor.somethingSelected() || editor.getSelections().length > 1) { 43 | return false; 44 | } 45 | event.codemirrorIgnore = true; 46 | event.preventDefault(); 47 | this.completer.startCompletion(); 48 | return true; 49 | } 50 | } 51 | return Original.handle_codemirror_keyevent.apply(this, arguments); 52 | }; 53 | }; 54 | 55 | exports.detach = function detach() { 56 | if (Original === undefined) { 57 | return; 58 | } 59 | CodeCell.prototype = Original; 60 | Original = undefined; 61 | }; 62 | 63 | return exports; 64 | }); 65 | -------------------------------------------------------------------------------- /lib/jupyter/completer.js: -------------------------------------------------------------------------------- 1 | // NOTE: 2 | // The default 'completer.js' does not support / completion 3 | // so monkey-patch the module to support that keys 4 | define([ 5 | 'jquery', 6 | 'base/js/keyboard', 7 | 'notebook/js/completer', 8 | ], function($, keyboard, completer) { 9 | "use strict"; 10 | var undefined; 11 | var exports = {}; 12 | var keycodes = keyboard.keycodes; 13 | var Original = undefined; 14 | var Completer = completer.Completer; 15 | 16 | exports.attach = function attach() { 17 | if (Original !== undefined) { 18 | return; 19 | } 20 | Original = $.extend({}, Completer.prototype); 21 | Completer.prototype.keydown = function(event) { 22 | var code = event.keyCode; 23 | var ctrl = event.ctrlKey; 24 | var alternative; 25 | if (ctrl && (code === keycodes.n || code === keycodes.p)) { 26 | alternative = { 27 | 'key': code == keycodes.p ? 'up' : 'down', 28 | 'code': code == keycodes.p ? keycodes.up : keycodes.down, 29 | }; 30 | } else if (ctrl && code === keycodes.y) { 31 | alternative = { 32 | 'key': '', 33 | 'code': keycodes.enter, 34 | }; 35 | } else if (ctrl && code === keycodes.e) { 36 | alternative = { 37 | 'key': '', 38 | 'code': keycodes.esc, 39 | }; 40 | } else if (ctrl && code === keycodes.l) { 41 | alternative = { 42 | 'key': '', 43 | 'code': keycodes.tab, 44 | }; 45 | } 46 | if (alternative !== undefined) { 47 | if (alternative.code !== keycodes.tab) { 48 | // the following could not be called in orignal code while we create 49 | // a new keyboard event so call these at this point 50 | event.codemirrorIgnore = true; 51 | event._ipkmIgnore = true; 52 | event.preventDefault(); 53 | } 54 | Original.keydown.call(this, new KeyboardEvent(event.type, { 55 | 'key': alternative.key, 56 | 'code': alternative.code, 57 | 'location': event.location, 58 | 'ctrlKey': alternative.ctrlKey !== undefined ? event.ctrlKey : alternative.ctrlKey, 59 | 'shiftKey': alternative.shiftKey !== undefined ? event.ctrlKey : alternative.ctrlKey, 60 | 'altKey': alternative.altKey !== undefined ? event.altKey : alternative.altKey, 61 | 'metaKey': alternative.metaKey !== undefined ? event.metaKey : alternative.metaKey, 62 | 'repeat': event.repeat, 63 | 'isComposing': event.isComposing, 64 | 'charCode': alternative.key === '' ? 0 : alternative.key.charCodeAt(0), 65 | 'keyCode': alternative.code, 66 | 'which': alternative.code, 67 | })); 68 | } else { 69 | Original.keydown.call(this, event); 70 | } 71 | }; 72 | }; 73 | 74 | exports.detach = function detach() { 75 | if (Original === undefined) { 76 | return; 77 | } 78 | Completer.prototype = Original; 79 | Original = undefined; 80 | }; 81 | 82 | return exports; 83 | }); 84 | -------------------------------------------------------------------------------- /lib/jupyter/keyboard.js: -------------------------------------------------------------------------------- 1 | // NOTE: 2 | // It seems that an internal function ('only_modifier_event') of keyboard.js 3 | // in jupyter-client 4.1 has a bug in Firefox. 4 | // It checks 'event.altKey || event.ctrlKey || ...' but the result is always 5 | // 'false' if only modifier keys are pressed. 6 | // So monkey-patch the prototype of the class to fix this issue. 7 | define([ 8 | 'jquery', 9 | 'base/js/keyboard' 10 | ], function($, keyboard) { 11 | "use strict"; 12 | var undefined; 13 | var exports = {}; 14 | var Original = undefined; 15 | var ShortcutManager = keyboard.ShortcutManager; 16 | 17 | var only_modifier_event = function only_modifier_event(event){ 18 | var key = keyboard.inv_keycodes[event.which]; 19 | return (key === 'alt'|| key === 'ctrl'|| key === 'meta'|| key === 'shift'); 20 | }; 21 | 22 | exports.attach = function attach() { 23 | if (Original === undefined) { 24 | Original = $.extend(ShortcutManager.prototype); 25 | ShortcutManager.prototype.call_handler = function(event) { 26 | this.clearsoon(); 27 | if(only_modifier_event(event)){ 28 | return true; 29 | } 30 | return Original.call_handler.apply(this, arguments); 31 | }; 32 | } 33 | }; 34 | 35 | exports.detach = function detach() { 36 | if (Original !== undefined) { 37 | ShortcutManager.prototype = Original; 38 | Original = undefined; 39 | } 40 | }; 41 | 42 | return exports; 43 | }); 44 | -------------------------------------------------------------------------------- /lib/jupyter/notebook.js: -------------------------------------------------------------------------------- 1 | // NOTE: 2 | // The default 'notebook.js' does not suite for Vimmer in some case 3 | // so monkey-patch the module to regulate the behavior 4 | define([ 5 | 'jquery', 6 | 'base/js/namespace', 7 | 'notebook/js/notebook', 8 | ], function($, ns, notebook) { 9 | "use strict"; 10 | var undefined; 11 | var exports = {}; 12 | var Original = undefined; 13 | var Notebook = notebook.Notebook; 14 | 15 | var was_in_insert_before_blur = false; 16 | var onBlurWindow = function() { 17 | var cell = ns.notebook.get_selected_cell(); 18 | if (cell && cell.code_mirror) { 19 | was_in_insert_before_blur = cell.code_mirror.state.vim.insertMode; 20 | } 21 | }; 22 | 23 | exports.attach = function attach() { 24 | if (Original !== undefined) { 25 | return; 26 | } 27 | Original = $.extend({}, Notebook.prototype); 28 | Notebook.prototype.handle_command_mode = function handle_command_mode(cell) { 29 | if (document.querySelector('.CodeMirror-dialog')) { 30 | // .CodeMirror-dialog exists, mean that user hit ':' to enter Vim's 31 | // command mode so do not leave Jupyter's edit mode in this case 32 | return; 33 | } 34 | return Original.handle_command_mode.apply(this, arguments); 35 | }; 36 | Notebook.prototype.handle_edit_mode = function handle_edit_mode(cell) { 37 | if (cell.code_mirror && !was_in_insert_before_blur) { 38 | cell.code_mirror.leaveInsertMode(); 39 | } 40 | was_in_insert_before_blur = false; 41 | return Original.handle_edit_mode.apply(this, arguments); 42 | }; 43 | window.addEventListener('blur', onBlurWindow); 44 | }; 45 | 46 | exports.detach = function detach() { 47 | if (Original === undefined) { 48 | return; 49 | } 50 | Notebook.prototype = Original; 51 | Original = undefined; 52 | window.removeEventListener('blur', onBlurWindow); 53 | }; 54 | 55 | return exports; 56 | }); 57 | -------------------------------------------------------------------------------- /lib/jupyter/quickhelp.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'base/js/utils', 3 | 'base/js/dialog', 4 | 'underscore', 5 | 'notebook/js/quickhelp' 6 | ], function(utils, dialog, _, quickhelp) { 7 | "use strict"; 8 | var exports = {}; 9 | exports.attach = function attach() { 10 | // Copyright (c) Jupyter Development Team. 11 | // Distributed under the terms of the Modified BSD License. 12 | var platform = utils.platform; 13 | 14 | // Override Jupyter's QuickHelp class 15 | var cmd_ctrl = 'Ctrl-'; 16 | var platform_specific; 17 | 18 | if (platform === 'MacOS') { 19 | // Mac OS X specific 20 | cmd_ctrl = 'Cmd-'; 21 | platform_specific = [ 22 | { shortcut: "Cmd-Up", help:"go to cell start" }, 23 | { shortcut: "Cmd-Down", help:"go to cell end" }, 24 | { shortcut: "Alt-Left", help:"go one word left" }, 25 | { shortcut: "Alt-Right", help:"go one word right" }, 26 | { shortcut: "Alt-Backspace", help:"delete word before" }, 27 | { shortcut: "Alt-Delete", help:"delete word after" }, 28 | ]; 29 | } else { 30 | // PC specific 31 | platform_specific = [ 32 | { shortcut: "Ctrl-Home", help:"go to cell start" }, 33 | { shortcut: "Ctrl-Up", help:"go to cell start" }, 34 | { shortcut: "Ctrl-End", help:"go to cell end" }, 35 | { shortcut: "Ctrl-Down", help:"go to cell end" }, 36 | { shortcut: "Ctrl-Left", help:"go one word left" }, 37 | { shortcut: "Ctrl-Right", help:"go one word right" }, 38 | { shortcut: "Ctrl-Backspace", help:"delete word before" }, 39 | { shortcut: "Ctrl-Delete", help:"delete word after" }, 40 | ]; 41 | } 42 | 43 | var cm_shortcuts = [ 44 | { shortcut: "Ctrl-n", help:"code completion forward" }, 45 | { shortcut: "Ctrl-p", help:"code completion backward" }, 46 | { shortcut: "Ctrl-g", help:"tooltip" }, 47 | { shortcut: ">>", help:"indent" }, 48 | { shortcut: "<<", help:"dedent" }, 49 | { shortcut: "ggVG", help:"select all" }, 50 | { shortcut: "u", help:"undo" }, 51 | { shortcut: "Ctrl-R", help:"redo" }, 52 | ].concat( platform_specific ); 53 | 54 | var mac_humanize_map = { 55 | // all these are unicode, will probably display badly on anything except macs. 56 | // these are the standard symbol that are used in MacOS native menus 57 | // cf http://apple.stackexchange.com/questions/55727/ 58 | // for htmlentities and/or unicode value 59 | 'cmd':'⌘', 60 | 'shift':'⇧', 61 | 'alt':'⌥', 62 | 'up':'↑', 63 | 'down':'↓', 64 | 'left':'←', 65 | 'right':'→', 66 | 'eject':'⏏', 67 | 'tab':'⇥', 68 | 'backtab':'⇤', 69 | 'capslock':'⇪', 70 | 'esc':'esc', 71 | 'ctrl':'⌃', 72 | 'enter':'↩', 73 | 'pageup':'⇞', 74 | 'pagedown':'⇟', 75 | 'home':'↖', 76 | 'end':'↘', 77 | 'altenter':'⌤', 78 | 'space':'␣', 79 | 'delete':'⌦', 80 | 'backspace':'⌫', 81 | 'apple':'', 82 | }; 83 | 84 | var default_humanize_map = { 85 | 'shift':'Shift', 86 | 'alt':'Alt', 87 | 'up':'Up', 88 | 'down':'Down', 89 | 'left':'Left', 90 | 'right':'Right', 91 | 'tab':'Tab', 92 | 'capslock':'Caps Lock', 93 | 'esc':'Esc', 94 | 'ctrl':'Ctrl', 95 | 'enter':'Enter', 96 | 'pageup':'Page Up', 97 | 'pagedown':'Page Down', 98 | 'home':'Home', 99 | 'end':'End', 100 | 'space':'Space', 101 | 'backspace':'Backspace', 102 | '-':'Minus' 103 | }; 104 | 105 | var humanize_map; 106 | 107 | if (platform === 'MacOS'){ 108 | humanize_map = mac_humanize_map; 109 | } else { 110 | humanize_map = default_humanize_map; 111 | } 112 | 113 | var special_case = { pageup: "PageUp", pagedown: "Page Down" }; 114 | 115 | function humanize_key(key){ 116 | if (key.length === 1){ 117 | return key.toUpperCase(); 118 | } 119 | 120 | key = humanize_map[key.toLowerCase()]||key; 121 | 122 | if (key.indexOf(',') === -1){ 123 | return ( special_case[key] ? special_case[key] : key.charAt(0).toUpperCase() + key.slice(1) ); 124 | } 125 | } 126 | 127 | // return an **html** string of the keyboard shortcut 128 | // for human eyes consumption. 129 | // the sequence is a string, comma sepparated linkt of shortcut, 130 | // where the shortcut is a list of dash-joined keys. 131 | // Each shortcut will be wrapped in tag, and joined by comma is in a 132 | // sequence. 133 | // 134 | // Depending on the platform each shortcut will be normalized, with or without dashes. 135 | // and replace with the corresponding unicode symbol for modifier if necessary. 136 | function humanize_sequence(sequence){ 137 | var joinchar = ','; 138 | var hum = _.map(sequence.replace(/meta/g, 'cmd').split(','), humanize_shortcut).join(joinchar); 139 | return hum; 140 | } 141 | 142 | function _humanize_sequence(sequence){ 143 | var joinchar = ','; 144 | var hum = _.map(sequence.replace(/meta/g, 'cmd').split(','), _humanize_shortcut).join(joinchar); 145 | return hum; 146 | } 147 | 148 | function _humanize_shortcut(shortcut){ 149 | var joinchar = '-'; 150 | if (platform === 'MacOS'){ 151 | joinchar = ''; 152 | } 153 | return _.map(shortcut.split('-'), humanize_key ).join(joinchar); 154 | } 155 | 156 | function humanize_shortcut(shortcut){ 157 | return ''+_humanize_shortcut(shortcut)+''; 158 | } 159 | 160 | 161 | quickhelp.QuickHelp.prototype.show_keyboard_shortcuts = function () { 162 | /** 163 | * toggles display of keyboard shortcut dialog 164 | */ 165 | var that = this; 166 | if ( this.force_rebuild ) { 167 | this.shortcut_dialog.remove(); 168 | delete(this.shortcut_dialog); 169 | this.force_rebuild = false; 170 | } 171 | if ( this.shortcut_dialog ){ 172 | // if dialog is already shown, close it 173 | $(this.shortcut_dialog).modal("toggle"); 174 | return; 175 | } 176 | var command_shortcuts = this.keyboard_manager.command_shortcuts.help(); 177 | var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help(); 178 | var help, shortcut; 179 | var i, half, n; 180 | var element = $('
'); 181 | 182 | // The documentation 183 | var doc = $('
').addClass('alert alert-info'); 184 | doc.append( 185 | 'The Jupyter Notebook with jupyter-vim-binding has two different keyboard input modes.' + 186 | 'Jupyter mode binds the keyboard to notebook level commands and is indicated by a grey cell border with a blue left margin.' + 187 | 'Vim command mode allows you to type code/text into a cell and is indicated by a green cell border.' 188 | ); 189 | element.append(doc); 190 | if (platform === 'MacOS') { 191 | doc = $('
').addClass('alert alert-info'); 192 | var key_div = this.build_key_names(); 193 | doc.append(key_div); 194 | element.append(doc); 195 | } 196 | 197 | // Command mode 198 | var cmd_div = this.build_command_help(); 199 | element.append(cmd_div); 200 | 201 | // Edit mode 202 | var edit_div = this.build_edit_help(cm_shortcuts); 203 | element.append(edit_div); 204 | 205 | this.shortcut_dialog = dialog.modal({ 206 | title : "Keyboard shortcuts", 207 | body : element, 208 | destroy : false, 209 | buttons : { 210 | Close : {} 211 | }, 212 | notebook: this.notebook, 213 | keyboard_manager: this.keyboard_manager, 214 | }); 215 | this.shortcut_dialog.addClass("modal_stretch"); 216 | 217 | this.events.on('rebuild.QuickHelp', function() { that.force_rebuild = true;}); 218 | }; 219 | 220 | quickhelp.QuickHelp.prototype.build_key_names = function () { 221 | var key_names_mac = [ 222 | { shortcut:"⌘", help:"Command" }, 223 | { shortcut:"⌃", help:"Control" }, 224 | { shortcut:"⌥", help:"Option" }, 225 | { shortcut:"⇧", help:"Shift" }, 226 | { shortcut:"↩", help:"Return" }, 227 | { shortcut:"␣", help:"Space" }, 228 | { shortcut:"⇥", help:"Tab" }]; 229 | var i, half, n; 230 | var div = $('
').append('Mac OS X modifier keys:'); 231 | var sub_div = $('
').addClass('container-fluid'); 232 | var col1 = $('
').addClass('col-md-6'); 233 | var col2 = $('
').addClass('col-md-6'); 234 | n = key_names_mac.length; 235 | half = ~~(n/2); 236 | for (i=0; iJupyter Mode (press Shift-Esc to enable)', command_shortcuts); 251 | }; 252 | 253 | 254 | quickhelp.QuickHelp.prototype.build_edit_help = function (cm_shortcuts) { 255 | var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help(); 256 | edit_shortcuts = jQuery.merge(jQuery.merge([], cm_shortcuts), edit_shortcuts); 257 | return build_div('

Vim Command Mode (press i to enable)

', edit_shortcuts); 258 | }; 259 | 260 | var build_one = function (s) { 261 | var help = s.help; 262 | var shortcut = ''; 263 | if(s.shortcut){ 264 | shortcut = humanize_sequence(s.shortcut); 265 | } 266 | return $('
').addClass('quickhelp'). 267 | append($('').addClass('shortcut_key').append($(shortcut))). 268 | append($('').addClass('shortcut_descr').text(' : ' + help)); 269 | 270 | }; 271 | 272 | var build_div = function (title, shortcuts) { 273 | 274 | // Remove jupyter-notebook:ignore shortcuts. 275 | shortcuts = shortcuts.filter(function(shortcut) { 276 | if (shortcut.help === 'ignore') { 277 | return false; 278 | } else { 279 | return true; 280 | } 281 | }); 282 | 283 | var i, half, n; 284 | var div = $('
').append($(title)); 285 | var sub_div = $('
').addClass('container-fluid'); 286 | var col1 = $('
').addClass('col-md-6'); 287 | var col2 = $('
').addClass('col-md-6'); 288 | n = shortcuts.length; 289 | half = ~~(n/2); // Truncate :) 290 | for (i=0; i is a prefix like in Vim's insert mode) 104 | 'ctrl-o,g,g': 'vim-binding:select-first-cell', 105 | 'ctrl-o,shift-g': 'vim-binding:select-last-cell', 106 | 'ctrl-o,o': 'vim-binding:insert-cell-below', 107 | 'ctrl-o,shift-o': 'vim-binding:insert-cell-above', 108 | 'ctrl-o,z,z': 'vim-binding:scroll-cell-center', 109 | 'ctrl-o,z,t': 'vim-binding:scroll-cell-top', 110 | 'ctrl-o,shift-m': 'vim-binding:merge-cells', 111 | 'ctrl-o,-': 'vim-binding:split-cell-at-cursor', 112 | 'ctrl-o,subtract': 'vim-binding:split-cell-at-cursor', 113 | 'ctrl-o,y,y': 'vim-binding:copy-cell', 114 | 'ctrl-o,d,d': 'vim-binding:cut-cell', 115 | 'ctrl-o,shift-p': 'vim-binding:paste-cell-above', 116 | 'ctrl-o,p': 'vim-binding:paste-cell-below', 117 | 'ctrl-o,u': 'vim-binding:undo-cell-deletion', 118 | 'ctrl-o,z,a': 'vim-binding:toggle-cell-output-collapsed', 119 | 'ctrl-o,z,shift-a': 'vim-binding:toggle-all-cells-output-collapsed', 120 | 'ctrl-o,z,m': 'vim-binding:collapse-output', 121 | 'ctrl-o,z,shift-m': 'vim-binding:collapse-all-output', 122 | 'ctrl-o,z,r': 'vim-binding:expand-output', 123 | 'ctrl-o,z,shift-r': 'vim-binding:expand-all-output', 124 | 'ctrl-o,shift-h': 'vim-binding:show-keyboard-shortcuts', 125 | 'ctrl-o,shift-l': 'vim-binding:toggle-cell-line-numbers', 126 | 'ctrl-o,shift-v': 'vim-binding:toggle-cell-output-collapsed', 127 | 'ctrl-o,shift-s': 'vim-binding:toggle-cell-output-scrolled', 128 | 'ctrl-o,ctrl-c': 'vim-binding:interrupt-kernel', 129 | // Defined in searchandreplace.js 130 | // https://github.com/jupyter/notebook/blob/4.x/notebook/static/notebook/js/searchandreplace.js#L375 131 | 'ctrl-o,/': 'jupyter-notebook:find-and-replace' 132 | }; 133 | }; 134 | 135 | exports.add_shortcuts = function add_shortcuts(manager, data){ 136 | for(var shortcut in data){ 137 | try { 138 | manager.add_shortcut(shortcut, data[shortcut], true); 139 | } catch(e){ 140 | console.error( 141 | 'Unable to add shortcut for ', shortcut, ' to action ', data[shortcut] 142 | ); 143 | } 144 | } 145 | manager.events.trigger('rebuild.QuickHelp'); 146 | }; 147 | 148 | exports.attach = function attach() { 149 | var km = ns.keyboard_manager; 150 | km.command_shortcuts.clear_shortcuts(); 151 | exports.add_shortcuts( 152 | km.command_shortcuts, 153 | exports.get_default_common_shortcuts() 154 | ); 155 | exports.add_shortcuts( 156 | km.command_shortcuts, 157 | exports.get_default_command_shortcuts() 158 | ); 159 | 160 | km.edit_shortcuts.clear_shortcuts(); 161 | exports.add_shortcuts( 162 | km.edit_shortcuts, 163 | exports.get_default_common_shortcuts() 164 | ); 165 | exports.add_shortcuts( 166 | km.edit_shortcuts, 167 | exports.get_default_edit_shortcuts() 168 | ); 169 | 170 | // Apply user defined Keyboard shortcuts 171 | if (km.config && km.config.data.keys) { 172 | exports.add_shortcuts( 173 | km.command_shortcuts, 174 | (km.config.data.keys.command || {}).bind || {} 175 | ); 176 | exports.add_shortcuts( 177 | km.edit_shortcuts, 178 | (km.config.data.keys.edit || {}).bind || {} 179 | ); 180 | } 181 | }; 182 | 183 | return exports; 184 | }); 185 | -------------------------------------------------------------------------------- /resource/completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdalisue/jupyter-vim-binding/0707a0087a691f459e5ecd36d412fcf23c476dbc/resource/completion.png -------------------------------------------------------------------------------- /resource/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdalisue/jupyter-vim-binding/0707a0087a691f459e5ecd36d412fcf23c476dbc/resource/screencast.gif -------------------------------------------------------------------------------- /resource/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdalisue/jupyter-vim-binding/0707a0087a691f459e5ecd36d412fcf23c476dbc/resource/tooltip.png -------------------------------------------------------------------------------- /vim_binding.css: -------------------------------------------------------------------------------- 1 | /* Improve command area of CodeMirror Vim */ 2 | .CodeMirror-dialog-bottom { 3 | position: absolute; 4 | width: 100%; 5 | bottom: 0; 6 | z-index: 100; 7 | color: #eee; 8 | background-color: #333; 9 | } 10 | .CodeMirror-dialog-bottom input { 11 | border: none; 12 | outline: none; 13 | color: inherit; 14 | background-color: inherit; 15 | width: calc(100% - 1em); 16 | } 17 | /* Jupyter cell is in normal mode when code mirror */ 18 | .edit_mode .cell.selected .CodeMirror-focused.cm-fat-cursor { 19 | background-color: #F5F6EB; 20 | } 21 | /* Jupyter cell is in insert mode when code mirror */ 22 | .edit_mode .cell.selected .CodeMirror-focused:not(.cm-fat-cursor) { 23 | background-color: #F6EBF1; 24 | } 25 | -------------------------------------------------------------------------------- /vim_binding.js: -------------------------------------------------------------------------------- 1 | /* 2 | * vim_binding.js 3 | * 4 | * A vim key binding plugin for Jupyter/IPython 5 | * 6 | * @author Alisue 7 | * @version 2.0.4 8 | * @license MIT license 9 | * @see http://github.com/lambdalisue/jupyter-vim-binding 10 | * @copyright 2015-2016, Alisue, hashnote.net 11 | * 12 | * Refs: 13 | * - https://github.com/ivanov/ipython-vimception 14 | * - http://stackoverflow.com/questions/25730516/vi-shortcuts-in-ipython-notebook 15 | * - http://mindtrove.info/#nb-server-exts 16 | * - http://akuederle.com/customize-ipython-keymap/ 17 | */ 18 | define([ 19 | 'require', 20 | 'jquery', 21 | 'services/config', 22 | 'base/js/namespace', 23 | 'base/js/utils', 24 | 'notebook/js/cell', 25 | './lib/codemirror', 26 | './lib/jupyter/actions', 27 | './lib/jupyter/codecell', 28 | './lib/jupyter/completer', 29 | './lib/jupyter/keyboard', 30 | './lib/jupyter/notebook', 31 | './lib/jupyter/shortcuts', 32 | './lib/jupyter/quickhelp', 33 | ], function(require, $, config, ns, utils, cell) { 34 | "use strict"; 35 | var undefined; 36 | var exports = {}; 37 | var modules = Array.prototype.slice.call(arguments, 6); 38 | var Cell = cell.Cell; 39 | var conf = new config.ConfigSection('notebook', { 40 | base_url: utils.get_body_data('baseUrl') 41 | }); 42 | var params = { 43 | 'scroll_unit': 30, 44 | }; 45 | 46 | 47 | var require_css = function(url) { 48 | var link = document.createElement('link'); 49 | link.type = 'text/css'; 50 | link.rel = 'stylesheet'; 51 | link.href = require.toUrl(url); 52 | document.getElementsByTagName('head')[0].appendChild(link); 53 | }; 54 | 55 | 56 | conf.loaded.then(function() { 57 | params = $.extend(params, conf.data); 58 | exports.attach(); 59 | }); 60 | 61 | exports.attach = function attach() { 62 | for(var i=0; i