mirror of
https://github.com/lewis6991/gitsigns.nvim
synced 2025-05-02 16:18:17 +00:00
- Simplify external diff code and make it's api consistent with FFI diff. - Move diff code into separate modules diff_ext and diff_ffi.
572 lines
12 KiB
Lua
Generated
572 lines
12 KiB
Lua
Generated
local a = require('plenary.async')
|
|
local void = a.void
|
|
local scheduler = a.util.scheduler
|
|
|
|
local Status = require("gitsigns.status")
|
|
local config = require('gitsigns.config').config
|
|
local mk_repeatable = require('gitsigns.repeat').mk_repeatable
|
|
local popup = require('gitsigns.popup')
|
|
local signs = require('gitsigns.signs')
|
|
local util = require('gitsigns.util')
|
|
local manager = require('gitsigns.manager')
|
|
|
|
local gs_cache = require('gitsigns.cache')
|
|
local cache = gs_cache.cache
|
|
local CacheEntry = gs_cache.CacheEntry
|
|
|
|
local gs_hunks = require('gitsigns.hunks')
|
|
local Hunk = gs_hunks.Hunk
|
|
|
|
local api = vim.api
|
|
local current_buf = api.nvim_get_current_buf
|
|
|
|
local NavHunkOpts = {}
|
|
|
|
|
|
|
|
|
|
|
|
local M = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local function get_cursor_hunk(bufnr, hunks)
|
|
bufnr = bufnr or current_buf()
|
|
hunks = hunks or cache[bufnr].hunks
|
|
|
|
local lnum = api.nvim_win_get_cursor(0)[1]
|
|
return gs_hunks.find_hunk(lnum, hunks)
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local function get_range_hunks(bufnr, hunks, range, strict)
|
|
bufnr = bufnr or current_buf()
|
|
hunks = hunks or cache[bufnr].hunks
|
|
|
|
local ret = {}
|
|
for _, hunk in ipairs(hunks) do
|
|
if range[1] == 1 and hunk.start == 0 and hunk.vend == 0 then
|
|
return { hunk }
|
|
end
|
|
|
|
if strict then
|
|
if (range[1] <= hunk.start and range[2] >= hunk.vend) then
|
|
ret[#ret + 1] = hunk
|
|
end
|
|
else
|
|
if (range[2] >= hunk.start and range[1] <= hunk.vend) then
|
|
ret[#ret + 1] = hunk
|
|
end
|
|
end
|
|
end
|
|
|
|
return ret
|
|
end
|
|
|
|
M.stage_hunk = mk_repeatable(void(function(range)
|
|
range = range or M.user_range
|
|
local valid_range = false
|
|
local bufnr = current_buf()
|
|
local bcache = cache[bufnr]
|
|
if not bcache then
|
|
return
|
|
end
|
|
|
|
if not util.path_exists(bcache.file) then
|
|
print("Error: Cannot stage lines. Please add the file to the working tree.")
|
|
return
|
|
end
|
|
|
|
local hunks = {}
|
|
|
|
if range and range[1] ~= range[2] then
|
|
valid_range = true
|
|
table.sort(range)
|
|
hunks = get_range_hunks(bufnr, bcache.hunks, range)
|
|
else
|
|
hunks[1] = get_cursor_hunk(bufnr, bcache.hunks)
|
|
end
|
|
|
|
if #hunks == 0 then
|
|
return
|
|
end
|
|
|
|
bcache.git_obj:stage_hunks(hunks)
|
|
|
|
for _, hunk in ipairs(hunks) do
|
|
table.insert(bcache.staged_diffs, hunk)
|
|
end
|
|
|
|
bcache.compare_text = nil
|
|
|
|
local hunk_signs = gs_hunks.process_hunks(hunks)
|
|
|
|
scheduler()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for lnum, _ in pairs(hunk_signs) do
|
|
signs.remove(bufnr, lnum)
|
|
end
|
|
a.void(manager.update)(bufnr)
|
|
end))
|
|
|
|
M.reset_hunk = mk_repeatable(function(range)
|
|
range = range or M.user_range
|
|
local bufnr = current_buf()
|
|
local hunks = {}
|
|
|
|
if range and range[1] ~= range[2] then
|
|
table.sort(range)
|
|
hunks = get_range_hunks(bufnr, nil, range)
|
|
else
|
|
hunks[1] = get_cursor_hunk(bufnr)
|
|
end
|
|
|
|
if #hunks == 0 then
|
|
return
|
|
end
|
|
|
|
local offset = 0
|
|
|
|
for _, hunk in ipairs(hunks) do
|
|
local lstart, lend
|
|
if hunk.type == 'delete' then
|
|
lstart = hunk.start
|
|
lend = hunk.start
|
|
else
|
|
local length = vim.tbl_count(vim.tbl_filter(function(l)
|
|
return vim.startswith(l, '+')
|
|
end, hunk.lines))
|
|
|
|
lstart = hunk.start - 1
|
|
lend = hunk.start - 1 + length
|
|
end
|
|
local lines = gs_hunks.extract_removed(hunk)
|
|
api.nvim_buf_set_lines(bufnr, lstart + offset, lend + offset, false, lines)
|
|
offset = offset + (#lines - (lend - lstart))
|
|
end
|
|
end)
|
|
|
|
M.reset_buffer = function()
|
|
local bufnr = current_buf()
|
|
local bcache = cache[bufnr]
|
|
if not bcache then
|
|
return
|
|
end
|
|
|
|
api.nvim_buf_set_lines(bufnr, 0, -1, false, bcache:get_compare_text())
|
|
end
|
|
|
|
M.undo_stage_hunk = mk_repeatable(void(function()
|
|
local bufnr = current_buf()
|
|
local bcache = cache[bufnr]
|
|
if not bcache then
|
|
return
|
|
end
|
|
|
|
local hunk = table.remove(bcache.staged_diffs)
|
|
if not hunk then
|
|
print("No hunks to undo")
|
|
return
|
|
end
|
|
|
|
bcache.git_obj:stage_hunks({ hunk }, true)
|
|
bcache.compare_text = nil
|
|
scheduler()
|
|
signs.add(config, bufnr, gs_hunks.process_hunks({ hunk }))
|
|
manager.update(bufnr)
|
|
end))
|
|
|
|
M.stage_buffer = void(function()
|
|
local bufnr = current_buf()
|
|
|
|
local bcache = cache[bufnr]
|
|
if not bcache then
|
|
return
|
|
end
|
|
|
|
|
|
local hunks = bcache.hunks
|
|
if #hunks == 0 then
|
|
print("No unstaged changes in file to stage")
|
|
return
|
|
end
|
|
|
|
if not util.path_exists(bcache.git_obj.file) then
|
|
print("Error: Cannot stage file. Please add it to the working tree.")
|
|
return
|
|
end
|
|
|
|
bcache.git_obj:stage_hunks(hunks)
|
|
|
|
for _, hunk in ipairs(hunks) do
|
|
table.insert(bcache.staged_diffs, hunk)
|
|
end
|
|
bcache.compare_text = nil
|
|
|
|
scheduler()
|
|
signs.remove(bufnr)
|
|
Status:clear_diff(bufnr)
|
|
end)
|
|
|
|
M.reset_buffer_index = void(function()
|
|
local bufnr = current_buf()
|
|
local bcache = cache[bufnr]
|
|
if not bcache then
|
|
return
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local hunks = bcache.staged_diffs
|
|
bcache.staged_diffs = {}
|
|
|
|
bcache.git_obj:unstage_file()
|
|
bcache.compare_text = nil
|
|
|
|
scheduler()
|
|
signs.add(config, bufnr, gs_hunks.process_hunks(hunks))
|
|
a.void(manager.update)(bufnr)
|
|
end)
|
|
|
|
local function nav_hunk(options)
|
|
local bcache = cache[current_buf()]
|
|
if not bcache then
|
|
return
|
|
end
|
|
local hunks = bcache.hunks
|
|
if not hunks or vim.tbl_isempty(hunks) then
|
|
return
|
|
end
|
|
local line = api.nvim_win_get_cursor(0)[1]
|
|
|
|
|
|
local wrap = vim.o.wrapscan
|
|
if options.wrap ~= nil then
|
|
wrap = options.wrap
|
|
end
|
|
|
|
local hunk, index = gs_hunks.find_nearest_hunk(line, hunks, options.forwards, wrap)
|
|
|
|
|
|
local show_navigation_msg = not string.find(vim.o.shortmess, 'S')
|
|
if options.navigation_message ~= nil then
|
|
show_navigation_msg = options.navigation_message
|
|
end
|
|
|
|
if hunk == nil then
|
|
if show_navigation_msg then
|
|
vim.api.nvim_echo({ { 'No more hunks', 'WarningMsg' } }, false, {})
|
|
end
|
|
return
|
|
end
|
|
|
|
local row = options.forwards and hunk.start or hunk.vend
|
|
if row then
|
|
|
|
if row == 0 then
|
|
row = 1
|
|
end
|
|
api.nvim_win_set_cursor(0, { row, 0 })
|
|
if pcall(api.nvim_buf_get_var, 0, '_gitsigns_preview_open') then
|
|
vim.schedule(M.preview_hunk)
|
|
end
|
|
|
|
if index ~= nil and show_navigation_msg then
|
|
vim.api.nvim_echo({ { string.format('Hunk %d of %d', index, #hunks), 'None' } }, false, {})
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
M.next_hunk = function(options)
|
|
options = options or {}
|
|
options.forwards = true
|
|
nav_hunk(options)
|
|
end
|
|
|
|
M.prev_hunk = function(options)
|
|
options = options or {}
|
|
options.forwards = false
|
|
nav_hunk(options)
|
|
end
|
|
|
|
local ns = api.nvim_create_namespace('gitsigns')
|
|
|
|
M.preview_hunk = function()
|
|
local cbuf = current_buf()
|
|
local bcache = cache[cbuf]
|
|
local hunk, index = get_cursor_hunk(cbuf, bcache.hunks)
|
|
|
|
if not hunk then return end
|
|
|
|
local lines = {
|
|
('Hunk %d of %d'):format(index, #bcache.hunks),
|
|
unpack(hunk.lines),
|
|
}
|
|
|
|
local _, bufnr = popup.create(lines, config.preview_config)
|
|
api.nvim_buf_set_option(bufnr, 'filetype', 'diff')
|
|
|
|
api.nvim_buf_add_highlight(bufnr, -1, 'Title', 0, 0, -1)
|
|
|
|
api.nvim_buf_set_var(cbuf, '_gitsigns_preview_open', true)
|
|
vim.cmd([[autocmd CursorMoved,CursorMovedI <buffer> ++once silent! unlet b:_gitsigns_preview_open]])
|
|
|
|
if config.use_internal_diff then
|
|
local regions = require('gitsigns.diff_ffi').run_word_diff(hunk.lines)
|
|
local offset = #lines - #hunk.lines
|
|
for _, region in ipairs(regions) do
|
|
local line, scol, ecol = region[1], region[3], region[4]
|
|
api.nvim_buf_set_extmark(bufnr, ns, line + offset - 1, scol, {
|
|
end_col = ecol,
|
|
hl_group = 'TermCursor',
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
M.select_hunk = function()
|
|
local hunk = get_cursor_hunk()
|
|
if not hunk then return end
|
|
|
|
vim.cmd('normal! ' .. hunk.start .. 'GV' .. hunk.vend .. 'G')
|
|
end
|
|
|
|
local function defer(duration, callback)
|
|
local timer = vim.loop.new_timer()
|
|
timer:start(duration, 0, function()
|
|
timer:stop()
|
|
timer:close()
|
|
vim.schedule_wrap(callback)()
|
|
end)
|
|
return timer
|
|
end
|
|
|
|
M.blame_line = void(function(full)
|
|
local bufnr = current_buf()
|
|
local bcache = cache[bufnr]
|
|
if not bcache then return end
|
|
|
|
local loading = defer(1000, function()
|
|
popup.create({ 'Loading...' }, config.preview_config)
|
|
end)
|
|
|
|
scheduler()
|
|
local buftext = api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
local lnum = api.nvim_win_get_cursor(0)[1]
|
|
local result = bcache.git_obj:run_blame(buftext, lnum)
|
|
pcall(function()
|
|
loading:close()
|
|
end)
|
|
|
|
local is_committed = tonumber('0x' .. result.sha) ~= 0
|
|
if is_committed then
|
|
local commit_message = {}
|
|
if full then
|
|
commit_message = bcache.git_obj:command({ 'show', '-s', '--format=%B', result.sha })
|
|
while commit_message[#commit_message] == '' do
|
|
commit_message[#commit_message] = nil
|
|
end
|
|
else
|
|
commit_message = { result.summary }
|
|
end
|
|
|
|
local date = os.date('%Y-%m-%d %H:%M', tonumber(result['author_time']))
|
|
|
|
local lines = {
|
|
('%s %s (%s):'):format(result.abbrev_sha, result.author, date),
|
|
unpack(commit_message),
|
|
}
|
|
|
|
scheduler()
|
|
local _, pbufnr = popup.create(lines, config.preview_config)
|
|
|
|
local p1 = #result.abbrev_sha
|
|
local p2 = #result.author
|
|
local p3 = #date
|
|
|
|
local function add_highlight(hlgroup, line, start, length)
|
|
api.nvim_buf_add_highlight(pbufnr, -1, hlgroup, line, start, start + length)
|
|
end
|
|
|
|
add_highlight('Directory', 0, 0, p1)
|
|
add_highlight('MoreMsg', 0, p1 + 1, p2)
|
|
add_highlight('Label', 0, p1 + p2 + 2, p3 + 2)
|
|
else
|
|
local lines = { result.author }
|
|
scheduler()
|
|
local _, pbufnr = popup.create(lines, config.preview_config)
|
|
api.nvim_buf_add_highlight(pbufnr, -1, 'MoreMsg', 0, 0, #result.author)
|
|
end
|
|
end)
|
|
|
|
local function calc_base(base)
|
|
if base and base:sub(1, 1):match('[~\\^]') then
|
|
base = 'HEAD' .. base
|
|
end
|
|
return base
|
|
end
|
|
|
|
M.change_base = function(base)
|
|
local buf = current_buf()
|
|
local bcache = cache[buf]
|
|
if bcache == nil then return end
|
|
base = calc_base(base)
|
|
bcache.base = base
|
|
bcache.compare_text = nil
|
|
a.void(manager.update)(buf, bcache)
|
|
end
|
|
|
|
M.diffthis = void(function(base)
|
|
local bufnr = current_buf()
|
|
local bcache = cache[bufnr]
|
|
if not bcache then return end
|
|
|
|
if api.nvim_win_get_option(0, 'diff') then return end
|
|
|
|
local text
|
|
local err
|
|
local comp_obj = bcache:get_compare_obj(calc_base(base))
|
|
if base then
|
|
text, err = bcache.git_obj:get_show_text(comp_obj)
|
|
if err then
|
|
print(err)
|
|
return
|
|
end
|
|
scheduler()
|
|
else
|
|
text = bcache:get_compare_text()
|
|
end
|
|
|
|
local ft = api.nvim_buf_get_option(bufnr, 'filetype')
|
|
|
|
local bufname = string.format('gitsigns://%s/%s', bcache.git_obj.gitdir, comp_obj)
|
|
|
|
|
|
vim.cmd("keepalt aboveleft vertical split " .. bufname)
|
|
|
|
local dbuf = current_buf()
|
|
|
|
api.nvim_buf_set_option(dbuf, 'modifiable', true)
|
|
api.nvim_buf_set_lines(dbuf, 0, -1, false, text)
|
|
api.nvim_buf_set_option(dbuf, 'modifiable', false)
|
|
|
|
api.nvim_buf_set_option(dbuf, 'filetype', ft)
|
|
api.nvim_buf_set_option(dbuf, 'buftype', 'nowrite')
|
|
|
|
vim.cmd(string.format('autocmd! WinClosed <buffer=%d> ++once call nvim_buf_delete(%d, {})', dbuf, dbuf))
|
|
|
|
vim.cmd([[windo diffthis]])
|
|
end)
|
|
|
|
M.get_actions = function()
|
|
local hunk = get_cursor_hunk()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local actions_l = {}
|
|
if hunk then
|
|
actions_l = {
|
|
'stage_hunk',
|
|
'undo_stage_hunk',
|
|
'reset_hunk',
|
|
'preview_hunk',
|
|
'select_hunk',
|
|
}
|
|
else
|
|
actions_l = {
|
|
'blame_line',
|
|
}
|
|
end
|
|
|
|
local actions = {}
|
|
for _, a in ipairs(actions_l) do
|
|
actions[a] = (M)[a]
|
|
end
|
|
|
|
return actions
|
|
end
|
|
|
|
M.refresh = void(function()
|
|
manager.setup_signs_and_highlights(true)
|
|
require('gitsigns.current_line_blame').setup()
|
|
for k, v in pairs(cache) do
|
|
v.compare_text = nil
|
|
manager.update(k, v)
|
|
end
|
|
end)
|
|
|
|
M.toggle_signs = function()
|
|
config.signcolumn = not config.signcolumn
|
|
M.refresh()
|
|
end
|
|
|
|
M.toggle_numhl = function()
|
|
config.numhl = not config.numhl
|
|
M.refresh()
|
|
end
|
|
|
|
M.toggle_linehl = function()
|
|
config.linehl = not config.linehl
|
|
M.refresh()
|
|
end
|
|
|
|
M.toggle_word_diff = function()
|
|
config.word_diff = not config.word_diff
|
|
M.refresh()
|
|
end
|
|
|
|
M.toggle_current_line_blame = function()
|
|
config.current_line_blame = not config.current_line_blame
|
|
M.refresh()
|
|
end
|
|
|
|
return M
|