wurli/jet.nvim

github github
code-runner
stars 24
issues 0
subscribers 0
forks 0
CREATED

UPDATED


jet.nvim ✈️

A Jupyter kernel supervisor for Neovim, built on top of Jet

GitHub Tag Nvim License: MIT

https://github.com/user-attachments/assets/940430ed-f0f3-498c-807e-efa6ae85cf86

Features

  • A repl which runs in Neovim's built-in terminal
  • An LSP server which provides live completions from the kernel
  • A Lua API with fine-grained control over running kernels, down to the level of individual Jupyter messages
  • Ability to connect to kernel sessions running outside of Neovim
  • AI-friendly: agents can use the Jet CLI to interact with your kernel sessions
  • Detailed (non-vibed) vimdoc documentation
  • Plug and play - No remote plugin stuff. No python requirements.

Not yet implemented

  • Notebooks
  • Windows support (contributions welcome!)

More demos

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 })

motions

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:

claude

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:

completions

jet.nvim provides a UI for kernel management, allowing you to easily start, stop or rename kernel sessions from Neovim:

ui

Installation

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.

Keymaps

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 })

Extending jet.nvim

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

jet.nvim vs similar plugins

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:

FAQ

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.