Neovim plugin for editing Jupyter notebooks (.ipynb) natively - Colab-style cell
rendering, full Vim modal editing, live kernel execution, and inline output.
╭── [ python · [3] ────────────────────────────────────────╮
import numpy as np
x = np.linspace(0, 10, 100)
print(x.mean())
╰── ✓ 0.12s ───────────────────────────────────────────────╯
5.0
- Neovim >= 0.9
- Python >= 3.12
- uv (recommended) or python3 (built-in venv fallback)
Required for image rendering (matplotlib plots, inline PNG/JPEG/SVG)
- folke/snacks.nvim with
imagemodule enabled - A terminal with Kitty graphics protocol unicode placeholder support:
| Terminal | Supported |
|---|---|
| kitty >= 0.28 | yes |
| Ghostty | yes |
| WezTerm | yes |
| tmux (wrapping any of the above) | yes - see note below |
| alacritty, iTerm2, others | no |
- tmux users: add to
~/.tmux.confand restart tmux:set -gq allow-passthrough on set -g visual-activity off set-option -g focus-events on
Optional - richer markdown cells
Optional - completion
Optional - language icons in cell borders
return {
{
"ansh-info/ipynb.nvim",
lazy = false,
build = "uv sync --project python/ || (python3 -m venv python/.venv && python/.venv/bin/pip install ./python/)",
opts = {},
},
}
lazy = falseis required - the plugin must load before any buffer is opened so it can intercept.ipynbfiles viaBufReadCmd.
The
buildhook installs the Python kernel bridge dependencies (jupyter_client,ipykernel,nbformat) into an isolated venv atpython/.venv/. It triesuvfirst; ifuvis not installed it falls back to the standardpython3 -m venv. Run:Lazy build ipynbto re-run it manually after updates.
With optional dependencies:
return {
{
"ansh-info/ipynb.nvim",
lazy = false,
build = "uv sync --project python/ || (python3 -m venv python/.venv && python/.venv/bin/pip install ./python/)",
dependencies = {
{
"folke/snacks.nvim",
opts = {
image = { enabled = true },
},
},
{ "MeanderingProgrammer/render-markdown.nvim", opts = {} },
{ "hrsh7th/nvim-cmp" },
{ "nvim-tree/nvim-web-devicons" },
},
opts = {},
},
}use({
"ansh-info/ipynb.nvim",
run = "uv sync --project python/ || (python3 -m venv python/.venv && python/.venv/bin/pip install ./python/)",
config = function()
require("ipynb").setup({})
end,
})Setup is called automatically when you pass opts = {} to lazy.nvim. If you
manage setup yourself:
require("ipynb").setup({})Open any .ipynb file - the plugin renders it automatically:
nvim my_notebook.ipynb
The kernel starts automatically when you run your first cell. No manual
:IpynbKernelStart needed unless auto_start is disabled.
<leader>rr run the cell under the cursor
]c / [c jump to next / previous cell
<leader>ji open variable inspector
By default the kernel runs on the plugin's own Python, which only has the
bridge dependencies (jupyter_client, ipykernel, nbformat). To use
your own packages, activate your project venv before launching Neovim:
# One-time setup per venv - ipykernel is required for the kernel to launch
uv pip install ipykernel numpy matplotlib # or: pip install ...
# Then just activate and open Neovim as normal
source .venv/bin/activate
nvim my_notebook.ipynbThe plugin auto-detects $VIRTUAL_ENV (uv/venv) and $CONDA_PREFIX
(conda) and uses that Python as the kernel. No config change needed.
Why ipykernel? The kernel is a separate process launched as
python -m ipykernel_launcher. That process must be able to importipykernel, so it needs to be installed in your venv alongside your packages. If it is missing you will see an error in:messageswith install instructions.
All keymaps are buffer-local and only active inside .ipynb buffers.
Press <leader>jh to show the help overlay at any time.
| Key | Action |
|---|---|
<leader>rr |
Run current cell |
<leader>rn |
Run cell and advance to next (Shift+Enter) |
<leader>ra |
Run all cells above cursor |
<leader>rb |
Run all cells from cursor downwards |
<leader>ri |
Interrupt kernel |
]c |
Next cell |
[c |
Previous cell |
<leader>co |
Add code cell below |
<leader>cO |
Add code cell above |
<leader>mo |
Add markdown cell below |
<leader>mO |
Add markdown cell above |
<leader>cd |
Delete current cell |
<leader>cx |
Clear current cell output |
<leader>cX |
Clear all cell outputs |
<leader>w |
Save notebook |
<leader>ji |
Variable inspector |
<leader>jh |
Keymap help overlay |
<C-x><C-o> |
Kernel completions (insert mode) |
| Command | Description |
|---|---|
:IpynbOpen [path] |
Open a notebook |
:IpynbSave |
Save the current notebook |
:IpynbKernelStart [name] |
Start a kernel (python3 default) |
:IpynbKernelStop |
Stop the kernel |
:IpynbKernelRestart |
Restart kernel and clear all output |
:IpynbKernelInterrupt |
Send interrupt (Ctrl-C) |
:IpynbKernelInfo |
Show kernel status window |
:IpynbRun |
Run current cell |
:IpynbRunAdvance |
Run cell and advance to next |
:IpynbRunAll |
Run all cells |
:IpynbRunAbove |
Run all cells above cursor |
:IpynbCellAdd |
Add code cell below |
:IpynbCellDelete |
Delete current cell |
:IpynbCellAddMarkdown |
Add markdown cell below |
:IpynbCellAddMarkdownAbove |
Add markdown cell above |
:IpynbCellMoveUp |
Move current cell up |
:IpynbCellMoveDown |
Move current cell down |
:IpynbCellDuplicate |
Duplicate current cell below |
:IpynbCellYank |
Yank cell into cell register |
:IpynbCellPaste |
Paste yanked cell below |
:IpynbCellToggleType |
Toggle cell type (code/markdown) |
:IpynbCellSplit |
Split cell at cursor line |
:IpynbCellMerge |
Merge cell with the cell below |
:IpynbClearOutput |
Clear output for cell under cursor |
:IpynbClearAllOutput |
Clear output for every cell |
:IpynbInspect |
Open variable inspector |
:IpynbHelp |
Show keymap reference |
The following is the default configuration. Pass any subset of these to opts
or setup() to override.
require("ipynb").setup({
kernel = {
default_kernel = "python3",
auto_start = true, -- start kernel automatically on first run
python_path = "python3", -- fallback if uv venv is not found
},
ui = {
show_execution_count = true,
show_elapsed_time = true,
output_max_lines = 50, -- max output lines per cell; 0 = unlimited
},
image = {
enabled = true, -- requires snacks.nvim + unicode placeholder terminal
max_width = 80,
max_height = 20,
},
keymaps = {
enabled = true,
run_cell = "<leader>rr",
run_cell_and_advance = "<leader>rn",
run_all_above = "<leader>ra",
run_all_below = "<leader>rb",
next_cell = "]c",
prev_cell = "[c",
add_cell_below = "<leader>co",
add_cell_above = "<leader>cO",
delete_cell = "<leader>cd",
interrupt_kernel = "<leader>ri",
clear_output = "<leader>cx",
clear_all_output = "<leader>cX",
add_markdown_below = "<leader>mo",
add_markdown_above = "<leader>mO",
move_cell_up = "<leader>ck",
move_cell_down = "<leader>cj",
duplicate_cell = "<leader>cc",
yank_cell = "<leader>cy",
paste_cell = "<leader>cv",
toggle_cell_type = "<leader>ct",
split_cell = "<leader>cs",
merge_cell = "<leader>cm",
},
notebook = {
auto_save = false,
},
})require("ipynb").statusline() returns a formatted string showing the kernel
name and status for the current buffer. Returns an empty string for non-notebook
buffers so the component disappears outside .ipynb files.
⬤ python3 [idle] -- kernel ready
⬤ python3 [busy] -- cell executing
⬤ python3 [starting] -- kernel booting
⬤ python3 [stopped] -- kernel dead or not started
require("ipynb").statusline_hl() returns a highlight group name matching the
status (DiagnosticOk, DiagnosticWarn, DiagnosticInfo, DiagnosticError).
require("lualine").setup({
sections = {
lualine_x = {
{
function() return require("ipynb").statusline() end,
cond = function() return require("ipynb").statusline() ~= "" end,
color = function()
return { fg = vim.fn.synIDattr(vim.fn.hlID(require("ipynb").statusline_hl()), "fg#") }
end,
},
},
},
})local IpynbStatus = {
condition = function() return require("ipynb").statusline() ~= "" end,
provider = function() return " " .. require("ipynb").statusline() .. " " end,
hl = function() return require("ipynb").statusline_hl() end,
}Issues and pull requests are welcome. See CONTRIBUTING.md for development setup, test instructions, and the contribution workflow.
MIT - see LICENSE.