在外部编辑器中编辑 IPython 个单元格

Edit IPython cell in an external editor

如果在 IPython 笔记本中有一个键盘快捷键就太好了,这样可以在外部编辑器(例如 gvim)中编辑当前单元格的内容。也许只是将当前单元格的内容复制到一个临时文件中,在其上启动 gvim,并在每次保存文件时更新当前单元格(并在退出 gvim 时删除临时文件) .此外,如果从浏览器编辑单元格,可能会更新临时文件,以便 gvim 知道文件已更改。

我知道 vim-ipython 和 ipython-vimception 等项目,但它们不符合我的需求。我认为浏览器对于简单的事情已经足够了,但是当需要更强大的编辑时就没有必要重新发明轮子了。

您知道 IPython 笔记本中是否已经存在这样的功能吗?

谢谢。

这是我想到的。我添加了 2 个快捷方式:

  • 'g' 使用当前单元格的内容启动 gvim(您可以用您喜欢的任何文本编辑器替换 gvim)。
  • 'u' 用 gvim 保存的内容更新当前单元格的内容。

因此,当您想使用首选编辑器编辑单元格时,点击 'g',对单元格进行所需的更改,在编辑器中保存文件(并退出),然后点击 'u'.

只需执行此单元格即可启用这些功能:

%%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;
    }}
);

基于@david-brocart 接受的答案,我已经获取了他的代码并将其包装成一个魔术函数,所以现在我只需要 运行 行魔术 %gvim在笔记本中,以便能够通过 Gvim 为整个笔记本编辑任何单元格的内容(我可以在我系统上的任何其他笔记本 运行ning 中重复使用相同的行魔术)。

如果您想做类似的事情,只需在 ipython 启动文件夹(您的 ipython 启动路径可能类似于 ~/.ipython/profile_default/startup),然后将以下代码放入该文件(并保存):

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)

现在启动你的 Jupyter notebook 内核,你应该可以在单元格中输入 %gvim%(你可以使用自动完成功能找到新的魔法命令)然后 运行单元格以启用在 Gvim 中编辑笔记本的单元格内容。您将收到一条输出消息,让您知道魔术命令已生效:

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.

感谢 Whosebug 问题中的人们以及他们为我提供了将这些整合在一起的要素。 :-)

  • How to run an IPython magic from a script (or timing a Python script)

上面@david-brocart 的代码片段是一个很好的 hack,但它有几个缺点:

  • 数据很容易丢失,例如不小心按错了单元格 'u'。
  • Python 内核在编辑文件时被阻塞。
  • 内核的全局命名空间被污染。
  • 无法并行编辑多个单元格。
  • 剩余的“.toto.py”文件保留在磁盘上。
  • 文件扩展名与单元格类型无关。

这是一个改进版本,解决了上述所有问题。它仍然是一种 hack(例如,当内核繁忙时不可能开始编辑单元格),但它在实践中工作得很好。它仍然滥用计算内核来读取和写入文件并启动编辑器,但这样做的方式会导致尽可能少的副作用。

要使用此代码段,它必须在 Jupyter 单元格中执行。它也可以添加到 ~/.jupyter/custom/custom.js。默认情况下 "emacsclient -c" 启动,但这可以被任何其他编辑器替换。只有一个键(默认情况下 "e")可以将单元格换出文件并启动编辑器,或者读取文件并将内容插入回单元格。

%%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;
    }}
);

对于使用 IPython terminal 应用程序发现此问题的人,有一个内置的键盘快捷键可以启动 $EDITOR 和内容当前单元格的。保存并退出编辑器会将单元格的内容替换(但尚未执行)为已保存文件的内容。

默认的键盘快捷键是 F2 键。这对应于 IPython 设置 IPython.terminal.shortcuts.open_input_in_editor.