diff --git a/README.md b/README.md index d02a14a..21320a9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,19 @@ # Rabbit.nvim -![logo](/rabbit.png) +logo Quickly jump between buffers +- [Rabbit.nvim](#rabbitnvim) + - [Why](#why) + - [Install](#install) + - [Usage](#usage) + - [Configuration](#configuration) + - [Preview](#preview) +- [API](#api) + - [Using Rabbit](#using-rabbit) + - [Internals](#internals) + - [Create your own Rabbit listing](#create-your-own-rabbit-listing) + + --- This tool tracks the history of buffers opened in an individual window. With a quick @@ -25,23 +37,27 @@ Lazy: return { "voxelprismatic/rabbit.nvim", config = function() - require("rabbit").setup("r") -- Any keybind you like + require("rabbit").setup({{opts}}) -- Detailed below end, } ``` ### Usage -Just run your keybind! +Just run your keybind! (or `:Rabbit {{mode}}`) + +Currently available modes: +- `history` - Current window's buffer history +- `reopen` - Current window's recently closed buffers -With Rabbit open, you can hit a number 1-0 (1-10) to jump to that buffer. You can +With Rabbit open, you can hit a number 1-9 to jump to that buffer. You can also move your cursor down to a specific line and hit enter to jump to that buffer. If you hit `` immediately after launching Rabbit, it'll open your previous buffer. You can hop back and forth between buffers very quickly, almost like a rabbit... -If you click away from the Rabbit window, it'll close. +By default, you can switch to the opposite mode mode by pressing `r` + -If you try to modify the Rabbit buffer, it'll close. ### Configuration ```lua @@ -101,6 +117,10 @@ require("rabbit").setup({ open = { -- Open Rabbit "r", }, + to = { + history = "r", -- Change to 'History' panel + reopen = "r", -- Change to 'Reopen' panel + }, }, paths = { @@ -111,32 +131,153 @@ require("rabbit").setup({ colors = { -- These should all be highlight group names title = "Statement", -- I don't feel like making a color API for this, just :hi and deal with it - box = "Function", + box = { + history = "Function", + reopen = "Macro", + }, index = "Comment", dir = "NonText", file = "", noname = "Error", + shell = "MoreMsg", }, }) ``` +### Preview + +https://github.com/VoxelPrismatic/rabbit.nvim/assets/45671764/da149bd5-4f6d-4c83-b6cb-67f1be762e2a -### API +--- + +# API ```lua local rabbit = require("rabbit") +``` -rabbit.Window() -- Toggle Rabbit window -rabbit.Close() -- Force close window; will NOT throw error +### Using Rabbit + +`mode` is any of the available modes. `history` and `reopen` are included. +```lua +rabbit.Window(mode) -- Close rabbit window, or open with mode +rabbit.Switch(mode) -- Open with mode +rabbit.Close() -- Close rabbit window rabbit.Select(n) -- Select an entry +rabbit.Setup(opts) -- Setup options +``` + +### Internals +```lua +rabbit.MakeBuf(mode) -- Create the buffer and window +rabbit.ShowMessage(msg) -- Clear and show a message rabbit.RelPath(src, target) -- Return the relative path object for highlighting +rabbit.ensure_listing(winid) -- Ensure that the window has a table for all listings +rabbit.ensure_autocmd(evt) -- Return winid if it's a valid event. Also calls rabbit.ensure_listing ``` +### Create your own Rabbit listing +Calling `require("rabbit")` returns the following structure: +```lua +{ + rab = { + win = 0, -- Winnr for the Rabbit window + buf = 0, -- Bufnr for the Rabbit buffer + ns = 0, -- Highlight namespace (used for CursorLine) + }, + + usr = { + win = 0, -- Winnr for your window + buf = 0, -- Bufnr for your buffer + ns = 0, -- Highlight namespace (unused) + }, -### Preview + ctx = { + border_color = "", -- Border color for this session + listing = {}, -- Listing for this session + mode = "", -- Mode for this session + }, -https://github.com/VoxelPrismatic/rabbit.nvim/assets/45671764/da149bd5-4f6d-4c83-b6cb-67f1be762e2a + opts = {}, -- Options, as detailed above + + listing = { + history = {}, -- History listing + reopen = {}, -- Reopen listing + }, + + messages = { + history = "", -- Message displayed when empty + reopen = "", -- Message displayed when empty + }, + + autocmd = { + BufEnter = function(evt) end, + BufDelete = function(evt) end, + }, +} +``` + +When adding your own plugin, you should add the following details *before* running `rabbit.setup(...)`, +as setup binds the autocmds globally, which can lead to conflicts if called multiple times. + +1. Initialize `rabbit.listing.plugin_name = {}` + - This is how Rabbit knows your plugin exists and can be opened. +2. Create your message strings in `rabbit.messages.plugin_name` + - There is a default message just in case. +3. Set up your autocmds. The function name is the autocmd event, eg BufEnter or BufDelete +```lua +-- Default autocmds, so you make your own. +function table.set_subtract(t1, e) + for i, v in ipairs(t1) do + if v == e then + table.remove(t1, i) + return true + end + end + return false +end + +function table.set_insert(t1, e) + table.set_subtract(t1, e) + table.insert(t1, 1, e) +end + + +function rabbit.autocmd.BufEnter(evt) + -- Grab current winid, and return if it's rabbit + local winid = rabbit.ensure_autocmd(evt) + if winid == nil then + return + end + + -- Put current buffer ID at top of history + table.set_insert(rabbit.listing.history[winid], evt.buf) + + -- Remove if reopened + table.set_subtract(rabbit.listing.reopen[winid], evt.file) +end + + +function rabbit.autocmd.BufDelete(evt) + -- Grab current winid, and return if it's rabbit + local winid = rabbit.ensure_autocmd(evt) + if winid == nil then + return + end + + -- Remove current buffer ID from history + local exists = table.set_subtract(rabbit.listing.history[winid], evt.buf) + + -- Only add to reopen if it's not blank and not a plugin (oil, shell, etc) + if exists and #evt.file > 0 and evt.file:sub(1, 1) ~= "/" then + table.set_insert(rabbit.listing.reopen[winid], evt.file) + end +end +``` + +**NOTE:** You can use buffer IDs or file names in your listing table. The first listing will only +be removed if the filename or buffer ID matches. Do NOT store buffer IDs on BufDelete, as the +buffer ID no longer exist and an error will be thrown. + +Buffers without a filename will be shown as `#nil ID`, where ID is the buffer ID. -### DISCLAIMER -This is my first project in Lua, and my first plugin for Neovim. -Instead of shaming me for bad choices, let me know how I can -improve instead. I greatly appreciate it. +Shell buffers, like Term will be shown like `#bash ID` or `#zsh ID` diff --git a/lua/rabbit/defaults.lua b/lua/rabbit/defaults.lua index 0367a2b..13f2b5c 100644 --- a/lua/rabbit/defaults.lua +++ b/lua/rabbit/defaults.lua @@ -46,11 +46,15 @@ local box = { local options = { color = { title = "Statement", - box = "Function", + box = { + history = "Function", + reopen = "Macro", + }, index = "Comment", dir = "NonText", file = "", noname = "Error", + shell = "MoreMsg", }, box = box.rounded, window = { @@ -68,6 +72,10 @@ local options = { quit = { "", "q", "" }, confirm = { "" }, open = { "r" }, + to = { + history = "r", + reopen = "r", + }, }, paths = { min_visible = 3, diff --git a/lua/rabbit/doc.lua b/lua/rabbit/doc.lua index 3f6645f..265905a 100644 --- a/lua/rabbit/doc.lua +++ b/lua/rabbit/doc.lua @@ -4,11 +4,17 @@ ---@class RabbitColor ---@field title VimHighlight Vim highlight group name. ----@field box VimHighlight Vim highlight group name. +---@field box RabbitBoxColor ---@field index VimHighlight Vim highlight group name. ---@field dir VimHighlight Vim highlight group name. ---@field file VimHighlight Vim highlight group name. ---@field noname VimHighlight Vim highlight group name. +---@field shell VimHighlight Vim highlight group name. +--. + + +---@class RabbitBoxColor +---@field [ValidMode] VimHighlight Vim highlight group name. --. @@ -16,9 +22,14 @@ ---@field quit string[] ---@field confirm string[] ---@field open string[] +---@field to RabbitModeKeys --. +---@class RabbitModeKeys +---@field [ValidMode] string +--. + ---@class RabbitWindow ---@field title string ---@field emphasis_width number @@ -82,6 +93,12 @@ --. +---@class RabbitReopen +---@field [winnr] filepath[] +--. + + + ---@class RabbitCornerPin ---@field [1] "bottom" | "top" ---@field [2] "left" | "right" @@ -92,3 +109,19 @@ ---@field top integer ---@field left integer --. + + +---@class RabbitContext +---@field border_color VimHighlight Vim highlight group name +---@field listing RabbitHistory | RabbitReopen +---@field mode ValidMode +--. + + +---@alias ValidMode "history" | "reopen" + + +---@class RabbitListing +---@field history RabbitHistory +---@field reopen RabbitReopen +--. diff --git a/lua/rabbit/init.lua b/lua/rabbit/init.lua index 2d46ce8..c86fe5d 100644 --- a/lua/rabbit/init.lua +++ b/lua/rabbit/init.lua @@ -1,11 +1,27 @@ local screen = require("rabbit.screen") local defaults = require("rabbit.defaults") +function table.set_subtract(t1, e) + for i, v in ipairs(t1) do + if v == e then + table.remove(t1, i) + return true + end + end + return false +end + +function table.set_insert(t1, e) + table.set_subtract(t1, e) + table.insert(t1, 1, e) +end + ---@class rabbit ---@field opts RabbitOptions ----@field history RabbitHistory +---@field listing RabbitListing ---@field rab RabbitWS ---@field usr RabbitWS +---@field ctx RabbitContext local rabbit = { rab = { win = nil, @@ -13,6 +29,12 @@ local rabbit = { ns = vim.api.nvim_create_namespace("rabbit"), }, + ctx = { + border_color = "Function", + listing = {}, + mode = "history", + }, + usr = { win = nil, buf = nil, @@ -20,55 +42,35 @@ local rabbit = { }, opts = defaults.options, - history = {} + + listing = { + history = {}, + reopen = {}, + }, + + messages = { + history = "There's nowhere to jump to! Get started by opening another buffer", + reopen = "There's no buffer to reopen! Get started by closing a buffer", + __default__ = "There's nothing to do! Also, be sure to add a custom message for this plugin", + }, + + autocmd = {}, } -- Expand a table, like js { ...obj, b = 1, c = 2 } ---@param template table ---@return fun(table: table): table local function spread(template) - local result = {} - for key, value in pairs(template) do - result[key] = value - end - return function(table) - for key, value in pairs(table) do - result[key] = value - end - return result + return vim.tbl_extend("force", template, table) end end -- Display a message in the buffer ----@param buf bufnr bufnr ---@param text string ----@param width number -function rabbit.ShowMessage(buf, text, width) - local line = 2 - local thisline = "" - local fullscreen = rabbit.rab.win == rabbit.usr.win and { text = "", color = "" } or false - for word in text:gmatch("[^ ]+") do - if (#thisline + #word > width - 4) and not fullscreen then - screen.render(rabbit.rab.win, buf, line, { - { color = rabbit.opts.color.box, text = rabbit.opts.box.vertical .. " " }, - { color = rabbit.opts.color.file, text = thisline }, - { color = rabbit.opts.color.box, text = rabbit.opts.box.vertical, expand = true }, - }) - line = line + 1 - thisline = "" - end - thisline = thisline .. word .. " " - end - - if #thisline > 1 then - screen.render(rabbit.rab.win, buf, line, { - fullscreen or { color = rabbit.opts.color.box, text = rabbit.opts.box.vertical .. " " }, - { color = rabbit.opts.color.file, text = thisline }, - fullscreen or { color = rabbit.opts.color.box, text = rabbit.opts.box.vertical, expand = true }, - }) - end +function rabbit.ShowMessage(text) + screen.display_message(text) end @@ -137,11 +139,8 @@ end function rabbit.Select(lineno) - if lineno <= 1 then - lineno = 2 -- Index 1 is the current buffer - else - lineno = lineno + 1 - end + lineno = math.max(lineno, 1) + if rabbit.rab.win ~= nil then if rabbit.rab.win == rabbit.usr.win then vim.api.nvim_win_set_buf(rabbit.usr.win, rabbit.usr.buf) @@ -156,27 +155,66 @@ function rabbit.Select(lineno) rabbit.usr.win = vim.fn.win_getid() end - if rabbit.history[rabbit.usr.win] == nil then - rabbit.history[rabbit.usr.win] = {} - end - - if lineno >= 1 and lineno <= #(rabbit.history[rabbit.usr.win]) then - vim.api.nvim_win_set_buf(rabbit.usr.win, rabbit.history[rabbit.usr.win][lineno]) + if lineno >= 1 and lineno <= #(rabbit.ctx.listing) then + local b = rabbit.ctx.listing[lineno] + if type(b) == "string" then + b = vim.cmd.edit(b) + else + vim.api.nvim_win_set_buf(rabbit.usr.win, b) + end end end -function rabbit.MakeBuf() - local buf = vim.api.nvim_create_buf(false, true) ---@type bufnr +---@param winid winnr +function rabbit.ensure_listing(winid) + if winid == nil then + winid = vim.api.nvim_get_current_win() + end + + for k, _ in pairs(rabbit.listing) do + if rabbit.listing[k] == nil then + rabbit.listing[k] = { [winid] = {} } + elseif rabbit.listing[k][winid] == nil then + rabbit.listing[k][winid] = {} + end + end +end - rabbit.rab.buf = buf +---@param mode ValidMode +function rabbit.MakeBuf(mode) rabbit.usr.buf = vim.api.nvim_get_current_buf() rabbit.usr.win = vim.api.nvim_get_current_win() rabbit.usr.ns = 0 +-- Ensure all lists exist + rabbit.ensure_listing(rabbit.usr.win) + +-- Prepare context to save time later + if mode == nil or rabbit.listing[mode] == nil then + mode = "history" + end + + rabbit.ctx.border_color = rabbit.opts.color.box[mode] or rabbit.opts.color.box.history + rabbit.ctx.mode = mode + rabbit.ctx.listing = vim.deepcopy(rabbit.listing[mode][rabbit.usr.win]) + + if #rabbit.ctx.listing > 0 then + local same_id = rabbit.ctx.listing[1] == rabbit.usr.buf + local same_name = rabbit.ctx.listing[1] == vim.api.nvim_buf_get_name(rabbit.usr.buf) + + if same_id or same_name then + table.remove(rabbit.ctx.listing, 1) + end + end + + local buf = vim.api.nvim_create_buf(false, true) + rabbit.rab.buf = buf + local win_conf = vim.api.nvim_win_get_config(rabbit.usr.win) +-- Generate configuration local opts = { width = math.min(rabbit.opts.window.width, win_conf.width), height = math.min(rabbit.opts.window.height, win_conf.height), @@ -217,6 +255,7 @@ function rabbit.MakeBuf() vim.api.nvim_win_set_hl_ns(rabbit.rab.win, rabbit.rab.ns) +-- Set key maps & auto commands for _, key in ipairs(rabbit.opts.keys.quit) do vim.api.nvim_buf_set_keymap( buf, "n", key, "lua require('rabbit').Close()", @@ -224,10 +263,21 @@ function rabbit.MakeBuf() ) end - vim.api.nvim_buf_set_keymap( - buf, "n", "", "lua require('rabbit').Select(vim.fn.line('.') - 2)", - { noremap = true, silent = true } - ) + for _, key in ipairs(rabbit.opts.keys.confirm) do + vim.api.nvim_buf_set_keymap( + buf, "n", key, "lua require('rabbit').Select(vim.fn.line('.') - 2)", + { noremap = true, silent = true } + ) + end + + for k, v in pairs(rabbit.opts.keys.to) do + if k ~= mode and rabbit.listing[k] ~= nil then + vim.api.nvim_buf_set_keymap( + buf, "n", v, "lua require('rabbit').Switch('" .. (k or "r") .. "')", + { noremap = true, silent = true } + ) + end + end vim.api.nvim_create_autocmd("WinLeave", { buffer = buf, callback = rabbit.Close }) vim.api.nvim_create_autocmd("BufLeave", { buffer = buf, callback = rabbit.Close }) @@ -236,7 +286,7 @@ function rabbit.MakeBuf() vim.api.nvim_create_autocmd("CursorMoved", { buffer = buf, callback = function() vim.api.nvim_buf_clear_namespace(rabbit.rab.buf, rabbit.rab.ns, 0, -1) - local len = #rabbit.history[rabbit.usr.win] - 1 + local len = #rabbit.ctx.listing local line = vim.fn.line(".") - 1 if line - 1 > 0 and line - 1 <= len then local fullscreen = rabbit.rab.win == rabbit.usr.win @@ -248,15 +298,29 @@ function rabbit.MakeBuf() end end}) + ---@type ScreenSetBorderKwargs + local b_kwargs = { + colors = rabbit.opts.color, + border_color = rabbit.ctx.border_color, + width = opts.width, + height = opts.height, + emph_width = rabbit.opts.window.emphasis_width, + box = rabbit.opts.box, + fullscreen = rabbit.usr.win == rabbit.rab.win, + title = rabbit.opts.window.title, + mode = mode, + } return { nr = buf, w = opts.width, h = opts.height, + fs = screen.set_border(rabbit.rab.win, buf, b_kwargs) } end -function rabbit.Window() +---@param mode ValidMode +function rabbit.Window(mode) if rabbit.rab.win ~= nil then local status, _ = pcall(rabbit.Close) rabbit.rab.win = nil @@ -265,207 +329,124 @@ function rabbit.Window() if status == true then return end end - local buf = rabbit.MakeBuf() + local buf = rabbit.MakeBuf(mode) - if rabbit.history[rabbit.usr.win] == nil then - rabbit.history[rabbit.usr.win] = {} + if #rabbit.ctx.listing < 1 then + rabbit.ShowMessage(rabbit.messages[rabbit.ctx.mode] or rabbit.messages.__default__) + return end - - local fullscreen = rabbit.rab.win == rabbit.usr.win and { text = "", color = "" } or false - local center = (buf.w - 2 - #(rabbit.opts.window.title)) / 2 - 1 - local emph_width = math.min(center - 4, rabbit.opts.window.emphasis_width) - center = center - emph_width - - - if fullscreen then - screen.render(rabbit.rab.win, buf.nr, 0, { - { text = rabbit.opts.box.emphasis:rep(emph_width), color = rabbit.opts.color.box }, - { text = " " .. rabbit.opts.window.title .. " ", color = rabbit.opts.color.title }, - { text = rabbit.opts.box.emphasis:rep(emph_width), color = rabbit.opts.color.box }, - }) - screen.render(rabbit.rab.win, buf.nr, 1, { fullscreen }) - else - screen.render(rabbit.rab.win, buf.nr, 0, { - { - color = rabbit.opts.color.box, - text = { - rabbit.opts.box.top_left, - rabbit.opts.box.horizontal:rep(center), - rabbit.opts.box.emphasis:rep(emph_width) - }, - }, - { - color = rabbit.opts.color.title, - text = " " .. rabbit.opts.window.title .. " ", - }, - { - color = rabbit.opts.color.box, - text = rabbit.opts.box.emphasis:rep(emph_width), - }, - { - color = rabbit.opts.color.box, - text = rabbit.opts.box.top_right, - expand = rabbit.opts.box.horizontal, - }, - }) - - screen.render(rabbit.rab.win, buf.nr, 1, {{ - color = rabbit.opts.color.box, - text = { - rabbit.opts.box.vertical, - (" "):rep(buf.w - 2), - rabbit.opts.box.vertical, - } - }}) - end - - local window_bufs = rabbit.history[rabbit.usr.win] - local has_name, buf_path = pcall(vim.api.nvim_buf_get_name, window_bufs[1]) - if not has_name then + local has_name, buf_path = pcall(vim.api.nvim_buf_get_name, rabbit.usr.buf) + if not has_name or buf_path:sub(1, 1) ~= "/" then buf_path = "" end - for i = 1, math.max(buf.h - 4, #window_bufs + 1) do - ---@type ScreenSpec[] - local parts = fullscreen and {} or {{ color = rabbit.opts.color.box, text = rabbit.opts.box.vertical .. " " }} - - if i < #window_bufs then - if i <= 10 then - vim.api.nvim_buf_set_keymap( - buf.nr, "n", ("" .. i):sub(-1), "lua require('rabbit').Select(" .. i .. ")", - { noremap = true, silent = true } - ) - end - local valid = vim.api.nvim_buf_is_valid(window_bufs[i + 1]) - while not valid do - table.remove(window_bufs, i + 1) - if #window_bufs == 1 then - vim.print("KILL") - break - end - valid = vim.api.nvim_buf_is_valid(window_bufs[i + 1]) + for i = 1, #rabbit.ctx.listing do + local target = "" + if type(rabbit.ctx.listing[i]) == "number" then + local valid = vim.api.nvim_buf_is_valid(rabbit.ctx.listing[i]) + while not valid and i < #rabbit.ctx.listing do + table.remove(rabbit.ctx.listing, i) + valid = vim.api.nvim_buf_is_valid(rabbit.ctx.listing[i]) end if not valid then break end - local target = vim.api.nvim_buf_get_name(window_bufs[i + 1]) - if target ~= "" then - local rel = rabbit.RelPath(buf_path, vim.fn.fnamemodify(target, ":p")) - table.insert(parts, { - { text = (fullscreen and " " or "") .. (i < 10 and " " or "") .. i .. ". ", color = rabbit.opts.color.index }, - { text = rel.dir, color = rabbit.opts.color.dir }, - { text = rel.name, color = rabbit.opts.color.file, } - }) - else - table.insert(parts, { - { text = (i < 10 and " " or "") .. i .. ". ", color = rabbit.opts.color.index }, - { text = "#nil " .. window_bufs[i + 1], color = rabbit.opts.color.noname } - }) - end + target = vim.api.nvim_buf_get_name(rabbit.ctx.listing[i]) + else + target = "" .. rabbit.ctx.listing[i] end - table.insert(parts, fullscreen or { - color = rabbit.opts.color.box, - text = rabbit.opts.box.vertical, - expand = true, - }) - screen.render(rabbit.rab.win, buf.nr, i + 1, parts) + if target == "" then + screen.add_entry({ + { text = "#nil ", color = rabbit.opts.color.noname }, + { text = rabbit.ctx.listing[i], color = rabbit.opts.color.file }, + }) + elseif target:sub(1, 1) ~= "/" then + local rel = rabbit.RelPath(buf_path, vim.fn.fnamemodify(target, ":p")) + screen.add_entry({ + { text = "#" .. rel.name .. " ", color = rabbit.opts.color.shell }, + { text = rabbit.ctx.listing[i], color = rabbit.opts.color.file }, + }) + else + local rel = rabbit.RelPath(buf_path, vim.fn.fnamemodify(target, ":p")) + screen.add_entry({ + { text = rel.dir, color = rabbit.opts.color.dir }, + { text = rel.name, color = rabbit.opts.color.file }, + }) + end end - if #window_bufs <= 1 then - rabbit.ShowMessage(buf.nr, "There's nowhere else to jump to! Get started by opening another buffer", buf.w) - else - vim.api.nvim_win_set_cursor(rabbit.rab.win, { 3, fullscreen and 0 or #(rabbit.opts.box.vertical) }) - end - - if not fullscreen then - screen.render(rabbit.rab.win, buf.nr, -1, {{ - color = rabbit.opts.color.box, - text = { - rabbit.opts.box.vertical, - (" "):rep(buf.w - 2), - rabbit.opts.box.vertical, - } - }}) - - screen.render(rabbit.rab.win, buf.nr, -1, {{ - color = rabbit.opts.color.box, - text = { - rabbit.opts.box.bottom_left, - rabbit.opts.box.horizontal:rep(buf.w - 2), - rabbit.opts.box.bottom_right, - }, - }}) - end + screen.draw_bottom() + vim.api.nvim_win_set_cursor(rabbit.rab.win, { 3, buf.fs and 0 or #(rabbit.opts.box.vertical) }) end -vim.api.nvim_create_autocmd("BufEnter", { - pattern = {"*"}, - callback = function(evt) - if evt.buf == rabbit.rab.buf then - return - end +function rabbit.Switch(mode) + rabbit.Close() + rabbit.Window(mode) +end - local winid = vim.fn.win_getid() - if #(evt.file) > 1 and evt.file:sub(1, 1) ~= "/" then - return - end +function rabbit.ensure_autocmd(evt) + if evt.buf == rabbit.rab.buf then + return nil + end + local winid = vim.fn.win_getid() + rabbit.ensure_listing(winid) + return winid +end - if rabbit.history[winid] == nil then - rabbit.history[winid] = {} - end - -- Remove duplicates - for i = 1, #(rabbit.history[winid]) do - if rabbit.history[winid][i] == evt.buf then - table.remove(rabbit.history[winid], i) - break - end - end - table.insert(rabbit.history[winid], 1, evt.buf) - end, -}) +function rabbit.autocmd.BufEnter(evt) + local winid = rabbit.ensure_autocmd(evt) + if winid == nil then + return + end + table.set_insert(rabbit.listing.history[winid], evt.buf) + table.set_subtract(rabbit.listing.reopen[winid], evt.file) +end -vim.api.nvim_create_autocmd("BufDelete", { - pattern = {"*"}, - callback = function(evt) - if evt.buf == rabbit.rab.buf then - return - end - local winid = vim.fn.win_getid() +function rabbit.autocmd.BufDelete(evt) + local winid = rabbit.ensure_autocmd(evt) + if winid == nil then + return + end - if rabbit.history[winid] == nil then - rabbit.history[winid] = {} - return - end - for i = 1, #(rabbit.history[winid]) do - if rabbit.history[winid][i] == evt.buf then - table.remove(rabbit.history[winid], i) - return - end - end - end, -}) + local exists = table.set_subtract(rabbit.listing.history[winid], evt.buf) + if exists and #evt.file > 0 and evt.file:sub(1, 1) == "/" then + table.set_insert(rabbit.listing.reopen[winid], evt.file) + end +end vim.api.nvim_create_autocmd("WinClosed", { pattern = {"*"}, callback = function(evt) - rabbit.history[evt.file] = nil + for k, _ in pairs(rabbit.listing) do + rabbit.listing[k][evt.file] = nil + end end, }) ---@param opts RabbitOptions | string function rabbit.setup(opts) - rabbit.history[vim.fn.win_getid()] = {} + rabbit.ensure_listing(vim.fn.win_getid()) + vim.api.nvim_create_user_command( + "Rabbit", + function(o) rabbit.Switch(o.fargs[1]) end, + { + nargs = "?", + complete = function() + return vim.tbl_keys(rabbit.listing) + end + } + ) if type(opts) == "string" then opts = { keys = { open = { opts } } } @@ -486,12 +467,29 @@ function rabbit.setup(opts) end end + if type(rabbit.opts.keys.to) == "string" then + local k = rabbit.opts.keys.to + rabbit.opts.keys.to = {} + for v, _ in pairs(rabbit.listing) do + rabbit.opts.keys.to[v] = k + end + end + for key, val in pairs(rabbit.opts.keys) do if type(val) == "string" then rabbit.opts.keys[key] = { val } end end + + if type(rabbit.opts.color.box) == "string" then + local c = rabbit.opts.color.box + rabbit.opts.color.box = {} + for v, _ in pairs(rabbit.listing) do + rabbit.opts.color.box[v] = c + end + end + for _, key in ipairs(rabbit.opts.keys.open) do vim.keymap.set("n", key, rabbit.Window, { desc = "Open Rabbit", @@ -499,6 +497,13 @@ function rabbit.setup(opts) silent = true }) end + + for key, val in pairs(rabbit.autocmd) do + vim.api.nvim_create_autocmd(key, { + pattern = {"*"}, + callback = val, + }) + end end return rabbit diff --git a/lua/rabbit/screen.lua b/lua/rabbit/screen.lua index 8e2e4ab..82bd906 100644 --- a/lua/rabbit/screen.lua +++ b/lua/rabbit/screen.lua @@ -2,7 +2,21 @@ -- @module screen -- @alias screen -local screen = {} +local screen = { + ctx = { + title = {}, + middle = {}, + footer = {}, + box = {}, + colors = {}, + border_color = "Function", + height = 0, + width = 0, + bufnr = nil, + winnr = nil, + fullscreen = false, + } +} --- Undo possible recursion in screen spec ---@param specs ScreenSpec[] @@ -79,4 +93,180 @@ function screen.render(win, buf, line, specs) end end + +--- Adds a new border to the screen +---@param win winnr +---@param buf bufnr +---@param kwargs ScreenSetBorderKwargs +---@return false | ScreenSpec +function screen.set_border(win, buf, kwargs) + local fs = kwargs.fullscreen and { text = "", color = "" } or false + local c = (kwargs.width - 2 - #(kwargs.title)) / 2 - 1 + local emph = math.min(c - 4, kwargs.emph_width) + + screen.ctx.height = kwargs.height + screen.ctx.width = kwargs.width + screen.ctx.bufnr = buf + screen.ctx.winnr = win + screen.ctx.box = kwargs.box + screen.ctx.colors = kwargs.colors + screen.fullscreen = fs + + if fs then + screen.ctx.title = { + { text = kwargs.box.emphasis(emph), color = kwargs.border_color }, + { text = " " .. kwargs.title .. " ", color = kwargs.colors.title }, + { text = kwargs.box.emphasis(emph), color = kwargs.border_color }, + } + screen.ctx.middle = { fs } + screen.ctx.footer = { fs } + + screen.draw_top() + + return fs + + end + + + screen.ctx.title = { + { + color = kwargs.border_color, + text = { + kwargs.box.top_left, + kwargs.box.horizontal:rep(c - emph), + kwargs.box.emphasis:rep(emph), + }, + }, { + color = kwargs.colors.title, + text = " " .. kwargs.title .. " ", + }, { + color = kwargs.border_color, + text = kwargs.box.emphasis:rep(emph), + }, { + color = kwargs.border_color, + text = kwargs.box.top_right, + expand = kwargs.box.horizontal, + }, + } + + screen.ctx.middle = {{ + color = kwargs.border_color, + text = { + kwargs.box.vertical, + (" "):rep(kwargs.width - 2), + kwargs.box.vertical, + }, + }} + + screen.ctx.footer = { + { + color = kwargs.border_color, + text = kwargs.box.bottom_left, + }, { + color = kwargs.border_color, + text = { + " " .. kwargs.mode .. " ", + kwargs.box.horizontal:rep(3), + kwargs.box.bottom_right, + }, + expand = kwargs.box.horizontal, + } + } + + screen.draw_top() + + return fs +end + + +function screen.draw_top() + if #screen.ctx.title == 0 then + return false + end + + vim.api.nvim_buf_set_lines(screen.ctx.bufnr, 0, -1, false, {}) + + screen.render(screen.ctx.winnr, screen.ctx.bufnr, 0, screen.ctx.title) + screen.render(screen.ctx.winnr, screen.ctx.bufnr, 1, screen.ctx.middle) +end + +function screen.draw_bottom() + local h = #vim.api.nvim_buf_get_lines(screen.ctx.bufnr, 0, -1, false) + + for i = 1, math.max(1, screen.ctx.height - h - 1) do + screen.render(screen.ctx.winnr, screen.ctx.bufnr, -1, screen.ctx.middle) + end + + screen.render(screen.ctx.winnr, screen.ctx.bufnr, -1, screen.ctx.footer) +end + +function screen.add_entry(spec) + local i = #vim.api.nvim_buf_get_lines(screen.ctx.bufnr, 0, -1, false) - 1 + + if i < 10 then + vim.api.nvim_buf_set_keymap( + screen.ctx.bufnr, "n", ("" .. i):sub(-1), "lua require('rabbit').Select(" .. i .. ")", + { noremap = true, silent = true } + ) + end + + local to_render = screen.ctx.fullscreen and {} or { + { + color = screen.ctx.border_color, + text = screen.ctx.box.vertical .. " " + }, { + color = screen.ctx.colors.index, + text = (screen.ctx.fullscreen and " " or "") .. (i < 10 and " " or "") .. i .. ". " + }, + } + + for _, v in ipairs(spec) do + table.insert(to_render, v) + end + + table.insert(to_render, screen.ctx.fullscreen or { + color = screen.ctx.border_color, + text = screen.ctx.box.vertical, + expand = true + }) + + screen.render(screen.ctx.winnr, screen.ctx.bufnr, -1, to_render) +end + + +function screen.display_message(msg) + screen.draw_top() + local fullscreen = screen.ctx.fullscreen and { text = "", color = "" } or false + local lines = { "" } + + for word in msg:gmatch("[^ ]+") do + if (#(lines[#lines]) + #word > screen.ctx.width - 4) and not fullscreen then + lines[#lines + 1] = "" + end + lines[#lines] = lines[#lines] .. word .. " " + end + + for _, line in ipairs(lines) do + screen.render(screen.ctx.winnr, screen.ctx.bufnr, -1, { + fullscreen or { color = screen.ctx.border_color, text = screen.ctx.box.vertical .. " " }, + { color = screen.ctx.colors.file, text = line }, + fullscreen or { color = screen.ctx.border_color, text = screen.ctx.box.vertical, expand = true }, + }) + end + + screen.draw_bottom() +end + +---@class ScreenSetBorderKwargs +---@field colors RabbitColor +---@field border_color VimHighlight +---@field width integer +---@field height integer +---@field emph_width integer +---@field box RabbitBox +---@field fullscreen boolean +---@field title string +---@field mode string +--. + return screen