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