mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-05 11:18:26 +00:00
feat(picker): added undo
picker to navigate the undo tree. Closes #638
This commit is contained in:
parent
b0d3266985
commit
5c45f1c193
5 changed files with 189 additions and 14 deletions
|
@ -34,6 +34,7 @@ local M = {}
|
|||
---@field end_pos? {[1]:number, [2]:number}
|
||||
---@field highlights? snacks.picker.Highlight[][]
|
||||
---@field preview? snacks.picker.Item.preview
|
||||
---@field resolve? fun(item:snacks.picker.Item)
|
||||
|
||||
---@class snacks.picker.finder.Item: snacks.picker.Item
|
||||
---@field idx? number
|
||||
|
@ -253,6 +254,9 @@ local defaults = {
|
|||
middle = "├╴",
|
||||
last = "└╴",
|
||||
},
|
||||
undo = {
|
||||
saved = " ",
|
||||
},
|
||||
ui = {
|
||||
live = " ",
|
||||
hidden = "h",
|
||||
|
|
|
@ -42,6 +42,11 @@ Snacks.util.set_hl({
|
|||
BufNr = "Number",
|
||||
BufFlags = "NonText",
|
||||
KeymapRhs = "NonText",
|
||||
Time = "Special",
|
||||
UndoAdded = "Added",
|
||||
UndoRemoved = "Removed",
|
||||
UndoCurrent = "@variable.builtin",
|
||||
UndoSaved = "Special",
|
||||
GitCommit = "@variable.builtin",
|
||||
GitBreaking = "Error",
|
||||
GitBranch = "Title",
|
||||
|
|
|
@ -571,6 +571,13 @@ M.spelling = {
|
|||
confirm = "item_action",
|
||||
}
|
||||
|
||||
M.undo = {
|
||||
finder = "vim_undo",
|
||||
format = "undo",
|
||||
preview = "preview",
|
||||
confirm = "item_action",
|
||||
}
|
||||
|
||||
-- Open a project from zoxide
|
||||
M.zoxide = {
|
||||
finder = "files_zoxide",
|
||||
|
|
|
@ -151,24 +151,59 @@ function M.git_branch(item, picker)
|
|||
return ret
|
||||
end
|
||||
|
||||
function M.indent(item, picker)
|
||||
local ret = {} ---@type snacks.picker.Highlight[]
|
||||
local indents = picker.opts.icons.indent
|
||||
local indent = {} ---@type string[]
|
||||
local node = item
|
||||
while node and node.depth > 0 do
|
||||
local is_last, icon = node.last, ""
|
||||
if node ~= item then
|
||||
icon = is_last and " " or indents.vertical
|
||||
else
|
||||
icon = is_last and indents.last or indents.middle
|
||||
end
|
||||
table.insert(indent, 1, icon)
|
||||
node = node.parent
|
||||
end
|
||||
ret[#ret + 1] = { table.concat(indent), "SnacksPickerIndent" }
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.undo(item, picker)
|
||||
local ret = {} ---@type snacks.picker.Highlight[]
|
||||
local entry = item.item ---@type vim.fn.undotree.entry
|
||||
local a = Snacks.picker.util.align
|
||||
if item.current then
|
||||
ret[#ret + 1] = { a("", 2), "SnacksPickerUndoCurrent" }
|
||||
else
|
||||
ret[#ret + 1] = { a("", 2) }
|
||||
end
|
||||
vim.list_extend(ret, M.indent(item, picker))
|
||||
|
||||
ret[#ret + 1] = { a(tostring(entry.seq), 4), "SnacksPickerIdx" }
|
||||
ret[#ret + 1] = { " " }
|
||||
ret[#ret + 1] = { a(Snacks.picker.util.reltime(entry.time), 13), "SnacksPickerTime" }
|
||||
ret[#ret + 1] = { " " }
|
||||
local function num(v, prefix)
|
||||
v = v or 0
|
||||
return a((v and v > 0 and prefix .. v or ""), 4)
|
||||
end
|
||||
ret[#ret + 1] = { num(item.added, "+"), "SnacksPickerUndoAdded" }
|
||||
ret[#ret + 1] = { " " }
|
||||
ret[#ret + 1] = { num(item.removed, "-"), "SnacksPickerUndoRemoved" }
|
||||
if entry.save then
|
||||
ret[#ret + 1] = { " " }
|
||||
ret[#ret + 1] = { a(picker.opts.icons.undo.saved, 2), "SnacksPickerUndoSaved" }
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.lsp_symbol(item, picker)
|
||||
local opts = picker.opts --[[@as snacks.picker.lsp.symbols.Config]]
|
||||
local ret = {} ---@type snacks.picker.Highlight[]
|
||||
if item.hierarchy and not opts.workspace then
|
||||
local indents = picker.opts.icons.indent
|
||||
local indent = {} ---@type string[]
|
||||
local node = item
|
||||
while node and node.depth > 0 do
|
||||
local is_last, icon = node.last, ""
|
||||
if node ~= item then
|
||||
icon = is_last and " " or indents.vertical
|
||||
else
|
||||
icon = is_last and indents.last or indents.middle
|
||||
end
|
||||
table.insert(indent, 1, icon)
|
||||
node = node.parent
|
||||
end
|
||||
ret[#ret + 1] = { table.concat(indent), "SnacksPickerIndent" }
|
||||
vim.list_extend(ret, M.indent(item, picker))
|
||||
end
|
||||
local kind = item.kind or "Unknown" ---@type string
|
||||
local kind_hl = "SnacksPickerIcon" .. kind
|
||||
|
|
|
@ -323,4 +323,128 @@ function M.spelling()
|
|||
return items
|
||||
end
|
||||
|
||||
---@type snacks.picker.finder
|
||||
function M.undo()
|
||||
local tree = vim.fn.undotree()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
local view = vim.api.nvim_win_call(win, vim.fn.winsaveview)
|
||||
local items = {} ---@type snacks.picker.finder.Item[]
|
||||
|
||||
---@param entries vim.fn.undotree.entry[]
|
||||
---@param parent? snacks.picker.finder.Item
|
||||
local function add(entries, parent)
|
||||
entries = entries or {}
|
||||
table.sort(entries, function(a, b)
|
||||
return a.seq > b.seq
|
||||
end)
|
||||
for e, entry in ipairs(entries) do
|
||||
local file = vim.api.nvim_buf_get_name(buf)
|
||||
---@param item snacks.picker.finder.Item
|
||||
local function resolve(item)
|
||||
---@type string[], string[]
|
||||
local before, after = {}, {}
|
||||
|
||||
local ei = vim.o.eventignore
|
||||
vim.o.eventignore = "all"
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
-- state after the undo
|
||||
vim.cmd("noautocmd silent undo " .. entry.seq)
|
||||
after = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
-- state before the undo
|
||||
vim.cmd("noautocmd silent undo")
|
||||
before = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
end)
|
||||
vim.o.eventignore = ei
|
||||
|
||||
local diff = vim.diff(table.concat(before, "\n"), table.concat(after, "\n"), { ctxlen = 4 }) --[[@as string]]
|
||||
local changes = {} ---@type string[]
|
||||
local added, removed = 0, 0
|
||||
|
||||
for _, line in ipairs(vim.split(diff, "\n")) do
|
||||
if line:sub(1, 1) == "+" then
|
||||
added = added + 1
|
||||
changes[#changes + 1] = line:sub(2)
|
||||
elseif line:sub(1, 1) == "-" then
|
||||
removed = removed + 1
|
||||
changes[#changes + 1] = line:sub(2)
|
||||
end
|
||||
end
|
||||
diff = Snacks.picker.util.tpl(
|
||||
"diff --git a/{file} b/{file}\n--- {file}\n+++ {file}\n{diff}",
|
||||
{ file = vim.fn.fnamemodify(file, ":."), diff = diff }
|
||||
)
|
||||
item.text = table.concat(changes, " ")
|
||||
item.added = added
|
||||
item.removed = removed
|
||||
item.preview = {
|
||||
text = diff,
|
||||
ft = "diff",
|
||||
}
|
||||
end
|
||||
|
||||
local item = {
|
||||
buf = buf,
|
||||
resolve = resolve,
|
||||
file = file,
|
||||
item = entry,
|
||||
current = entry.seq == tree.seq_cur,
|
||||
last = e == #entries,
|
||||
parent = parent,
|
||||
depth = parent and parent.depth + 1 or 1,
|
||||
action = function()
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
vim.cmd("undo " .. entry.seq)
|
||||
end)
|
||||
end,
|
||||
}
|
||||
items[#items + 1] = item
|
||||
add(entry.alt, item)
|
||||
end
|
||||
end
|
||||
add(tree.entries)
|
||||
|
||||
-- disable folding to prevent fold re-calculations
|
||||
local foldmethod = vim.wo[win].foldmethod
|
||||
vim.wo[win].foldmethod = "manual"
|
||||
vim.b[buf].snacks_scroll = false
|
||||
|
||||
local function restore()
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
-- reset to the original state
|
||||
vim.cmd("noautocmd silent undo " .. tree.seq_cur)
|
||||
vim.wo[win].foldmethod = foldmethod
|
||||
end)
|
||||
vim.api.nvim_win_call(win, function()
|
||||
vim.fn.winrestview(view)
|
||||
end)
|
||||
vim.b[buf].snacks_scroll = nil
|
||||
end
|
||||
|
||||
-- Resolve the items in batches to prevent blocking the UI
|
||||
---@param cb async fun(item: snacks.picker.finder.Item)
|
||||
---@param task snacks.picker.Async
|
||||
---@async
|
||||
return function(cb, task)
|
||||
task:on("abort", vim.schedule_wrap(restore))
|
||||
|
||||
while #items > 0 do
|
||||
vim.schedule(function()
|
||||
local count = 0
|
||||
while #items > 0 and count < 5 do
|
||||
count = count + 1
|
||||
local item = table.remove(items, 1)
|
||||
Snacks.picker.util.resolve(item)
|
||||
cb(item)
|
||||
end
|
||||
if #items == 0 then
|
||||
restore()
|
||||
end
|
||||
task:resume()
|
||||
end)
|
||||
task:suspend()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue