mirror of
https://github.com/folke/snacks.nvim
synced 2025-12-23 08:47:57 +00:00
feat(picker.diff): moved git_diff finder to separate file so it can be re-used + made it more robust. Closes #2366
This commit is contained in:
parent
f2f05e5cff
commit
3049ad8beb
3 changed files with 164 additions and 54 deletions
|
|
@ -334,8 +334,10 @@ M.git_status = {
|
|||
}
|
||||
|
||||
---@class snacks.picker.git.diff.Config: snacks.picker.git.Config
|
||||
---@field group? boolean group changes by file (when false, show individual hunks)
|
||||
---@field base? string base commit/branch/tag to diff against (default: HEAD)
|
||||
M.git_diff = {
|
||||
group = true,
|
||||
finder = "git_diff",
|
||||
format = "file",
|
||||
preview = "diff",
|
||||
|
|
|
|||
152
lua/snacks/picker/source/diff.lua
Normal file
152
lua/snacks/picker/source/diff.lua
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
local M = {}
|
||||
|
||||
---@class snacks.picker.diff.Config: snacks.picker.proc.Config
|
||||
---@field cmd? string optional since diff can be passed as string
|
||||
---@field group? boolean Group hunks by file
|
||||
---@field diff? string|number diff string or buffer number
|
||||
|
||||
---@class snacks.picker.diff.Hunk
|
||||
---@field diff string[]
|
||||
---@field line number
|
||||
|
||||
---@class snacks.picker.diff.Block
|
||||
---@field file string
|
||||
---@field header string[]
|
||||
---@field hunks snacks.picker.diff.Hunk[]
|
||||
|
||||
---@param opts? snacks.picker.diff.Config
|
||||
---@type snacks.picker.finder
|
||||
function M.diff(opts, ctx)
|
||||
opts = opts or {}
|
||||
local lines = {} ---@type string[]
|
||||
local finder ---@type snacks.picker.finder.result?
|
||||
|
||||
do
|
||||
if opts.cmd then
|
||||
finder = require("snacks.picker.source.proc").proc(opts, ctx)
|
||||
else
|
||||
local diff = opts.diff
|
||||
if not diff and vim.bo.filetype == "diff" then
|
||||
diff = 0
|
||||
end
|
||||
if type(diff) == "number" then
|
||||
lines = vim.api.nvim_buf_get_lines(diff, 0, -1, false)
|
||||
elseif type(diff) == "string" then
|
||||
lines = vim.split(diff, "\n", { plain = true })
|
||||
else
|
||||
Snacks.notify.error("snacks.picker.diff: opts.diff must be a string or buffer number")
|
||||
return {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local cwd = ctx.filter.cwd
|
||||
return function(cb)
|
||||
if finder then
|
||||
finder(function(proc_item)
|
||||
lines[#lines + 1] = proc_item.text
|
||||
end)
|
||||
end
|
||||
|
||||
---@param file string
|
||||
---@param line? number
|
||||
---@param diff string[]
|
||||
local function add(file, line, diff)
|
||||
line = line or 1
|
||||
cb({
|
||||
text = file .. ":" .. line,
|
||||
diff = table.concat(diff, "\n"),
|
||||
file = file,
|
||||
cwd = cwd,
|
||||
pos = { line, 0 },
|
||||
})
|
||||
end
|
||||
|
||||
local blocks = M.parse(lines)
|
||||
for _, block in ipairs(blocks) do
|
||||
local diff = {} ---@type string[]
|
||||
for _, h in ipairs(block.hunks) do
|
||||
if opts.group then
|
||||
vim.list_extend(diff, h.diff)
|
||||
else
|
||||
add(block.file, h.line, vim.list_extend(vim.deepcopy(block.header), h.diff))
|
||||
end
|
||||
end
|
||||
if opts.group or #block.hunks == 0 then
|
||||
local line = block.hunks[1] and block.hunks[1].line or 1
|
||||
add(block.file, line, vim.list_extend(vim.deepcopy(block.header), diff))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param lines string[]
|
||||
function M.parse(lines)
|
||||
local hunk ---@type snacks.picker.diff.Hunk?
|
||||
local block ---@type snacks.picker.diff.Block?
|
||||
local ret = {} ---@type snacks.picker.diff.Block[]
|
||||
|
||||
local function emit()
|
||||
if block and hunk then
|
||||
hunk = nil
|
||||
end
|
||||
if block then
|
||||
table.sort(block.hunks, function(a, b)
|
||||
return a.line < b.line
|
||||
end)
|
||||
ret[#ret + 1] = block
|
||||
block = nil
|
||||
end
|
||||
end
|
||||
|
||||
for _, text in ipairs(lines) do
|
||||
if not block and text:find("^%s*$") then
|
||||
-- Ignore empty lines before a diff block
|
||||
elseif text:find("^diff") or (text:find("^%-%-%- ") and (not block or hunk)) then
|
||||
emit()
|
||||
local file ---@type string?
|
||||
if text:find("^diff") then
|
||||
local parsed = Snacks.picker.util.parse_cmdline(text)
|
||||
for i = 2, #parsed do
|
||||
local arg = parsed[i]
|
||||
if not arg:find("^%-") then
|
||||
file = arg:match("^%a/(.*)") or arg
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif text:find("^%-%-%-") then
|
||||
file = text:match("^%-%-%- %a/([^\t]+)") or text:match("^%-%-%- ([^\t]+)")
|
||||
end
|
||||
block = {
|
||||
file = file or "unknown",
|
||||
header = { text },
|
||||
hunks = {},
|
||||
}
|
||||
elseif text:find("@@", 1, true) == 1 and block then
|
||||
-- Hunk header
|
||||
local line = 1
|
||||
if text:find("@@@", 1, true) == 1 then
|
||||
line = tonumber(text:match("^@@@ %-%d+,?%d* %-%d+,?%d* %+(%d+),?%d* @@@")) or 1
|
||||
else
|
||||
line = tonumber(text:match("^@@ %-%d+,?%d* %+(%d+),?%d* @@")) or 1
|
||||
end
|
||||
hunk = {
|
||||
line = line,
|
||||
diff = { text },
|
||||
}
|
||||
block.hunks[#block.hunks + 1] = hunk
|
||||
elseif hunk then
|
||||
-- Hunk body
|
||||
hunk.diff[#hunk.diff + 1] = text
|
||||
elseif block then
|
||||
-- File header
|
||||
block.header[#block.header + 1] = text
|
||||
else
|
||||
Snacks.notify.error("unexpected line: " .. text, { title = "Snacks Picker Diff" })
|
||||
end
|
||||
end
|
||||
emit()
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -256,63 +256,19 @@ end
|
|||
---@param opts snacks.picker.git.diff.Config
|
||||
---@type snacks.picker.finder
|
||||
function M.diff(opts, ctx)
|
||||
local args =
|
||||
M.git("diff", "--no-color", "--no-ext-diff", { args = { "-c", "diff.noprefix=false", "--no-pager" } }, opts)
|
||||
opts = opts or {}
|
||||
local args = M.git("diff", "--no-color", "--no-ext-diff", "--default-prefix", { args = { "--no-pager" } }, opts)
|
||||
if opts.base then
|
||||
vim.list_extend(args, { "--merge-base", opts.base })
|
||||
end
|
||||
local file, line ---@type string?, number?
|
||||
local header, hunk = {}, {} ---@type string[], string[]
|
||||
local header_len = 4
|
||||
|
||||
local cwd = svim.fs.normalize(opts and opts.cwd or uv.cwd() or ".") or nil
|
||||
cwd = Snacks.git.get_root(cwd) or cwd
|
||||
|
||||
local finder = require("snacks.picker.source.proc").proc({
|
||||
opts,
|
||||
{ cmd = "git", args = args, cwd = cwd },
|
||||
}, ctx)
|
||||
return function(cb)
|
||||
local function add()
|
||||
if file and line and #hunk > 0 then
|
||||
local diff = table.concat(header, "\n") .. "\n" .. table.concat(hunk, "\n")
|
||||
cb({
|
||||
text = file .. ":" .. line,
|
||||
diff = diff,
|
||||
file = file,
|
||||
cwd = cwd,
|
||||
pos = { line, 0 },
|
||||
preview = { text = diff, ft = "diff", loc = false },
|
||||
})
|
||||
end
|
||||
hunk = {}
|
||||
end
|
||||
finder(function(proc_item)
|
||||
local text = proc_item.text
|
||||
if text:find("diff", 1, true) == 1 then
|
||||
add()
|
||||
file = text:match("^diff .* a/(.*) b/.*$")
|
||||
header = { text }
|
||||
header_len = 4
|
||||
elseif file and #header < header_len then
|
||||
if text:find("^deleted file") then
|
||||
header_len = 5
|
||||
end
|
||||
header[#header + 1] = text
|
||||
elseif text:find("@", 1, true) == 1 then
|
||||
add()
|
||||
-- Hunk header
|
||||
-- @example "@@ -157,20 +157,6 @@ some content"
|
||||
line = tonumber(string.match(text, "@@ %-.*,.* %+(.*),.* @@"))
|
||||
hunk = { text }
|
||||
elseif #hunk > 0 then
|
||||
hunk[#hunk + 1] = text
|
||||
else
|
||||
error("unexpected line: " .. text)
|
||||
end
|
||||
end)
|
||||
add()
|
||||
end
|
||||
return require("snacks.picker.source.diff").diff(
|
||||
ctx:opts({
|
||||
cmd = "git",
|
||||
args = args,
|
||||
cwd = ctx:git_root(),
|
||||
}),
|
||||
ctx
|
||||
)
|
||||
end
|
||||
|
||||
---@param opts snacks.picker.git.branches.Config
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue