Edit IPython cell in an external editor
Asked Answered
I

4

11

It would be great to have a keyboard short-cut in IPython notebook, which would allow to edit the content of the current cell in an external editor (e.g. gvim). Maybe just copy the content of the current cell into a temporary file, launch gvim on it, and update the current cell each time the file is saved (and delete the temporary file when exiting gvim). Also, maybe update the temporary file if the cell is edited from the browser, so that gvim knows the file has changed.

I am aware of projects like vim-ipython and ipython-vimception, but they don't correspond to my needs. I think the browser is enough for simple things, but when more powerful editing is required there is no need to reinvent the wheel.

Do you know if such a feature exists in IPython notebook already?

Thanks.

Immoderacy answered 3/2, 2015 at 21:50 Comment(0)
I
11

This is what I came up with. I added 2 shortcuts:

  • 'g' to launch gvim with the content of the current cell (you can replace gvim with whatever text editor you like).
  • 'u' to update the content of the current cell with what was saved by gvim.

So, when you want to edit the cell with your preferred editor, hit 'g', make the changes you want to the cell, save the file in your editor (and quit), then hit 'u'.

Just execute this cell to enable these features:

%%javascript

IPython.keyboard_manager.command_shortcuts.add_shortcut('g', {
    handler : function (event) {
        var input = IPython.notebook.get_selected_cell().get_text();
        var cmd = "f = open('.toto.py', 'w');f.close()";
        if (input != "") {
            cmd = '%%writefile .toto.py\n' + input;
        }
        IPython.notebook.kernel.execute(cmd);
        cmd = "import os;os.system('gvim .toto.py')";
        IPython.notebook.kernel.execute(cmd);
        return false;
    }}
);

IPython.keyboard_manager.command_shortcuts.add_shortcut('u', {
    handler : function (event) {
        function handle_output(msg) {
            var ret = msg.content.text;
            IPython.notebook.get_selected_cell().set_text(ret);
        }
        var callback = {'output': handle_output};
        var cmd = "f = open('.toto.py', 'r');print(f.read())";
        IPython.notebook.kernel.execute(cmd, {iopub: callback}, {silent: false});
        return false;
    }}
);
Immoderacy answered 17/2, 2015 at 18:1 Comment(2)
This didn't work for Sublime Text for me. I changed cmd = "import os;os.system('gvim .toto.py')"; to cmd = "import os;os.system('subs .toto.py')"; and Sublime Text opens, but it does not read .toto.py.Acoustician
eh didn't start my windows emacs but i just made emacs automatically reload the temporary "toto" file. (the update part works) HAPPY!Yodle
K
12

For people who find this question who are using the IPython terminal application, there is a keyboard shortcut built in which launches $EDITOR with the contents of the current cell. Saving and exiting the editor replaces (but does not yet execute) the contents of the cell with that of the saved file.

The default keyboard shortcut is the F2 key. This corresponds to the IPython setting IPython.terminal.shortcuts.open_input_in_editor.

Kinetics answered 11/10, 2019 at 23:28 Comment(1)
There is a way to change the shortcut key?Parlormaid
I
11

This is what I came up with. I added 2 shortcuts:

  • 'g' to launch gvim with the content of the current cell (you can replace gvim with whatever text editor you like).
  • 'u' to update the content of the current cell with what was saved by gvim.

So, when you want to edit the cell with your preferred editor, hit 'g', make the changes you want to the cell, save the file in your editor (and quit), then hit 'u'.

Just execute this cell to enable these features:

%%javascript

IPython.keyboard_manager.command_shortcuts.add_shortcut('g', {
    handler : function (event) {
        var input = IPython.notebook.get_selected_cell().get_text();
        var cmd = "f = open('.toto.py', 'w');f.close()";
        if (input != "") {
            cmd = '%%writefile .toto.py\n' + input;
        }
        IPython.notebook.kernel.execute(cmd);
        cmd = "import os;os.system('gvim .toto.py')";
        IPython.notebook.kernel.execute(cmd);
        return false;
    }}
);

IPython.keyboard_manager.command_shortcuts.add_shortcut('u', {
    handler : function (event) {
        function handle_output(msg) {
            var ret = msg.content.text;
            IPython.notebook.get_selected_cell().set_text(ret);
        }
        var callback = {'output': handle_output};
        var cmd = "f = open('.toto.py', 'r');print(f.read())";
        IPython.notebook.kernel.execute(cmd, {iopub: callback}, {silent: false});
        return false;
    }}
);
Immoderacy answered 17/2, 2015 at 18:1 Comment(2)
This didn't work for Sublime Text for me. I changed cmd = "import os;os.system('gvim .toto.py')"; to cmd = "import os;os.system('subs .toto.py')"; and Sublime Text opens, but it does not read .toto.py.Acoustician
eh didn't start my windows emacs but i just made emacs automatically reload the temporary "toto" file. (the update part works) HAPPY!Yodle
T
3

Building off of the accepted answer by @david-brochart, I've taken his code and wrapped it up into a magic function so now I only need to run the line magic%gvim in a notebook in order to enable editing any cell's contents via Gvim for the entire notebook (and I can reuse the same line magic in any other notebook running on my system).

If you'd like to do something similar, just create a file named something like my_magic_functions.py inside your ipython startup folder (your ipython startup path is likely similar to ~/.ipython/profile_default/startup) and then put the following code inside that file (and save it):

