mirror of
https://github.com/folke/snacks.nvim
synced 2025-12-23 08:47:57 +00:00
parent
8bb3ad6c53
commit
b6e4eb7e60
2 changed files with 194 additions and 26 deletions
|
|
@ -10,7 +10,10 @@ local M = {}
|
|||
---@field line number
|
||||
|
||||
---@class snacks.picker.diff.Block
|
||||
---@field type? "new"|"delete"|"rename"|"copy"|"mode"
|
||||
---@field file string
|
||||
---@field left? string
|
||||
---@field right? string
|
||||
---@field header string[]
|
||||
---@field hunks snacks.picker.diff.Hunk[]
|
||||
|
||||
|
|
@ -86,17 +89,88 @@ function M.parse(lines)
|
|||
local block ---@type snacks.picker.diff.Block?
|
||||
local ret = {} ---@type snacks.picker.diff.Block[]
|
||||
|
||||
---@param file? string
|
||||
---@return string?
|
||||
local function norm(file)
|
||||
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
|
||||
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
|
||||
return file:sub(#s + 2)
|
||||
end
|
||||
end
|
||||
return file
|
||||
end
|
||||
end
|
||||
|
||||
local function emit()
|
||||
if block and hunk then
|
||||
hunk = nil
|
||||
elseif not block then
|
||||
return
|
||||
end
|
||||
if block then
|
||||
table.sort(block.hunks, function(a, b)
|
||||
return a.line < b.line
|
||||
end)
|
||||
ret[#ret + 1] = block
|
||||
block = nil
|
||||
for _, line in ipairs(block.header) do
|
||||
if line:find("^%-%-%- ") then
|
||||
block.left = norm(line:sub(5))
|
||||
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 (.*)"))
|
||||
elseif line:find("^rename to") then
|
||||
block.type = "rename"
|
||||
block.right = norm(line:match("^rename to (.*)"))
|
||||
elseif line:find("^copy from") then
|
||||
block.type = "copy"
|
||||
block.left = norm(line:match("^copy from (.*)"))
|
||||
elseif line:find("^copy to") then
|
||||
block.type = "copy"
|
||||
block.right = norm(line:match("^copy to (.*)"))
|
||||
elseif line:find("^new file mode") then
|
||||
block.type = "new"
|
||||
elseif line:find("^deleted file mode") then
|
||||
block.type = "delete"
|
||||
elseif line:find("^old mode") or line:find("^new mode") then
|
||||
block.type = "mode"
|
||||
end
|
||||
end
|
||||
local first = block.header[1] or ""
|
||||
if not block.right and not block.left and first:find("^diff") then
|
||||
-- no left/right so for sure no rename.
|
||||
-- this means the diff header is for the same file
|
||||
if first:find("^diff %-%-cc") then
|
||||
block.left = norm(first:match("^diff %-%-cc (.+)$"))
|
||||
block.right = block.left
|
||||
else
|
||||
first = first:gsub("^diff ", ""):gsub("^%s*%-%S+%s*", "") --[[@as string]]
|
||||
local idx = 1
|
||||
while idx <= #first do
|
||||
local s = first:find(" ", idx, true)
|
||||
if not s then
|
||||
break
|
||||
end
|
||||
idx = s + 1
|
||||
local l = norm(first:sub(1, s - 1))
|
||||
local r = norm(first:sub(s + 1))
|
||||
if l == r then
|
||||
block.left = l
|
||||
block.right = r
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
block.file = block.right or block.left or block.file
|
||||
table.sort(block.hunks, function(a, b)
|
||||
return a.line < b.line
|
||||
end)
|
||||
ret[#ret + 1] = block
|
||||
block = nil
|
||||
end
|
||||
|
||||
local with_diff_header = vim.trim(table.concat(lines, "\n")):find("^diff") ~= nil
|
||||
|
|
@ -106,15 +180,8 @@ function M.parse(lines)
|
|||
-- Ignore empty lines before a diff block
|
||||
elseif text:find("^diff") or (not with_diff_header and text:find("^%-%-%- ") and (not block or hunk)) then
|
||||
emit()
|
||||
local file ---@type string?
|
||||
if text:find("^diff") then
|
||||
file = text:gsub("^diff%s*", ""):gsub("^%-%S+%s*", "")
|
||||
file = file:match('^"%a/(.-)"') or file:match("^%a/(.-) %a/") or file:match("^%a/(.*)$") or file
|
||||
elseif text:find("^%-%-%-") then
|
||||
file = text:match("^%-%-%- %a/([^\t]+)") or text:match("^%-%-%- ([^\t]+)")
|
||||
end
|
||||
block = {
|
||||
file = file or "unknown",
|
||||
file = "", --file or "unknown",
|
||||
header = { text },
|
||||
hunks = {},
|
||||
}
|
||||
|
|
@ -135,7 +202,6 @@ function M.parse(lines)
|
|||
-- 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" })
|
||||
|
|
|
|||
|
|
@ -23,6 +23,29 @@ describe("picker.diff", function()
|
|||
assert.equals(4, #blocks[1].hunks[1].diff)
|
||||
end)
|
||||
|
||||
it("doesn't parse a filename from deleted lua comment", function()
|
||||
local lines = {
|
||||
"diff --git a/lua/todo-comments/config.lua b/lua/todo-comments/config.lua",
|
||||
"index 0e2d34e..a8e1077 100644",
|
||||
"--- a/lua/todo-comments/config.lua",
|
||||
"+++ b/lua/todo-comments/config.lua",
|
||||
"@@ -11,7 +11,6 @@ M.loaded = false",
|
||||
' M.ns = vim.api.nvim_create_namespace("todo-comments")',
|
||||
"",
|
||||
" --- @class TodoOptions",
|
||||
"--- TODO: add support for markdown todos",
|
||||
" local defaults = {",
|
||||
" signs = true, -- show icons in the signs column",
|
||||
" sign_priority = 8, -- sign priority",
|
||||
" }",
|
||||
" end)",
|
||||
"",
|
||||
}
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(1, #blocks)
|
||||
assert.equals("lua/todo-comments/config.lua", blocks[1].file)
|
||||
end)
|
||||
|
||||
it("parses plain diff format (no git header)", function()
|
||||
local lines = {
|
||||
"--- file1.txt\t2024-01-01 12:00:00",
|
||||
|
|
@ -35,12 +58,42 @@ describe("picker.diff", function()
|
|||
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(1, #blocks)
|
||||
assert.equals("file1.txt", blocks[1].file)
|
||||
assert.equals("file2.txt", blocks[1].file)
|
||||
assert.equals(2, #blocks[1].header)
|
||||
assert.equals(1, #blocks[1].hunks)
|
||||
assert.equals(1, blocks[1].hunks[1].line)
|
||||
end)
|
||||
|
||||
it("parses plain diff format (recursive)", function()
|
||||
local lines = {
|
||||
"diff -Naur old/file1.txt new/file1.txt",
|
||||
"--- old/file1.txt 2025-01-01 13:00:00.000000000 +0100",
|
||||
"+++ new/file1.txt 1970-01-01 01:00:00.000000000 +0100",
|
||||
"@@ -1,3 +0,0 @@",
|
||||
"-context1",
|
||||
"-old content",
|
||||
"-context3",
|
||||
"diff -Naur old/file2.txt new/file2.txt",
|
||||
"--- old/file2.txt 1970-01-01 01:00:00.000000000 +0100",
|
||||
"+++ new/file2.txt 2025-01-01 13:00:00.000000000 +0100",
|
||||
"@@ -0,0 +1,3 @@",
|
||||
"+context1",
|
||||
"+new line",
|
||||
"+context3",
|
||||
}
|
||||
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(2, #blocks)
|
||||
assert.equals(3, #blocks[1].header)
|
||||
assert.equals("file1.txt", blocks[1].file)
|
||||
assert.equals(1, #blocks[1].hunks)
|
||||
assert.equals(0, blocks[1].hunks[1].line)
|
||||
assert.equals(3, #blocks[2].header)
|
||||
assert.equals("file2.txt", blocks[2].file)
|
||||
assert.equals(1, #blocks[2].hunks)
|
||||
assert.equals(1, blocks[2].hunks[1].line)
|
||||
end)
|
||||
|
||||
it("parses combined diff format (merge commits)", function()
|
||||
local lines = {
|
||||
"diff --cc file.txt",
|
||||
|
|
@ -134,6 +187,20 @@ describe("picker.diff", function()
|
|||
assert.equals(0, #blocks[1].hunks) -- no hunks for binary
|
||||
end)
|
||||
|
||||
it("handles binary files with prefixes in the path", function()
|
||||
local lines = {
|
||||
"diff --git a/ b/image.png b/ b/image.png",
|
||||
"index abc123..def456 100644",
|
||||
"Binary files a/image.png and b/image.png differ",
|
||||
}
|
||||
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(1, #blocks)
|
||||
assert.equals(" b/image.png", blocks[1].file)
|
||||
assert.equals(3, #blocks[1].header) -- diff line + binary notice
|
||||
assert.equals(0, #blocks[1].hunks) -- no hunks for binary
|
||||
end)
|
||||
|
||||
it("handles pure renames", function()
|
||||
local lines = {
|
||||
"diff --git a/old.txt b/new.txt",
|
||||
|
|
@ -144,11 +211,33 @@ describe("picker.diff", function()
|
|||
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(1, #blocks)
|
||||
assert.equals("old.txt", blocks[1].file)
|
||||
assert.equals("new.txt", blocks[1].file)
|
||||
assert.equals(4, #blocks[1].header)
|
||||
assert.equals(0, #blocks[1].hunks)
|
||||
end)
|
||||
|
||||
it("handles renames with a diff", function()
|
||||
local lines = {
|
||||
"diff --git a/old.txt b/new.txt",
|
||||
"similarity index 66%",
|
||||
"rename from old.txt",
|
||||
"rename to new.txt",
|
||||
"--- a/old.text",
|
||||
"+++ b/new.txt",
|
||||
"@@ -1,3 +1,3 @@",
|
||||
"-line0",
|
||||
" line1",
|
||||
" line2",
|
||||
"+line3",
|
||||
}
|
||||
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(1, #blocks)
|
||||
assert.equals("new.txt", blocks[1].file)
|
||||
assert.equals(6, #blocks[1].header)
|
||||
assert.equals(1, #blocks[1].hunks)
|
||||
end)
|
||||
|
||||
it("handles mode changes", function()
|
||||
local lines = {
|
||||
"diff --git a/script.sh b/script.sh",
|
||||
|
|
@ -222,9 +311,9 @@ describe("picker.diff", function()
|
|||
|
||||
it("handles files with spaces in name", function()
|
||||
local lines = {
|
||||
'diff --git "a/my file.txt" b/my file.txt',
|
||||
"--- a/my file.txt",
|
||||
"+++ b/my file.txt",
|
||||
"diff --git a/dir c/my file.txt b/dir c/my file.txt",
|
||||
"--- a/dir c/my file.txt",
|
||||
"+++ b/dir c/my file.txt",
|
||||
"@@ -1,1 +1,1 @@",
|
||||
"-old",
|
||||
"+new",
|
||||
|
|
@ -232,14 +321,14 @@ describe("picker.diff", function()
|
|||
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(1, #blocks)
|
||||
assert.equals("my file.txt", blocks[1].file)
|
||||
assert.equals("dir c/my file.txt", blocks[1].file)
|
||||
end)
|
||||
|
||||
it("handles files with spaces in name without quotes", function()
|
||||
it("handles quoted filenames", function()
|
||||
local lines = {
|
||||
"diff --git a/my file.txt b/my file.txt",
|
||||
"--- a/my file.txt",
|
||||
"+++ b/my file.txt",
|
||||
'diff --git "a/my file.txt" "b/my file.txt"',
|
||||
'--- "a/my file.txt"',
|
||||
'+++ "b/my file.txt"',
|
||||
"@@ -1,1 +1,1 @@",
|
||||
"-old",
|
||||
"+new",
|
||||
|
|
@ -409,12 +498,25 @@ describe("picker.diff", function()
|
|||
"@@ -1,1 +1,1 @@",
|
||||
"-old",
|
||||
"+new",
|
||||
"--- plain2.txt",
|
||||
"+++ plain2.txt",
|
||||
"@@ -1,1 +1,1 @@",
|
||||
"-old",
|
||||
"+new",
|
||||
"diff --git a/git2.txt b/git2.txt",
|
||||
"--- a/git2.txt",
|
||||
"+++ b/git2.txt",
|
||||
"@@ -1,1 +1,1 @@",
|
||||
"-old",
|
||||
"+new",
|
||||
}
|
||||
|
||||
local blocks = diff.parse(lines)
|
||||
assert.equals(2, #blocks)
|
||||
assert.equals(4, #blocks)
|
||||
assert.equals("plain1.txt", blocks[1].file)
|
||||
assert.equals("git1.txt", blocks[2].file)
|
||||
assert.equals("plain2.txt", blocks[3].file)
|
||||
assert.equals("git2.txt", blocks[4].file)
|
||||
end)
|
||||
|
||||
it("handles symlink changes", function()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue