## Problem Three minor issues remain before the v0.2.0 release: 1. Git quotes filenames containing spaces, unicode, or special characters in the fugitive status buffer. `parse_file_line` passed the quotes through verbatim, causing file-not-found errors on diff operations. 2. Navigation wrap-around in both conflict and merge modules was silent, giving no indication when jumping past the last/first item back to the beginning/end. 3. `resolved_hunks` and `(resolved)` virtual text in the merge module persisted across buffer re-reads, showing stale markers for hunks that were no longer resolved. ## Solution 1. Add an `unquote()` helper to fugitive.lua that strips surrounding quotes and unescapes `\\`, `\"`, `\n`, `\t`, and octal `\NNN` sequences. Applied to both return paths in `parse_file_line`. 2. Add `vim.notify` before the wrap-around jump in all four navigation functions (`goto_next`/`goto_prev` in conflict.lua and merge.lua). 3. Clear `resolved_hunks[bufnr]` and the merge namespace at the top of `setup_keymaps` so each buffer init starts fresh. Closes #66
264 lines
6.9 KiB
Lua
264 lines
6.9 KiB
Lua
local M = {}
|
|
|
|
local commands = require('diffs.commands')
|
|
local git = require('diffs.git')
|
|
local dbg = require('diffs.log').dbg
|
|
|
|
---@alias diffs.FugitiveSection 'staged' | 'unstaged' | 'untracked' | nil
|
|
|
|
---@param bufnr integer
|
|
---@param lnum integer
|
|
---@return diffs.FugitiveSection
|
|
function M.get_section_at_line(bufnr, lnum)
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, lnum, false)
|
|
|
|
for i = #lines, 1, -1 do
|
|
local line = lines[i]
|
|
if line:match('^Staged ') then
|
|
return 'staged'
|
|
elseif line:match('^Unstaged ') then
|
|
return 'unstaged'
|
|
elseif line:match('^Untracked ') then
|
|
return 'untracked'
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
---@param s string
|
|
---@return string
|
|
local function unquote(s)
|
|
if s:sub(1, 1) ~= '"' then
|
|
return s
|
|
end
|
|
local inner = s:sub(2, -2)
|
|
local result = {}
|
|
local i = 1
|
|
while i <= #inner do
|
|
if inner:sub(i, i) == '\\' and i < #inner then
|
|
local next_char = inner:sub(i + 1, i + 1)
|
|
if next_char == 'n' then
|
|
table.insert(result, '\n')
|
|
i = i + 2
|
|
elseif next_char == 't' then
|
|
table.insert(result, '\t')
|
|
i = i + 2
|
|
elseif next_char == '"' then
|
|
table.insert(result, '"')
|
|
i = i + 2
|
|
elseif next_char == '\\' then
|
|
table.insert(result, '\\')
|
|
i = i + 2
|
|
elseif next_char:match('%d') then
|
|
local oct = inner:match('^(%d%d%d)', i + 1)
|
|
if oct then
|
|
table.insert(result, string.char(tonumber(oct, 8)))
|
|
i = i + 4
|
|
else
|
|
table.insert(result, next_char)
|
|
i = i + 2
|
|
end
|
|
else
|
|
table.insert(result, next_char)
|
|
i = i + 2
|
|
end
|
|
else
|
|
table.insert(result, inner:sub(i, i))
|
|
i = i + 1
|
|
end
|
|
end
|
|
return table.concat(result)
|
|
end
|
|
|
|
---@param line string
|
|
---@return string?, string?, string?
|
|
local function parse_file_line(line)
|
|
local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$')
|
|
if old and new then
|
|
return unquote(vim.trim(new)), unquote(vim.trim(old)), 'R'
|
|
end
|
|
|
|
local status, filename = line:match('^([MADRCU?])[MADRCU%s]*%s+(.+)$')
|
|
if status and filename then
|
|
return unquote(vim.trim(filename)), nil, status
|
|
end
|
|
|
|
return nil, nil, nil
|
|
end
|
|
|
|
---@param line string
|
|
---@return diffs.FugitiveSection?
|
|
local function parse_section_header(line)
|
|
if line:match('^Staged %(%d') then
|
|
return 'staged'
|
|
elseif line:match('^Unstaged %(%d') then
|
|
return 'unstaged'
|
|
elseif line:match('^Untracked %(%d') then
|
|
return 'untracked'
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param lnum integer
|
|
---@return string?, diffs.FugitiveSection, boolean, string?, string?
|
|
function M.get_file_at_line(bufnr, lnum)
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
local current_line = lines[lnum]
|
|
|
|
if not current_line then
|
|
return nil, nil, false, nil, nil
|
|
end
|
|
|
|
local section_header = parse_section_header(current_line)
|
|
if section_header then
|
|
return nil, section_header, true, nil, nil
|
|
end
|
|
|
|
local filename, old_filename, status = parse_file_line(current_line)
|
|
if filename then
|
|
local section = M.get_section_at_line(bufnr, lnum)
|
|
return filename, section, false, old_filename, status
|
|
end
|
|
|
|
local prefix = current_line:sub(1, 1)
|
|
if prefix == '+' or prefix == '-' or prefix == ' ' then
|
|
for i = lnum - 1, 1, -1 do
|
|
local prev_line = lines[i]
|
|
filename, old_filename, status = parse_file_line(prev_line)
|
|
if filename then
|
|
local section = M.get_section_at_line(bufnr, i)
|
|
return filename, section, false, old_filename, status
|
|
end
|
|
if prev_line:match('^%w+ %(') or prev_line == '' then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil, nil, false, nil, nil
|
|
end
|
|
|
|
---@class diffs.HunkPosition
|
|
---@field hunk_header string
|
|
---@field offset integer
|
|
|
|
---@param bufnr integer
|
|
---@param lnum integer
|
|
---@return diffs.HunkPosition?
|
|
function M.get_hunk_position(bufnr, lnum)
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, lnum, false)
|
|
local current = lines[lnum]
|
|
|
|
if not current then
|
|
return nil
|
|
end
|
|
|
|
local prefix = current:sub(1, 1)
|
|
if prefix ~= '+' and prefix ~= '-' and prefix ~= ' ' then
|
|
return nil
|
|
end
|
|
|
|
for i = lnum - 1, 1, -1 do
|
|
local line = lines[i]
|
|
if line:match('^@@.-@@') then
|
|
return {
|
|
hunk_header = line,
|
|
offset = lnum - i,
|
|
}
|
|
end
|
|
if line:match('^[MADRCU?!]%s') or line:match('^%w+ %(') then
|
|
break
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return string?
|
|
local function get_repo_root_from_fugitive(bufnr)
|
|
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
|
local fugitive_path = bufname:match('^fugitive://(.+)///')
|
|
if fugitive_path then
|
|
return fugitive_path
|
|
end
|
|
|
|
local cwd = vim.fn.getcwd()
|
|
local root = git.get_repo_root(cwd .. '/.')
|
|
return root
|
|
end
|
|
|
|
---@param vertical boolean
|
|
function M.diff_file_under_cursor(vertical)
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
|
|
|
local filename, section, is_header, old_filename, status = M.get_file_at_line(bufnr, lnum)
|
|
|
|
local repo_root = get_repo_root_from_fugitive(bufnr)
|
|
if not repo_root then
|
|
vim.notify('[diffs.nvim]: could not determine repository root', vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
if is_header then
|
|
dbg('diff_section: %s', section or 'unknown')
|
|
if section == 'untracked' then
|
|
vim.notify('[diffs.nvim]: cannot diff untracked section', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
commands.gdiff_section(repo_root, {
|
|
vertical = vertical,
|
|
staged = section == 'staged',
|
|
})
|
|
return
|
|
end
|
|
|
|
if not filename then
|
|
vim.notify('[diffs.nvim]: no file under cursor', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
|
|
local filepath = repo_root .. '/' .. filename
|
|
local old_filepath = old_filename and (repo_root .. '/' .. old_filename) or nil
|
|
local hunk_position = M.get_hunk_position(bufnr, lnum)
|
|
|
|
dbg(
|
|
'diff_file_under_cursor: %s (section: %s, old: %s, hunk_offset: %s)',
|
|
filename,
|
|
section or 'unknown',
|
|
old_filename or 'none',
|
|
hunk_position and tostring(hunk_position.offset) or 'none'
|
|
)
|
|
|
|
commands.gdiff_file(filepath, {
|
|
vertical = vertical,
|
|
staged = section == 'staged',
|
|
untracked = section == 'untracked',
|
|
unmerged = status == 'U',
|
|
old_filepath = old_filepath,
|
|
hunk_position = hunk_position,
|
|
})
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param config { horizontal: string|false, vertical: string|false }
|
|
function M.setup_keymaps(bufnr, config)
|
|
if config.horizontal and config.horizontal ~= '' then
|
|
vim.keymap.set('n', config.horizontal, function()
|
|
M.diff_file_under_cursor(false)
|
|
end, { buffer = bufnr, desc = 'Unified diff (horizontal)' })
|
|
dbg('set keymap %s for buffer %d', config.horizontal, bufnr)
|
|
end
|
|
|
|
if config.vertical and config.vertical ~= '' then
|
|
vim.keymap.set('n', config.vertical, function()
|
|
M.diff_file_under_cursor(true)
|
|
end, { buffer = bufnr, desc = 'Unified diff (vertical)' })
|
|
dbg('set keymap %s for buffer %d', config.vertical, bufnr)
|
|
end
|
|
end
|
|
|
|
return M
|