feat(fugitive): add section header and untracked file support
Section headers (Staged/Unstaged) now show all diffs in that section, matching fugitive's behavior. Untracked files show as all-added diffs. Deleted files show as all-removed diffs. Also handles edge cases: - Empty new/old content for deleted/new files - Section header detection returns is_header flag
This commit is contained in:
parent
ce8fe3b89b
commit
6072dd0156
3 changed files with 150 additions and 23 deletions
|
|
@ -91,6 +91,7 @@ end
|
||||||
---@class diffs.GdiffFileOpts
|
---@class diffs.GdiffFileOpts
|
||||||
---@field vertical? boolean
|
---@field vertical? boolean
|
||||||
---@field staged? boolean
|
---@field staged? boolean
|
||||||
|
---@field untracked? boolean
|
||||||
|
|
||||||
---@param filepath string
|
---@param filepath string
|
||||||
---@param opts? diffs.GdiffFileOpts
|
---@param opts? diffs.GdiffFileOpts
|
||||||
|
|
@ -106,16 +107,22 @@ function M.gdiff_file(filepath, opts)
|
||||||
local old_lines, new_lines, err
|
local old_lines, new_lines, err
|
||||||
local diff_label
|
local diff_label
|
||||||
|
|
||||||
if opts.staged then
|
if opts.untracked then
|
||||||
|
old_lines = {}
|
||||||
|
new_lines, err = git.get_working_content(filepath)
|
||||||
|
if not new_lines then
|
||||||
|
vim.notify('[diffs.nvim]: ' .. (err or 'cannot read file'), vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
diff_label = 'untracked'
|
||||||
|
elseif opts.staged then
|
||||||
old_lines, err = git.get_file_content('HEAD', filepath)
|
old_lines, err = git.get_file_content('HEAD', filepath)
|
||||||
if not old_lines then
|
if not old_lines then
|
||||||
vim.notify('[diffs.nvim]: ' .. (err or 'file not in HEAD'), vim.log.levels.ERROR)
|
old_lines = {}
|
||||||
return
|
|
||||||
end
|
end
|
||||||
new_lines, err = git.get_index_content(filepath)
|
new_lines, err = git.get_index_content(filepath)
|
||||||
if not new_lines then
|
if not new_lines then
|
||||||
vim.notify('[diffs.nvim]: ' .. (err or 'file not in index'), vim.log.levels.ERROR)
|
new_lines = {}
|
||||||
return
|
|
||||||
end
|
end
|
||||||
diff_label = 'staged'
|
diff_label = 'staged'
|
||||||
else
|
else
|
||||||
|
|
@ -133,8 +140,7 @@ function M.gdiff_file(filepath, opts)
|
||||||
end
|
end
|
||||||
new_lines, err = git.get_working_content(filepath)
|
new_lines, err = git.get_working_content(filepath)
|
||||||
if not new_lines then
|
if not new_lines then
|
||||||
vim.notify('[diffs.nvim]: ' .. (err or 'cannot read file'), vim.log.levels.ERROR)
|
new_lines = {}
|
||||||
return
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -163,6 +169,50 @@ function M.gdiff_file(filepath, opts)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class diffs.GdiffSectionOpts
|
||||||
|
---@field vertical? boolean
|
||||||
|
---@field staged? boolean
|
||||||
|
|
||||||
|
---@param repo_root string
|
||||||
|
---@param opts? diffs.GdiffSectionOpts
|
||||||
|
function M.gdiff_section(repo_root, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
|
||||||
|
local cmd = { 'git', '-C', repo_root, 'diff', '--no-ext-diff', '--no-color' }
|
||||||
|
if opts.staged then
|
||||||
|
table.insert(cmd, '--cached')
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = vim.fn.systemlist(cmd)
|
||||||
|
if vim.v.shell_error ~= 0 then
|
||||||
|
vim.notify('[diffs.nvim]: git diff failed', vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if #result == 0 then
|
||||||
|
vim.notify('[diffs.nvim]: no changes in section', vim.log.levels.INFO)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local diff_label = opts.staged and 'staged' or 'unstaged'
|
||||||
|
local diff_buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, result)
|
||||||
|
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = diff_buf })
|
||||||
|
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = diff_buf })
|
||||||
|
vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf })
|
||||||
|
vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf })
|
||||||
|
vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. diff_label .. ':all')
|
||||||
|
|
||||||
|
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||||
|
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||||
|
|
||||||
|
dbg('opened section diff buffer %d (%s)', diff_buf, diff_label)
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
require('diffs').attach(diff_buf)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
vim.api.nvim_create_user_command('Gdiff', function(opts)
|
vim.api.nvim_create_user_command('Gdiff', function(opts)
|
||||||
M.gdiff(opts.args ~= '' and opts.args or nil, false)
|
M.gdiff(opts.args ~= '' and opts.args or nil, false)
|
||||||
|
|
|
||||||
|
|
@ -42,21 +42,39 @@ local function parse_file_line(line)
|
||||||
return nil
|
return nil
|
||||||
end
|
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 bufnr integer
|
||||||
---@param lnum integer
|
---@param lnum integer
|
||||||
---@return string?, diffs.FugitiveSection
|
---@return string?, diffs.FugitiveSection, boolean
|
||||||
function M.get_file_at_line(bufnr, lnum)
|
function M.get_file_at_line(bufnr, lnum)
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
local current_line = lines[lnum]
|
local current_line = lines[lnum]
|
||||||
|
|
||||||
if not current_line then
|
if not current_line then
|
||||||
return nil, nil
|
return nil, nil, false
|
||||||
|
end
|
||||||
|
|
||||||
|
local section_header = parse_section_header(current_line)
|
||||||
|
if section_header then
|
||||||
|
return nil, section_header, true
|
||||||
end
|
end
|
||||||
|
|
||||||
local filename = parse_file_line(current_line)
|
local filename = parse_file_line(current_line)
|
||||||
if filename then
|
if filename then
|
||||||
local section = M.get_section_at_line(bufnr, lnum)
|
local section = M.get_section_at_line(bufnr, lnum)
|
||||||
return filename, section
|
return filename, section, false
|
||||||
end
|
end
|
||||||
|
|
||||||
local prefix = current_line:sub(1, 1)
|
local prefix = current_line:sub(1, 1)
|
||||||
|
|
@ -66,7 +84,7 @@ function M.get_file_at_line(bufnr, lnum)
|
||||||
filename = parse_file_line(prev_line)
|
filename = parse_file_line(prev_line)
|
||||||
if filename then
|
if filename then
|
||||||
local section = M.get_section_at_line(bufnr, i)
|
local section = M.get_section_at_line(bufnr, i)
|
||||||
return filename, section
|
return filename, section, false
|
||||||
end
|
end
|
||||||
if prev_line:match('^%w+ %(') or prev_line == '' then
|
if prev_line:match('^%w+ %(') or prev_line == '' then
|
||||||
break
|
break
|
||||||
|
|
@ -74,7 +92,7 @@ function M.get_file_at_line(bufnr, lnum)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil, false
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
|
|
@ -96,12 +114,7 @@ function M.diff_file_under_cursor(vertical)
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
|
||||||
local filename, section = M.get_file_at_line(bufnr, lnum)
|
local filename, section, is_header = M.get_file_at_line(bufnr, lnum)
|
||||||
|
|
||||||
if not filename then
|
|
||||||
vim.notify('[diffs.nvim]: no file under cursor', vim.log.levels.WARN)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local repo_root = get_repo_root_from_fugitive(bufnr)
|
local repo_root = get_repo_root_from_fugitive(bufnr)
|
||||||
if not repo_root then
|
if not repo_root then
|
||||||
|
|
@ -109,18 +122,32 @@ function M.diff_file_under_cursor(vertical)
|
||||||
return
|
return
|
||||||
end
|
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 filepath = repo_root .. '/' .. filename
|
||||||
|
|
||||||
dbg('diff_file_under_cursor: %s (section: %s)', filename, section or 'unknown')
|
dbg('diff_file_under_cursor: %s (section: %s)', filename, section or 'unknown')
|
||||||
|
|
||||||
if section == 'untracked' then
|
|
||||||
vim.notify('[diffs.nvim]: cannot diff untracked file (no base version)', vim.log.levels.WARN)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
commands.gdiff_file(filepath, {
|
commands.gdiff_file(filepath, {
|
||||||
vertical = vertical,
|
vertical = vertical,
|
||||||
staged = section == 'staged',
|
staged = section == 'staged',
|
||||||
|
untracked = section == 'untracked',
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,5 +173,55 @@ describe('fugitive', function()
|
||||||
assert.equals('unstaged', section2)
|
assert.equals('unstaged', section2)
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('detects section header for Staged', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Head: main',
|
||||||
|
'',
|
||||||
|
'Staged (2)',
|
||||||
|
'M file1.lua',
|
||||||
|
})
|
||||||
|
local filename, section, is_header = fugitive.get_file_at_line(buf, 3)
|
||||||
|
assert.is_nil(filename)
|
||||||
|
assert.equals('staged', section)
|
||||||
|
assert.is_true(is_header)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('detects section header for Unstaged', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (3)',
|
||||||
|
'M file1.lua',
|
||||||
|
})
|
||||||
|
local filename, section, is_header = fugitive.get_file_at_line(buf, 1)
|
||||||
|
assert.is_nil(filename)
|
||||||
|
assert.equals('unstaged', section)
|
||||||
|
assert.is_true(is_header)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('detects section header for Untracked', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Untracked (1)',
|
||||||
|
'? newfile.lua',
|
||||||
|
})
|
||||||
|
local filename, section, is_header = fugitive.get_file_at_line(buf, 1)
|
||||||
|
assert.is_nil(filename)
|
||||||
|
assert.equals('untracked', section)
|
||||||
|
assert.is_true(is_header)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns is_header=false for file lines', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Staged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
})
|
||||||
|
local filename, section, is_header = fugitive.get_file_at_line(buf, 2)
|
||||||
|
assert.equals('file.lua', filename)
|
||||||
|
assert.equals('staged', section)
|
||||||
|
assert.is_false(is_header)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue