Files
gitlab.nvim/lua/gitlab/reviewer/diffview.lua
Harrison (Harry) Cramer b6f023373a Fix: De-Register Tabpage on Correct Close Event (#168)
This fixes a bug where the reviewer tab would be erroneously marked as closed. This is a PATCH release.
2024-01-22 07:30:08 -05:00

328 lines
11 KiB
Lua

-- This Module contains all of the reviewer code for diffview
local u = require("gitlab.utils")
local state = require("gitlab.state")
local async_ok, async = pcall(require, "diffview.async")
local diffview_lib = require("diffview.lib")
local M = {
bufnr = nil,
tabnr = nil,
}
local all_git_manged_files_unmodified = function()
-- check local managed files are unmodified, matching the state in the MR
-- TODO: ensure correct CWD?
return vim.fn.trim(vim.fn.system({ "git", "status", "--short", "--untracked-files=no" })) == ""
end
M.open = function()
local diff_refs = state.INFO.diff_refs
if diff_refs == nil then
u.notify("Gitlab did not provide diff refs required to review this MR", vim.log.levels.ERROR)
return
end
if diff_refs.base_sha == "" or diff_refs.head_sha == "" then
u.notify("Merge request contains no changes", vim.log.levels.ERROR)
return
end
local diffview_open_command = "DiffviewOpen"
local diffview_feature_imply_local = {
user_requested = state.settings.reviewer_settings.diffview.imply_local,
usable = all_git_manged_files_unmodified(),
}
if diffview_feature_imply_local.user_requested and diffview_feature_imply_local.usable then
diffview_open_command = diffview_open_command .. " --imply-local"
end
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
M.tabnr = vim.api.nvim_get_current_tabpage()
if diffview_feature_imply_local.user_requested and not diffview_feature_imply_local.usable then
u.notify(
"There are uncommited changes in the working tree, cannot use 'imply_local' setting for gitlab reviews. Stash or commit all changes to use.",
vim.log.levels.WARN
)
end
if state.INFO.has_conflicts then
u.notify("This merge request has conflicts!", vim.log.levels.WARN)
end
-- Register Diffview hook for close event to set tab page # to nil
local on_diffview_closed = function(view)
if view.tabpage == M.tabnr then
M.tabnr = nil
end
end
require("diffview.config").user_emitter:on("view_closed", function(_, ...)
on_diffview_closed(...)
end)
if state.settings.discussion_tree.auto_open then
local discussions = require("gitlab.actions.discussions")
discussions.close()
discussions.toggle()
end
end
M.close = function()
vim.cmd("DiffviewClose")
local discussions = require("gitlab.actions.discussions")
discussions.close()
end
M.jump = function(file_name, new_line, old_line, opts)
if M.tabnr == nil then
u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
return
end
vim.api.nvim_set_current_tabpage(M.tabnr)
vim.cmd("DiffviewFocusFiles")
local view = diffview_lib.get_current_view()
if view == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
return
end
local files = view.panel:ordered_file_list()
local layout = view.cur_layout
for _, file in ipairs(files) do
if file.path == file_name then
if not async_ok then
u.notify("Could not load Diffview async", vim.log.levels.ERROR)
return
end
async.await(view:set_file(file))
-- TODO: Ranged comments on unchanged lines will have both a
-- new line and a old line.
--
-- The same is true when the user leaves a single-line comment
-- on an unchanged line in the "b" buffer.
--
-- We need to distinguish them somehow from
-- range comments (which also have this) so that we can know
-- which buffer to jump to. Right now, we jump to the wrong
-- buffer for ranged comments on unchanged lines.
if new_line ~= nil and not opts.is_undefined_type then
layout.b:focus()
vim.api.nvim_win_set_cursor(0, { tonumber(new_line), 0 })
elseif old_line ~= nil then
layout.a:focus()
vim.api.nvim_win_set_cursor(0, { tonumber(old_line), 0 })
end
break
end
end
end
---Get the location of a line within the diffview. If range is specified, then also the location
---of the lines in range.
---@param range LineRange | nil Line range to get location for
---@return ReviewerInfo | nil nil is returned only if error was encountered
M.get_location = function(range)
if M.tabnr == nil then
u.notify("Diffview reviewer must be initialized first", vim.log.levels.ERROR)
return
end
local bufnr = vim.api.nvim_get_current_buf()
-- If there's a range, use the start of the visual selection, not the current line
local current_line = range and range.start_line or vim.api.nvim_win_get_cursor(0)[1]
-- check if we are in the diffview tab
local tabnr = vim.api.nvim_get_current_tabpage()
if tabnr ~= M.tabnr then
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
return
end
-- check if we are in the diffview buffer
local view = diffview_lib.get_current_view()
if view == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
return
end
local layout = view.cur_layout
local result = {}
local type
local is_new
if
layout.a.file.bufnr == bufnr
or (M.lines_are_same(view.cur_layout) and layout.b.file.bufnr == bufnr and range == nil)
then
result.file_name = layout.a.file.path
result.old_line = current_line
type = "old"
is_new = false
elseif layout.b.file.bufnr == bufnr then
result.file_name = layout.b.file.path
result.new_line = current_line
type = "new"
is_new = true
else
u.notify("Line location can only be determined within reviewer window")
return
end
local hunks = u.parse_hunk_headers(result.file_name, state.INFO.target_branch)
if hunks == nil then
u.notify("Could not parse hunks", vim.log.levels.ERROR)
return
end
local current_line_info
if is_new then
current_line_info = u.get_lines_from_hunks(hunks, result.new_line, is_new)
else
current_line_info = u.get_lines_from_hunks(hunks, result.old_line, is_new)
end
-- If single line comment is outside of changed lines then we need to specify both new line and old line
-- otherwise the API returns error.
-- https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff
if not current_line_info.in_hunk then
result.old_line = current_line_info.old_line
result.new_line = current_line_info.new_line
end
-- If users leave single-line comments in the new buffer that should be in the old buffer, we can
-- tell because the line will not have changed. Send the correct payload.
if M.lines_are_same(view.cur_layout) and layout.b.file.bufnr == bufnr and range == nil then
local a_win = u.get_win_from_buf(layout.a.file.bufnr)
local a_cursor = vim.api.nvim_win_get_cursor(a_win)[1]
result.old_line = a_cursor
result.new_line = a_cursor
type = "old"
end
if range == nil then
return result
end
result.range_info = { start = {}, ["end"] = {} }
if current_line == range.start_line then
result.range_info.start.old_line = current_line_info.old_line
result.range_info.start.new_line = current_line_info.new_line
result.range_info.start.type = type
else
local start_line_info = u.get_lines_from_hunks(hunks, range.start_line, is_new)
result.range_info.start.old_line = start_line_info.old_line
result.range_info.start.new_line = start_line_info.new_line
result.range_info.start.type = type
end
if current_line == range.end_line then
result.range_info["end"].old_line = current_line_info.old_line
result.range_info["end"].new_line = current_line_info.new_line
result.range_info["end"].type = type
else
local end_line_info = u.get_lines_from_hunks(hunks, range.end_line, is_new)
result.range_info["end"].old_line = end_line_info.old_line
result.range_info["end"].new_line = end_line_info.new_line
result.range_info["end"].type = type
end
return result
end
---Return content between start_line and end_line
---@param start_line integer
---@param end_line integer
---@return string[]
M.get_lines = function(start_line, end_line)
return vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
end
---@return boolean
M.lines_are_same = function(layout)
local a_win = u.get_win_from_buf(layout.a.file.bufnr)
local b_win = u.get_win_from_buf(layout.b.file.bufnr)
local a_cursor = vim.api.nvim_win_get_cursor(a_win)[1]
local b_cursor = vim.api.nvim_win_get_cursor(b_win)[1]
local line_a = u.get_line_content(layout.a.file.bufnr, a_cursor)
local line_b = u.get_line_content(layout.b.file.bufnr, b_cursor)
return line_a == line_b
end
---Get currently shown file
M.get_current_file = function()
local view = diffview_lib.get_current_view()
if not view then
return
end
return view.panel.cur_file.path
end
---Place a sign in currently reviewed file. Use new line for identifing lines after changes, old
---line for identifing lines before changes and both if line was not changed.
---@param signs SignTable[] table of signs. See :h sign_placelist
---@param type string "new" if diagnostic should be in file after changes else "old"
M.place_sign = function(signs, type)
local view = diffview_lib.get_current_view()
if not view then
return
end
if type == "new" then
for _, sign in ipairs(signs) do
sign.buffer = view.cur_layout.b.file.bufnr
end
elseif type == "old" then
for _, sign in ipairs(signs) do
sign.buffer = view.cur_layout.a.file.bufnr
end
end
vim.fn.sign_placelist(signs)
end
---Set diagnostics in currently reviewed file.
---@param namespace integer namespace for diagnostics
---@param diagnostics table see :h vim.diagnostic.set
---@param type string "new" if diagnostic should be in file after changes else "old"
---@param opts table? see :h vim.diagnostic.set
M.set_diagnostics = function(namespace, diagnostics, type, opts)
local view = diffview_lib.get_current_view()
if not view then
return
end
if type == "new" and view.cur_layout.b.file.bufnr then
vim.diagnostic.set(namespace, view.cur_layout.b.file.bufnr, diagnostics, opts)
elseif type == "old" and view.cur_layout.a.file.bufnr then
vim.diagnostic.set(namespace, view.cur_layout.a.file.bufnr, diagnostics, opts)
end
end
---Diffview exposes events which can be used to setup autocommands.
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
M.set_callback_for_file_changed = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.file_changed", {})
vim.api.nvim_create_autocmd("User", {
pattern = { "DiffviewDiffBufWinEnter", "DiffviewViewEnter" },
group = group,
callback = function(...)
if M.tabnr == vim.api.nvim_get_current_tabpage() then
callback(...)
end
end,
})
end
---Diffview exposes events which can be used to setup autocommands.
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
M.set_callback_for_reviewer_leave = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.leave", {})
vim.api.nvim_create_autocmd("User", {
pattern = { "DiffviewViewLeave", "DiffviewViewClosed" },
group = group,
callback = function(...)
if M.tabnr == vim.api.nvim_get_current_tabpage() then
callback(...)
end
end,
})
end
return M