From 8f01c2d38115b91780fb4e274ff883ca8cae2047 Mon Sep 17 00:00:00 2001 From: VoxelPrismatic Date: Wed, 19 Jun 2024 23:09:46 -0500 Subject: [PATCH] now accepts user input --- lua/rabbit/defaults.lua | 14 ++- lua/rabbit/init.lua | 19 +++- lua/rabbit/input.lua | 84 +++++++++++++++ lua/rabbit/luadoc/init.lua | 12 ++- lua/rabbit/luadoc/options.lua | 1 + lua/rabbit/luadoc/plugin.lua | 16 ++- lua/rabbit/luadoc/screen.lua | 16 +-- lua/rabbit/plugins/harpoon.lua | 185 ++++++++++++++++++++++++++++++--- lua/rabbit/plugins/util.lua | 36 +++++-- lua/rabbit/screen.lua | 10 +- 10 files changed, 345 insertions(+), 48 deletions(-) create mode 100644 lua/rabbit/input.lua diff --git a/lua/rabbit/defaults.lua b/lua/rabbit/defaults.lua index 926a13e..04862c1 100644 --- a/lua/rabbit/defaults.lua +++ b/lua/rabbit/defaults.lua @@ -41,12 +41,16 @@ local box = { } -local function grab_color(name) +---@param name string Highlight group name +---@param key? string Key, eg `fg` or `bg` +---@return string | nil +local function grab_color(name, key) local details = vim.api.nvim_get_hl(0, { name = name }) - if details == nil or details.fg == nil then + key = key or "fg" + if details == nil or details[key] == nil then return nil end - return string.format("#%06x", details.fg) + return string.format("#%06x", details[key]) end ---@type Rabbit.Options @@ -58,7 +62,7 @@ local options = { file = { fg = grab_color("Normal") }, term = { fg = grab_color("Constant"), italic = true }, noname = { fg = grab_color("Function"), italic = true }, - message = { fg = grab_color("Identifier"), italic = true }, + message = { fg = grab_color("Identifier"), italic = true, bold = true }, }, window = { box = box.round, @@ -81,6 +85,8 @@ local options = { open = { "r" }, file_add = { "a" }, file_del = { "" }, + group = { "A" }, + group_up = { "-" }, }, plugin_opts = {}, enable = { diff --git a/lua/rabbit/init.lua b/lua/rabbit/init.lua index 7343f72..5cbf824 100644 --- a/lua/rabbit/init.lua +++ b/lua/rabbit/init.lua @@ -41,6 +41,8 @@ local rabbit = { plugins = {}, compat = compat, + + input = require("rabbit.input"), } -- Display a message in the buffer @@ -128,6 +130,10 @@ end -- Close the Rabbit window function rabbit.func.close(_) + if screen.ctx.in_input then + return + end + if rabbit.rabbit.win ~= nil then if rabbit.rabbit.win == rabbit.user.win then vim.api.nvim_win_set_buf(rabbit.user.win, rabbit.user.buf) @@ -151,6 +157,10 @@ end -- The window should not scroll when exiting Rabbit local function close_with_cursor(_) + if screen.ctx.in_input then + return + end + (rabbit.ctx.plugin.func.close or rabbit.func.close)() local term_codes = (""):rep(rabbit.user.view.topline) .. @@ -334,6 +344,8 @@ function rabbit.MakeBuf(mode) fullscreen = rabbit.user.win == rabbit.rabbit.win, title = b_title or "", mode = b_mode, + pos_col = opts.col, + pos_row = opts.row, } local fs = screen.set_border(rabbit.rabbit.win, buf, b_kwargs) @@ -422,8 +434,13 @@ function rabbit.Redraw() { text = rabbit.ctx.listing[i] .. "", color = "RabbitFile" }, }, i + 1) elseif string.find(target, "rabbitmsg://") == 1 then + target = target:sub(#"rabbitmsg://" + 1) .. "\n" + local msg = vim.split(target, "\n")[1] + local extra = vim.split(target, "\n")[2] + local space = (" "):rep(buf.w - vim.fn.strwidth(msg .. extra .. "| . |") - math.max(2, #(tostring(i)))) screen.add_entry({ - { text = target:sub(#"rabbitmsg://" + 1), color = "RabbitMsg" }, + { text = msg, color = "RabbitMsg" }, + { text = space .. extra, color = "RabbitDir" }, }, i + 1) elseif target:sub(1, 1) ~= "/" then local rel = rabbit.RelPath(buf_path, target) diff --git a/lua/rabbit/input.lua b/lua/rabbit/input.lua new file mode 100644 index 0000000..4bf4c34 --- /dev/null +++ b/lua/rabbit/input.lua @@ -0,0 +1,84 @@ +local screen = require("rabbit.screen") + + +---@class Rabbit.Input +local M = {} + + +-- Prompts the user with a question for short-text input +---@param title string The question/input prompt +---@param callback fun(response: string) The callback to be called with the user's input +---@param check? fun(response: string): boolean The callback to check the user's input +function M.prompt(title, callback, check) + local rabbit = require("rabbit") + + check = check or function() return true end + + screen.ctx.in_input = true + local buf = vim.api.nvim_create_buf(false, true) + + local win = vim.api.nvim_open_win(buf, true, { + relative = "win", + width = screen.ctx.width - 4, + height = 1, + row = (screen.ctx.height - 3) / 2, + col = 1, + + style = "minimal", + border = { + screen.ctx.box.top_left, + screen.ctx.box.horizontal, + screen.ctx.box.top_right, + screen.ctx.box.vertical, + screen.ctx.box.bottom_right, + screen.ctx.box.horizontal, + screen.ctx.box.bottom_left, + screen.ctx.box.vertical + }, + title = {{ + screen.ctx.box.horizontal .. " " .. title .. " ", + "FloatBorder" + }} + }) + + vim.fn.feedkeys("i", "n") + + vim.api.nvim_create_autocmd("InsertLeave", { + buffer = buf, + callback = function() + screen.ctx.in_input = false + vim.api.nvim_win_close(win, true) + vim.api.nvim_buf_delete(buf, { force = true }) + vim.fn.feed_termcodes("", "n") + end + }) + + vim.api.nvim_create_autocmd("TextChangedI", { + buffer = buf, + callback = function() + vim.api.nvim_buf_clear_namespace(buf, 42, 0, -1) + if not check(vim.fn.getline(".")) then + vim.api.nvim_buf_add_highlight(buf, 42, "Error", 0, 0, -1) + end + end + }) + + local cb = function() + local line = vim.fn.getline(".") + if not check(line) then + return + end + screen.ctx.in_input = false + vim.api.nvim_win_close(win, true) + vim.api.nvim_buf_delete(buf, { force = true }) + callback(line) + vim.fn.feed_termcodes("", "n") + end + + for _, k in ipairs(rabbit.opts.default_keys.select) do + vim.api.nvim_buf_set_keymap(buf, "i", k, "", { callback = cb }) + end +end + + +return M diff --git a/lua/rabbit/luadoc/init.lua b/lua/rabbit/luadoc/init.lua index 9b1f072..a513138 100644 --- a/lua/rabbit/luadoc/init.lua +++ b/lua/rabbit/luadoc/init.lua @@ -1,9 +1,11 @@ ---@class Rabbit.Keymap ----@field public open? string[] Keys to open the window ----@field public select? string[] Keys to select the current entry ----@field public close? string[] Keys to close the window ----@field public file_add? string[] Keys to add a file, like in Harpoon ----@field public file_del? string[] Keys to delete a file, like in Harpoon +---@field public open string[] Keys to open the window +---@field public select string[] Keys to select the current entry +---@field public close string[] Keys to close the window +---@field public file_add string[] Keys to add a file, like in Harpoon +---@field public file_del string[] Keys to delete a file, like in Harpoon +---@field public group string[] Keys to create a collection of files +---@field public group_up string[] Keys to move up a collection ---@field [string] string[] diff --git a/lua/rabbit/luadoc/options.lua b/lua/rabbit/luadoc/options.lua index a934cb3..dea27c3 100644 --- a/lua/rabbit/luadoc/options.lua +++ b/lua/rabbit/luadoc/options.lua @@ -84,5 +84,6 @@ ---| "RabbitNil" # Color of Blank Filename ---| "RabbitTerm" # Color of extras, eg :term or :Oil ---| "RabbitMsg" # Color of messages +---| "RabbitInput" # Color of the input prompt background diff --git a/lua/rabbit/luadoc/plugin.lua b/lua/rabbit/luadoc/plugin.lua index bd17a1b..8de263d 100644 --- a/lua/rabbit/luadoc/plugin.lua +++ b/lua/rabbit/luadoc/plugin.lua @@ -25,6 +25,8 @@ ---@field close? fun(integer) Close Rabbit window ---@field file_add? fun(integer) Add a file, like in Harpoon ---@field file_del? fun(integer) Delete a file, like in Harpoon +---@field group? fun(integer) Create a collection of files +---@field group_up? fun(integer) Move up a collection ---@field [string] fun(integer) @@ -33,6 +35,8 @@ ---@field close? string[] Keys to close the window ---@field file_add? string[] Keys to add a file, like in Harpoon ---@field file_del? string[] Keys to delete a file, like in Harpoon +---@field group? string[] Keys to create a collection of files +---@field group_up? string[] Keys to move up a collection ---@field [string] string[] @@ -40,6 +44,9 @@ ---@field [0] Rabbit.Plugin.Listing.Window Listing shown to the user ---@field persist? Rabbit.Plugin.Listing.Persist Internal persistent listing ---@field opened? Rabbit.Plugin.Listing.Window Tracks open files +---@field collections? Rabbit.Plugin.Listing.Persist.Recursive Tracks collections +---@field recursive? Rabbit.Plugin.Listing.Persist.Recursive +---@field paths? Rabbit.Plugin.Listing.Persist.Table[] ---@field [integer] Rabbit.Plugin.Listing.Window ---@field [string] Rabbit.Plugin.Listing.Window @@ -89,6 +96,7 @@ ---@field RabbitEnter? fun(winnr: integer) Called when the Rabbit window is opened ---@field [string] Rabbit.Event.Handler + ---@alias Rabbit.Event.Handler fun(evt: NvimEvent, winid: integer) @@ -98,10 +106,16 @@ ---@class Rabbit.Plugin.Listing.Persist.Table ---@field [integer] string Just the filename; no Oxide details ----@field [string] Rabbit.Plugin.Listing.Persist.Entry `File Name : Entry` table +---@field [string] Rabbit.Plugin.Listing.Persist.Entry | Rabbit.Plugin.Listing.Persist.Table `File Name : Entry` table ---@class Rabbit.Plugin.Listing.Persist.Entry ---@field age integer The last time the file was accessed ---@field count integer The total number of times this file was accessed + +---@alias Rabbit.Plugin.Listing.Persist.Recursive +---| Rabbit.Plugin.Listing.Persist +---| Rabbit.Plugin.Listing.Persist.Table +---| Rabbit.Plugin.Listing.Persist.Entry + diff --git a/lua/rabbit/luadoc/screen.lua b/lua/rabbit/luadoc/screen.lua index 7ca6ad8..0332db8 100644 --- a/lua/rabbit/luadoc/screen.lua +++ b/lua/rabbit/luadoc/screen.lua @@ -1,13 +1,15 @@ ---@class (exact) Rabbit.Screen.Border_Kwargs ---@field colors Rabbit.Options.Color ---@field border_color NvimHlKwargs | string ----@field width integer ----@field height integer ----@field emph_width integer ----@field box Rabbit.Box ----@field fullscreen boolean ----@field title string ----@field mode string +---@field width integer Window width +---@field height integer Window height +---@field emph_width integer Emphasis character width +---@field box Rabbit.Box Box style +---@field fullscreen boolean Full screen +---@field title string Window title +---@field mode string Plugin name +---@field pos_col integer Window position: column +---@field pos_row integer Window position: row ---@class Rabbit.Screen.Spec diff --git a/lua/rabbit/plugins/harpoon.lua b/lua/rabbit/plugins/harpoon.lua index 7a99aec..89860bf 100644 --- a/lua/rabbit/plugins/harpoon.lua +++ b/lua/rabbit/plugins/harpoon.lua @@ -26,11 +26,86 @@ local M = { ---@type Rabbit.Plugin ---@param p Rabbit.Plugin.Harpoon init = function(p) p.listing[0] = {} + p.listing.paths = {} p.listing.persist = set.clean(set.read(p.memory)) p.listing.opened = {} + p.listing.collections = {} + p.listing.recursive = nil end, + + ctx = {}, } + +---@param n integer +function M.func.group(n) + require("rabbit.input").prompt("Collection name", function(name) + n = math.max(1, math.min(#M.listing[0] + 1, n)) + if #M.listing.paths > 0 then + set.add(M.listing[0], "rabbitmsg://#up!\n" .. M._path()) + n = math.max(2, n) + end + + if string.find(name, "#up!") == 1 then + vim.print("That's a reserved name!") + return + end + + if set.index(M.listing[0], "rabbitmsg://" .. name) ~= nil then + vim.print("That name already exists!") + return + end + + table.insert(M.listing[0], n, "rabbitmsg://" .. name) + table.insert(M.listing.recursive, n, { name }) + + set.save(M.memory, M.listing.persist) + require("rabbit").Redraw() + end, function(name) + if name == "" then + return false + end + if string.find(name, "#up!") == 1 then + return false + end + if set.index(M.listing[0], "rabbitmsg://" .. name) ~= nil then + return false + end + return true + end) +end + + +---@param n integer +function M.func.select(n) + M.listing[0] = require("rabbit").ctx.listing + if string.find(M.listing[0][n], "rabbitmsg://") ~= 1 then + return require("rabbit").func.select(n) + end + + local entry = M.listing[0][n]:sub(#"rabbitmsg://" + 1) + + if string.find(entry, "#up!") == 1 then + table.remove(M.listing.paths, #M.listing.paths) + M.listing.recursive = M.listing.persist[vim.fn.getcwd()] + for _, v in ipairs(M.listing.paths) do + M.listing.recursive = M.listing.recursive[v] + end + else + table.insert(M.listing.paths, n) + local t = M.listing.recursive[n] + if type(t) == "table" then + M.listing.recursive = t + end + end + + M._generate() + if #M.listing.paths > 0 then + set.add(M.listing[0], "rabbitmsg://#up!\n" .. M._path()) + end + require("rabbit").Redraw() +end + ---@param evt NvimEvent ---@param winid integer function M.evt.BufEnter(evt, winid) @@ -52,19 +127,31 @@ end ---@param n integer function M.func.file_add(n) M.listing[0] = require("rabbit").ctx.listing - local cwd = vim.fn.getcwd() - M.listing.persist[cwd] = M.listing.persist[cwd] or {} - M.listing.opened[1] = M.listing.opened[1] or vim.api.nvim_buf_get_name(require("rabbit").user.buf) - if vim.uv.fs_stat(M.listing.opened[1] .. "") == nil then + local cur = M.listing.opened[1] or vim.api.nvim_buf_get_name(require("rabbit").user.buf) + M.listing.opened[1] = cur + + local collection = M.listing.collections[cur] + if collection == nil and vim.uv.fs_stat(tostring(cur)) == nil then return end - set.sub(M.listing.persist[cwd], M.listing.opened[1]) - set.sub(M.listing[0], M.listing.opened[1]) - n = math.max(1, math.min(#M.listing[0] + 1, n)) - table.insert(M.listing.persist[cwd], n, M.listing.opened[1]) - table.insert(M.listing[0], n, M.listing.opened[1]) + set.sub(M.listing.recursive, cur) + set.sub(M.listing[0], cur) + n = math.max((#M.listing.paths > 0 and 2 or 1), math.min(#M.listing[0] + 1, n)) + + if collection ~= nil then + for i, v in ipairs(M.listing.recursive) do + if v[1] == collection[1] then + table.remove(M.listing.recursive, i) + end + end + table.insert(M.listing.recursive, n, collection) + table.insert(M.listing[0], n, "rabbitmsg://" .. collection[1]) + else + table.insert(M.listing.recursive, n, cur) + table.insert(M.listing[0], n, cur) + end set.save(M.memory, M.listing.persist) require("rabbit").Redraw() end @@ -73,10 +160,30 @@ end ---@param n integer function M.func.file_del(n) M.listing[0] = require("rabbit").ctx.listing - local cwd = vim.fn.getcwd() - M.listing.opened[1] = M.listing[0][n] - set.sub(M.listing.persist[cwd], M.listing[0][n]) - set.sub(M.listing[0], M.listing[0][n]) + local entry = M.listing[0][n] + if entry == nil then + return + end + + if string.find(entry, "#up!") == #"rabbitmsg://" then + vim.print("That's a reserved name!") + return + end + + set.sub(M.listing[0], entry) + if string.find(entry, "rabbitmsg://") == 1 then + local t = M.listing.recursive[n] + if type(t) == "table" then + M.listing.collections[entry] = vim.deepcopy(t) + M.listing.opened[1] = entry + end + + table.remove(M.listing.recursive, n) + else + set.sub(M.listing.recursive, entry) + M.listing.opened[1] = entry + end + set.save(M.memory, M.listing.persist) require("rabbit").Redraw() end @@ -84,14 +191,58 @@ end ---@param winid integer function M.evt.RabbitEnter(winid) + M.listing.opened[1] = nil + M.ctx.winid = winid + if M.listing.recursive == nil then + local cwd = vim.fn.getcwd() + M.listing.persist[cwd] = M.listing.persist[cwd] or {} + M.listing.recursive = M.listing.persist[cwd] + end + M._generate() + if #M.listing.paths > 0 then + table.insert(M.listing[0], 1, "rabbitmsg://#up!\n" .. M._path()) + end +end + + +function M._generate() M.listing[0] = {} - local cwd = vim.fn.getcwd() - M.listing.persist[cwd] = M.listing.persist[cwd] or {} - for _, v in ipairs(M.listing.persist[cwd]) do - if not M.opts.ignore_opened or set.index(M.listing[winid], v) == nil then + for i, v in pairs(M.listing.recursive) do + if i == 1 and #M.listing.paths > 0 then + -- pass + elseif type(v) == "table" then + table.insert(M.listing[0], "rabbitmsg://" .. v[1]) + elseif not M.opts.ignore_opened or set.index(M.listing[M.ctx.winid], v) == nil then table.insert(M.listing[0], v) end end end + +---Returns the collection path +---@return string string +function M._path() + local s = (#M.listing.paths > 3 and require("rabbit").opts.window.overflow or "~") + local recur = M.listing.persist[vim.fn.getcwd()] + local l = require("rabbit").opts.window.path_len + for i = 1, #M.listing.paths do + recur = recur[M.listing.paths[i]] + if i > #M.listing.paths - 3 then + local a = recur[1] + if #a > l then + a = a:sub(1, l - 1) .. "…" + end + s = s .. "/" .. a + end + end + return s +end + + +function M.func.group_up() + if #M.listing.paths > 0 then + M.func.select(1) + end +end + return M diff --git a/lua/rabbit/plugins/util.lua b/lua/rabbit/plugins/util.lua index ac91cbb..0eec020 100644 --- a/lua/rabbit/plugins/util.lua +++ b/lua/rabbit/plugins/util.lua @@ -88,19 +88,37 @@ function M.clean(tbl) goto continue end - for key, file in pairs(ls) do - if type(key) == "number" and file == vim.NIL then - table.remove(tbl[dir], key) - elseif type(key) == "number" and vim.uv.fs_stat(file) == nil then - table.remove(tbl[dir], key) - elseif type(key) == "string" and vim.uv.fs_stat(key) == nil then - tbl[dir][key] = nil - end - end + M.clean_recursive(ls) ::continue:: end return tbl end +-- Removes references to deleted files and folders +---@param tbl Rabbit.Plugin.Listing.Persist.Table +---@return Rabbit.Plugin.Listing.Persist.Table +function M.clean_recursive(tbl) + for key, file in pairs(tbl) do + if type(key) == "number" and file == vim.NIL then + table.remove(tbl, key) + elseif type(key) == "number" and type(file) == "table" then + local name = file[1] + M.clean_recursive(file) + table.insert(file, 1, name) + elseif type(key) == "number" and vim.uv.fs_stat(file) == nil then + table.remove(tbl, key) + elseif type(key) == "table" then + local name = key.__name__ + M.clean_recursive(key) + key.__name__ = name + elseif type(key) == "string" and vim.uv.fs_stat(key) == nil then + if type(tbl.age) == "number" and type(tbl.count) == "number" then + tbl[key] = nil + end + end + end + return tbl +end + return M diff --git a/lua/rabbit/screen.lua b/lua/rabbit/screen.lua index 07cdf80..2ecac27 100644 --- a/lua/rabbit/screen.lua +++ b/lua/rabbit/screen.lua @@ -11,6 +11,7 @@ local screen = { bufnr = nil, winnr = nil, fullscreen = false, + in_input = false, } } @@ -46,8 +47,8 @@ function screen.helper(specs, width) end if spec.expand then - local char = type(spec.expand) == "string" and spec.expand or " " - spectext = ("" .. char):rep(width - vim.fn.strwidth(fulltext .. spectext)) .. spectext + local char = type(spec.expand) == "string" and tostring(spec.expand) or " " + spectext = (char):rep(width - vim.fn.strwidth(fulltext .. spectext)) .. spectext end if spec.color ~= nil and #(spec.color) > 1 then @@ -107,10 +108,11 @@ end ---@param c string | NvimHlKwargs +---@param key? string The default highlight key, eg `fg` or `bg` ---@return vim.api.keyset.highlight -local function maybe_hl(c) +local function maybe_hl(c, key) if type(c) == "string" then - return { fg = c } ---@type vim.api.keyset.highlight + return { [key or "fg"] = c } ---@type vim.api.keyset.highlight end ---@type vim.api.keyset.highlight return c