Experimental Neovim plugin that recreates core Jupyter-like behavior with cell markers, per-cell execution, and a simple virtual cell outline.
This plugin is a good fit if:
- You prefer editing in Neovim but need a notebook-like workflow for exploration, MVPs, or personal projects.
- You want a lightweight, Neovim-native compromise before moving work into a more robust notebook stack.
- You value fast iteration, simple setup, and readable notebooks over full Jupyter feature parity.
This plugin may not be a fit if:
- You need full Jupyter kernel compatibility, rich outputs (plots/HTML/LaTeX/images), or collaborative notebook features.
- You rely on browser-based notebook UIs or multi-kernel workflows.
- Add this repo to your Neovim runtime path or plugin manager.
- In a buffer, define cells using markers like:
# %% [code]
print("hello")
# %% [markdown]
# Title- Use commands:
:NeoNotebookCellNew [code|markdown]inserts a new cell below the cursor.:NeoNotebookCellToggleTypetoggles the current cell type.:NeoNotebookCellRunexecutes the current code cell with a persistent Python session and shows output.:NeoNotebookCellRunAndNextruns the current cell and creates a new code cell below.:NeoNotebookRenderredraws virtual cell borders.:NeoNotebookCellDuplicateduplicates the current cell.:NeoNotebookCellSplitsplits the current cell at the cursor.:NeoNotebookCellFoldfolds the current cell.:NeoNotebookCellUnfoldunfolds the current cell.:NeoNotebookCellFoldToggletoggles fold for the current cell.:NeoNotebookOutputClearclears inline output for the current cell.:NeoNotebookOutputClearAllclears inline output for all cells.:NeoNotebookOutputCollapseToggletoggles collapsed output for the current cell.:NeoNotebookOutputPrintprints the current cell output to:messages.- Typed outputs are supported (text + image/png).
image_renderer = "auto" | "kitty" | "none"controls image rendering backend.image_protocol = "auto" | "kitty" | "none"controls kitty graphics usage/detection.image_render_target = "pane" | "inline"controls whether images render in a right-side pane (default) or inline.image_pane_tty = "/dev/pts/XX"optional explicit TTY for the image pane. If unset and running inside tmux, a right split pane is created automatically.- If no pane is configured, the plugin auto-creates a right tmux pane on first image render and reuses it for the rest of the session. Use
:NeoNotebookImagePaneResetto force a new pane. - If you are running inside tmux and Ghostty, set
image_protocol = "kitty"if auto-detection doesn't pick it up. - For tmux, enable passthrough so Kitty graphics reach Ghostty:
set -g allow-passthrough on. image_pane_tmux_percent = numberpercent width for auto-created tmux image pane (default 25).image_pane_spacing_lines = numberblank lines inserted between rendered images in the pane (default 1).image_size_mode = "pane"|"default"controls image sizing."pane"sizes pane-rendered images to the tmux pane usingpane_width/pane_height."default"uses fixed defaults (image_default_rows/image_default_cols) for inline sizing.
image_pane_margin_cols = numbercolumns to subtract from pane width (default 2).image_pane_margin_rows = numberrows to subtract from pane height (default 5).image_pane_sizes = {25,33,50}toggle sizes for<leader>pt(percent of window width).image_pane_statusline = trueappend an image pane size indicator to the statusline.image_pane_tmp_dir = "/tmp/neo_notebooks-images"directory for saved image files.image_pane_mode = "page"|"stack"set to"page"to show one image at a time.image_pane_preserve_aspect = truepreserve image aspect ratio when fitting to pane.image_pane_cell_ratio = 2.0cell height/width ratio used for aspect correction.image_max_rows = numbercaps image height in rows (default 30).image_default_rows = numberdefault image height in rows when no metadata is available (default 6).image_default_cols = numberdefault image width in cols (default 12).image_fallback = "placeholder"shows a notice when images cannot render.mpl_backend = "Agg"forces a non-GUI backend for inline capture (prevents popup windows).plt.show()is intercepted to signal an inline capture without a GUI popup.
:NeoNotebookCellDeletedeletes the current cell.:NeoNotebookCellYankyanks the current cell to the default register.:NeoNotebookCellMoveUpmoves the current cell up.:NeoNotebookCellMoveDownmoves the current cell down.:NeoNotebookRunAllruns all code cells.:NeoNotebookRestartrestarts the Python session and clears outputs.:NeoNotebookKernelRestartrestarts the kernel session.:NeoNotebookKernelInterruptsends an interrupt to active execution.:NeoNotebookKernelStopstops/shuts down the current kernel session.:NeoNotebookKernelPauseTogglepauses/resumes queue dispatch (does not suspend the process).:NeoNotebookKernelStatusshows kernel state details (one-shot).:NeoNotebookKernelStatusToggletoggles a persistent kernel status panel.:NeoNotebookKernelBadgeToggletoggles the inline virtual kernel status badge.- If the kernel process exits during active execution, NeoNotebooks now reconciles stale busy state to
errorand allows clean recovery on the next run. :NeoNotebookOutputToggletoggles output mode between inline and floating.- While a cell is executing, a spinner animates on the first inline output row.
- While a cell runs, an inline placeholder output shows
cell executing.... - Streaming stdout/stderr is now rendered incrementally while a cell runs (including batch-progress text).
- Carriage-return progress updates (for example
tqdm) are handled as in-place line replacement during execution. - Live stream preview preserves event arrival order and applies one global preview cap across streams.
- Recognized non-
tqdmprogress lines (for exampleSOAK_PROGRESS 30% (25500/85000)) render as bars by default. - Streaming safety caps are configurable:
stream_preview_max_lines(default400)stream_render_interval_ms(default80)stream_render_min_delta(default50)stream_placeholder_text(default"cell executing...")stream_progress_style(default"bar"; supports"bar"|"pct"|"ratio"|"raw")stream_progress_bar_width(default20)
- After execution, inline output includes a right-aligned timing line (e.g.
[8.56ms]). - Moving cells preserves outputs by stable cell ID.
:NeoNotebookCellSelectselects the current cell body.:NeoNotebookStatsshows a cell count summary.:NeoNotebookRunAboveruns all code cells above the cursor.:NeoNotebookRunBelowruns all code cells below the cursor.:NeoNotebookAutoRenderToggletoggles auto-rendering.:NeoNotebookCellIndexToggletoggles numeric cell index labels on borders.:NeoNotebookHelpshows a quick help window.:NeoNotebookCellEditopens the current cell in a floating editor.:NeoNotebookCellSavesaves the floating editor back to the buffer.:NeoNotebookCellRunFromEditorsaves and runs the edited cell.:NeoNotebookSnakeCellinserts a new code cell and starts a mini inline snake mode (auto-moving snake; fixed default board25x10;h/j/k/lturns direction;<leader>pauses/resumes;<Esc>or game over deletes the snake cell and exits mode).- Snake colors are themed via highlight groups:
NeoNotebookSnakeBorder(default white),NeoNotebookSnakeHead(@, default yellow),NeoNotebookSnakeBody(o, default green),NeoNotebookSnakeApple(*, default red). :NeoNotebookImportIpynb {path}imports a.ipynbfile.:NeoNotebookOpenIpynb {path}opens a.ipynbinto a new buffer.:NeoNotebookImportJupytext {path}imports a Jupytextpy:percentfile into the current notebook buffer.:NeoNotebookOpenJupytext {path}opens a Jupytextpy:percentfile in a new notebook view buffer.:NeoNotebookExportIpynb {path}exports the current buffer to.ipynb.- Import reliability hardening:
- malformed
.ipynbtop-level/cells shapes fail with explicit errors, - object-shaped
cellspayloads (map/object instead of list) are rejected, - unknown/nonstandard imported cell types normalize to
code, - string
sourcepayloads are normalized to stable line arrays, - malformed code-cell
outputscontainers are normalized to empty output lists.
- malformed
require("neo_notebooks").setup({
python_cmd = "python3",
auto_render = true,
output = "inline",
image_renderer = "auto",
image_protocol = "auto",
image_render_target = "pane",
image_pane_tty = nil,
image_pane_tmux_percent = 25,
image_pane_spacing_lines = 1,
image_size_mode = "pane",
image_pane_margin_cols = 2,
image_pane_margin_rows = 5,
image_pane_sizes = { 25, 33, 50 },
image_pane_statusline = true,
image_pane_tmp_dir = "/tmp/neo_notebooks-images",
image_pane_mode = "page",
image_pane_preserve_aspect = true,
image_pane_cell_ratio = 2.0,
image_max_rows = 30,
image_default_rows = 6,
image_default_cols = 12,
image_fallback = "placeholder",
mpl_backend = "Agg",
filetypes = { "neo_notebook", "ipynb" },
auto_open_ipynb = true,
require_markers = false,
auto_insert_first_cell = true,
overlay_preview = false,
kernel_status_virtual = true,
viewport_virtual_padding = { top = 2, bottom = 2 },
suppress_completion_in_markdown = true,
suppress_completion_popup = false,
auto_insert_on_jump = false,
border_hl_code = "NeoNotebookBorderCode",
border_hl_markdown = "NeoNotebookBorderMarkdown",
show_cell_index = true,
vertical_borders = true,
cell_width_ratio = 0.75,
cell_min_width = 60,
cell_max_width = 140,
top_padding = 1,
trim_cell_spacing = true,
cell_gap_lines = 1,
soft_contain = true,
strict_containment = "soft",
contain_line_nav = true,
textwidth_in_cells = true,
notebook_scrolloff = 5,
interrupt_on_rerun = true,
skip_unchanged_rerun = true,
kernel_recovery_retries = 1,
stream_preview_max_lines = 400,
stream_render_interval_ms = 80,
stream_render_min_delta = 50,
stream_placeholder_text = "cell executing...",
stream_progress_style = "bar",
stream_progress_bar_width = 20,
keymaps = {
new_code = "]c",
new_markdown = "]m",
run = "<leader>r",
toggle = "<leader>m",
preview = "<leader>p",
run_and_next = "<S-CR>",
next_cell = "<C-n>",
prev_cell = "<C-p>",
cell_list = "<leader>l",
duplicate_cell = "<leader>yd",
split_cell = "<leader>xs",
fold_cell = "<leader>zf",
unfold_cell = "<leader>zu",
toggle_fold = "<leader>zz",
clear_output = "<leader>co",
clear_all_output = "<leader>cO",
delete_cell = "<leader>dd",
yank_cell = "<leader>yy",
move_up = "<M-k>",
move_down = "<M-j>",
move_top = "<leader>mG",
move_bottom = "<leader>mgg",
run_all = "<leader>ra",
restart = "<leader>rs",
kernel_restart = "<leader>kr",
kernel_interrupt = "<leader>ki",
kernel_stop = "<leader>ks",
kernel_pause = "<leader>kp",
kernel_status = "<leader>kk",
toggle_output = "<leader>tt",
toggle_output_collapse = "<leader>of",
select_cell = "<leader>vs",
stats = "<leader>ns",
run_above = "<leader>rk",
run_below = "<leader>rj",
toggle_auto_render = "<leader>tr",
toggle_overlay = "<leader>to",
help = "<leader>nh",
edit_cell = "<leader>ee",
save_cell = "<leader>es",
run_cell = "<leader>er",
snake_game = "<leader>sg",
},
})Disable virtual kernel status badge:
require("neo_notebooks").setup({
kernel_status_virtual = false,
})Optional viewport virtual padding (to keep notebook cells from visually pinning to the top/bottom viewport edges):
require("neo_notebooks").setup({
viewport_virtual_padding = { top = 2, bottom = 2 },
})- Cells are separated by lines like
# %% [code]or# %% [markdown]. - Virtual borders are rendered using virtual lines; output is inline by default.
- The last expression in a code cell is printed automatically (Jupyter-like).
- Cell execution is serialized per buffer via an internal FIFO queue (including run-all/above/below), so outputs land in predictable order.
- Notebook buffers set
scrolloffto keep a few lines visible below the cursor. - Notebook buffers can also render virtual viewport padding (
viewport_virtual_padding) to preserve top/bottom breathing room while scrolling. - Non-
tqdmprogress shape is producer-controlled;tests/fixtures/perf/manual_exec_soak.*includespct,ratio, andbarexamples. - This is a minimal experimental baseline and intended to be expanded.
The plugin maintains a per-buffer cell index cache with lazy invalidation.
Buffer mutations mark the cache as dirty, and reads rebuild only when needed.
The cache also tracks buffer changedtick to avoid stale reads.
The cache stores both an ordered list and an ID map for O(1) access.
Each cell has a stable cell_id stored as an extmark on the marker line.
High-frequency updates (text changes and execution spinner ticks) are coalesced by a small per-buffer render scheduler. This reduces redundant full redraws during bursts of edits while keeping output and borders in sync.
If rich is installed in your Python environment, the last expression in a cell is rendered using Rich.
You can toggle this at runtime inside a notebook:
neo_rich(False) # disable rich rendering
neo_rich(True) # enable rich renderingFor pandas DataFrames/Series, Rich renders a table (limited to 20 rows/columns by default). You can override limits:
__neo_notebooks_rich_max_rows = 50
__neo_notebooks_rich_max_cols = 30Run tests in headless Neovim:
nvim --headless -u NONE -c \"lua dofile('tests/run.lua')\"
Lane-specific runs:
# Required core lane
nvim --headless -u NONE -c "set shadafile=NONE" -c "luafile tests/core_contract.lua" -c qa
# Required integration lane
nvim --headless -u NONE -c "set shadafile=NONE" -c "luafile tests/integration.lua" -c qa
# Compatibility dispatcher (required lanes, skips optional kitty lane)
nvim --headless -u NONE -c "set shadafile=NONE" -c "let g:neo_notebooks_test_skip_optional_kitty=1" -c "luafile tests/run.lua" -c qa
# Optional kitty/image backend lane (expected failure signal on non-kitty setups)
nvim --headless -u NONE -c "set shadafile=NONE" -c "luafile tests/optional_kitty.lua" -c qa
# Optional performance/scalability lane (large synthetic fixtures + timing budgets)
nvim --headless -u NONE -c "set shadafile=NONE" -c "luafile tests/performance.lua" -c qa
# Optional strict budget profile (useful for local regression detection)
nvim --headless -u NONE -c "set shadafile=NONE" -c "let g:neo_notebooks_perf_budget_profile='strict'" -c "luafile tests/performance.lua" -c qa
# Optional budget scaling (for slower/faster environments)
nvim --headless -u NONE -c "set shadafile=NONE" -c "let g:neo_notebooks_perf_budget_scale=1.50" -c "luafile tests/performance.lua" -c qa
# Run dispatcher + include performance lane
nvim --headless -u NONE -c "set shadafile=NONE" -c "let g:neo_notebooks_test_skip_optional_kitty=1" -c "let g:neo_notebooks_test_include_performance=1" -c "luafile tests/run.lua" -c qa
# Optional: include real network fetch workload in performance lane
nvim --headless -u NONE -c "set shadafile=NONE" -c "let g:neo_notebooks_test_include_network=1" -c "luafile tests/performance.lua" -c qa
Manual stress fixtures:
tests/fixtures/perf/manual_exec_stress.ipynb(quick execution stress)tests/fixtures/perf/manual_exec_soak.ipynb(heavier 2-3 min soak target with tunable knobs)
When opening an empty python buffer, the plugin inserts a starter markdown cell:
# %% [markdown]Default Shift+Enter (<S-CR>) behavior:
- Markdown cell: create a new code cell below and enter it.
- Code cell: execute the cell, show output inline below it, create a new code cell, and enter it.
- If the trailing code cell is empty, it will not create another empty trailing code cell.
You can switch output style to a floating window with:
require("neo_notebooks").setup({ output = "float" })Run :NeoNotebookMarkdownPreview in a markdown cell to open a floating preview window with markdown highlighting.
Inline markdown cells also get lightweight virtual formatting for headings and emphasis/code spans when not actively edited.
For fenced markdown blocks tagged as python (python ... ), NeoNotebooks uses Tree-sitter token captures when available; it falls back to raw-block highlighting if parser/query support is unavailable.
Enable a floating, read-only overlay that mirrors the current cell:
require("neo_notebooks").setup({ overlay_preview = true })You can toggle it on demand with :NeoNotebookCellOverlayToggle.
By default, completion popups are disabled while your cursor is inside a markdown cell:
require("neo_notebooks").setup({ suppress_completion_in_markdown = true })This sets vim.b.completion = false when entering markdown cells and restores the previous value in code cells.
If you use blink.cmp and want to keep completion while disabling the popup menu in notebooks, add this to your blink.cmp config:
completion = {
menu = {
auto_show = require("neo_notebooks").blink_cmp_auto_show,
},
}If you want to disable completion entirely in notebooks, set:
require("neo_notebooks").setup({ suppress_completion_popup = true })<leader><leader>acnew code cell below<leader><leader>amnew markdown cell below<leader>rrun current cell<leader>tctoggle cell type<S-CR>run cell and create new code cell<C-n>next cell<C-p>previous cell<leader>ydduplicate cell<leader>xssplit cell at cursor<leader>zffold current cell<leader>zuunfold current cell<leader>zztoggle fold for current cell<leader>coclear output for current cell<leader>cOclear output for all cells<leader>oiclear image output for current cell<leader>oIclear image pane<leader>pttoggle image pane size (25/33/50% default)<leader>pccollapse/close image pane<leader>pnnext image (page mode)<leader>ppprevious image (page mode)<leader>dddelete current cell<leader>yyyank current cell<M-k>move cell up (accepts counts, e.g.3<M-k>)<M-j>move cell down (accepts counts, e.g.2<M-j>)<leader>mGmove cell to top<leader>mggmove cell to bottomj/kstay inside the active cell body whensoft_contain=trueandcontain_line_nav=trueu(undo) preserves native undo behavior and then re-clamps cursor within current cell bounds (use<C-n>/<C-p>to move between cells)<leader>sgstarts snake mode by creating a new code cell below the current cell
If you use a custom statusline (e.g. lualine), add the component:
require("neo_notebooks.image_pane").statusline()
Note: <M-...> means the Meta key (typically Alt on most keyboards).
When the pane is collapsed with <leader>pc, new images are saved to disk and not auto-rendered until the pane is reopened (use <leader>pt or :NeoNotebookImagePaneTest to reopen).
<leader>rarun all code cells<leader>rsrestart python session<leader>krkernel restart<leader>kikernel interrupt<leader>kskernel stop<leader>kpkernel pause/resume queue dispatch<leader>kktoggle persistent kernel status panel<leader>vsselect current cell body<leader>nsshow cell stats<leader>rkrun all code cells above<leader>rjrun current + below code cells<leader>trtoggle auto-render
Import:
:NeoNotebookImportIpynb path/to/notebook.ipynb
Export:
:NeoNotebookExportIpynb path/to/notebook.ipynb
Notes:
.ipynbmetadata, execution counts, and cell outputs are preserved on import/export.- Existing outputs are rendered on import for code cells.
- Markdown and code cells are supported; other cell types are treated as code.
- Import drops a leading blank code cell if it appears before the first markdown cell.
- Import rejects malformed notebook documents where
cellsis not a JSON list. - After
.ipynbimport/open, undo baseline is reset so extraudoes not revert to raw JSON import state.
Import into current buffer:
:NeoNotebookImportJupytext path/to/notebook.py
Open in a new notebook-view buffer:
:NeoNotebookOpenJupytext path/to/notebook.py
Notes:
- Supports Jupytext percent cell markers (
# %%,# %% [markdown],# %% [md]), including indented marker lines. - Markdown percent lines are converted to normal markdown cell text in NeoNotebook view.
- A default
metadata.jupytextblock is seeded when missing. - Malformed/partial Jupytext headers (for example missing closing
# ---) fall back safely to default metadata. - Exporting to
.ipynbpreserves/round-tripsmetadata.jupytext.
*.nnfiles are detected as Python for LSP/syntax and opt-in to NeoNotebook via a buffer flag.*.ipynbfiles auto-open into a Python buffer (converted to marker format) whenauto_open_ipynb = true.- Saving (
:w) in an.ipynbbuffer exports the current cells back to the.ipynbfile.
Note: top_padding inserts real blank lines at the top of the buffer on first open to keep the top border visible.
By default, the plugin defines:
NeoNotebookBorderCode(green)NeoNotebookBorderMarkdown(cyan)NeoNotebookOutput(purple)
You can override:
vim.api.nvim_set_hl(0, "NeoNotebookBorderCode", { fg = "#00ff00" })
vim.api.nvim_set_hl(0, "NeoNotebookBorderMarkdown", { fg = "#00ffff" })
vim.api.nvim_set_hl(0, "NeoNotebookOutput", { fg = "#a020f0" })
require("neo_notebooks").setup({
border_hl_code = "NeoNotebookBorderCode",
border_hl_markdown = "NeoNotebookBorderMarkdown",
})Set show_cell_index = false to remove numeric labels from cell borders.
Cells are centered and responsive to window size:
cell_width_ratiosets the width as a percentage of the window (default0.9).cell_min_width/cell_max_widthclamp the width.
Set vertical_borders = false to disable left/right cell edges.
When enabled, jumping to another cell (next/prev/list) enters insert mode automatically. Creating a new cell also enters insert mode by default.