gitsigns.nvim/lua/gitsigns/actions.lua
Lewis Russell 0180dfd7ae Refactor diff code
- Simplify external diff code and make it's api consistent with FFI diff.
- Move diff code into separate modules diff_ext and diff_ffi.
2021-07-21 16:09:20 +01:00

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