From ccf0402e685dc6e0b3466296cce294164a8c158b Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Sat, 12 Sep 2026 12:28:01 +0100 Subject: [PATCH 01/24] feat!(kernel): remove Kernel.metadata Better to just subclass Kernel I think. --- lua/jet/core/kernel/init.lua | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index 4f8ed60..ee11a25 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -73,8 +73,6 @@ local STARTING_KERNEL_SENTINEL = "" ---Kernel-specific hooks. Basically for convenience on top of the 'global' ---hooks you can set in |jet.Config| ---@field hooks jet.Hooks ----Arbitrary extra data, e.g. for use by extensions ----@field metadata table ---Defaults to 100. `api.get_kernel()` will try to use this field to select a ---single kernel. Typically you would set the priority using the kernel's init ---hook (see |jet.Hooks|). @@ -97,7 +95,6 @@ local init_defaults = function() incomplete_line = "", }, on_started = {}, - metadata = {}, priority = 100, hooks = hooks.init_hooks(), } @@ -806,6 +803,19 @@ end ---@field listener_interval? integer In milliseconds, default 50ms ---@field callback? fun(msg: jupyter.Msg) +---Find a comm id using the comm name +---@param name string +---@param pattern boolean? +---@return string?, table? +function Kernel:get_comm(name, pattern) + for comm_id, comm in pairs(self.open_comms) do + local is_match = pattern and comm.name:match(name) or comm.name == name + if is_match then + return comm_id, comm.data + end + end +end + ---Open a comm channel to the kernel --- ---Kernels may implement custom messages using comm channels. This function From 9737c36f164b83f817d8684fe71046fdd9b9e0f9 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Mon, 14 Sep 2026 09:32:16 +0100 Subject: [PATCH 02/24] feat(kernel): add Kernel.subclass --- doc/jet-nvim.txt | 16 ++++++++++++++-- lua/jet/core/kernel/init.lua | 4 +++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/doc/jet-nvim.txt b/doc/jet-nvim.txt index c71f2c4..6f93410 100644 --- a/doc/jet-nvim.txt +++ b/doc/jet-nvim.txt @@ -607,12 +607,12 @@ THE KERNEL CLASS *jet-the-kernel-class* • {hooks} (`jet.Hooks`) Kernel-specific hooks. Basically for convenience on top of the 'global' hooks you can set in |jet.Config| - • {metadata} (`table`) Arbitrary extra data, - e.g. for use by extensions • {priority} (`integer`) Defaults to 100. `api.get_kernel()` will try to use this field to select a single kernel. Typically you would set the priority using the kernel's init hook (see |jet.Hooks|). + • {subclass} (`string?`) For use by extensions which may want + to extend the `Kernel` class Kernel.init_owned({opts}) *jet-Kernel.init_owned()* @@ -748,6 +748,18 @@ Kernel:stop({callback}) *jet-Kernel:stop()* See: ~ • |Kernel:close()| +Kernel:get_comm({name}, {pattern}) *jet-Kernel:get_comm()* + + Convenience; find a comm id (from `Kernel.open_comms`) using the comm name + + Parameters: ~ + • {name} (`string`) + • {pattern} (`boolean?`) + + Return: ~ + (`string?`) + (`table?`) + Kernel:comm_open({name}, {data}, {opts}) *jet-Kernel:comm_open()* Open a comm channel to the kernel diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index ee11a25..2b5a295 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -77,6 +77,8 @@ local STARTING_KERNEL_SENTINEL = "" ---single kernel. Typically you would set the priority using the kernel's init ---hook (see |jet.Hooks|). ---@field priority integer +---For use by extensions which may want to extend the `Kernel` class +---@field subclass string? ---@field private stream jet.callback ---@field private ui_expand boolean ---@field private on_started table @@ -803,7 +805,7 @@ end ---@field listener_interval? integer In milliseconds, default 50ms ---@field callback? fun(msg: jupyter.Msg) ----Find a comm id using the comm name +---Convenience; find a comm id (from `Kernel.open_comms`) using the comm name ---@param name string ---@param pattern boolean? ---@return string?, table? From e001c51aa943687426f4a1e2a050e73fd7e64703 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Mon, 14 Sep 2026 09:39:36 +0100 Subject: [PATCH 03/24] docs(readme): reword ai FAQ --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a670d9a..ba9bcee 100644 --- a/README.md +++ b/README.md @@ -438,10 +438,9 @@ Architecture differences aside, a high-level feature comparison is as follows: ## FAQ
-Was jet.nvim vibe coded? +Is jet.nvim vibe coded? -> 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. +> No. Some AI was used to develop Jet proper, jet.nvim's Rust backend.
From 3abb9f956adceef632747f23143abd78ce59c221 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Mon, 14 Sep 2026 18:58:26 +0100 Subject: [PATCH 04/24] fix(filter_kernels): normalise spec path before comparing --- lua/jet/core/manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/jet/core/manager.lua b/lua/jet/core/manager.lua index d78c276..23acb16 100644 --- a/lua/jet/core/manager.lua +++ b/lua/jet/core/manager.lua @@ -97,7 +97,7 @@ Manager.filter_kernels = function(kernels, filters) return false end - if filters.spec_path and k.spec_path ~= filters.spec_path then + if filters.spec_path and utils.path_normalise(k.spec_path) ~= utils.path_normalise(filters.spec_path) then return false end From 0e986320f12cf784caa33788ec94928c6e2a1cb2 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Mon, 14 Sep 2026 18:58:46 +0100 Subject: [PATCH 05/24] chore(build): bump version to 0.2.0 --- lua/jet/core/config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/jet/core/config.lua b/lua/jet/core/config.lua index e8b97a1..0f7233a 100644 --- a/lua/jet/core/config.lua +++ b/lua/jet/core/config.lua @@ -90,7 +90,7 @@ M.defaults = { hooks = require("jet.core.hooks").init_hooks(), } -M.jet_nvim_version = "0.1.0" +M.jet_nvim_version = "0.2.0" ---@class jet.Config.Data M.data = { From dcb6d9860ba089dc4150de38251551de0eb5e109 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 17 Sep 2026 12:08:37 +0100 Subject: [PATCH 06/24] feat!(kernel): add Kernel.windows --- lua/jet/core/kernel/buf.lua | 66 ++++------------ lua/jet/core/kernel/img.lua | 16 +--- lua/jet/core/kernel/init.lua | 45 +++++++++-- lua/jet/core/kernel/term.lua | 9 +-- lua/jet/core/kernel/win.lua | 145 +++++++++++++++++++++++++++++++++++ lua/jet/core/ui/init.lua | 2 +- 6 files changed, 206 insertions(+), 77 deletions(-) create mode 100644 lua/jet/core/kernel/win.lua diff --git a/lua/jet/core/kernel/buf.lua b/lua/jet/core/kernel/buf.lua index 1065285..4a36162 100644 --- a/lua/jet/core/kernel/buf.lua +++ b/lua/jet/core/kernel/buf.lua @@ -1,12 +1,10 @@ -local utils = require("jet.core.utils") - ---@class jet.Buf ---@field buf integer +---@field layout_pos integer ---@field name string ---@field augroup integer ---@field ns integer ---@field kernel jet.Kernel ----@field open_opts vim.api.keyset.win_config | fun(): vim.api.keyset.win_config local Buf = {} Buf.__index = Buf ---@private @@ -14,7 +12,7 @@ Buf.__index = Buf ---@private ---@field name string ---@field ns integer ---@field kernel jet.Kernel ----@field open_opts vim.api.keyset.win_config | fun(): vim.api.keyset.win_config +---@field layout_pos integer ---@generic T ---@param class? T @@ -25,66 +23,34 @@ function Buf.init(class, opts) kernel = opts.kernel, ns = opts.ns, name = opts.name, - open_opts = opts.open_opts, + layout_pos = opts.layout_pos, augroup = vim.api.nvim_create_augroup(opts.name, { clear = true }), buf = vim.api.nvim_create_buf(false, true), }, class or Buf) - vim.b[out.buf].jet = { session_id = out.kernel.session_id } + vim.b[out.buf].jet = { + session_id = out.kernel.session_id, + layout_pos = out.layout_pos, + } vim.api.nvim_buf_set_name(out.buf, out.name) return out end ---Get a window displaying the buffer, if there is one ----@return integer? -function Buf:win() return utils.buf_get_win(self.buf) end +---@return jet.Win +function Buf:win() + local w = self.kernel.windows[self.layout_pos] + assert(w, "Kernel window %d not found. Kernel has %d registered windows", self.layout_pos, #self.kernel.windows) + return w +end +---@param opts vim.api.keyset.win_config? ---@param focus? boolean ----@param opts? vim.api.keyset.win_config ---@return integer # The opened window -function Buf:open(focus, opts) - local win = self:win() - - if win then - if focus then - vim.api.nvim_set_current_win(win) - if vim.bo[self.buf].buftype == "terminal" then - vim.cmd.startinsert() - end - end - return win - end - - local open_opts = opts - or type(self.open_opts) == "function" and self.open_opts() - or type(self.open_opts) == "table" and self.open_opts - or {} - - ---@type integer - win = vim.api.nvim_open_win(self.buf, false, open_opts) - vim.api.nvim_win_set_hl_ns(win, self.ns) +function Buf:open(opts, focus) return self:win():open(self.buf, opts, focus) end - if vim.bo[self.buf].buftype == "terminal" then - -- When the cursor is at the bottom of the REPL you get auto-scroll - -- when new lines appear. This is a good state to start in. - vim.api.nvim_win_set_cursor(win, { vim.api.nvim_buf_line_count(self.buf), 0 }) - end - - vim.wo[win].number = false - vim.wo[win].relativenumber = false - - return win -end - -function Buf:toggle() - local win = self:win() - if win then - vim.api.nvim_win_close(win, true) - else - self:open() - end -end +function Buf:toggle() self:win():toggle(self) end function Buf:delete() vim.schedule(function() diff --git a/lua/jet/core/kernel/img.lua b/lua/jet/core/kernel/img.lua index 6f61f82..6544918 100644 --- a/lua/jet/core/kernel/img.lua +++ b/lua/jet/core/kernel/img.lua @@ -20,14 +20,7 @@ function Img.init(opts) name = opts.kernel:friendly_name() .. " - Images", ns = opts.ns, kernel = opts.kernel, - open_opts = function() - local term_win = opts.kernel.term and opts.kernel.term:win() - return { - split = term_win and "above" or "right", - win = term_win or -1, - style = "minimal", - } - end, + layout_pos = 2, }) out.kernel = opts.kernel @@ -45,7 +38,7 @@ end ---@param which? string | integer ---@return integer # Win number function Img:open(focus, which) - local win = buf.open(self, focus) + local win = buf.open(self, nil, focus) self:display(which) -- For folks who want to display image stuff in the winbar/statusline vim.api.nvim__redraw({ win = win, winbar = true, statusline = true }) @@ -92,9 +85,7 @@ function Img:display(which) return end - local win = self:win() - - if not win then + if not self:win():get_curr_jet_buf(self) then return end @@ -132,7 +123,6 @@ function Img:display(which) if ok and image_api then local img = image_api.from_file(src, { buffer = self.buf, - window = win, }) if img then img:render() diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index 2b5a295..93f8b4d 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -3,6 +3,7 @@ local cfg = require("jet.core.config").options local utils = require("jet.core.utils") local lsp = require("jet.core.kernel.lsp") local hooks = require("jet.core.hooks") +local win = require("jet.core.kernel.win") local STARTING_KERNEL_SENTINEL = "" @@ -77,7 +78,9 @@ local STARTING_KERNEL_SENTINEL = "" ---single kernel. Typically you would set the priority using the kernel's init ---hook (see |jet.Hooks|). ---@field priority integer ----For use by extensions which may want to extend the `Kernel` class +---Windows used to show kernel UI. Kernels use 2 windows by default (one for +---the repl, one for all other stuff). This field is extensible! +---@field windows jet.Win[] ---@field subclass string? ---@field private stream jet.callback ---@field private ui_expand boolean @@ -86,6 +89,37 @@ local STARTING_KERNEL_SENTINEL = "" local Kernel = {} Kernel.__index = Kernel ---@private +local jet_hl_ns = vim.api.nvim_create_namespace("jet_highlights") +vim.api.nvim_set_hl(jet_hl_ns, "Normal", { link = "JetRepl" }) + +---@param k jet.Kernel +local init_wins = function(k) + local repl_win = win.init({ + ns = jet_hl_ns, + kernel = k, + focus = true, + open_opts = function() + ---@type vim.api.keyset.win_config + return { split = "right", win = -1 } + end, + }) + + local secondary_win = win.init({ + ns = jet_hl_ns, + kernel = k, + focus = true, + open_opts = function(wins) + vim.print(wins) + return { + split = wins[1] and "above" or "right", + win = wins[1] or -1, + } + end, + }) + + return { repl_win, secondary_win } +end + ---@return Partial local init_defaults = function() return { @@ -99,6 +133,7 @@ local init_defaults = function() on_started = {}, priority = 100, hooks = hooks.init_hooks(), + windows = {}, } end @@ -119,6 +154,7 @@ function Kernel.init_owned(opts) local base = vim.tbl_extend("keep", opts, init_defaults(), { owned = true }) local out = setmetatable(base, Kernel) + out.windows = init_wins(out) out:try_resolve_filetype() out:do_kernel_init() @@ -149,6 +185,8 @@ function Kernel.init_external(opts) Kernel ) + out.windows = init_wins(out) + manager:insert(out) Kernel.try_resolve_filetype(out) @@ -179,9 +217,6 @@ function Kernel:term_toggle() end end -local jet_hl_ns = vim.api.nvim_create_namespace("jet_highlights") -vim.api.nvim_set_hl(jet_hl_ns, "Normal", { link = "JetRepl" }) - ---Open a terminal window for the kernel. ---If no terminal is active, one will be created and opened ---@param callback? fun(t: jet.Kernel.Term) @@ -189,7 +224,7 @@ vim.api.nvim_set_hl(jet_hl_ns, "Normal", { link = "JetRepl" }) function Kernel:term_open(callback, focus) self:term_create(function() assert(self.term, "kernel.term is nil") - self.term:open(focus) + self.term:open(nil, focus) if callback then callback(self.term) end diff --git a/lua/jet/core/kernel/term.lua b/lua/jet/core/kernel/term.lua index ad2c79e..c58310c 100644 --- a/lua/jet/core/kernel/term.lua +++ b/lua/jet/core/kernel/term.lua @@ -20,14 +20,7 @@ function Term.init(opts) name = opts.kernel:friendly_name(), ns = opts.ns, kernel = opts.kernel, - open_opts = function() - local img_win = opts.kernel.img and opts.kernel.img:win() - return { - split = img_win and "below" or "right", - win = img_win or -1, - style = "minimal", - } - end, + layout_pos = 1, }) out.kernel = opts.kernel diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua new file mode 100644 index 0000000..444eae9 --- /dev/null +++ b/lua/jet/core/kernel/win.lua @@ -0,0 +1,145 @@ +---@class jet.Win +---@field private win integer +---@field ns integer +---If `true` then `Win:open()` will focus an already open window, otherwise +---it's a no-op. +---@field focus boolean +---@field kernel jet.Kernel +---@field open_opts? vim.api.keyset.win_config | fun(wins: integer[]): vim.api.keyset.win_config +local Win = {} +Win.__index = Win + +---@class jet.Win.init.Opts +---@field open_opts jet.Win["open_opts"] +---@field ns jet.Win["ns"] +---@field focus jet.Win["focus"] +---@field kernel jet.Win["kernel"] + +---@param buf integer | jet.Buf +local to_bufnr = function(buf) return type(buf) == "number" and buf or buf.buf end + +---@param opts jet.Win.init.Opts +Win.init = function(opts) + if opts.focus == nil then + opts.focus = false + end + return setmetatable(vim.tbl_extend("force", opts, { win = -99 }), Win) +end + +---@return integer +function Win:layout_pos() + for i, w in ipairs(self.kernel.windows) do + if w == self then + return i + end + end + error("Could not determine the window layout position") +end + +---Opens with the following algorithm +--- +---* If the window is not open, opens with `buf` +---* If the window is open with `buf`, focus the window (if `Win.focus`) +---* Otherwise, if the window is open with another buffer: +--- * Does the buffer have `vim.b.jet` set? If so, show `buf` in the window +--- * Otherwise, open a fresh window, resetting `self.win`. +--- +---@param buf integer | jet.Buf +---@param opts vim.api.keyset.win_config? +---@param focus? boolean +---@return integer +function Win:open(buf, opts, focus) + local curr_jet_buf = self:get_curr_jet_buf() + + if curr_jet_buf == buf then + if focus or self.focus then + vim.api.nvim_set_current_win(self.win) + if vim.bo[buf].buftype == "terminal" then + vim.cmd.startinsert() + end + end + return self.win + end + + local bufnr = to_bufnr(buf) + + if curr_jet_buf and vim.api.nvim_win_is_valid(self.win) then + vim.api.nvim_win_set_buf(self.win, bufnr) + else + self.win = vim.api.nvim_open_win(bufnr, false, opts or self:make_open_opts()) + end + + vim.api.nvim_win_set_hl_ns(self.win, self.ns) + + if vim.bo[bufnr].buftype == "terminal" then + -- When the cursor is at the bottom of the REPL you get auto-scroll + -- when new lines appear. This is a good state to start in. + vim.api.nvim_win_set_cursor(self.win, { vim.api.nvim_buf_line_count(bufnr), 0 }) + end + + return self.win +end + +---@private +---@return vim.api.keyset.win_config +function Win:make_open_opts() + if type(self.open_opts) == "function" then + local wins = vim.tbl_map(function(win) return win:winnr() end, self.kernel.windows) + local out = self.open_opts(wins) + out.style = "minimal" + return out + elseif type(self.open_opts) == "table" then + local out = self.open_opts + out.style = "minimal" + return out + else + return { + style = "minimal", + split = "right", + win = -1, + } + end +end + +---@param buf? jet.Buf | integer +function Win:close(buf) + if self:get_curr_jet_buf(buf) and vim.api.nvim_win_is_valid(self.win) then + vim.api.nvim_win_close(self.win, true) + end +end + +---@param buf integer | jet.Buf +function Win:toggle(buf) + if self:get_curr_jet_buf(buf) then + self:close(buf) + else + self:open(buf) + end +end + +---@param buf? integer | jet.Buf +---@return integer? +function Win:get_curr_jet_buf(buf) + local curr_buf = self:get_buf() + if buf and to_bufnr(buf) ~= curr_buf then + return nil + end + local buf_pos = curr_buf and vim.b[curr_buf].jet and vim.b[curr_buf].jet.layout_pos + return buf_pos == self:layout_pos() and curr_buf or nil +end + +---@param buf? integer | jet.Buf +function Win:winnr(buf) + if self:get_curr_jet_buf(buf) then + return self.win + end +end + +---@return integer? +function Win:get_buf() + if vim.api.nvim_win_is_valid(self.win) then + return vim.api.nvim_win_get_buf(self.win) + end +end + +return Win diff --git a/lua/jet/core/ui/init.lua b/lua/jet/core/ui/init.lua index d4ef01a..53151a3 100644 --- a/lua/jet/core/ui/init.lua +++ b/lua/jet/core/ui/init.lua @@ -477,7 +477,7 @@ M.show = function() if l and l.data and l.data.kernel then ---@type jet.Kernel local k = l.data.kernel - local should_focus = k.term and k.term:win() + local should_focus = k.term and k.term:layout_pos() k:term_open(function() if should_focus then ui:close() From 56184a675f065ab0c0563c3640822864d03d9000 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 17 Sep 2026 14:15:48 +0100 Subject: [PATCH 07/24] refactor: move kernel buffers into Kernel.bufs --- .emmyrc.json | 3 +- lua/jet/core/kernel/buf.lua | 26 +++++----- lua/jet/core/kernel/img.lua | 2 +- lua/jet/core/kernel/init.lua | 89 ++++++++++++++++++++-------------- lua/jet/core/kernel/term.lua | 2 +- lua/jet/core/kernel/win.lua | 28 ++++++----- lua/jet/core/ui/init.lua | 2 +- lua/lualine/extensions/jet.lua | 4 +- 8 files changed, 89 insertions(+), 67 deletions(-) diff --git a/.emmyrc.json b/.emmyrc.json index ce1f079..98236b3 100644 --- a/.emmyrc.json +++ b/.emmyrc.json @@ -2,7 +2,8 @@ "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json", "diagnostics": { "disable": [ - "unnecessary-if" + "unnecessary-if", + "unnecessary-assert" ] }, "runtime": { diff --git a/lua/jet/core/kernel/buf.lua b/lua/jet/core/kernel/buf.lua index 4a36162..8572dfd 100644 --- a/lua/jet/core/kernel/buf.lua +++ b/lua/jet/core/kernel/buf.lua @@ -1,6 +1,6 @@ ---@class jet.Buf ---@field buf integer ----@field layout_pos integer +---@field win_name string ---@field name string ---@field augroup integer ---@field ns integer @@ -10,9 +10,9 @@ Buf.__index = Buf ---@private ---@class jet.Buf.init.Opts ---@field name string +---@field win_name string | keyof jet.Kernel.Windows ---@field ns integer ---@field kernel jet.Kernel ----@field layout_pos integer ---@generic T ---@param class? T @@ -23,26 +23,30 @@ function Buf.init(class, opts) kernel = opts.kernel, ns = opts.ns, name = opts.name, - layout_pos = opts.layout_pos, + win_name = opts.win_name, augroup = vim.api.nvim_create_augroup(opts.name, { clear = true }), buf = vim.api.nvim_create_buf(false, true), }, class or Buf) - vim.b[out.buf].jet = { - session_id = out.kernel.session_id, - layout_pos = out.layout_pos, - } + vim.b[out.buf].jet = { session_id = out.kernel.session_id } vim.api.nvim_buf_set_name(out.buf, out.name) return out end ----Get a window displaying the buffer, if there is one +---Get the kernel window associated with the buffer ---@return jet.Win function Buf:win() - local w = self.kernel.windows[self.layout_pos] - assert(w, "Kernel window %d not found. Kernel has %d registered windows", self.layout_pos, #self.kernel.windows) - return w + local out = self.kernel.wins[self.win_name] + assert( + out, + string.format( + "Kernel window '%s' not found. Kernel has windows %s", + self.win_name, + vim.inspect(vim.tbl_keys(self.kernel.wins)) + ) + ) + return out end ---@param opts vim.api.keyset.win_config? diff --git a/lua/jet/core/kernel/img.lua b/lua/jet/core/kernel/img.lua index 6544918..7d3f81f 100644 --- a/lua/jet/core/kernel/img.lua +++ b/lua/jet/core/kernel/img.lua @@ -20,7 +20,7 @@ function Img.init(opts) name = opts.kernel:friendly_name() .. " - Images", ns = opts.ns, kernel = opts.kernel, - layout_pos = 2, + win_name = "secondary", }) out.kernel = opts.kernel diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index 93f8b4d..10c38d1 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -11,6 +11,19 @@ local STARTING_KERNEL_SENTINEL = "" ---@alias jet.Kernel.PartialSpec { display_name: string, language: string } ---@alias jet.Kernel.execution_state "busy" | "idle" | "starting" +---@class jet.Kernel.Buffers +---@field term? jet.Kernel.Term +---@field img? jet.Kernel.Img + +---Windows 'owned' by the kernel +--- +---Note, you can extend this to add additional windows, or modify the default +---windows directly. +--- +---@class jet.Kernel.Windows +---@field primary jet.Win +---@field secondary jet.Win + ---The `Kernel` class is jet.nvim's central abstraction for working with ---Jupyter kernels. You can create a new instance using two methods: --- @@ -44,10 +57,6 @@ local STARTING_KERNEL_SENTINEL = "" ---@field client_id? string ---Information about the kernel's Jet LSP process. ---@field lsp jet.Lsp ----Repl buffer data ----@field term? jet.Kernel.Term ----Image buffer data ----@field img? jet.Kernel.Img ---`true` if the kernel session was started by this nvim session; `false` ---otherwise. ---@field owned boolean @@ -80,7 +89,10 @@ local STARTING_KERNEL_SENTINEL = "" ---@field priority integer ---Windows used to show kernel UI. Kernels use 2 windows by default (one for ---the repl, one for all other stuff). This field is extensible! ----@field windows jet.Win[] +---@field wins jet.Kernel.Windows +---Buffers used to show kernel UI. By default includes the repl and a plots +---buffer. +---@field bufs jet.Kernel.Buffers ---@field subclass string? ---@field private stream jet.callback ---@field private ui_expand boolean @@ -94,30 +106,33 @@ vim.api.nvim_set_hl(jet_hl_ns, "Normal", { link = "JetRepl" }) ---@param k jet.Kernel local init_wins = function(k) - local repl_win = win.init({ + k.wins.primary = win.init({ ns = jet_hl_ns, kernel = k, focus = true, - open_opts = function() - ---@type vim.api.keyset.win_config - return { split = "right", win = -1 } + ---@param kernel jet.Kernel + open_opts = function(kernel) + local secondary_win = kernel.wins.secondary:winnr() + return { + split = secondary_win and "below" or "right", + win = secondary_win or -1, + } end, }) - local secondary_win = win.init({ + k.wins.secondary = win.init({ ns = jet_hl_ns, kernel = k, focus = true, - open_opts = function(wins) - vim.print(wins) + ---@param kernel jet.Kernel + open_opts = function(kernel) + local primary_win = kernel.wins.primary:winnr() return { - split = wins[1] and "above" or "right", - win = wins[1] or -1, + split = primary_win and "above" or "right", + win = primary_win or -1, } end, }) - - return { repl_win, secondary_win } end ---@return Partial @@ -133,7 +148,7 @@ local init_defaults = function() on_started = {}, priority = 100, hooks = hooks.init_hooks(), - windows = {}, + buf = {}, } end @@ -154,7 +169,7 @@ function Kernel.init_owned(opts) local base = vim.tbl_extend("keep", opts, init_defaults(), { owned = true }) local out = setmetatable(base, Kernel) - out.windows = init_wins(out) + out.wins = init_wins(out) out:try_resolve_filetype() out:do_kernel_init() @@ -185,7 +200,7 @@ function Kernel.init_external(opts) Kernel ) - out.windows = init_wins(out) + out.wins = init_wins(out) manager:insert(out) Kernel.try_resolve_filetype(out) @@ -210,8 +225,8 @@ Kernel.do_currentness_changed = hooks.do_currentness_changed ---@private ---Toggle the terminal window for the kernel. ---If no terminal is active, one will be created and opened. function Kernel:term_toggle() - if self.term then - self.term:toggle() + if self.bufs.term then + self.bufs.term:toggle() else self:term_open() end @@ -223,10 +238,10 @@ end ---@param focus? boolean function Kernel:term_open(callback, focus) self:term_create(function() - assert(self.term, "kernel.term is nil") - self.term:open(nil, focus) + assert(self.bufs.term, "kernel.buf.term is nil") + self.bufs.term:open(nil, focus) if callback then - callback(self.term) + callback(self.bufs.term) end end) end @@ -241,12 +256,12 @@ function Kernel:set_current() manager:set_current(self) end ---@param callback? fun(k: jet.Kernel) function Kernel:term_create(callback) self:start_lua_client(function() - if not self.term then + if not self.bufs.term then assert(self.session_id, "Kernel has no session id") - self.term = require("jet.core.kernel.term").init({ kernel = self, ns = jet_hl_ns }) - self.term:create_autocmd("TermEnter", function() self:set_current() end) + self.bufs.term = require("jet.core.kernel.term").init({ kernel = self, ns = jet_hl_ns }) + self.bufs.term:create_autocmd("TermEnter", function() self:set_current() end) if cfg.stop_on_buf_wipeout then - self.term:create_autocmd("BufWipeout", function() self:close("BufWipeout") end) + self.bufs.term:create_autocmd("BufWipeout", function() self:close("BufWipeout") end) end end if callback then @@ -423,8 +438,8 @@ function Kernel:update_execution_state(msg) self.last_execution.end_time = os.time() end - if self.term and self.term.buf and vim.api.nvim_buf_is_valid(self.term.buf) then - vim.api.nvim__redraw({ statusline = true, buf = self.term.buf }) + if self.bufs.term and self.bufs.term.buf and vim.api.nvim_buf_is_valid(self.bufs.term.buf) then + vim.api.nvim__redraw({ statusline = true, buf = self.bufs.term.buf }) end self:do_execution_state_changed(new_state) @@ -440,8 +455,8 @@ end ---Toggle the image display function Kernel:img_toggle() - if self.img then - self.img:toggle() + if self.bufs.img then + self.bufs.img:toggle() else self:img_open() end @@ -451,11 +466,11 @@ end ---@param which? string | integer ---@return integer # Win number function Kernel:img_open(which) - if not self.img then + if not self.bufs.img then assert(self.session_id, "Kernel has no session id") - self.img = require("jet.core.kernel.img").init({ kernel = self, ns = jet_hl_ns }) + self.bufs.img = require("jet.core.kernel.img").init({ kernel = self, ns = jet_hl_ns }) end - return self.img:open(false, which) + return self.bufs.img:open(false, which) end ---Get a unique, human-readable name for the kernel @@ -788,8 +803,8 @@ function Kernel:close(reason) if self.augroup then pcall(vim.api.nvim_del_augroup_by_id, self.augroup) end - if self.term then - self.term:delete() + if self.bufs.term then + self.bufs.term:delete() end if self.owned then diff --git a/lua/jet/core/kernel/term.lua b/lua/jet/core/kernel/term.lua index c58310c..6348d5f 100644 --- a/lua/jet/core/kernel/term.lua +++ b/lua/jet/core/kernel/term.lua @@ -20,7 +20,7 @@ function Term.init(opts) name = opts.kernel:friendly_name(), ns = opts.ns, kernel = opts.kernel, - layout_pos = 1, + win_name = "Primary", }) out.kernel = opts.kernel diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua index 444eae9..82a8f3a 100644 --- a/lua/jet/core/kernel/win.lua +++ b/lua/jet/core/kernel/win.lua @@ -5,18 +5,18 @@ ---it's a no-op. ---@field focus boolean ---@field kernel jet.Kernel ----@field open_opts? vim.api.keyset.win_config | fun(wins: integer[]): vim.api.keyset.win_config +---@field open_opts? vim.api.keyset.win_config | fun(k: jet.Kernel): vim.api.keyset.win_config local Win = {} Win.__index = Win +---@param buf integer | jet.Buf +local to_bufnr = function(buf) return type(buf) == "number" and buf or buf.buf end + ---@class jet.Win.init.Opts ---@field open_opts jet.Win["open_opts"] ---@field ns jet.Win["ns"] ----@field focus jet.Win["focus"] ---@field kernel jet.Win["kernel"] - ----@param buf integer | jet.Buf -local to_bufnr = function(buf) return type(buf) == "number" and buf or buf.buf end +---@field focus jet.Win["focus"] ---@param opts jet.Win.init.Opts Win.init = function(opts) @@ -26,11 +26,11 @@ Win.init = function(opts) return setmetatable(vim.tbl_extend("force", opts, { win = -99 }), Win) end ----@return integer -function Win:layout_pos() - for i, w in ipairs(self.kernel.windows) do +---@return string +function Win:name() + for name, w in pairs(self.kernel.wins) do if w == self then - return i + return name end end error("Could not determine the window layout position") @@ -84,8 +84,7 @@ end ---@return vim.api.keyset.win_config function Win:make_open_opts() if type(self.open_opts) == "function" then - local wins = vim.tbl_map(function(win) return win:winnr() end, self.kernel.windows) - local out = self.open_opts(wins) + local out = self.open_opts(self.kernel) out.style = "minimal" return out elseif type(self.open_opts) == "table" then @@ -124,8 +123,11 @@ function Win:get_curr_jet_buf(buf) if buf and to_bufnr(buf) ~= curr_buf then return nil end - local buf_pos = curr_buf and vim.b[curr_buf].jet and vim.b[curr_buf].jet.layout_pos - return buf_pos == self:layout_pos() and curr_buf or nil + for _, kernel_buf in pairs(self.kernel.bufs) do + if kernel_buf.buf == curr_buf and kernel_buf.win_name == self.name then + return curr_buf + end + end end ---@param buf? integer | jet.Buf diff --git a/lua/jet/core/ui/init.lua b/lua/jet/core/ui/init.lua index 53151a3..34857fc 100644 --- a/lua/jet/core/ui/init.lua +++ b/lua/jet/core/ui/init.lua @@ -477,7 +477,7 @@ M.show = function() if l and l.data and l.data.kernel then ---@type jet.Kernel local k = l.data.kernel - local should_focus = k.term and k.term:layout_pos() + local should_focus = k.bufs.term and k.bufs.term:win():winnr() k:term_open(function() if should_focus then ui:close() diff --git a/lua/lualine/extensions/jet.lua b/lua/lualine/extensions/jet.lua index 2c62979..1c2712d 100644 --- a/lua/lualine/extensions/jet.lua +++ b/lua/lualine/extensions/jet.lua @@ -41,11 +41,11 @@ end ---@param k jet.Kernel local jet_img = function(k) - if not k.img then + if not k.bufs.img then return "" end - local files, curr_file_index = k.img:list_files() + local files, curr_file_index = k.bufs.img:list_files() if not curr_file_index then return "" From 310d05774e28874f3635d0417f2c1339c0e5b4a2 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 17 Sep 2026 14:18:12 +0100 Subject: [PATCH 08/24] docs: make docs --- doc/jet-nvim.txt | 29 +++++++++++-- doc/jet-types.txt | 102 ++++++++++++++++++++++++++++++++++++++++------ docs/jet-nvim.md | 18 ++++++++ docs/jet-types.md | 6 +++ 4 files changed, 139 insertions(+), 16 deletions(-) diff --git a/doc/jet-nvim.txt b/doc/jet-nvim.txt index 6f93410..6231700 100644 --- a/doc/jet-nvim.txt +++ b/doc/jet-nvim.txt @@ -577,8 +577,6 @@ THE KERNEL CLASS *jet-the-kernel-class* connection to the kernel. • {lsp} (`jet.Lsp`) Information about the kernel's Jet LSP process. - • {term} (`jet.Kernel.Term?`) Repl buffer data - • {img} (`jet.Kernel.Img?`) Image buffer data • {owned} (`boolean`) `true` if the kernel session was started by this nvim session; `false` otherwise. • {filetype} (`string?`) The kernel's filetype. jet.nvim will @@ -611,8 +609,14 @@ THE KERNEL CLASS *jet-the-kernel-class* will try to use this field to select a single kernel. Typically you would set the priority using the kernel's init hook (see |jet.Hooks|). - • {subclass} (`string?`) For use by extensions which may want - to extend the `Kernel` class + • {wins} (`jet.Kernel.Windows`) Windows used to show + kernel UI. Kernels use 2 windows by default (one + for the repl, one for all other stuff). This + field is extensible! + • {bufs} (`jet.Kernel.Buffers`) Buffers used to show + kernel UI. By default includes the repl and a + plots buffer. + • {subclass} (`string?`) Kernel.init_owned({opts}) *jet-Kernel.init_owned()* @@ -928,4 +932,21 @@ tree-sitter node: >lua jet.filetype.python = { get_expr = get_python_expr } < +CUSTOM UI COMPONENTS *jet-custom-ui-components* + +Extensions may want to add custom UI for a particular kernel. For example, +jet.ark adds a custom variables pane. jet.nvim has a mechanism for this: + +• Each kernel owns some 'windows', any of which may or may not be displayed at + a given time. The default number of windows owned by a kernel is 2, but you + can add more. + + • By default the first window opens as a right split + • By default the second window opens as a split above the first window if + the first window exists; otherwise it opens as a right split. +• Each kernel also owns some buffers, each of which is associated with a. + Multiple buffers may be associated with the same window. If you open a + kernel buffer, the buffer will replace any other buffer the associated + window. + vim:tw=78:sw=4:ts=4:ft=help:norl:et: diff --git a/doc/jet-types.txt b/doc/jet-types.txt index 01b7664..5803f81 100644 --- a/doc/jet-types.txt +++ b/doc/jet-types.txt @@ -212,15 +212,18 @@ Img:display({which}) *jet-types-Img:display()* Parameters: ~ • {which} (`(string|integer)?`) +------------------------------------------------------------------------------ +Buffer/window abstractions *jet-types-bufferwindow-abstractions* + *jet.Buf* Fields: ~ - • {buf} (`integer`) - • {name} (`string`) - • {augroup} (`integer`) - • {ns} (`integer`) - • {kernel} (`jet.Kernel`) - • {open_opts} (`(vim.api.keyset.win_config|fun() -> vim.api.keyset.win_config)`) + • {buf} (`integer`) + • {win_name} (`string`) + • {name} (`string`) + • {augroup} (`integer`) + • {ns} (`integer`) + • {kernel} (`jet.Kernel`) Buf.init({class}, {opts}) *jet-types-Buf.init()* @@ -228,26 +231,25 @@ Buf.init({class}, {opts}) *jet-types-Buf.init()* • {class} (`T?`) • {opts} (`jet.Buf.init.Opts`) • name (`string`) + • win_name (`(string|keyof jet.Kernel.Windows)`) • ns (`integer`) • kernel (`jet.Kernel`) - • open_opts - (`(vim.api.keyset.win_config|fun() -> vim.api.keyset.win_config)`) Return: ~ (`T`) Buf:win() *jet-types-Buf:win()* - Get a window displaying the buffer, if there is one + Get the kernel window associated with the buffer Return: ~ - (`integer?`) + (`jet.Win`) -Buf:open({focus}, {opts}) *jet-types-Buf:open()* +Buf:open({opts}, {focus}) *jet-types-Buf:open()* Parameters: ~ - • {focus} (`boolean?`) • {opts} (`vim.api.keyset.win_config?`) + • {focus} (`boolean?`) Return: ~ (`integer`) The opened window @@ -262,4 +264,80 @@ Buf:create_autocmd({event}, {callback}) *jet-types-Buf:create_autocmd()* • {event} (`(vim.api.keyset.events|vim.api.keyset.events[])`) • {callback} (`(string|fun(args: vim.api.keyset.create_autocmd.callback_args) -> boolean?)`) +*jet.Win* + + Fields: ~ + • {ns} (`integer`) + • {focus} (`boolean`) If `true` then `Win:open()` will focus an + already open window, otherwise it's a no-op. + • {kernel} (`jet.Kernel`) + • {open_opts} (`(vim.api.keyset.win_config|fun(k: jet.Kernel) -> vim.api.keyset.win_config)?`) + • {__index} (`jet.Win`) + +Win.init({opts}) *jet-types-Win.init()* + + Parameters: ~ + • {opts} (`jet.Win.init.Opts`) + • open_opts (`jet.Win["open_opts"]`) + • ns (`jet.Win["ns"]`) + • kernel (`jet.Win["kernel"]`) + • focus (`jet.Win["focus"]`) + + Return: ~ + (`jet.Win`) + +Win:name() *jet-types-Win:name()* + + Return: ~ + (`string`) + +Win:open({buf}, {opts}, {focus}) *jet-types-Win:open()* + + Opens with the following algorithm + + • If the window is not open, opens with `buf` + • If the window is open with `buf`, focus the window (if `Win.focus`) + • Otherwise, if the window is open with another buffer: + • Does the buffer have `vim.b.jet` set? If so, show `buf` in the window + • Otherwise, open a fresh window, resetting `self.win`. + + Parameters: ~ + • {buf} (`(integer|jet.Buf)`) + • {opts} (`vim.api.keyset.win_config?`) + • {focus} (`boolean?`) + + Return: ~ + (`integer`) + +Win:close({buf}) *jet-types-Win:close()* + + Parameters: ~ + • {buf} (`(jet.Buf|integer)?`) + +Win:toggle({buf}) *jet-types-Win:toggle()* + + Parameters: ~ + • {buf} (`(integer|jet.Buf)`) + +Win:get_curr_jet_buf({buf}) *jet-types-Win:get_curr_jet_buf()* + + Parameters: ~ + • {buf} (`(integer|jet.Buf)?`) + + Return: ~ + (`integer?`) + +Win:winnr({buf}) *jet-types-Win:winnr()* + + Parameters: ~ + • {buf} (`(integer|jet.Buf)?`) + + Return: ~ + (`integer?`) + +Win:get_buf() *jet-types-Win:get_buf()* + + Return: ~ + (`integer?`) + vim:tw=78:sw=4:ts=4:ft=help:norl:et: diff --git a/docs/jet-nvim.md b/docs/jet-nvim.md index e642a06..bd00070 100644 --- a/docs/jet-nvim.md +++ b/docs/jet-nvim.md @@ -392,3 +392,21 @@ end local jet = require("jet") jet.filetype.python = { get_expr = get_python_expr } ``` + +### Custom UI components + +Extensions may want to add custom UI for a particular kernel. For example, +jet.ark adds a custom variables pane. jet.nvim has a mechanism for this: + +* Each kernel owns some 'windows', any of which may or may not be displayed at + a given time. The default number of windows owned by a kernel is 2, but you + can add more. + + * By default the first window opens as a right split + * By default the second window opens as a split above the first window if the + first window exists; otherwise it opens as a right split. + +* Each kernel also owns some buffers, each of which is associated with a. + Multiple buffers may be associated with the same window. If you open a kernel + buffer, the buffer will replace any other buffer the associated window. + diff --git a/docs/jet-types.md b/docs/jet-types.md index 7c2627d..23746c1 100644 --- a/docs/jet-types.md +++ b/docs/jet-types.md @@ -26,6 +26,12 @@ python3 scripts/emmylua-to-md.py --type jet.Kernel.Term python3 scripts/emmylua-to-md.py --type jet.Kernel.Img ``` +## Buffer/window abstractions + ```{.sh include=true} python3 scripts/emmylua-to-md.py --type jet.Buf ``` + +```{.sh include=true} +python3 scripts/emmylua-to-md.py --type jet.Win +``` From 22a8cedab185b26a74362a9843ece85c443ea583 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 17 Sep 2026 14:51:46 +0100 Subject: [PATCH 09/24] fix: broken image open --- lua/jet/core/kernel/init.lua | 71 +++++++++++++++++++----------------- lua/jet/core/kernel/term.lua | 2 +- lua/jet/core/kernel/win.lua | 6 +-- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index 10c38d1..b51b3ad 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -104,37 +104,6 @@ Kernel.__index = Kernel ---@private local jet_hl_ns = vim.api.nvim_create_namespace("jet_highlights") vim.api.nvim_set_hl(jet_hl_ns, "Normal", { link = "JetRepl" }) ----@param k jet.Kernel -local init_wins = function(k) - k.wins.primary = win.init({ - ns = jet_hl_ns, - kernel = k, - focus = true, - ---@param kernel jet.Kernel - open_opts = function(kernel) - local secondary_win = kernel.wins.secondary:winnr() - return { - split = secondary_win and "below" or "right", - win = secondary_win or -1, - } - end, - }) - - k.wins.secondary = win.init({ - ns = jet_hl_ns, - kernel = k, - focus = true, - ---@param kernel jet.Kernel - open_opts = function(kernel) - local primary_win = kernel.wins.primary:winnr() - return { - split = primary_win and "above" or "right", - win = primary_win or -1, - } - end, - }) -end - ---@return Partial local init_defaults = function() return { @@ -148,7 +117,7 @@ local init_defaults = function() on_started = {}, priority = 100, hooks = hooks.init_hooks(), - buf = {}, + bufs = {}, } end @@ -169,7 +138,7 @@ function Kernel.init_owned(opts) local base = vim.tbl_extend("keep", opts, init_defaults(), { owned = true }) local out = setmetatable(base, Kernel) - out.wins = init_wins(out) + out:initialise_wins() out:try_resolve_filetype() out:do_kernel_init() @@ -200,7 +169,7 @@ function Kernel.init_external(opts) Kernel ) - out.wins = init_wins(out) + out:initialise_wins() manager:insert(out) Kernel.try_resolve_filetype(out) @@ -210,6 +179,40 @@ function Kernel.init_external(opts) return out end +function Kernel:initialise_wins() + self.wins = { + primary = win.init({ + ns = jet_hl_ns, + kernel = self, + focus = true, + ---@param kernel jet.Kernel + open_opts = function(kernel) + local secondary_win = kernel.wins.secondary:winnr() + return { + split = secondary_win and "below" or "right", + win = secondary_win or -1, + } + end, + }), + + secondary = win.init({ + ns = jet_hl_ns, + kernel = self, + focus = true, + ---@param kernel jet.Kernel + open_opts = function(kernel) + print("---------------------------") + local primary_win = kernel.wins.primary:winnr() + return { + split = primary_win and "above" or "right", + win = primary_win or -1, + } + end, + }), + } +end + + -- stylua: ignore start Kernel.do_execution_state_changed = hooks.do_execution_state_changed ---@private Kernel.do_kernel_close = hooks.do_kernel_close ---@private diff --git a/lua/jet/core/kernel/term.lua b/lua/jet/core/kernel/term.lua index 6348d5f..8cd2de7 100644 --- a/lua/jet/core/kernel/term.lua +++ b/lua/jet/core/kernel/term.lua @@ -20,7 +20,7 @@ function Term.init(opts) name = opts.kernel:friendly_name(), ns = opts.ns, kernel = opts.kernel, - win_name = "Primary", + win_name = "primary", }) out.kernel = opts.kernel diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua index 82a8f3a..72ec6e8 100644 --- a/lua/jet/core/kernel/win.lua +++ b/lua/jet/core/kernel/win.lua @@ -33,7 +33,7 @@ function Win:name() return name end end - error("Could not determine the window layout position") + error("Could not determine the window name") end ---Opens with the following algorithm @@ -123,8 +123,8 @@ function Win:get_curr_jet_buf(buf) if buf and to_bufnr(buf) ~= curr_buf then return nil end - for _, kernel_buf in pairs(self.kernel.bufs) do - if kernel_buf.buf == curr_buf and kernel_buf.win_name == self.name then + for _, k_buf in pairs(self.kernel.bufs) do + if k_buf.buf == curr_buf and k_buf.win_name == self:name() then return curr_buf end end From b80fa45beaa944942b1c198380df7c411ee29e12 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 17 Sep 2026 15:18:59 +0100 Subject: [PATCH 10/24] fix(send): terminal auto-focus --- lua/jet/core/kernel/init.lua | 12 ++++++++---- lua/jet/core/kernel/win.lua | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index b51b3ad..c0c9e53 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -11,7 +11,11 @@ local STARTING_KERNEL_SENTINEL = "" ---@alias jet.Kernel.PartialSpec { display_name: string, language: string } ---@alias jet.Kernel.execution_state "busy" | "idle" | "starting" ----@class jet.Kernel.Buffers +---Buffers used by the kernel +--- +---Can be extended to implement custom UI. +--- +---@class (partial) jet.Kernel.Buffers ---@field term? jet.Kernel.Term ---@field img? jet.Kernel.Img @@ -20,7 +24,7 @@ local STARTING_KERNEL_SENTINEL = "" ---Note, you can extend this to add additional windows, or modify the default ---windows directly. --- ----@class jet.Kernel.Windows +---@class (partial) jet.Kernel.Windows ---@field primary jet.Win ---@field secondary jet.Win @@ -179,6 +183,7 @@ function Kernel.init_external(opts) return out end +---@private function Kernel:initialise_wins() self.wins = { primary = win.init({ @@ -201,7 +206,6 @@ function Kernel:initialise_wins() focus = true, ---@param kernel jet.Kernel open_opts = function(kernel) - print("---------------------------") local primary_win = kernel.wins.primary:winnr() return { split = primary_win and "above" or "right", @@ -945,7 +949,7 @@ end ---@param code string | string[] Code to be sent ---@param tabstop? integer Optional; number of spaces to use for tab characters function Kernel:send_repl(code, tabstop) - self:term_open(function(t) t:send(code, tabstop) end) + self:term_open(function(t) t:send(code, tabstop) end, false) end ---Send code to the kernel via the Lua client diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua index 72ec6e8..17fae8f 100644 --- a/lua/jet/core/kernel/win.lua +++ b/lua/jet/core/kernel/win.lua @@ -49,10 +49,14 @@ end ---@param focus? boolean ---@return integer function Win:open(buf, opts, focus) + if focus == nil then + focus = self.focus + end + local curr_jet_buf = self:get_curr_jet_buf() if curr_jet_buf == buf then - if focus or self.focus then + if focus then vim.api.nvim_set_current_win(self.win) if vim.bo[buf].buftype == "terminal" then vim.cmd.startinsert() From a6031e2bfb63999c01e20cafcd0b617b77d00c7b Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 17 Sep 2026 15:28:42 +0100 Subject: [PATCH 11/24] refactor: misc tidying --- lua/jet/core/kernel/init.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index c0c9e53..8b715ac 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -1,9 +1,9 @@ local manager = require("jet.core.manager") -local cfg = require("jet.core.config").options local utils = require("jet.core.utils") -local lsp = require("jet.core.kernel.lsp") local hooks = require("jet.core.hooks") local win = require("jet.core.kernel.win") +local lsp = require("jet.core.kernel.lsp") +local cfg = require("jet.core.config").options local STARTING_KERNEL_SENTINEL = "" @@ -226,7 +226,7 @@ Kernel.do_message_received = hooks.do_message_received ---@private Kernel.do_send_pre = hooks.do_send_pre ---@private Kernel.do_status_changed = hooks.do_status_changed ---@private Kernel.do_image_display_pre = hooks.do_image_display_pre ---@private -Kernel.do_currentness_changed = hooks.do_currentness_changed ---@private +Kernel.do_currentness_changed = hooks.do_currentness_changed ---@private -- stylua: ignore end ---Toggle the terminal window for the kernel. From 26cea92b52a479a2f097cf1aa319eca1cd018f80 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 17 Sep 2026 15:49:00 +0100 Subject: [PATCH 12/24] fix(emmylua): missing type annotation --- lua/jet/core/kernel/term.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/jet/core/kernel/term.lua b/lua/jet/core/kernel/term.lua index 8cd2de7..c46109d 100644 --- a/lua/jet/core/kernel/term.lua +++ b/lua/jet/core/kernel/term.lua @@ -61,7 +61,7 @@ end function Term:send(code, tabstop) tabstop = tabstop or vim.bo.tabstop or 4 if type(code) == "string" then - code = vim.split(code, "[\n\r]", { plain = false }) + code = vim.split(code, "[\n\r]", { plain = false }) --[[@as string[] ]] end -- Remove trailing empty lines From 4d00ba0cc84e98c839d2b215b2d3681e56dd6d41 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 11:44:58 +0100 Subject: [PATCH 13/24] fix(emmylua): type issue --- lua/jet/core/kernel/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index 8b715ac..c071b2c 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -965,7 +965,7 @@ end function Kernel:send_lua(code, silent, callback) assert(self.client_id, "Kernel has no client id") if type(code) == "string" then - code = vim.split(code, "[\n\r]") + code = vim.split(code, "[\n\r]") --[[@as string[] ]] end self:do_send_pre(code) From 882600eb6e7bab6b6376c2b8ad45e49cbaaae896 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 11:45:11 +0100 Subject: [PATCH 14/24] chore(ci): don't use emmylua_check --format github It's cool, but often makes it hard to find the problematic code --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9d34407..be6e660 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -110,7 +110,7 @@ jobs: - name: Run emmylua_check run: | export VIMRUNTIME=$(nvim --clean --headless +'lua io.write(vim.env.VIMRUNTIME)' +qa) - emmylua_check . --warnings-as-errors --output-format github + emmylua_check . --warnings-as-errors # Ark needs a real R at runtime. Install it out-of-band; the script only # downloads the ark binary and writes the kernelspec. From 7272f04f2482389cdc5145bf213ff50fd760e1f6 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 12:02:38 +0100 Subject: [PATCH 15/24] fix(emmylua): point to $VIMRUNTIME/lua --- .emmyrc.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.emmyrc.json b/.emmyrc.json index 98236b3..61f28f7 100644 --- a/.emmyrc.json +++ b/.emmyrc.json @@ -16,20 +16,20 @@ "workspace": { "ignoreDir": [ "deps", - ".venv" + ".venv" ], "ignoreGlobs": [ "*.log" ], "library": [ - "$VIMRUNTIME", + "$VIMRUNTIME/lua", "./deps", "$HOME/.local/share/nvim/lazy/image.nvim/", "$HOME/.local/share/nvim/lazy/snacks.nvim/", "$HOME/.local/share/nvim/lazy/lualine.nvim/" ], - "workspaceRoots": [ - "./lua" - ] + "workspaceRoots": [ + "./lua" + ] } } From 309b9a8af6711b67097356139752f7fc5251a541 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 12:03:50 +0100 Subject: [PATCH 16/24] chore(emmylua): remove unneeded annotation --- lua/jet/core/kernel/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index c071b2c..8b715ac 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -965,7 +965,7 @@ end function Kernel:send_lua(code, silent, callback) assert(self.client_id, "Kernel has no client id") if type(code) == "string" then - code = vim.split(code, "[\n\r]") --[[@as string[] ]] + code = vim.split(code, "[\n\r]") end self:do_send_pre(code) From 42659fecbd8632cbf390d75da66754995b9ac5a5 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 12:14:01 +0100 Subject: [PATCH 17/24] fix(emmylua): missing annotation (nvim nightly) --- lua/jet/core/ui/line.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/jet/core/ui/line.lua b/lua/jet/core/ui/line.lua index 8c3e7a9..c84057d 100644 --- a/lua/jet/core/ui/line.lua +++ b/lua/jet/core/ui/line.lua @@ -52,7 +52,7 @@ function Line:resolve() ---@return vim.api.keyset.set_extmark[] local to_extmarks = function(x) x = type(x) == "table" and vim.isarray(x) and x or { x } ---@type string[] | vim.api.keyset.set_extmark[] - return vim.tbl_map(function(xi) return type(xi) == "string" and { hl_group = xi } or xi end, x) + return vim.tbl_map(function(xi) return type(xi) == "string" and { hl_group = xi } or xi end, x) --[[@as vim.api.keyset.set_extmark[] ]] end for _, part in ipairs(self.parts) do From ed4a29b9da8e3c2b82d621e27f1b79b63d39f2d6 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 12:33:19 +0100 Subject: [PATCH 18/24] fix(test): give test_img a bit more time --- tests/test_image.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_image.lua b/tests/test_image.lua index 5751314..0677cc2 100644 --- a/tests/test_image.lua +++ b/tests/test_image.lua @@ -11,11 +11,10 @@ local T = new_set({ require("jet.core.config").options.hooks.on_image_display_pre.test = function(k, src) _G.rendered_image = src - _G.image_buf = k.img.buf + _G.image_buf = k.bufs.img.buf end _G.k:start_lua_client(function(k) - _G.bla = 123 k:send_lua("hist(iris$Sepal.Width)") end) ]]) @@ -25,7 +24,7 @@ local T = new_set({ }) T["Image files are produced"] = function() - vim.wait(5000, function() return child.lua_get("_G.rendered_image") ~= vim.NIL end) + vim.wait(10000, function() return child.lua_get("_G.rendered_image") ~= vim.NIL end) local file = child.lua_get("_G.rendered_image") file = file ~= vim.NIL and file or nil assert(file, "No image was produced by the kernel") From 790dbc9968f9dce63943dbc2332a5d8a6a7814a6 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 12:34:00 +0100 Subject: [PATCH 19/24] fix(tests): use kernel.buf.term, not kernel.term --- tests/test_send_repl.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_send_repl.lua b/tests/test_send_repl.lua index 719e4e2..14f8d73 100644 --- a/tests/test_send_repl.lua +++ b/tests/test_send_repl.lua @@ -22,8 +22,8 @@ local run_send = function() ]]) local TERM_TEXT = [[ - _G.kernel and _G.kernel.term - and table.concat(vim.api.nvim_buf_get_lines(_G.kernel.term.buf, 0, -1, false), "\n") + _G.kernel and _G.kernel.bufs.term + and table.concat(vim.api.nvim_buf_get_lines(_G.kernel.bufs.term.buf, 0, -1, false), "\n") or "" ]] From 6cadcd721f8cdac2400f146190e61c83fcc8920b Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 13:11:03 +0100 Subject: [PATCH 20/24] docs: document new API --- doc/jet-nvim.txt | 14 ++++++++++---- doc/jet-recipes.txt | 2 +- doc/jet-types.txt | 10 +++++----- docs/jet-nvim.md | 2 +- docs/jet-recipes.md | 2 +- scripts/emmylua-to-md.py | 30 ++++++++++++++++++++---------- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/doc/jet-nvim.txt b/doc/jet-nvim.txt index 6231700..168f9b7 100644 --- a/doc/jet-nvim.txt +++ b/doc/jet-nvim.txt @@ -438,7 +438,7 @@ THE API MODULE *jet-jet.api* api.next_expr_boundary({opts}, {p}) *jet-api.next_expr_boundary()* Parameters: ~ - • {opts} (`jet.send.next_expr_boundary.Opts?`) + • {opts} (`jet.send.next_expr_boundary.Opts?`) Fields: • direction (`(1|-1)?`) • boundary (`("start"|"end"|"any")?`) • current_ok (`boolean?`) @@ -613,9 +613,15 @@ THE KERNEL CLASS *jet-the-kernel-class* kernel UI. Kernels use 2 windows by default (one for the repl, one for all other stuff). This field is extensible! + Fields: + • primary (`jet.Win`) + • secondary (`jet.Win`) • {bufs} (`jet.Kernel.Buffers`) Buffers used to show kernel UI. By default includes the repl and a plots buffer. + Fields: + • term (`jet.Kernel.Term?`) + • img (`jet.Kernel.Img?`) • {subclass} (`string?`) Kernel.init_owned({opts}) *jet-Kernel.init_owned()* @@ -624,7 +630,7 @@ Kernel.init_owned({opts}) *jet-Kernel.init_owned()* process. Parameters: ~ - • {opts} (`jet.kernel.init_owned.Opts`) + • {opts} (`jet.kernel.init_owned.Opts`) Fields: • spec_path (`string`) • session_name (`string?`) • spec @@ -640,7 +646,7 @@ Kernel.init_external({opts}) *jet-Kernel.init_external()* session Parameters: ~ - • {opts} (`jet.kernel.init_external.Opts`) + • {opts} (`jet.kernel.init_external.Opts`) Fields: • session_id (`string`) • priority (`integer?`) @@ -777,7 +783,7 @@ Kernel:comm_open({name}, {data}, {opts}) *jet-Kernel:comm_open()* Parameters: ~ • {name} (`string`) • {data} (`table?`) - • {opts} (`jet.kernel.comm_open.Opts?`) + • {opts} (`jet.kernel.comm_open.Opts?`) Fields: • listener (`(fun(res: jupyter.Msg))?`) • listener_interval (`integer?`): In milliseconds, default 50ms diff --git a/doc/jet-recipes.txt b/doc/jet-recipes.txt index 8584643..1649aa7 100644 --- a/doc/jet-recipes.txt +++ b/doc/jet-recipes.txt @@ -49,7 +49,7 @@ visible. >lua local code = input and input.code local text = result and result.data and result.data["text/plain"] - local term_is_open = k.term and k.term:win() + local term_is_open = k.buf.term and k.buf.term:win() if code and text and not term_is_open then vim.notify(string.format("Ran `%s`:\nResult: %s", code, text)) diff --git a/doc/jet-types.txt b/doc/jet-types.txt index 5803f81..93c7335 100644 --- a/doc/jet-types.txt +++ b/doc/jet-types.txt @@ -126,7 +126,7 @@ Range:code({opts}) *jet-types-Range:code()* Also returns the local filetype at the range start. Parameters: ~ - • {opts} (`jet.send.range_code.Opts?`) + • {opts} (`jet.send.range_code.Opts?`) Fields: • comments (`boolean?`): Set to `true` to include comments in the returned code. @@ -161,7 +161,7 @@ Repl and image buffers *jet-types-repl-and-image-buffers* Term.init({opts}) *jet-types-Term.init()* Parameters: ~ - • {opts} (`jet.Kernel.Term.init.Opts`) + • {opts} (`jet.Kernel.Term.init.Opts`) Fields: • kernel (`jet.Kernel`) • ns (`integer`) @@ -185,7 +185,7 @@ Term:send({code}, {tabstop}) *jet-types-Term:send()* Img.init({opts}) *jet-types-Img.init()* Parameters: ~ - • {opts} (`jet.Kernel.Img.init.Opts`) + • {opts} (`jet.Kernel.Img.init.Opts`) Fields: • kernel (`jet.Kernel`) • ns (`integer`) @@ -229,7 +229,7 @@ Buf.init({class}, {opts}) *jet-types-Buf.init()* Parameters: ~ • {class} (`T?`) - • {opts} (`jet.Buf.init.Opts`) + • {opts} (`jet.Buf.init.Opts`) Fields: • name (`string`) • win_name (`(string|keyof jet.Kernel.Windows)`) • ns (`integer`) @@ -277,7 +277,7 @@ Buf:create_autocmd({event}, {callback}) *jet-types-Buf:create_autocmd()* Win.init({opts}) *jet-types-Win.init()* Parameters: ~ - • {opts} (`jet.Win.init.Opts`) + • {opts} (`jet.Win.init.Opts`) Fields: • open_opts (`jet.Win["open_opts"]`) • ns (`jet.Win["ns"]`) • kernel (`jet.Win["kernel"]`) diff --git a/docs/jet-nvim.md b/docs/jet-nvim.md index bd00070..5dd09d4 100644 --- a/docs/jet-nvim.md +++ b/docs/jet-nvim.md @@ -322,7 +322,7 @@ python3 scripts/emmylua-to-md.py --type jet.api.Filters ### The Kernel Class ```{.sh include=true} -python3 scripts/emmylua-to-md.py --type jet.Kernel +python3 scripts/emmylua-to-md.py --type jet.Kernel --inline jet.Kernel.Buffers,jet.Kernel.Windows ``` ### Highlights diff --git a/docs/jet-recipes.md b/docs/jet-recipes.md index 7960c2b..f2787ae 100644 --- a/docs/jet-recipes.md +++ b/docs/jet-recipes.md @@ -48,7 +48,7 @@ hooks.on_message_received.notify = function(k, msg) local code = input and input.code local text = result and result.data and result.data["text/plain"] - local term_is_open = k.term and k.term:win() + local term_is_open = k.buf.term and k.buf.term:win() if code and text and not term_is_open then vim.notify(string.format("Ran `%s`:\nResult: %s", code, text)) diff --git a/scripts/emmylua-to-md.py b/scripts/emmylua-to-md.py index c65d9e7..55ddba5 100644 --- a/scripts/emmylua-to-md.py +++ b/scripts/emmylua-to-md.py @@ -254,7 +254,7 @@ def _render_tags(tags: list[Tag] | None) -> list[str]: return [line for block in tag_blocks for line in block] -def _render_fn(doc: Doc, x: Fn, parent=None): +def _render_fn(doc: Doc, x: Fn, parent=None, always_inline: list[str] | None = None): sep = ":" if x.is_meth else "." name = x.name if parent is None else f"{parent}{sep}{x.name}" params = ", ".join([f"{{{param.name}}}" for param in x.params]) @@ -271,7 +271,7 @@ def _render_fn(doc: Doc, x: Fn, parent=None): f"{{{param.name}}}", f": (`{param.typ or 'any'}`)", *([] if param.desc is None else [param.desc]), - *_render_opts_fields(doc, param.typ), + *_inline_type(doc, param.typ, always_inline), "", ] for param in x.params @@ -303,11 +303,15 @@ def _render_fn(doc: Doc, x: Fn, parent=None): ) -def _render_opts_fields(doc: Doc, which: str | None, indent=2) -> list[str]: +def _inline_type( + doc: Doc, which: str | None, force: list[str] | None = None, indent=2 +) -> list[str]: if which is not None and which.endswith("?"): which = which[0:-1] - if which is None or not which.endswith("Opts") or indent > 8: + force = force or [] + + if which is None or indent > 8 or not (which in force or which.endswith("Opts")): return [] x = _get_type(doc, which) @@ -321,7 +325,7 @@ def render_one(item: Field | Fn) -> list[str]: return [ f"{ind}* {item.name} (`{item.typ}`)" + ("" if item.description is None else f": {item.description}"), - *_render_opts_fields(doc, item.typ, indent + 2), + *_inline_type(doc, item.typ, force, indent + 2), ] elif type(item) is Fn: return [f"{ind}* {item.name} (function): {item.description}"] @@ -329,10 +333,10 @@ def render_one(item: Field | Fn) -> list[str]: raise ValueError("Value is not a Field or Fn") fields = [line for f in x.members for line in render_one(f)] - return ["", *fields] + return [f"{ind}Fields:", *fields] -def _render_class(x: Class | Module, doc: Doc): +def _render_class(x: Class | Module, doc: Doc, always_inline: list[str] | None = None): fields = [ [ f"{{{field.name}}}", @@ -342,7 +346,7 @@ def _render_class(x: Class | Module, doc: Doc): if field.description is None or field.description == "" else [" " + line for line in field.description.split("\n")] + [""] ), - *_render_opts_fields(doc, field.typ), + *_inline_type(doc, field.typ, always_inline), "", ] for field in x.members @@ -387,6 +391,7 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--type", help="Type to render", required=False) parser.add_argument("--mod", help="Module to render", required=False) + parser.add_argument("--inline", help="Elements to expand 'inline'", required=False) args = parser.parse_args() with open("emmylua_doc_cli/doc.json") as f: @@ -394,16 +399,21 @@ def main() -> int: doc = parse(raw) + inline = args.inline + + if args.inline is not None: + inline = inline.split(",") + if args.type: emmylua_type = _get_type(doc, args.type) if emmylua_type is not None: - print("\n".join(_render_class(emmylua_type, doc))) + print("\n".join(_render_class(emmylua_type, doc, inline))) return 0 if args.mod: emmylua_mod = _get_mod(doc, args.mod) if emmylua_mod is not None: - print("\n".join(_render_class(emmylua_mod, doc))) + print("\n".join(_render_class(emmylua_mod, doc, inline))) return 0 print("No emmylua docs found") From e0b548f9b9feefcea51914a872c50716c69925f0 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 17:09:37 +0100 Subject: [PATCH 21/24] refactor: highlight namespaces --- lua/jet/core/kernel/buf.lua | 3 +-- lua/jet/core/kernel/img.lua | 4 +--- lua/jet/core/kernel/init.lua | 9 ++------- lua/jet/core/kernel/term.lua | 2 -- lua/jet/core/kernel/win.lua | 6 +++--- lua/jet/core/ui/colours.lua | 5 ++++- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lua/jet/core/kernel/buf.lua b/lua/jet/core/kernel/buf.lua index 8572dfd..4f5fc3e 100644 --- a/lua/jet/core/kernel/buf.lua +++ b/lua/jet/core/kernel/buf.lua @@ -11,7 +11,6 @@ Buf.__index = Buf ---@private ---@class jet.Buf.init.Opts ---@field name string ---@field win_name string | keyof jet.Kernel.Windows ----@field ns integer ---@field kernel jet.Kernel ---@generic T @@ -21,7 +20,7 @@ Buf.__index = Buf ---@private function Buf.init(class, opts) local out = setmetatable({ kernel = opts.kernel, - ns = opts.ns, + ns = vim.api.nvim_create_namespace("jet_highlights"), name = opts.name, win_name = opts.win_name, augroup = vim.api.nvim_create_augroup(opts.name, { clear = true }), diff --git a/lua/jet/core/kernel/img.lua b/lua/jet/core/kernel/img.lua index 7d3f81f..458101b 100644 --- a/lua/jet/core/kernel/img.lua +++ b/lua/jet/core/kernel/img.lua @@ -9,7 +9,6 @@ Img.__index = Img ---@private ---@class jet.Kernel.Img.init.Opts ---@field kernel jet.Kernel ----@field ns integer ---@param opts jet.Kernel.Img.init.Opts ---@return jet.Kernel.Img @@ -18,13 +17,12 @@ function Img.init(opts) local out = buf.init(Img, { name = opts.kernel:friendly_name() .. " - Images", - ns = opts.ns, kernel = opts.kernel, win_name = "secondary", }) out.kernel = opts.kernel - vim.bo[out.buf].filetype = "jetimg" -- Note: Snacks.image overrides to "image" + vim.bo[out.buf].filetype = "jetimg" out:create_autocmd("BufWinEnter", function() out:display() end) diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index 8b715ac..d5e1c8a 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -105,9 +105,6 @@ local STARTING_KERNEL_SENTINEL = "" local Kernel = {} Kernel.__index = Kernel ---@private -local jet_hl_ns = vim.api.nvim_create_namespace("jet_highlights") -vim.api.nvim_set_hl(jet_hl_ns, "Normal", { link = "JetRepl" }) - ---@return Partial local init_defaults = function() return { @@ -187,7 +184,6 @@ end function Kernel:initialise_wins() self.wins = { primary = win.init({ - ns = jet_hl_ns, kernel = self, focus = true, ---@param kernel jet.Kernel @@ -201,7 +197,6 @@ function Kernel:initialise_wins() }), secondary = win.init({ - ns = jet_hl_ns, kernel = self, focus = true, ---@param kernel jet.Kernel @@ -265,7 +260,7 @@ function Kernel:term_create(callback) self:start_lua_client(function() if not self.bufs.term then assert(self.session_id, "Kernel has no session id") - self.bufs.term = require("jet.core.kernel.term").init({ kernel = self, ns = jet_hl_ns }) + self.bufs.term = require("jet.core.kernel.term").init({ kernel = self }) self.bufs.term:create_autocmd("TermEnter", function() self:set_current() end) if cfg.stop_on_buf_wipeout then self.bufs.term:create_autocmd("BufWipeout", function() self:close("BufWipeout") end) @@ -475,7 +470,7 @@ end function Kernel:img_open(which) if not self.bufs.img then assert(self.session_id, "Kernel has no session id") - self.bufs.img = require("jet.core.kernel.img").init({ kernel = self, ns = jet_hl_ns }) + self.bufs.img = require("jet.core.kernel.img").init({ kernel = self }) end return self.bufs.img:open(false, which) end diff --git a/lua/jet/core/kernel/term.lua b/lua/jet/core/kernel/term.lua index c46109d..fd45859 100644 --- a/lua/jet/core/kernel/term.lua +++ b/lua/jet/core/kernel/term.lua @@ -9,7 +9,6 @@ Term.__index = Term ---@private ---@class jet.Kernel.Term.init.Opts ---@field kernel jet.Kernel ----@field ns integer ---@param opts jet.Kernel.Term.init.Opts ---@return jet.Kernel.Term @@ -18,7 +17,6 @@ function Term.init(opts) local out = buf.init(Term, { name = opts.kernel:friendly_name(), - ns = opts.ns, kernel = opts.kernel, win_name = "primary", }) diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua index 17fae8f..62bd199 100644 --- a/lua/jet/core/kernel/win.lua +++ b/lua/jet/core/kernel/win.lua @@ -13,9 +13,8 @@ Win.__index = Win local to_bufnr = function(buf) return type(buf) == "number" and buf or buf.buf end ---@class jet.Win.init.Opts ----@field open_opts jet.Win["open_opts"] ----@field ns jet.Win["ns"] ---@field kernel jet.Win["kernel"] +---@field open_opts jet.Win["open_opts"] ---@field focus jet.Win["focus"] ---@param opts jet.Win.init.Opts @@ -23,7 +22,8 @@ Win.init = function(opts) if opts.focus == nil then opts.focus = false end - return setmetatable(vim.tbl_extend("force", opts, { win = -99 }), Win) + local ns = vim.api.nvim_create_namespace("jet_highlights") + return setmetatable(vim.tbl_extend("force", opts, { win = -99, ns = ns }), Win) end ---@return string diff --git a/lua/jet/core/ui/colours.lua b/lua/jet/core/ui/colours.lua index 104f92b..42fff90 100644 --- a/lua/jet/core/ui/colours.lua +++ b/lua/jet/core/ui/colours.lua @@ -17,7 +17,7 @@ M.colours = { JetIdle = { link = "DiagnosticOk" }, JetItalic = { italic = true }, JetLabel = { link = "@label" }, - JetRepl = { link = "NormalFloat" }, + JetNormal = { link = "NormalFloat" }, JetSpecial = { link = "@punctuation.special" }, JetSuccess = { link = "DiagnosticOk" }, JetUrl = { link = "@markup.link" }, @@ -39,6 +39,9 @@ M.setup = function() return end + local jet_hl_ns = vim.api.nvim_create_namespace("jet_highlights") + vim.api.nvim_set_hl(jet_hl_ns, "Normal", { link = "JetNormal" }) + M.did_setup = true M.set_highlights() From f648ff12a5b9b9abd691fbef8e0f8407b7165268 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 17:18:05 +0100 Subject: [PATCH 22/24] feat(hooks): add hooks.on_win_open --- lua/jet/core/hooks.lua | 5 +++++ lua/jet/core/kernel/init.lua | 3 ++- lua/jet/core/kernel/win.lua | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/jet/core/hooks.lua b/lua/jet/core/hooks.lua index b150ee3..aedf1ea 100644 --- a/lua/jet/core/hooks.lua +++ b/lua/jet/core/hooks.lua @@ -61,6 +61,10 @@ M.init_hooks = function() ---its filetype. ---@type table on_currentness_changed = {}, + + ---After a window is opened + ---@type table + on_win_open = {}, } return hooks @@ -110,6 +114,7 @@ M.do_send_pre = make_caller(h.on_send_pre) M.do_status_changed = make_caller(h.on_status_changed) M.do_image_display_pre = make_caller(h.on_image_display_pre) M.do_currentness_changed = make_caller(h.on_currentness_changed) +M.do_win_open = make_caller(h.on_win_open) -- stylua: ignore end return M diff --git a/lua/jet/core/kernel/init.lua b/lua/jet/core/kernel/init.lua index d5e1c8a..493c86c 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -221,7 +221,8 @@ Kernel.do_message_received = hooks.do_message_received ---@private Kernel.do_send_pre = hooks.do_send_pre ---@private Kernel.do_status_changed = hooks.do_status_changed ---@private Kernel.do_image_display_pre = hooks.do_image_display_pre ---@private -Kernel.do_currentness_changed = hooks.do_currentness_changed ---@private +Kernel.do_currentness_changed = hooks.do_currentness_changed ---@private +Kernel.do_win_open = hooks.do_win_open ---@private -- stylua: ignore end ---Toggle the terminal window for the kernel. diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua index 62bd199..cf7abaa 100644 --- a/lua/jet/core/kernel/win.lua +++ b/lua/jet/core/kernel/win.lua @@ -71,6 +71,7 @@ function Win:open(buf, opts, focus) vim.api.nvim_win_set_buf(self.win, bufnr) else self.win = vim.api.nvim_open_win(bufnr, false, opts or self:make_open_opts()) + require("jet.core.hooks").do_win_open(self.kernel, self, buf) end vim.api.nvim_win_set_hl_ns(self.win, self.ns) From c6109100ef99123647086749c1784b17ac1af550 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 17:28:57 +0100 Subject: [PATCH 23/24] feat!(win): don't handle jet.Buf objects --- lua/jet/core/hooks.lua | 2 +- lua/jet/core/kernel/buf.lua | 2 +- lua/jet/core/kernel/img.lua | 2 +- lua/jet/core/kernel/win.lua | 15 +++++++++------ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lua/jet/core/hooks.lua b/lua/jet/core/hooks.lua index aedf1ea..a7b902f 100644 --- a/lua/jet/core/hooks.lua +++ b/lua/jet/core/hooks.lua @@ -63,7 +63,7 @@ M.init_hooks = function() on_currentness_changed = {}, ---After a window is opened - ---@type table + ---@type table on_win_open = {}, } diff --git a/lua/jet/core/kernel/buf.lua b/lua/jet/core/kernel/buf.lua index 4f5fc3e..e1d8ada 100644 --- a/lua/jet/core/kernel/buf.lua +++ b/lua/jet/core/kernel/buf.lua @@ -53,7 +53,7 @@ end ---@return integer # The opened window function Buf:open(opts, focus) return self:win():open(self.buf, opts, focus) end -function Buf:toggle() self:win():toggle(self) end +function Buf:toggle() self:win():toggle(self.buf) end function Buf:delete() vim.schedule(function() diff --git a/lua/jet/core/kernel/img.lua b/lua/jet/core/kernel/img.lua index 458101b..153b6de 100644 --- a/lua/jet/core/kernel/img.lua +++ b/lua/jet/core/kernel/img.lua @@ -83,7 +83,7 @@ function Img:display(which) return end - if not self:win():get_curr_jet_buf(self) then + if not self:win():get_curr_jet_buf(self.buf) then return end diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua index cf7abaa..9543312 100644 --- a/lua/jet/core/kernel/win.lua +++ b/lua/jet/core/kernel/win.lua @@ -44,7 +44,7 @@ end --- * Does the buffer have `vim.b.jet` set? If so, show `buf` in the window --- * Otherwise, open a fresh window, resetting `self.win`. --- ----@param buf integer | jet.Buf +---@param buf integer ---@param opts vim.api.keyset.win_config? ---@param focus? boolean ---@return integer @@ -105,14 +105,14 @@ function Win:make_open_opts() end end ----@param buf? jet.Buf | integer +---@param buf? integer function Win:close(buf) if self:get_curr_jet_buf(buf) and vim.api.nvim_win_is_valid(self.win) then vim.api.nvim_win_close(self.win, true) end end ----@param buf integer | jet.Buf +---@param buf integer function Win:toggle(buf) if self:get_curr_jet_buf(buf) then self:close(buf) @@ -121,11 +121,14 @@ function Win:toggle(buf) end end ----@param buf? integer | jet.Buf +---@param buf? integer ---@return integer? function Win:get_curr_jet_buf(buf) local curr_buf = self:get_buf() - if buf and to_bufnr(buf) ~= curr_buf then + if not curr_buf then + return nil + end + if buf and buf ~= curr_buf then return nil end for _, k_buf in pairs(self.kernel.bufs) do @@ -135,7 +138,7 @@ function Win:get_curr_jet_buf(buf) end end ----@param buf? integer | jet.Buf +---@param buf? integer function Win:winnr(buf) if self:get_curr_jet_buf(buf) then return self.win From 1e72d80958ed07581ef42fbca244f7587a7cdf9c Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 18 Sep 2026 17:48:07 +0100 Subject: [PATCH 24/24] docs: show how to open plots in floating window --- doc/jet-nvim.txt | 52 +++++++++++++++++++++++++++++++++------------ doc/jet-recipes.txt | 3 +++ doc/jet-types.txt | 16 ++++++-------- docs/jet-nvim.md | 49 ++++++++++++++++++++++++++++++++---------- docs/jet-recipes.md | 5 +++++ 5 files changed, 91 insertions(+), 34 deletions(-) diff --git a/doc/jet-nvim.txt b/doc/jet-nvim.txt index 168f9b7..9fa0e44 100644 --- a/doc/jet-nvim.txt +++ b/doc/jet-nvim.txt @@ -162,6 +162,8 @@ The full set of configuration options is as follows: After a kernel becomes or stops becoming the 'current' kernel for its filetype. + • {on_win_open} (`table`) + After a window is opened ------------------------------------------------------------------------------ Extensions *jet-extensions* @@ -872,7 +874,7 @@ jet.nvim uses the following highlight groups, which can be overridden: • {JetIdle} (`{ link = "DiagnosticOk" }`) • {JetItalic} (`{ italic = true }`) • {JetLabel} (`{ link = "@label" }`) - • {JetRepl} (`{ link = "NormalFloat" }`) + • {JetNormal} (`{ link = "NormalFloat" }`) • {JetSpecial} (`{ link = "@punctuation.special" }`) • {JetSuccess} (`{ link = "DiagnosticOk" }`) • {JetUrl} (`{ link = "@markup.link" }`) @@ -940,19 +942,43 @@ tree-sitter node: >lua CUSTOM UI COMPONENTS *jet-custom-ui-components* -Extensions may want to add custom UI for a particular kernel. For example, -jet.ark adds a custom variables pane. jet.nvim has a mechanism for this: +Extensions may want to add custom UI for a particular kernel. `Kernel` objects +manage UI via two fields: + +• `Kernel.wins`: a table with the following fields: + • `wins.primary` (|jet.Win|) + • `wins.secondary` (|jet.Win|) +• `Kernel.bufs` a table with the following fields + • `bufs.term`: (|jet.Kernel.Term|, inherits from |jet.Buf|) targets the + `primary` window + • `bufs.img`: (|jet.Kernel.Img|, inherits from |jet.Buf|) targets the + `secondary` window + +You can add more fields to `Kernel.wins` or `Kernel.bufs` to define custom UI, +or modify existing fields to control behaviour. For example, to open the +secondary window in a float: >lua + local hooks = require("jet").hooks -• Each kernel owns some 'windows', any of which may or may not be displayed at - a given time. The default number of windows owned by a kernel is 2, but you - can add more. + -- Patch the secondary window's `open_opts` to open a float + ---@param k jet.Kernel + table.insert(hooks.on_kernel_init, function(k) + ---@type vim.api.keyset.win_config + k.wins.secondary.open_opts = { + relative = "editor", + col = math.floor(vim.o.columns * 0.1), + width = math.floor(vim.o.columns * 0.8), + row = math.floor(vim.o.lines * 0.1), + height = math.floor(vim.o.lines * 0.8), + } + end) - • By default the first window opens as a right split - • By default the second window opens as a split above the first window if - the first window exists; otherwise it opens as a right split. -• Each kernel also owns some buffers, each of which is associated with a. - Multiple buffers may be associated with the same window. If you open a - kernel buffer, the buffer will replace any other buffer the associated - window. + -- Focus the window when it opens and add a keymap to close + table.insert(k.hooks.on_win_open, function(_, win, buf) + if win:name() == "secondary" then + vim.keymap.set("n", "q", "q", { buf = buf }) + vim.api.nvim_set_current_win(assert(win:winnr())) + end + end) +< vim:tw=78:sw=4:ts=4:ft=help:norl:et: diff --git a/doc/jet-recipes.txt b/doc/jet-recipes.txt index 1649aa7..328a2e0 100644 --- a/doc/jet-recipes.txt +++ b/doc/jet-recipes.txt @@ -137,4 +137,7 @@ alternative approach, e.g. for R, would be to override the `print` method for console. jet.nvim could then pick up the `execute_reply` message and continue to call visidata as above. +------------------------------------------------------------------------------ +Floating plots window *jet-recipes-floating-plots-window* + vim:tw=78:sw=4:ts=4:ft=help:norl:et: diff --git a/doc/jet-types.txt b/doc/jet-types.txt index 93c7335..533b076 100644 --- a/doc/jet-types.txt +++ b/doc/jet-types.txt @@ -163,7 +163,6 @@ Term.init({opts}) *jet-types-Term.init()* Parameters: ~ • {opts} (`jet.Kernel.Term.init.Opts`) Fields: • kernel (`jet.Kernel`) - • ns (`integer`) Return: ~ (`jet.Kernel.Term`) @@ -187,7 +186,6 @@ Img.init({opts}) *jet-types-Img.init()* Parameters: ~ • {opts} (`jet.Kernel.Img.init.Opts`) Fields: • kernel (`jet.Kernel`) - • ns (`integer`) Return: ~ (`jet.Kernel.Img`) @@ -232,7 +230,6 @@ Buf.init({class}, {opts}) *jet-types-Buf.init()* • {opts} (`jet.Buf.init.Opts`) Fields: • name (`string`) • win_name (`(string|keyof jet.Kernel.Windows)`) - • ns (`integer`) • kernel (`jet.Kernel`) Return: ~ @@ -278,9 +275,8 @@ Win.init({opts}) *jet-types-Win.init()* Parameters: ~ • {opts} (`jet.Win.init.Opts`) Fields: - • open_opts (`jet.Win["open_opts"]`) - • ns (`jet.Win["ns"]`) • kernel (`jet.Win["kernel"]`) + • open_opts (`jet.Win["open_opts"]`) • focus (`jet.Win["focus"]`) Return: ~ @@ -302,7 +298,7 @@ Win:open({buf}, {opts}, {focus}) *jet-types-Win:open()* • Otherwise, open a fresh window, resetting `self.win`. Parameters: ~ - • {buf} (`(integer|jet.Buf)`) + • {buf} (`integer`) • {opts} (`vim.api.keyset.win_config?`) • {focus} (`boolean?`) @@ -312,17 +308,17 @@ Win:open({buf}, {opts}, {focus}) *jet-types-Win:open()* Win:close({buf}) *jet-types-Win:close()* Parameters: ~ - • {buf} (`(jet.Buf|integer)?`) + • {buf} (`integer?`) Win:toggle({buf}) *jet-types-Win:toggle()* Parameters: ~ - • {buf} (`(integer|jet.Buf)`) + • {buf} (`integer`) Win:get_curr_jet_buf({buf}) *jet-types-Win:get_curr_jet_buf()* Parameters: ~ - • {buf} (`(integer|jet.Buf)?`) + • {buf} (`integer?`) Return: ~ (`integer?`) @@ -330,7 +326,7 @@ Win:get_curr_jet_buf({buf}) *jet-types-Win:get_curr_jet_buf()* Win:winnr({buf}) *jet-types-Win:winnr()* Parameters: ~ - • {buf} (`(integer|jet.Buf)?`) + • {buf} (`integer?`) Return: ~ (`integer?`) diff --git a/docs/jet-nvim.md b/docs/jet-nvim.md index 5dd09d4..2a7d6ea 100644 --- a/docs/jet-nvim.md +++ b/docs/jet-nvim.md @@ -395,18 +395,45 @@ jet.filetype.python = { get_expr = get_python_expr } ### Custom UI components -Extensions may want to add custom UI for a particular kernel. For example, -jet.ark adds a custom variables pane. jet.nvim has a mechanism for this: +Extensions may want to add custom UI for a particular kernel. `Kernel` objects +manage UI via two fields: -* Each kernel owns some 'windows', any of which may or may not be displayed at - a given time. The default number of windows owned by a kernel is 2, but you - can add more. +* `Kernel.wins`: a table with the following fields: + * `wins.primary` (|jet.Win|) + * `wins.secondary` (|jet.Win|) - * By default the first window opens as a right split - * By default the second window opens as a split above the first window if the - first window exists; otherwise it opens as a right split. +* `Kernel.bufs` a table with the following fields + * `bufs.term`: (|jet.Kernel.Term|, inherits from |jet.Buf|) targets the + `primary` window + * `bufs.img`: (|jet.Kernel.Img|, inherits from |jet.Buf|) targets the + `secondary` window -* Each kernel also owns some buffers, each of which is associated with a. - Multiple buffers may be associated with the same window. If you open a kernel - buffer, the buffer will replace any other buffer the associated window. +You can add more fields to `Kernel.wins` or `Kernel.bufs` to define custom UI, +or modify existing fields to control behaviour. For example, to open the +secondary window in a float: + +``` lua +local hooks = require("jet").hooks + +-- Patch the secondary window's `open_opts` to open a float +---@param k jet.Kernel +table.insert(hooks.on_kernel_init, function(k) + ---@type vim.api.keyset.win_config + k.wins.secondary.open_opts = { + relative = "editor", + col = math.floor(vim.o.columns * 0.1), + width = math.floor(vim.o.columns * 0.8), + row = math.floor(vim.o.lines * 0.1), + height = math.floor(vim.o.lines * 0.8), + } +end) + +-- Focus the window when it opens and add a keymap to close +table.insert(k.hooks.on_win_open, function(_, win, buf) + if win:name() == "secondary" then + vim.keymap.set("n", "q", "q", { buf = buf }) + vim.api.nvim_set_current_win(assert(win:winnr())) + end +end) +``` diff --git a/docs/jet-recipes.md b/docs/jet-recipes.md index f2787ae..5d947c9 100644 --- a/docs/jet-recipes.md +++ b/docs/jet-recipes.md @@ -142,3 +142,8 @@ alternative approach, e.g. for R, would be to override the `print` method for `data.frame`s to write the tempfile, then simply print the file path to the console. jet.nvim could then pick up the `execute_reply` message and continue to call visidata as above. + +## Floating plots window + + +