mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-06 03:38:16 +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 end_pos? {[1]:number, [2]:number}
|
||||||
---@field highlights? snacks.picker.Highlight[][]
|
---@field highlights? snacks.picker.Highlight[][]
|
||||||
---@field preview? snacks.picker.Item.preview
|
---@field preview? snacks.picker.Item.preview
|
||||||
|
---@field resolve? fun(item:snacks.picker.Item)
|
||||||
|
|
||||||
---@class snacks.picker.finder.Item: snacks.picker.Item
|
---@class snacks.picker.finder.Item: snacks.picker.Item
|
||||||
---@field idx? number
|
---@field idx? number
|
||||||
|
@ -253,6 +254,9 @@ local defaults = {
|
||||||
middle = "├╴",
|
middle = "├╴",
|
||||||
last = "└╴",
|
last = "└╴",
|
||||||
},
|
},
|
||||||
|
undo = {
|
||||||
|
saved = " ",
|
||||||
|
},
|
||||||
ui = {
|
ui = {
|
||||||
live = " ",
|
live = " ",
|
||||||
hidden = "h",
|
hidden = "h",
|
||||||
|
|
|
@ -42,6 +42,11 @@ Snacks.util.set_hl({
|
||||||
BufNr = "Number",
|
BufNr = "Number",
|
||||||
BufFlags = "NonText",
|
BufFlags = "NonText",
|
||||||
KeymapRhs = "NonText",
|
KeymapRhs = "NonText",
|
||||||
|
Time = "Special",
|
||||||
|
UndoAdded = "Added",
|
||||||
|
UndoRemoved = "Removed",
|
||||||
|
UndoCurrent = "@variable.builtin",
|
||||||
|
UndoSaved = "Special",
|
||||||
GitCommit = "@variable.builtin",
|
GitCommit = "@variable.builtin",
|
||||||
GitBreaking = "Error",
|
GitBreaking = "Error",
|
||||||
GitBranch = "Title",
|
GitBranch = "Title",
|
||||||
|
|
|
@ -571,6 +571,13 @@ M.spelling = {
|
||||||
confirm = "item_action",
|
confirm = "item_action",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
M.undo = {
|
||||||
|
finder = "vim_undo",
|
||||||
|
format = "undo",
|
||||||
|
preview = "preview",
|
||||||
|
confirm = "item_action",
|
||||||
|
}
|
||||||
|
|
||||||
-- Open a project from zoxide
|
-- Open a project from zoxide
|
||||||
M.zoxide = {
|
M.zoxide = {
|
||||||
finder = "files_zoxide",
|
finder = "files_zoxide",
|
||||||
|
|
|
@ -151,24 +151,59 @@ function M.git_branch(item, picker)
|
||||||
return ret
|
return ret
|
||||||
end
|
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)
|
function M.lsp_symbol(item, picker)
|
||||||
local opts = picker.opts --[[@as snacks.picker.lsp.symbols.Config]]
|
local opts = picker.opts --[[@as snacks.picker.lsp.symbols.Config]]
|
||||||
local ret = {} ---@type snacks.picker.Highlight[]
|
local ret = {} ---@type snacks.picker.Highlight[]
|
||||||
if item.hierarchy and not opts.workspace then
|
if item.hierarchy and not opts.workspace then
|
||||||
local indents = picker.opts.icons.indent
|
vim.list_extend(ret, M.indent(item, picker))
|
||||||
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" }
|
|
||||||
end
|
end
|
||||||
local kind = item.kind or "Unknown" ---@type string
|
local kind = item.kind or "Unknown" ---@type string
|
||||||
local kind_hl = "SnacksPickerIcon" .. kind
|
local kind_hl = "SnacksPickerIcon" .. kind
|
||||||
|
|
|
@ -323,4 +323,128 @@ function M.spelling()
|
||||||
return items
|
return items
|
||||||
end
|
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
|
return M
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue