Edit Jupyter notebooks in Neovim with full LSP support -- no scattered files, no missed changes.
callisto-demo.mov
Data scientists who use Neovim face a fundamental friction with Jupyter notebooks:
- No editor intelligence.
.ipynbfiles are JSON. LSP, treesitter, completion -- none of it works. You lose everything that makes Neovim productive. - Scattered
.pyfiles. Tools like jupytext solve the editing problem by creating paired.pyfiles, but those files clutter your project tree and need to be gitignored or managed separately. - Invisible external changes. When Jupyter, Claude Code, or any other tool modifies a notebook, your Neovim buffer has no idea. You are editing a stale version and will silently overwrite the external changes on save.
- No execution from the editor. To run a notebook or export results, you have to leave Neovim entirely.
callisto.nvim converts .ipynb to Python in a temporary directory -- your project stays clean. A file watcher monitors the original .ipynb so that external edits (from Jupyter, AI agents, collaborators) are reflected in your buffer instantly. Output cells are always preserved. You can execute notebooks and export Markdown without leaving the editor.
:edit experiment.ipynb -- opens as Python with full LSP
(edit as normal)
:write -- saves back to .ipynb (outputs preserved)
If something else modifies experiment.ipynb while you are editing, callisto detects it and updates your buffer automatically. If you have unsaved changes, you get a warning instead of silent data loss.
By default, converted .py files live in a temporary directory and are cleaned up automatically. Your project tree never sees them. If you prefer keeping .py alongside .ipynb (e.g. for version control), the sync option enables that.
callisto uses libuv's fs_event to watch the original .ipynb file. When an external tool -- Jupyter, an AI agent, a collaborator's script -- modifies the notebook, your buffer updates immediately without polling or manual reload. If you have unsaved changes, you get a warning instead of silent data loss.
:NotebookRun executes the notebook in-place and reloads the results. :NotebookExport exports to Markdown. Both work from within the buffer.
Every save uses jupytext's --update flag, so execution results, plots, and other outputs in the .ipynb are never lost -- even though you are editing only the code cells.
- Opens
.ipynbfiles as Python with full LSP, treesitter, and completion support - Saves back to
.ipynbtransparently on:w(output cells are preserved) - Watches for external changes (e.g. from Jupyter, Claude Code) and auto-reloads the buffer
- Execute notebooks in-place via
nbconvert - Export notebooks to Markdown
- Optional
syncmode to keep.pyalongside.ipynb(like native jupytext pairing) - Buffer-local keymaps for notebook operations
- User autocmd events for custom workflows
- Neovim >= 0.10
- jupytext (
pip install jupytext) - jupyter nbconvert (
pip install nbconvert) -- for:NotebookRunand:NotebookExport
{ "sunbluesome/callisto.nvim", opts = {} }Open any .ipynb file and edit it as Python. Save with :w. That's it.
:edit notebook.ipynb " opens as Python with LSP
:NotebookRun " execute and reload results
:NotebookExport " export to MarkdownDefault values are shown below. You only need to specify values you want to change.
{
"sunbluesome/callisto.nvim",
opts = {
tmpdir = nil, -- base directory for temp files (nil = OS default)
sync = false, -- save .py alongside .ipynb (like native jupytext)
venv = ".venv", -- venv directory name (nil to disable)
run = {
auto_export = true, -- auto-export markdown after :NotebookRun
},
watcher = {
debounce_ms = 200, -- debounce delay for external change detection (ms)
restart_delay_ms = 300, -- delay before restarting watcher after write (ms)
},
keys = {
run = "<leader>nr", -- keymap for :NotebookRun (nil to disable)
export = "<leader>ne", -- keymap for :NotebookExport (nil to disable)
},
},
}| Option | Type | Default | Description |
|---|---|---|---|
tmpdir |
string? |
nil |
Base directory for temp files. nil uses OS default. Ignored when sync = true |
sync |
boolean |
false |
Save .py files alongside .ipynb instead of in tmpdir. Opening a .py with a paired .ipynb auto-syncs |
venv |
string? |
".venv" |
Venv directory name to find jupytext/jupyter. Searched in cwd. nil to disable |
run.auto_export |
boolean |
true |
Automatically export to Markdown after :NotebookRun |
watcher.debounce_ms |
integer |
200 |
Debounce delay for external change detection |
watcher.restart_delay_ms |
integer |
300 |
Delay before restarting watcher after write |
keys.run |
string? |
"<leader>nr" |
Buffer-local keymap for :NotebookRun. nil to disable |
keys.export |
string? |
"<leader>ne" |
Buffer-local keymap for :NotebookExport. nil to disable |
| Command | Description |
|---|---|
:NotebookRun |
Execute the notebook in-place via nbconvert. Auto-exports to Markdown if run.auto_export is enabled. |
:NotebookExport |
Export the notebook as Markdown to the same directory as the .ipynb file. |
The following buffer-local keymaps are registered in notebook buffers by default:
| Key | Command | Description |
|---|---|---|
<leader>nr |
:NotebookRun |
Execute notebook |
<leader>ne |
:NotebookExport |
Export notebook to Markdown |
These keymaps are only active in buffers opened via callisto (i.e. .ipynb files). They do not affect normal Python files. Keymaps can be customized or disabled via opts.keys.
callisto.nvim emits User autocmd events that you can hook into for custom workflows:
| Event | Description |
|---|---|
CallistoNotebookWritten |
Fired after buffer changes are saved back to .ipynb |
CallistoNotebookReloaded |
Fired after an external change is detected and the buffer is reloaded |
Example:
vim.api.nvim_create_autocmd("User", {
pattern = "CallistoNotebookWritten",
callback = function(args)
print("Notebook saved: buffer " .. args.data.bufnr)
end,
})Run :checkhealth callisto to verify dependencies and view current configuration.
See docs/architecture.md for internal design details, module structure, and sequence diagrams.