mirror of
https://github.com/folke/snacks.nvim
synced 2025-12-23 08:47:57 +00:00
fix(picker.diff): parse full diff including diff and hunk headers. Closes #2429
Some checks are pending
CI / ci (push) Waiting to run
Some checks are pending
CI / ci (push) Waiting to run
This commit is contained in:
parent
2e2cf50514
commit
53d8012e5e
2 changed files with 83 additions and 27 deletions
|
|
@ -564,7 +564,7 @@ function M.git_status(item, picker)
|
|||
local status = item.status
|
||||
if not status and item.block then
|
||||
local block = item.block ---@type snacks.picker.diff.Block
|
||||
status = ({ new = "A", delete = "D", rename = "R", copy = "C" })[block.type] or "M"
|
||||
status = block.new and "A" or block.delete and "D" or block.rename and "R" or block.copy and "C" or "M"
|
||||
status = block.unmerged and (status .. status) or item.staged and (status .. " ") or (" " .. status)
|
||||
elseif not status then
|
||||
return M.filename(item, picker)
|
||||
|
|
|
|||
|
|
@ -8,15 +8,26 @@ local M = {}
|
|||
---@class snacks.picker.diff.Hunk
|
||||
---@field diff string[]
|
||||
---@field line number
|
||||
---@field context? string
|
||||
---@field left { line: number, count: number } old (normal) /ours (merge)
|
||||
---@field right { line: number, count: number } new (normal) /working (merge)
|
||||
---@field parents? { line: number, count: number }[] theirs (merge)
|
||||
|
||||
---@class snacks.picker.diff.Block
|
||||
---@field type? "new"|"delete"|"rename"|"copy"|"mode"
|
||||
---@field unmerged? boolean
|
||||
---@field file string
|
||||
---@field left? string
|
||||
---@field right? string
|
||||
---@field header string[]
|
||||
---@field hunks snacks.picker.diff.Hunk[]
|
||||
---@field mode? {from:string, to:string}
|
||||
---@field copy? {from:string, to:string}
|
||||
---@field rename? {from:string, to:string}
|
||||
---@field delete? string (mode of deleted file)
|
||||
---@field new? string (mode of new file)
|
||||
---@field similarity? number
|
||||
---@field dissimilarity? number
|
||||
---@field index? {from:string, to:string, mode:string}
|
||||
|
||||
---@param opts? snacks.picker.diff.Config
|
||||
---@type snacks.picker.finder
|
||||
|
|
@ -63,7 +74,7 @@ function M.diff(opts, ctx)
|
|||
diff = table.concat(diff, "\n"),
|
||||
file = file,
|
||||
cwd = cwd,
|
||||
rename = block.type == "rename" and block.left or nil,
|
||||
rename = block.rename and block.rename.from or nil,
|
||||
block = block,
|
||||
pos = { line, 0 },
|
||||
})
|
||||
|
|
@ -94,14 +105,18 @@ function M.parse(lines)
|
|||
local ret = {} ---@type snacks.picker.diff.Block[]
|
||||
|
||||
---@param file? string
|
||||
---@param strip_prefix? boolean
|
||||
---@return string?
|
||||
local function norm(file)
|
||||
local function norm(file, strip_prefix)
|
||||
if file then
|
||||
file = file:gsub("\t.*$", "") -- remove tab and after
|
||||
file = file:gsub('^"(.-)"$', "%1") -- remove quotes
|
||||
if file == "/dev/null" then -- no file
|
||||
return
|
||||
end
|
||||
if strip_prefix == false then
|
||||
return file
|
||||
end
|
||||
local prefix = { "a", "b", "i", "w", "c", "o", "old", "new" }
|
||||
for _, s in ipairs(prefix) do -- remove prefixes
|
||||
if file:sub(1, #s + 1) == s .. "/" then
|
||||
|
|
@ -124,23 +139,40 @@ function M.parse(lines)
|
|||
elseif line:find("^%+%+%+ ") then
|
||||
block.right = norm(line:sub(5))
|
||||
elseif line:find("^rename from") then
|
||||
block.type = "rename"
|
||||
block.left = norm(line:match("^rename from (.*)"))
|
||||
block.rename = block.rename or {}
|
||||
block.left = norm(line:match("^rename from (.*)"), false)
|
||||
block.rename.from = block.left
|
||||
elseif line:find("^rename to") then
|
||||
block.type = "rename"
|
||||
block.right = norm(line:match("^rename to (.*)"))
|
||||
block.rename = block.rename or {}
|
||||
block.right = norm(line:match("^rename to (.*)"), false)
|
||||
block.rename.to = block.right
|
||||
elseif line:find("^copy from") then
|
||||
block.type = "copy"
|
||||
block.left = norm(line:match("^copy from (.*)"))
|
||||
block.copy = block.copy or {}
|
||||
block.left = norm(line:match("^copy from (.*)"), false)
|
||||
block.copy.from = block.left
|
||||
elseif line:find("^copy to") then
|
||||
block.type = "copy"
|
||||
block.right = norm(line:match("^copy to (.*)"))
|
||||
block.copy = block.copy or {}
|
||||
block.right = norm(line:match("^copy to (.*)"), false)
|
||||
block.copy.to = block.right
|
||||
elseif line:find("^new file mode") then
|
||||
block.type = "new"
|
||||
block.new = line:match("^new file mode (.*)")
|
||||
elseif line:find("^deleted file mode") then
|
||||
block.type = "delete"
|
||||
elseif line:find("^old mode") or line:find("^new mode") then
|
||||
block.type = "mode"
|
||||
block.delete = line:match("^deleted file mode (.*)")
|
||||
elseif line:find("^old mode") then
|
||||
block.mode = block.mode or {}
|
||||
block.mode.from = line:match("^old mode (.*)")
|
||||
elseif line:find("^new mode") then
|
||||
block.mode = block.mode or {}
|
||||
block.mode.to = line:match("^new mode (.*)")
|
||||
elseif line:find("^similarity index") then
|
||||
local sim = line:match("^similarity index (%d+)%%")
|
||||
block.similarity = tonumber(sim) or 0
|
||||
elseif line:find("^dissimilarity index") then
|
||||
local dis = line:match("^dissimilarity index (%d+)%%")
|
||||
block.dissimilarity = tonumber(dis) or 0
|
||||
elseif line:find("^index ") then
|
||||
local from, to, mode = line:match("^index (%S+)%.%.(%S+)%s*(%d*)$")
|
||||
block.index = { from = from, to = to, mode = mode ~= "" and mode or nil }
|
||||
end
|
||||
end
|
||||
local first = block.header[1] or ""
|
||||
|
|
@ -191,29 +223,53 @@ function M.parse(lines)
|
|||
}
|
||||
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
|
||||
block.unmerged = true
|
||||
hunk = M.parse_hunk_header(text)
|
||||
if hunk then
|
||||
block.unmerged = block.unmerged or (hunk.parents ~= nil) or nil
|
||||
block.hunks[#block.hunks + 1] = hunk
|
||||
else
|
||||
line = tonumber(text:match("^@@ %-%d+,?%d* %+(%d+),?%d* @@")) or 1
|
||||
Snacks.notify.error("Invalid hunk header: " .. text, { title = "Snacks Picker Diff" })
|
||||
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
|
||||
block.header[#block.header + 1] = text
|
||||
else
|
||||
Snacks.notify.error("unexpected line: " .. text, { title = "Snacks Picker Diff" })
|
||||
Snacks.notify.error("Unexpected line: " .. text, { title = "Snacks Picker Diff" })
|
||||
end
|
||||
end
|
||||
emit()
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param line string
|
||||
function M.parse_hunk_header(line)
|
||||
local count_start, inner, count_end, context = line:match("^(@+)%s*(.-)%s*(@+)%s*(.*)$")
|
||||
if not count_start or not count_end or count_start ~= count_end or #count_start < 2 then
|
||||
return
|
||||
end
|
||||
local ret = {} ---@type {line:number, count:number}[]
|
||||
for _, part in ipairs(vim.split(inner, "%s+")) do
|
||||
local l, c = part:match("^[%-+](%d+),?(%d*)$")
|
||||
if not l then
|
||||
return
|
||||
end
|
||||
ret[#ret + 1] = { line = tonumber(l) or 1, count = tonumber(c) or 1 }
|
||||
end
|
||||
if #ret ~= #count_start then
|
||||
return
|
||||
end
|
||||
local right = table.remove(ret)
|
||||
---@type snacks.picker.diff.Hunk
|
||||
return {
|
||||
diff = { line },
|
||||
line = right and right.line or 1,
|
||||
left = table.remove(ret, 1),
|
||||
right = right,
|
||||
parents = #ret > 0 and ret or nil,
|
||||
context = context ~= "" and context or nil,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue