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