import IPython.core.magic as ipym
from IPython import get_ipython

@ipym.magics_class
class MareBearMagics(ipym.Magics):
    @ipym.line_magic
    def gvim(self, line):
        cell_text = """
IPython.keyboard_manager.command_shortcuts.add_shortcut('g', {
    handler : function (event) {
        var input = IPython.notebook.get_selected_cell().get_text();
        var cmd = "f = open('.toto.py', 'w');f.close()";
        if (input != "") {
            cmd = '%%writefile .toto.py\\n' + input;
        }
        IPython.notebook.kernel.execute(cmd);
        cmd = "import os;os.system('gvim .toto.py')";
        IPython.notebook.kernel.execute(cmd);
        return false;
    }}
);

IPython.keyboard_manager.command_shortcuts.add_shortcut('u', {
    handler : function (event) {
        function handle_output(msg) {
            var ret = msg.content.text;
            IPython.notebook.get_selected_cell().set_text(ret);
        }
        var callback = {'output': handle_output};
        var cmd = "f = open('.toto.py', 'r');print(f.read())";
        IPython.notebook.kernel.execute(cmd, {iopub: callback}, {silent: false});
        return false;
    }}
);
        """
        ipython = get_ipython()
        ipython.run_cell_magic(
            magic_name='javascript', line=None, cell=cell_text)
        print("Cell contents can now be edited via Gvim. From command mode "
              "use 'g' to open current cell contents in Gvim. After ':wq' "
              "from Gvim, use 'u' in command mode to update cell contents.")


if __name__ == '__main__':
    get_ipython().register_magics(MareBearMagics)

Now start up your Jupyter notebook kernel, and you should be able to type %gvim% into a cell (and you can use auto-completion to find the new magic command) and then run the cell to enable editing the notebook's cell contents in Gvim. You'll receive an output message that lets you know the magic command took effect:

Cell contents can now be edited via Gvim. From command mode use 'g' to open current cell contents in Gvim. After ':wq' from Gvim, use 'u' in command mode to update cell contents.

Thanks to the folks in this stackoverflow question and these as well for giving me the ingredients to pull this together. :-)

Tetrabranchiate answered 8/6, 2018 at 23:37 Comment(0)
D
3

The above code snippet by @david-brochart is a nice hack, but it has several shortcomings:

  • It's easy to loose data, for example by accidentally pressing 'u' on the wrong cell.
  • The Python kernel is blocked while a file is being edited.
  • The global namespace of the kernel gets polluted.
  • It's not possible to edit multiple cells in parallel.
  • Leftover '.toto.py' files remain on disk.
  • The file extension does not depend on the cell type.

Here's an improved version that solves all of the above problems. It's still a hack (for example it's impossible to start editing cells while the kernel is busy), but it works well enough in practice. It still abuses the computation kernel to read and write files and launch the editor, but this is done in a way that causes as little side effects as possible.

To use this snippet, it must be executed in a Jupyter cell. It may be also added to ~/.jupyter/custom/custom.js. By default "emacsclient -c" is launched, but this may be replaced by any other editor. There's only one key ("e" by default) that either swaps out a cell to a file and launches the editor, or reads the file and inserts the content back into the cell.

%%javascript

Jupyter.keyboard_manager.command_shortcuts.add_shortcut('e', {
    handler : function (event) {
        function callback(msg) {
            cell.set_text(msg.content.text);
        }
        var cell = Jupyter.notebook.get_selected_cell();
        // Quote the cell text and *then* double any backslashes.
        var cell_text = JSON.stringify(cell.get_text()).replace(/\\/g, "\\\\");
        var cmd = `exec("""
cell_text = ${cell_text}
ext = "${cell.cell_type == 'code' ? 'py' : 'txt'}"
sep = "#-#-# under edit in file "
prefix, _, fname = cell_text.partition(sep)

if not fname or prefix:
    # Create file and open editor, pass back placeholder.
    import itertools, subprocess

    for i in itertools.count():
        fname = 'cell_{}.{}'.format(i, ext)
        try:
            with open(fname, 'x') as f:
                f.write(cell_text)
        except FileExistsError:
            pass
        else:
            break

    # Run editor in the background.
    subprocess.Popen(['emacsclient', '-c', fname],
                     stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    print(sep, fname, sep='', end='')
else:
    # Cell has been in edit: read it in and pass it back, and delete it.
    import os

    try:
        with open(fname, 'r') as f:
            cell_text = f.read()
    except FileNotFoundError:
        print("# File {} could not be inserted back.".format(fname), end='')
    else:
        if cell_text.endswith('\\\\n'):
            cell_text = cell_text[:-1]
        print(cell_text, end='')
        os.remove(fname)
        try:
            os.remove(fname + '~')
        except FileNotFoundError:
            pass
""", None, {})`;
        Jupyter.notebook.kernel.execute(cmd, {iopub: {output: callback}},
                                        {silent: false});
        return false;
    }}
);
Draftsman answered 27/6, 2018 at 20:53 Comment(1)
For some reason the print function doesn't work in the SoS polyglot notebook (vatlab.github.io/sos-docs/index.html#content). Changing it to : print(f"{sep}{fname}",end='') worksActh

© 2022 - 2024 — McMap. All rights reserved.