The bridge between Neovim and Jupyter Lab, edit in Neovim and preview/run in Jupyter Lab.
This project includes two parts: a JupyterLab extension and a Neovim plugin
- The
JupyterLab extensionexposes functions ofJupyter lab, and provides a remote procedure call(RPC) service - The
Neovim plugincalls the RPC service when it receives events fromNeovimviaautocmd
This project provides two work modes for different network environments. If the browser where your jupyter lab is
located cannot directly access nvim, you must use proxy mode; If you need to collaborate and use the same Jupyter with
others, you must use direct mode
| direct | proxy | |
|---|---|---|
| Architecture |
|
|
| Advantage |
|
|
| Disadvantage |
|
|
-
directmode: (default, recommended) In this mode, neovim is server and neovim plugin(neopyter) is listening toremote_address, the browser where jupyter lab is located will connect to neovim -
proxymode: In this mode, Jupyter lab server(server side, the host you runjupyter labto start JupyterLab) is server and jupyter lab server extension(neopyter) is listening to{IP}:{Port}`, the neovim plugin(neopyter) will connect to{IP}:{Port}`
Ultimately, Neopyter can control Juppyter lab. Neopyter can implement abilities like jupynium.nvim.
output.mp4
| Completion (blink.cmp) | Completion (nvim-cmp) |
|---|---|
|
|
| Code Cell | Markdown Cell |
|
|
| Cell Magic | Line Magic |
|
|
Note
More screenshots and spec, please refer to doc/specification.ipynb and doc/specification.ju.py
Neopyter support two parts, so we need to install them separately.
- πJupyterLab >= 4.0.0
- βοΈ Neovim latest stable version or nightly version
- π
nvim-lua/plenary.nvim - π€
AbaoFromCUG/websocket.nvim(optional formode="direct")
- π
To install the jupyterlab extension, execute:
pip install neopyterConfigure JupyterLab in side panel

mode: Refer to the previous introduction about modeIP: Ifmode=proxy, set to the IP of the host where jupyter server is located. Ifproxy=direct, set to the IP of the host where neovim is locatedPort: Idle port of theIP's' host
NOTICE: all settings is saved to localStorage
- With π€lazy.nvim:
{
"SUSTech-data/neopyter",
dependencies = {
'nvim-lua/plenary.nvim',
'nvim-treesitter/nvim-treesitter', -- neopyter don't depend on `nvim-treesitter`, but does depend on treesitter parser of python
'AbaoFromCUG/websocket.nvim', -- for mode='direct'
},
---@type neopyter.Option
opts = {
mode="direct",
remote_address = "127.0.0.1:9001",
file_pattern = { "*.ju.*" },
on_attach = function(bufnr)
-- do some buffer keymap
end,
},
}Default configuration
---@type neopyter.Option
local default_config = {
remote_address = "127.0.0.1:9001",
file_pattern = { "*.ju.*" },
filename_mapper = function(ju_path)
local ipynb_path = vim.fn.fnamemodify(ju_path, ":r:r:r") .. ".ipynb"
if is_windows then
ipynb_path = ipynb_path:gsub("\\", "/")
end
return ipynb_path
end,
--- auto attach to buffer
auto_attach = true,
--- auto connect with remote jupyterlab
auto_connect = true,
mode = "direct",
---@type neopyter.JupyterOption # ref `:h neopyter.JupyterOption`
jupyter = {
auto_activate_file = true,
partial_sync = false,
-- Always scroll to the current cell.
scroll = {
enable = true,
align = "center",
},
},
---@type neopyter.HighlightOption # ref `:h neopyter.HighlightOption`
highlight = {
enable = true,
mode = "separator",
},
---@type neopyter.TextObjectOption # ref `:h neopyter.TextObjectOption`
textobject = {
enable = true,
-- more capture, poorer performance
queries = { "cellseparator", "cellcontent", "cell" },
},
---@type neopyter.InjectionOption # ref `:h neopyter.InjectionOption`
injection = {
enable = true,
},
---@type neopyter.ParserOption # ref `:h neopyter.ParserOption`
parser = {
trim_whitespace = false,
python = {},
r = {},
},
}See :h neopyter-configuration-types for all option type description.
Suggest keymaps(neopyter don't provide default keymap):
on_attach = function(buf)
local function map(mode, lhs, rhs, desc)
vim.keymap.set(mode, lhs, rhs, { desc = desc, buffer = buf })
end
-- same, recommend the former
map("n", "<C-Enter>", "<cmd>Neopyter execute notebook:run-cell<cr>", "run selected")
-- map("n", "<C-Enter>", "<cmd>Neopyter run current<cr>", "run selected")
-- same, recommend the former
map("n", "<space>X", "<cmd>Neopyter execute notebook:run-all-above<cr>", "run all above cell")
-- map("n", "<space>X", "<cmd>Neopyter run allAbove<cr>", "run all above cell")
-- same, recommend the former, but the latter is silent
map("n", "<space>nt", "<cmd>Neopyter execute kernelmenu:restart<cr>", "restart kernel")
-- map("n", "<space>nt", "<cmd>Neopyter kernel restart<cr>", "restart kernel")
map("n", "<S-Enter>", "<cmd>Neopyter execute notebook:run-cell-and-select-next<cr>", "run selected and select next")
map("n", "<M-Enter>", "<cmd>Neopyter execute notebook:run-cell-and-insert-below<cr>", "run selected and insert below")
map("n", "<F5>", "<cmd>Neopyter execute notebook:restart-run-all<cr>", "restart kernel and run all")
end- Open JupyterLab
jupyter lab, there is a sidebar namedNeopyter, which display neopyter ip+port - Open a
*.ju.pyfile in neovim - Now you can type
# %%in Neovim to create a code cell.- You'll see everything you type below that will be synchronised in the browser
- Status
:Neopyter statusalias to:checkhealth neopytercurrently
- Server
:Neopyter connect [remote 'ip:port'], e.g.:Neopyter command 127.0.0.1:9001, connectJupyter labmanually:Neopyter disconnect
- Sync
:Neopyter sync current, make sync current*.ju.*file with the currently open*.ipynb:Neopyter sync [filename], e.g.:Neopyter sync main.ipynb
- Run
:Neopyter run current, same asRun>Run Selected Cell and Do not Advancemenu inJupyter lab:Neopyter run allAbove, same asRun>Run All Above Selected Cellmenu inJupyter lab:Neopyter run allBelow, same asRun>Run Selected Cell and All Belowmenu inJupyter lab:Neopyter run all, same asRun>Run All Cellsmenu inJupyter lab
- Kernel
:Neopyter kernel restart, same asKernel>Restart Kernelmenu inJupyter lab:Neopyter kernel restartRunAll, same asKernel>Restart Kernel and Run All Cellsmenu inJupyter lab
- Jupyter
:Neopyter execute [command_id] [args], executeJupyter lab's command directly, e.g.:Neopyter execute notebook:export-to-format {"format":"html"}
As Neopyter includes two parts, if you meet any issues, please check both JupyterLab extension and Neovim plugin:
- For
JupyterLab extension, please check the sidebar and the console log of the browser.- If
mode=proxy, please check whether the port is listening on the host where jupyter server is located, e.g.lsof -i :9001on Linux/MacOS ornetstat -an | findstr 9001on Windows
- If
- For
Neovim plugin, please run:Neopyter status.- If
mode=direct, you could check port listening status on the host where neovim is located, e.g.lsof -i :9001on Linux/MacOS ornetstat -an | findstr 9001on Windows
- If
We provide minimize reproduction example in ./example:
- In one terminal, start jupyter lab
cd example
uv run jupyter lab --ip=0.0.0.0 --port=7088
In another terminal, start neovim
cd example
nvim -u repro.lua main.ju.pyFeel free to open an issue if you still cannot solve your problem, and provide the following information:
- repro.lua
- The command you used to start
Jupyter lab - main.ju.py
:Neopyter statusoutput- The console log of the browser, e.g.:
Neopyter provides rich lua APIs, you could use below code as initialization:
-- Reference to `:h neopyter-jupyterlab-api` for all api document
local current_lab = require("neopyter.jupyter").jupyterlab
current_lab:execute_command("notebook:export-to-format", {format="html"})
-- Reference to `:h neopyter-notebook-api` for all api document
local current_notebook = require("neopyter.jupyter").notebook
current_notebook:run_selected_cell()
current_notebook:run_all_above()
current_notebook:run_all_below()
- Notebook API:
:h neopyter-notebook-api - JupyterLab API
:h neopyter-jupyterlab-api-api
Notebook and JupyterLab APIs are wrapped by async context automatically.
- If you call api from async context, anything is OK. Otherwise, the calling order cannot be guaranteed
- A single API call always works
vim.defer_fn(function()
-- non-async context, API response may be unordered
current_notebook:run_selected_cell()
current_notebook:run_all_above()
current_notebook:run_all_below()
end, 0)
require("neopyter.async").run(function()
-- async context, so which will call and return in order
current_notebook:run_selected_cell()
current_notebook:run_all_above()
current_notebook:run_all_below()
end)
If neoconf.nvim is available, neopyter will automatically register/read neoconf settings
{
"neopyter": {
"mode": "proxy",
"remote_address": "127.0.0.1:9001"
}
}nvim-cmplspkind.nvim
local lspkind = require("lspkind")
local cmp = require("cmp")
cmp.setup({
sources = cmp.config.sources({
-- default: all source, maybe some noice
{ name = "neopyter" },
-- { name = "neopyter", option={ source = { "CompletionProvider:kernel" } } },
}),
formatting = {
format = lspkind.cmp_format({
mode = "symbol_text",
menu = {
buffer = "[Buf]",
nvim_lsp = "[LSP]",
nvim_lua = "[Lua]",
neopyter = "[Neopyter]",
},
symbol_map = {
-- specific complete item kind icon
["Magic"] = "πͺ",
["Path"] = "π",
["Dict key"] = "π",
["Instance"] = "σ±»",
["Statement"] = "σ±―",
},
}),
},
)}
-- menu item highlight
vim.api.nvim_set_hl(0, "CmpItemKindMagic", { bg = "NONE", fg = "#D4D434" })
vim.api.nvim_set_hl(0, "CmpItemKindPath", { link = "CmpItemKindFolder" })
vim.api.nvim_set_hl(0, "CmpItemKindDictkey", { link = "CmpItemKindKeyword" })
vim.api.nvim_set_hl(0, "CmpItemKindInstance", { link = "CmpItemKindVariable" })
vim.api.nvim_set_hl(0, "CmpItemKindStatement", { link = "CmpItemKindVariable" })
More information, see nvim-cmp wiki
blink.cmp
require("blink-cmp").setup({
sources = {
per_filetype = {
python = { inherit_defaults = true, "neopyter" },
},
providers = {
neopyter = {
name = "Neopyter",
module = "neopyter.blink",
---@type neopyter.BlinkCompleterOption
opts = {},
},
},
},
})
Neopyter load textobjects.scm dynamic according config.textobject.queries:
{
"SUSTech-data/neopyter",
---@type neopyter.Option
opts = {
textobject = {
enable = true,
queries = {
"linemagic",
"cellseparator",
"cellcontent",
"cell"
},
},
},
}The more queries you added, the poorer performance to capture, so only add what you need.
Then you can config you nvim-treesitter-textobjects as usually:
require'nvim-treesitter.configs'.setup {
textobjects = {
select = {
enable = true,
lookahead = true,
keymaps = {
["aj"] = { query = "@cell", desc = "Select cell" },
["ij"] = { query = "@cellcontent", desc = "Select cell content" },
},
},
move = {
enable = true,
goto_next_start = {
["]j"] = "@cellseparator",
},
goto_previous_start = {
["[j"] = "@cellseparator",
},
},
},
}
Supported queries:
@linemagic@cellseparator@cellseparator.code@cellseparator.markdown@cellseparator.raw
@cellcontent@cell
- Neovim
- Full sync
- Partial sync
- Scroll view automatically
- Activate cell automatically
- Save notebook automatically
- Completion
- Magic completion item
- Path completion item
- Tree-sitter
- Highlight
- Separator+non-code
- Shortsighted
- Textobjects
- Fold
- Highlight
- Kernel management
- Restart kernel
- Restart kernel and run all
- Run cell
- Run selected cell
- Run all above selected cell
- Run selected cell and all below
- Run all cell
- Sync
- Set synchronized
.ipynbmanually
- Set synchronized
- Notebook manager
- Open corresponding notebook if exists
- Sync with untitled notebook default
- Close notebook when buffer unload
- Jupyter Lab
- Settings
- TCP server host/port settings
- Status Sidebar
- Settings
ip:port - Display client info
- Settings
- Settings
- Performance
- Rewrite
RpcClient, support async RPC requestvim.rpcrequestandvim.rpcnotify - Rewrite
highlightsandtextobjectsqueries - Rewrite parser with tree-sitter
- Unified
highlights,textobjects,parserto unified parser
- Rewrite
- Document
- API Document
- jupynium.nvim: Selenium-automated Jupyter Notebook that is synchronised with Neovim in real-time.
- snacks.nvim: The
zenhighlight is inspired bysnacks.zen








