29

In the IPython Notebook environment, it is possible to define custom keyboard shortcuts using the IPython Javascript API. Using the %%javascript magic, one may write a javascript within IPython's interactive console as follows (example described here):

%%javascript

IPython.keyboard_manager.command_shortcuts.add_shortcut('r', {
    help : 'run cell',
    help_index : 'zz',
    handler : function (event) {
        IPython.notebook.execute_cell();
        return false;
    }}
);

I'd like to write a javascript that creates a shortcut during edit mode that binds Ctrl-Alt-Down to the action of 'duplicate current line'---that is, move the cursor to the start of the current line, select the line, copy the line, return, paste. Essentially, I want to emulate the keyboard shortcut of Eclipse, or Ctrl-d in Notepad++, or C-a C-SPACE C-n M-w C-y in Emacs. The javascript file will take the form of the following:

%%javascript

IPython.keyboard_manager.edit_shortcuts.add_shortcut('ctrl-alt-down', {
    help : 'run cell',
    help_index : 'zz',
    handler : function (event) {
        [Code that duplicates the line];
        return false;
    }}
);

though my attempts suggest 'ctrl-alt-down' is the incorrect way to represent the shortcut sequence, and I can't find any documentation for the keyboard_manager.

I'd rather not go with an (e.g.,) AutoHotKey solution since I want to restrict this shortcut to the edit mode of IPython Notebook.

resueman
  • 10,572
  • 10
  • 31
  • 45
brorgschlr
  • 407
  • 1
  • 4
  • 8

3 Answers3

32

Step1.

Create a new JS file under ~/.jupyter/custom/custom.js if it does not exist and add the next code:

/**
*
* Duplicate a current line in the Jupyter Notebook
* Used only CodeMirror API - https://codemirror.net
*
**/
CodeMirror.keyMap.pcDefault["Ctrl-Down"] = function(cm){

    // get a position of a current cursor in a current cell
    var current_cursor = cm.doc.getCursor();

    // read a content from a line where is the current cursor
    var line_content = cm.doc.getLine(current_cursor.line);

    // go to the end the current line
    CodeMirror.commands.goLineEnd(cm);

    // make a break for a new line
    CodeMirror.commands.newlineAndIndent(cm);

    // filled a content of the new line content from line above it
    cm.doc.replaceSelection(line_content);

    // restore position cursor on the new line
    cm.doc.setCursor(current_cursor.line + 1, current_cursor.ch);
};

Step 2.

Restart Jupyter

Result

enter image description here

Tested in a next environment

wlysenko@wlysenko-Aspire ~ $ google-chrome --version
Google Chrome 53.0.2785.116 
wlysenko@wlysenko-Aspire ~ $ jupyter --version
4.1.0
wlysenko@wlysenko-Aspire ~ $ uname -a
Linux wlysenko-Aspire 3.13.0-37-generic #64-Ubuntu SMP Mon Sep 22 21:28:38 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
Ali Ben Messaoud
  • 11,690
  • 8
  • 54
  • 87
PADYMKO
  • 4,217
  • 2
  • 36
  • 41
  • I am not seeing such a directory. Do you need to add the directory yourself? Also I am using version 4.4.0 as well. – Newskooler Oct 09 '18 at 13:29
  • @Newskooler Worked for me (but on windows) when adding the subdirectory "config" in the "jupyter config" path. You could try to check whether the path is right via jupyter_config_dir() as in https://stackoverflow.com/questions/32170197/how-do-can-i-use-a-custom-js-file-under-jupyter-notebook#39921293 – dasWesen Oct 21 '18 at 11:07
  • 2
    Works for me, thanks! Possible improvement: Prevent doubling tabs when copying a line starting with tab ;) – dasWesen Oct 21 '18 at 11:09
  • It does not help me on Mac (10.13.6) Firefox (88.0.1). I changed pcDefault to macDefault but nothing happend. I even can not understand if my custom.js launched without any bugs while I'm starting jupyther lab – Anton May 17 '21 at 09:29
9

This is a simple adjustment to this great answer, fulfilling dasWesen's request to avoid doubling tabs. This version uses CodeMirror's goLineStartSmart function to go to just the start of the current line's text, so that when it copies the text, it doesn't grab leading spaces or tabs.

As mentioned in Seti's post, put the code in the file ~/.jupyter/custom/custom.js

On Windows, I found the .jupyter folder in C:\Users\YourUserName, and then had to create the \custom folder and the custom.js file. Restarting Jupyter picked up the changes.

CodeMirror.keyMap.pcDefault["Ctrl-Down"] = function(cm){

    // get current cursor position
    var current_cursor = cm.doc.getCursor();

    // First go to end of line, to avoid the problem where if cursor was at start
    // of indented text, goLineStartSmart would go to very beginning of line,
    // and so we'd get unwanted tabs/spaces in the getRange function.
    CodeMirror.commands.goLineEnd(cm);
    // now we can safely call goLineStartSmart
    CodeMirror.commands.goLineStartSmart(cm);
    var start_cursor = cm.doc.getCursor();
    var start = {'line': start_cursor.line, 'ch': start_cursor.ch};

    // go to the end of line
    CodeMirror.commands.goLineEnd(cm);
    var end_cursor = cm.doc.getCursor();
    var end = {'line': end_cursor.line, 'ch': end_cursor.ch};

    // get content
    var line_content = cm.doc.getRange(start, end);

    // make a break for a new line
    CodeMirror.commands.newlineAndIndent(cm);

    // filled a content of the new line content from line above it
    cm.doc.replaceSelection(line_content);

    // restore position cursor on the new line
    cm.doc.setCursor(current_cursor.line + 1, current_cursor.ch);
};
Jean-François Corbett
  • 37,420
  • 30
  • 139
  • 188
snubber
  • 91
  • 1
  • 3
1

PADYMKO has provided a nice answer https://stackoverflow.com/a/40505055/11784913. However if you are a mac user, you need to modify that a bit. You need to change the first line of the code to:

CodeMirror.keyMap.macDefault["Ctrl-Down"] = function(cm){

The rest of the code remains the same. Here is the full snippet.

CodeMirror.keyMap.macDefault["Ctrl-Down"] = function(cm){

    // get a position of a current cursor in a current cell
    var current_cursor = cm.doc.getCursor();

    // read a content from a line where is the current cursor
    var line_content = cm.doc.getLine(current_cursor.line);

    // go to the end the current line
    CodeMirror.commands.goLineEnd(cm);

    // make a break for a new line
    CodeMirror.commands.newlineAndIndent(cm);

    // filled a content of the new line content from line above it
    cm.doc.replaceSelection(line_content);

    // restore position cursor on the new line
    cm.doc.setCursor(current_cursor.line + 1, current_cursor.ch);
};

Arman
  • 126
  • 3
  • 14