Merge pull request #64 from barrettruth/feat/fugitive-keymaps
feat(fugitive): add unified diff keymaps to status buffer
This commit is contained in:
commit
b5ec99fd06
6 changed files with 773 additions and 0 deletions
|
|
@ -88,6 +88,134 @@ function M.gdiff(revision, vertical)
|
|||
end)
|
||||
end
|
||||
|
||||
---@class diffs.GdiffFileOpts
|
||||
---@field vertical? boolean
|
||||
---@field staged? boolean
|
||||
---@field untracked? boolean
|
||||
---@field old_filepath? string
|
||||
|
||||
---@param filepath string
|
||||
---@param opts? diffs.GdiffFileOpts
|
||||
function M.gdiff_file(filepath, opts)
|
||||
opts = opts or {}
|
||||
|
||||
local rel_path = git.get_relative_path(filepath)
|
||||
if not rel_path then
|
||||
vim.notify('[diffs.nvim]: not in a git repository', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local old_rel_path = opts.old_filepath and git.get_relative_path(opts.old_filepath) or rel_path
|
||||
|
||||
local old_lines, new_lines, err
|
||||
local diff_label
|
||||
|
||||
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', opts.old_filepath or filepath)
|
||||
if not old_lines then
|
||||
old_lines = {}
|
||||
end
|
||||
new_lines, err = git.get_index_content(filepath)
|
||||
if not new_lines then
|
||||
new_lines = {}
|
||||
end
|
||||
diff_label = 'staged'
|
||||
else
|
||||
old_lines, err = git.get_index_content(opts.old_filepath or filepath)
|
||||
if not old_lines then
|
||||
old_lines, err = git.get_file_content('HEAD', opts.old_filepath or filepath)
|
||||
if not old_lines then
|
||||
old_lines = {}
|
||||
diff_label = 'untracked'
|
||||
else
|
||||
diff_label = 'unstaged'
|
||||
end
|
||||
else
|
||||
diff_label = 'unstaged'
|
||||
end
|
||||
new_lines, err = git.get_working_content(filepath)
|
||||
if not new_lines then
|
||||
new_lines = {}
|
||||
end
|
||||
end
|
||||
|
||||
local diff_lines = generate_unified_diff(old_lines, new_lines, old_rel_path, rel_path)
|
||||
|
||||
if #diff_lines == 0 then
|
||||
vim.notify('[diffs.nvim]: no changes', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local diff_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, diff_lines)
|
||||
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 .. ':' .. rel_path)
|
||||
|
||||
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
|
||||
dbg('opened diff buffer %d for %s (%s)', diff_buf, rel_path, diff_label)
|
||||
|
||||
vim.schedule(function()
|
||||
require('diffs').attach(diff_buf)
|
||||
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()
|
||||
vim.api.nvim_create_user_command('Gdiff', function(opts)
|
||||
M.gdiff(opts.args ~= '' and opts.args or nil, false)
|
||||
|
|
|
|||
179
lua/diffs/fugitive.lua
Normal file
179
lua/diffs/fugitive.lua
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
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 line string
|
||||
---@return string?, string?
|
||||
local function parse_file_line(line)
|
||||
local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$')
|
||||
if old and new then
|
||||
return vim.trim(new), vim.trim(old)
|
||||
end
|
||||
|
||||
local filename = line:match('^[MADRCU?][MADRCU%s]*%s+(.+)$')
|
||||
if filename then
|
||||
return vim.trim(filename), nil
|
||||
end
|
||||
|
||||
return 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?
|
||||
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
|
||||
end
|
||||
|
||||
local section_header = parse_section_header(current_line)
|
||||
if section_header then
|
||||
return nil, section_header, true, nil
|
||||
end
|
||||
|
||||
local filename, old_filename = parse_file_line(current_line)
|
||||
if filename then
|
||||
local section = M.get_section_at_line(bufnr, lnum)
|
||||
return filename, section, false, old_filename
|
||||
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 = parse_file_line(prev_line)
|
||||
if filename then
|
||||
local section = M.get_section_at_line(bufnr, i)
|
||||
return filename, section, false, old_filename
|
||||
end
|
||||
if prev_line:match('^%w+ %(') or prev_line == '' then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil, false, 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 = 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
|
||||
|
||||
dbg(
|
||||
'diff_file_under_cursor: %s (section: %s, old: %s)',
|
||||
filename,
|
||||
section or 'unknown',
|
||||
old_filename or 'none'
|
||||
)
|
||||
|
||||
commands.gdiff_file(filepath, {
|
||||
vertical = vertical,
|
||||
staged = section == 'staged',
|
||||
untracked = section == 'untracked',
|
||||
old_filepath = old_filepath,
|
||||
})
|
||||
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
|
||||
|
|
@ -45,4 +45,69 @@ function M.get_relative_path(filepath)
|
|||
return vim.fn.fnamemodify(filepath, ':.')
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
---@return string[]?, string?
|
||||
function M.get_index_content(filepath)
|
||||
local repo_root = M.get_repo_root(filepath)
|
||||
if not repo_root then
|
||||
return nil, 'not in a git repository'
|
||||
end
|
||||
|
||||
local rel_path = M.get_relative_path(filepath)
|
||||
if not rel_path then
|
||||
return nil, 'could not determine relative path'
|
||||
end
|
||||
|
||||
local result = vim.fn.systemlist({ 'git', '-C', repo_root, 'show', ':0:' .. rel_path })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
return nil, 'file not in index'
|
||||
end
|
||||
return result, nil
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
---@return string[]?, string?
|
||||
function M.get_working_content(filepath)
|
||||
if vim.fn.filereadable(filepath) ~= 1 then
|
||||
return nil, 'file not readable'
|
||||
end
|
||||
local lines = vim.fn.readfile(filepath)
|
||||
return lines, nil
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
---@return boolean
|
||||
function M.file_exists_in_index(filepath)
|
||||
local repo_root = M.get_repo_root(filepath)
|
||||
if not repo_root then
|
||||
return false
|
||||
end
|
||||
|
||||
local rel_path = M.get_relative_path(filepath)
|
||||
if not rel_path then
|
||||
return false
|
||||
end
|
||||
|
||||
vim.fn.system({ 'git', '-C', repo_root, 'ls-files', '--stage', '--', rel_path })
|
||||
return vim.v.shell_error == 0
|
||||
end
|
||||
|
||||
---@param revision string
|
||||
---@param filepath string
|
||||
---@return boolean
|
||||
function M.file_exists_at_revision(revision, filepath)
|
||||
local repo_root = M.get_repo_root(filepath)
|
||||
if not repo_root then
|
||||
return false
|
||||
end
|
||||
|
||||
local rel_path = M.get_relative_path(filepath)
|
||||
if not rel_path then
|
||||
return false
|
||||
end
|
||||
|
||||
vim.fn.system({ 'git', '-C', repo_root, 'cat-file', '-e', revision .. ':' .. rel_path })
|
||||
return vim.v.shell_error == 0
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -12,11 +12,16 @@
|
|||
---@field treesitter diffs.TreesitterConfig
|
||||
---@field vim diffs.VimConfig
|
||||
|
||||
---@class diffs.FugitiveConfig
|
||||
---@field horizontal string|false
|
||||
---@field vertical string|false
|
||||
|
||||
---@class diffs.Config
|
||||
---@field debug boolean
|
||||
---@field debounce_ms integer
|
||||
---@field hide_prefix boolean
|
||||
---@field highlights diffs.Highlights
|
||||
---@field fugitive diffs.FugitiveConfig
|
||||
|
||||
---@class diffs
|
||||
---@field attach fun(bufnr?: integer)
|
||||
|
|
@ -78,6 +83,10 @@ local default_config = {
|
|||
max_lines = 200,
|
||||
},
|
||||
},
|
||||
fugitive = {
|
||||
horizontal = 'du',
|
||||
vertical = 'dU',
|
||||
},
|
||||
}
|
||||
|
||||
---@type diffs.Config
|
||||
|
|
@ -219,6 +228,25 @@ local function init()
|
|||
end
|
||||
end
|
||||
|
||||
if opts.fugitive then
|
||||
vim.validate({
|
||||
['fugitive.horizontal'] = {
|
||||
opts.fugitive.horizontal,
|
||||
function(v)
|
||||
return v == false or type(v) == 'string'
|
||||
end,
|
||||
'string or false',
|
||||
},
|
||||
['fugitive.vertical'] = {
|
||||
opts.fugitive.vertical,
|
||||
function(v)
|
||||
return v == false or type(v) == 'string'
|
||||
end,
|
||||
'string or false',
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
if opts.debounce_ms and opts.debounce_ms < 0 then
|
||||
error('diffs: debounce_ms must be >= 0')
|
||||
end
|
||||
|
|
@ -354,4 +382,10 @@ function M.detach_diff()
|
|||
end
|
||||
end
|
||||
|
||||
---@return diffs.FugitiveConfig
|
||||
function M.get_fugitive_config()
|
||||
init()
|
||||
return config.fugitive
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ vim.api.nvim_create_autocmd('FileType', {
|
|||
return
|
||||
end
|
||||
diffs.attach(args.buf)
|
||||
|
||||
if args.match == 'fugitive' then
|
||||
local fugitive_config = diffs.get_fugitive_config()
|
||||
if fugitive_config.horizontal or fugitive_config.vertical then
|
||||
require('diffs.fugitive').setup_keymaps(args.buf, fugitive_config)
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
|
|
|
|||
360
spec/fugitive_spec.lua
Normal file
360
spec/fugitive_spec.lua
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local fugitive = require('diffs.fugitive')
|
||||
|
||||
describe('fugitive', function()
|
||||
describe('get_section_at_line', function()
|
||||
local function create_status_buffer(lines)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
return buf
|
||||
end
|
||||
|
||||
it('returns staged for lines in Staged section', function()
|
||||
local buf = create_status_buffer({
|
||||
'Head: main',
|
||||
'',
|
||||
'Staged (2)',
|
||||
'M file1.lua',
|
||||
'A file2.lua',
|
||||
'',
|
||||
'Unstaged (1)',
|
||||
'M file3.lua',
|
||||
})
|
||||
assert.equals('staged', fugitive.get_section_at_line(buf, 4))
|
||||
assert.equals('staged', fugitive.get_section_at_line(buf, 5))
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('returns unstaged for lines in Unstaged section', function()
|
||||
local buf = create_status_buffer({
|
||||
'Head: main',
|
||||
'',
|
||||
'Staged (1)',
|
||||
'M file1.lua',
|
||||
'',
|
||||
'Unstaged (2)',
|
||||
'M file2.lua',
|
||||
'M file3.lua',
|
||||
})
|
||||
assert.equals('unstaged', fugitive.get_section_at_line(buf, 7))
|
||||
assert.equals('unstaged', fugitive.get_section_at_line(buf, 8))
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('returns untracked for lines in Untracked section', function()
|
||||
local buf = create_status_buffer({
|
||||
'Head: main',
|
||||
'',
|
||||
'Untracked (2)',
|
||||
'? newfile.lua',
|
||||
'? another.lua',
|
||||
})
|
||||
assert.equals('untracked', fugitive.get_section_at_line(buf, 4))
|
||||
assert.equals('untracked', fugitive.get_section_at_line(buf, 5))
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('returns nil for lines before any section', function()
|
||||
local buf = create_status_buffer({
|
||||
'Head: main',
|
||||
'Push: origin/main',
|
||||
'',
|
||||
'Staged (1)',
|
||||
'M file1.lua',
|
||||
})
|
||||
assert.is_nil(fugitive.get_section_at_line(buf, 1))
|
||||
assert.is_nil(fugitive.get_section_at_line(buf, 2))
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_file_at_line', function()
|
||||
local function create_status_buffer(lines)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
return buf
|
||||
end
|
||||
|
||||
it('parses simple modified file', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M src/foo.lua',
|
||||
})
|
||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('src/foo.lua', filename)
|
||||
assert.equals('unstaged', section)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('parses added file', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'A newfile.lua',
|
||||
})
|
||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('newfile.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('parses deleted file', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'D oldfile.lua',
|
||||
})
|
||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('oldfile.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('parses renamed file and returns both names', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R oldname.lua -> newname.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('newname.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
assert.is_false(is_header)
|
||||
assert.equals('oldname.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('parses renamed file with similarity index', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R100 old.lua -> new.lua',
|
||||
})
|
||||
local filename, section, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('new.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
assert.equals('old.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('returns nil old_filename for non-renames', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'M modified.lua',
|
||||
})
|
||||
local filename, section, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('modified.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
assert.is_nil(old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed file with spaces in name', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R old file.lua -> new file.lua',
|
||||
})
|
||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('new file.lua', filename)
|
||||
assert.equals('old file.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed file in subdirectory', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R src/old.lua -> src/new.lua',
|
||||
})
|
||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('src/new.lua', filename)
|
||||
assert.equals('src/old.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed file moved to different directory', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R old/file.lua -> new/file.lua',
|
||||
})
|
||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('new/file.lua', filename)
|
||||
assert.equals('old/file.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('KNOWN LIMITATION: filename containing arrow parsed incorrectly', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R a -> b.lua -> c.lua',
|
||||
})
|
||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('b.lua -> c.lua', filename)
|
||||
assert.equals('a', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles double extensions', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'M test.spec.lua',
|
||||
})
|
||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('test.spec.lua', filename)
|
||||
assert.is_nil(old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles hyphenated filenames', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M my-component-test.lua',
|
||||
})
|
||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('my-component-test.lua', filename)
|
||||
assert.equals('unstaged', section)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles underscores and numbers', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'A test_file_123.lua',
|
||||
})
|
||||
local filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('test_file_123.lua', filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles dotfiles', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M .gitignore',
|
||||
})
|
||||
local filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('.gitignore', filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed with complex names', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R src/old-file.spec.lua -> src/new-file.spec.lua',
|
||||
})
|
||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('src/new-file.spec.lua', filename)
|
||||
assert.equals('src/old-file.spec.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles deeply nested paths', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M lua/diffs/ui/components/diff-view.lua',
|
||||
})
|
||||
local filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('lua/diffs/ui/components/diff-view.lua', filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('parses untracked file', function()
|
||||
local buf = create_status_buffer({
|
||||
'Untracked (1)',
|
||||
'? untracked.lua',
|
||||
})
|
||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('untracked.lua', filename)
|
||||
assert.equals('untracked', section)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('returns nil for section header', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M file.lua',
|
||||
})
|
||||
local filename = fugitive.get_file_at_line(buf, 1)
|
||||
assert.is_nil(filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('walks back from hunk line to find file', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M file.lua',
|
||||
'@@ -1,3 +1,4 @@',
|
||||
' local M = {}',
|
||||
'+local new = true',
|
||||
' return M',
|
||||
})
|
||||
local filename, section = fugitive.get_file_at_line(buf, 5)
|
||||
assert.equals('file.lua', filename)
|
||||
assert.equals('unstaged', section)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles file with both staged and unstaged indicator', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'M both.lua',
|
||||
'',
|
||||
'Unstaged (1)',
|
||||
'M both.lua',
|
||||
})
|
||||
local filename1, section1 = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('both.lua', filename1)
|
||||
assert.equals('staged', section1)
|
||||
|
||||
local filename2, section2 = fugitive.get_file_at_line(buf, 5)
|
||||
assert.equals('both.lua', filename2)
|
||||
assert.equals('unstaged', section2)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue