A Jupyter kernel supervisor for Neovim, built on top of Jet
https://github.com/user-attachments/assets/940430ed-f0f3-498c-807e-efa6ae85cf86
Not yet implemented
jet.nvim provides an 'expression' text object, which can be used to send discrete chunks of code to the Jet repl. The textobject is configurable per filetype, so for example, installing jet.nvim extensions like jet.ark will give better expression detection in R scripts.
In the demo below, ]e and [e are used to go the next/previous expression,
and ie is used as the expression textobject. These compose nicely, so e.g.
the following keymap can be used to send the current expression to the repl
with a single keypress:
vim.keymap.set("n", "<enter>", "goie]e", { remap = true })
The Jet CLI allows multiple users to connect to the same kernel session. Jet provides a simple skill teaching AI agents how to do this. Agent code is clearly marked as such in the repl:
Why is this kind of AI integration useful? Say you have some Python code which produces a single DataFrame and takes 10 minutes to run. Once you have the resulting DataFrame loaded in your Python session, to perform any analysis using AI in a traditional workflow, you will either need to first tell the AI how to reproduce the DataFrame, or serialise it to a file which the AI can quickly read. Both of these options take time and introduce plenty of room for things to go wrong. Using Jet, the AI acts as 'player 2' in your session and can work with the data directly. This can save a tonne of time, and greatly reduces context/token usage for certain types of problems.
jet.nvim provides kernel completions via an LSP middle-layer. These can include runtime information not available to other LSP servers, e.g. the column names in a Pandas DataFrame:
jet.nvim provides a UI for kernel management, allowing you to easily start, stop or rename kernel sessions from Neovim:
Using vim.pack:
vim.pack.add({ "https://github.com/wurli/jet.nvim" })
require("jet").setup({})
This will enable the :Jet command to bring up the jet.nvim kernel management
UI.
Since most users will want to work with running kernels in different ways, jet.nvim avoids setting default keymaps and instead aims to provide a flexible, low-level Lua API to allow users to implement the behaviour that works for them. The following mappings should give some idea of what's possible:
jet.nvim supports running many kernels simultaneously, and each kernel may also
run many instances. get_kernel() uses some heuristics to determine the best
kernel to use; see the docs for more information:
local toggle_repl = function(ft)
return function()
require("jet.api").get_kernel({ filetype = ft }, function(k) k:term_toggle() end)
end
end
vim.keymap.set("n", "<leader>jp", toggle_repl("python"), { desc = "Open Python (Jet)" })
vim.keymap.set("n", "<leader>jr", toggle_repl("r"), { desc = "Open R (Jet)" })
The Jet repl and image buffers set vim.b.jet.session_id, which can be used to
get the Kernel object which 'owns' the buffers. This mechanism can be used to
set toggle keymaps like so:
vim.api.nvim_create_autocmd("BufWinEnter", {
callback = function()
local session_id = vim.b.jet and vim.b.jet.session_id
local k = session_id and require("jet.api").get_kernel_by_id(session_id)
if k then
vim.keymap.set({ "n", "t" }, "<c-o>", function() k:img_toggle() end, { buffer = 0 })
end
end,
})
The following keymap adds go as an operator which sends the current motion to
the repl. So, for example, goi( will send everything within the current
parentheses to an active jet repl matching the current filetype:
vim.keymap.set(
{ "n", "x" },
"go",
require("jet.api").handle_motion(function(range, filetype)
require("jet.api").get_kernel({
filetype = filetype,
current = true,
status = { "connected", "connecting" },
}, function(k)
local code = range:code({ comments = false })
if code then
k:send_repl(code)
end
end)
end),
{ desc = "Execute code (Jet)", expr = true }
)
Jet allows you to configure what a current 'expression' looks like for a given
language. For jet.nvim's purposes, an expression is just the smallest block of
code around (or ahead of) the cursor which it makes sense to send to the kernel
in one go. This works great with go above, so with the combined mappings you
could use goie to send the current/next expression to the repl:
local api = require("jet.api")
vim.keymap.set({ "x", "o" }, "ie", function()
local expr = api.get_expr()
if not expr then
local pos = api.next_expr_boundary({
current_ok = false,
boundary = "start",
})
expr = pos and api.get_expr(pos)
end
if expr then
expr:textobject()
end
end, { desc = "textobject (jet): [i]n [e]xpression" })
]e and [e can be used to navigate between 'expressions':
vim.keymap.set("n", "]e", function()
local pos = api.next_expr_boundary({ direction = 1, boundary = "start" })
if pos then
vim.fn.cursor(pos:to_cursor())
end
end)
vim.keymap.set("n", "[e", function()
local pos = api.next_expr_boundary({ direction = -1, boundary = "start" })
if pos then
vim.fn.cursor(pos:to_cursor())
end
end)
Finally, if you like to blast through a script sending expressions to the repl as you go, you might like a mapping to send the current expression and move to the next one in a single keypress:
vim.keymap.set("n", "<enter>", "goie]e", { remap = true })
vim.keymap.set("x", "<enter>", "go", { remap = true })
jet.nvim exposes an API for working with Jupyter kernels using Lua. The idea is to allow other plugins to build on jet.nvim to expose kernel-specific functionality.
Existing extensions
| Repo | Kernel | Language | Features |
|---|---|---|---|
| jet.ark | Ark | R | LSP server, resizable plots, kernel prioritisation, R 'expression' resolution |
| jet.ipy | ipykernel | Python | Venv kernel prioritisation, python 'expression' resolution |
Many other plugins provide some level of Jupyter integration, mostly via some kind of Python backend. jet.nvim takes a different approach, using a custom Rust library (Jet) to handle implementation details such as ZMQ and Jupyter's wire protocol. Jet's integration with Neovim happens via 2 mechanisms:
The Jet Lua API: jet.nvim bundles the Jet Lua library, allowing a fully featured kernel supervisor to be built in Neovim's Lua runtime. This makes jet.nvim more extensible than any other Jupyter plugin in Neovim's ecosystem, and allows jet.nvim itself to stay fairly lean, delegating kernel-specific problems to extension plugins such as jet.ark.
The Jet CLI: Jet provides a command-line tool implementing a full, completion-enabled repl which runs in any terminal emulator. jet.nvim runs the Jet CLI in Neovim's built-in terminal to provide a repl experience which feels like native Neovim. A bonus of this architecture is that, since any number of Jet processes can connect to a single kernel instance, any AI agent can also connect to your kernel session and run code, evaluate results, etc alongside you.
Architecture differences aside, a high-level feature comparison is as follows:
No. I did use AI quite a bit to develop Jet proper, i.e. the Rust backend. I wrote this README with a keyboard using my own two mucky paws.
Jupyter ~= notebooks. Jupyter is really a standard/protocol for how interactive languages tell editors about results and environment state.
If you want to implement the Jupyter protocol for a language, you wrap the language in a Jupyter kernel. IPykernel is a popular kernel for Python, Ark is another for R. There are many other kernels which exist for other languages.
Once you've got a kernel, your editor needs to implement a Jupyter client to talk to it. Most editors which implement a Jupyter client use it for some kind of notebook experience, but many also include some kind of REPL (notable examples are Positron and Jupyter's Qt Console).
One of the main benefits of a purpose-built client like Jet is that it allows Neovim to access special/non-standard features that some kernels implement above and beyond the Jupyter spec. E.g. Ark adds a debugger, LSP server, variables pane, a dedicated help window, etc, all of which are unlocked by jet.nvim's Lua API.