diff --git a/.emmyrc.json b/.emmyrc.json index ce1f079..61f28f7 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": { @@ -15,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" + ] } } 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. 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.
diff --git a/doc/jet-nvim.txt b/doc/jet-nvim.txt index c71f2c4..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* @@ -438,7 +440,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?`) @@ -577,8 +579,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 @@ -607,12 +607,24 @@ 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|). + • {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! + 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()* @@ -620,7 +632,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 @@ -636,7 +648,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?`) @@ -748,6 +760,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 @@ -761,7 +785,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 @@ -850,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" }`) @@ -916,4 +940,45 @@ 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. `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 + + -- 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) +< + vim:tw=78:sw=4:ts=4:ft=help:norl:et: diff --git a/doc/jet-recipes.txt b/doc/jet-recipes.txt index 8584643..328a2e0 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)) @@ -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 01b7664..533b076 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,9 +161,8 @@ 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`) Return: ~ (`jet.Kernel.Term`) @@ -185,9 +184,8 @@ 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`) Return: ~ (`jet.Kernel.Img`) @@ -212,42 +210,43 @@ 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()* Parameters: ~ • {class} (`T?`) - • {opts} (`jet.Buf.init.Opts`) + • {opts} (`jet.Buf.init.Opts`) Fields: • name (`string`) - • ns (`integer`) + • win_name (`(string|keyof jet.Kernel.Windows)`) • 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 +261,79 @@ 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`) Fields: + • kernel (`jet.Win["kernel"]`) + • open_opts (`jet.Win["open_opts"]`) + • 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`) + • {opts} (`vim.api.keyset.win_config?`) + • {focus} (`boolean?`) + + Return: ~ + (`integer`) + +Win:close({buf}) *jet-types-Win:close()* + + Parameters: ~ + • {buf} (`integer?`) + +Win:toggle({buf}) *jet-types-Win:toggle()* + + Parameters: ~ + • {buf} (`integer`) + +Win:get_curr_jet_buf({buf}) *jet-types-Win:get_curr_jet_buf()* + + Parameters: ~ + • {buf} (`integer?`) + + Return: ~ + (`integer?`) + +Win:winnr({buf}) *jet-types-Win:winnr()* + + Parameters: ~ + • {buf} (`integer?`) + + 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..2a7d6ea 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 @@ -392,3 +392,48 @@ 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. `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 + +-- 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 7960c2b..5d947c9 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)) @@ -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 + + + 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 +``` 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 = { diff --git a/lua/jet/core/hooks.lua b/lua/jet/core/hooks.lua index b150ee3..a7b902f 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/buf.lua b/lua/jet/core/kernel/buf.lua index 1065285..e1d8ada 100644 --- a/lua/jet/core/kernel/buf.lua +++ b/lua/jet/core/kernel/buf.lua @@ -1,20 +1,17 @@ -local utils = require("jet.core.utils") - ---@class jet.Buf ---@field buf integer +---@field win_name string ---@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 ---@class jet.Buf.init.Opts ---@field name string ----@field ns integer +---@field win_name string | keyof jet.Kernel.Windows ---@field kernel jet.Kernel ----@field open_opts vim.api.keyset.win_config | fun(): vim.api.keyset.win_config ---@generic T ---@param class? T @@ -23,9 +20,9 @@ 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, - open_opts = opts.open_opts, + 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) @@ -36,55 +33,27 @@ function Buf.init(class, opts) 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 +---Get the kernel window associated with the buffer +---@return jet.Win +function Buf:win() + 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? ---@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 {} +function Buf:open(opts, focus) return self:win():open(self.buf, opts, focus) end - ---@type integer - win = vim.api.nvim_open_win(self.buf, false, open_opts) - vim.api.nvim_win_set_hl_ns(win, self.ns) - - 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.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 6f61f82..153b6de 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,20 +17,12 @@ function Img.init(opts) local out = buf.init(Img, { 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, + 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) @@ -45,7 +36,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 +83,7 @@ function Img:display(which) return end - local win = self:win() - - if not win then + if not self:win():get_curr_jet_buf(self.buf) then return end @@ -132,7 +121,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 4f8ed60..493c86c 100644 --- a/lua/jet/core/kernel/init.lua +++ b/lua/jet/core/kernel/init.lua @@ -1,8 +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 = "" @@ -10,6 +11,23 @@ local STARTING_KERNEL_SENTINEL = "" ---@alias jet.Kernel.PartialSpec { display_name: string, language: string } ---@alias jet.Kernel.execution_state "busy" | "idle" | "starting" +---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 + +---Windows 'owned' by the kernel +--- +---Note, you can extend this to add additional windows, or modify the default +---windows directly. +--- +---@class (partial) 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: --- @@ -43,10 +61,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 @@ -73,12 +87,17 @@ 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|). ---@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 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 ---@field private on_started table @@ -97,9 +116,9 @@ local init_defaults = function() incomplete_line = "", }, on_started = {}, - metadata = {}, priority = 100, hooks = hooks.init_hooks(), + bufs = {}, } end @@ -120,6 +139,7 @@ function Kernel.init_owned(opts) local base = vim.tbl_extend("keep", opts, init_defaults(), { owned = true }) local out = setmetatable(base, Kernel) + out:initialise_wins() out:try_resolve_filetype() out:do_kernel_init() @@ -150,6 +170,8 @@ function Kernel.init_external(opts) Kernel ) + out:initialise_wins() + manager:insert(out) Kernel.try_resolve_filetype(out) @@ -158,6 +180,38 @@ function Kernel.init_external(opts) return out end +---@private +function Kernel:initialise_wins() + self.wins = { + primary = win.init({ + 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({ + kernel = self, + 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 + + -- stylua: ignore start Kernel.do_execution_state_changed = hooks.do_execution_state_changed ---@private Kernel.do_kernel_close = hooks.do_kernel_close ---@private @@ -167,32 +221,30 @@ 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. ---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 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) ---@param focus? boolean function Kernel:term_open(callback, focus) self:term_create(function() - assert(self.term, "kernel.term is nil") - self.term:open(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 @@ -207,12 +259,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 }) + 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 @@ -389,8 +441,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) @@ -406,8 +458,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 @@ -417,11 +469,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 }) end - return self.img:open(false, which) + return self.bufs.img:open(false, which) end ---Get a unique, human-readable name for the kernel @@ -754,8 +806,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 @@ -806,6 +858,19 @@ end ---@field listener_interval? integer In milliseconds, default 50ms ---@field callback? fun(msg: jupyter.Msg) +---Convenience; find a comm id (from `Kernel.open_comms`) 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 @@ -880,7 +945,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/term.lua b/lua/jet/core/kernel/term.lua index ad2c79e..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,16 +17,8 @@ function Term.init(opts) local out = buf.init(Term, { 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, + win_name = "primary", }) out.kernel = opts.kernel @@ -68,7 +59,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 diff --git a/lua/jet/core/kernel/win.lua b/lua/jet/core/kernel/win.lua new file mode 100644 index 0000000..9543312 --- /dev/null +++ b/lua/jet/core/kernel/win.lua @@ -0,0 +1,155 @@ +---@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(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 kernel jet.Win["kernel"] +---@field open_opts jet.Win["open_opts"] +---@field focus jet.Win["focus"] + +---@param opts jet.Win.init.Opts +Win.init = function(opts) + if opts.focus == nil then + opts.focus = false + end + local ns = vim.api.nvim_create_namespace("jet_highlights") + return setmetatable(vim.tbl_extend("force", opts, { win = -99, ns = ns }), Win) +end + +---@return string +function Win:name() + for name, w in pairs(self.kernel.wins) do + if w == self then + return name + end + end + error("Could not determine the window name") +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 +---@param opts vim.api.keyset.win_config? +---@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 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()) + require("jet.core.hooks").do_win_open(self.kernel, self, buf) + 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 out = self.open_opts(self.kernel) + 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? 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 +function Win:toggle(buf) + if self:get_curr_jet_buf(buf) then + self:close(buf) + else + self:open(buf) + end +end + +---@param buf? integer +---@return integer? +function Win:get_curr_jet_buf(buf) + local curr_buf = self:get_buf() + 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 + if k_buf.buf == curr_buf and k_buf.win_name == self:name() then + return curr_buf + end + end +end + +---@param buf? integer +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/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 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() diff --git a/lua/jet/core/ui/init.lua b/lua/jet/core/ui/init.lua index d4ef01a..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:win() + 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/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 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 "" 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") 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") 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 "" ]]