-- 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, } 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 vim.api.nvim_command(string.format("DiffviewOpen %s..%s", diff_refs.base_sha, diff_refs.head_sha)) M.tabnr = vim.api.nvim_get_current_tabpage() if state.INFO.has_conflicts then u.notify("This merge request has conflicts!", vim.log.levels.WARN) end local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.close", {}) vim.api.nvim_create_autocmd("User", { pattern = { "DiffviewViewClosed" }, group = group, callback = function() --Check if our diffview tab was closed if vim.api.nvim_tabpage_is_valid(M.tabnr) then M.tabnr = nil end end, }) end M.jump = function(file_name, new_line, old_line) 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)) if new_line ~= nil 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") return end local bufnr = vim.api.nvim_get_current_buf() local current_line = 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") 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 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 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 ---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 table 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