This MR is a #MAJOR breaking change to the plugin. While the plugin will continue to work for users with their existing settings, they will be informed of outdated configuration (diagnostics and signs have been simplified) the next time they open the reviewer.

Fix: Trim trailing slash from custom URLs
Update: .github/CONTRIBUTING.md, .github/ISSUE_TEMPLATE/bug_report.md
Feat: Improve discussion tree toggling (#192)
Fix: Toggle modified notes (#188)
Fix: Toggle discussion nodes correctly
Feat: Show Help keymap in discussion tree winbar
Fix: Enable toggling nodes from the note body
Fix: Enable toggling resolved status from child nodes
Fix: Only try to show emoji popup on note nodes
Feat: Add keymap for toggling tree type
Fix: Disable tree type toggling in Notes
Fix Multi Line Issues (Large Refactor) (#197)
Fix: Multi-line discussions. The calculation of a range for a multiline comment has been consolidated and moved into the location.lua file. This does not attempt to fix diagnostics.
Refactor: It refactors the discussions code to split hunk parsing and management into a separate module
Fix: Don't allow comments on modified buffers #194 by preventing comments on the reviewer when using --imply-local and when the working tree is dirty entirely.
Refactor: It introduces a new List class for data aggregation, filtering, etc.
Fix: It removes redundant API calls and refreshes from the discussion pane
Fix: Location provider (#198)
Fix: add nil check for Diffview performance issue (#199)
Fix: Switch Tabs During Comment Creation (#200)
Fix: Check if file is modified (#201)
Fix: Off-By-One Issue in Old SHA (#202)
Fix: Rebuild Diagnostics + Signs (#203)
Fix: Off-By-One Issue in New SHA (#205)
Fix: Reviewer Jumps to wrong location (#206)

BREAKING CHANGE: Changes configuration of diagnostics and signs in the setup call.
This commit is contained in:
Harrison (Harry) Cramer
2024-03-03 11:52:37 -05:00
committed by GitHub
parent f6a5238d4b
commit b5b475ce8b
31 changed files with 1529 additions and 1298 deletions

View File

@@ -1,432 +0,0 @@
-- 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
-- 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
---@type ReviewerInfo
local reviewer_info = {
file_name = layout.a.file.path,
new_line = nil,
old_line = nil,
range_info = nil,
}
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
local current_win = vim.fn.win_getid()
local is_current_sha = current_win == b_win
if a_win == nil or b_win == nil then
u.notify("Error retrieving window IDs for current files", vim.log.levels.ERROR)
return
end
local current_file = M.get_current_file()
if current_file == nil then
u.notify("Error retrieving current file from Diffview", vim.log.levels.ERROR)
return
end
local a_linenr = vim.api.nvim_win_get_cursor(a_win)[1]
local b_linenr = vim.api.nvim_win_get_cursor(b_win)[1]
local data = u.parse_hunk_headers(current_file, state.INFO.target_branch)
if data.hunks == nil then
u.notify("Could not parse hunks", vim.log.levels.ERROR)
return
end
-- Will be different depending on focused window.
local modification_type =
M.get_modification_type(a_linenr, b_linenr, is_current_sha, data.hunks, data.all_diff_output)
if modification_type == "bad_file_unmodified" then
u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN)
end
-- Comment on new line: Include only new_line in payload.
if modification_type == "added" then
reviewer_info.old_line = nil
reviewer_info.new_line = b_linenr
-- Comment on deleted line: Include only new_line in payload.
elseif modification_type == "deleted" then
reviewer_info.old_line = a_linenr
reviewer_info.new_line = nil
-- The line was not found in any hunks, only send the old line number
elseif modification_type == "unmodified" or modification_type == "bad_file_unmodified" then
reviewer_info.old_line = a_linenr
reviewer_info.new_line = b_linenr
end
if range == nil then
return reviewer_info
end
-- If leaving a multi-line comment, we want to also add range_info to the payload.
local is_new = reviewer_info.new_line ~= nil
local current_line_info = is_new and u.get_lines_from_hunks(data.hunks, reviewer_info.new_line, is_new)
or u.get_lines_from_hunks(data.hunks, reviewer_info.old_line, is_new)
local type = is_new and "new" or "old"
---@type ReviewerRangeInfo
local range_info = { start = {}, ["end"] = {} }
if current_line == range.start_line then
range_info.start.old_line = current_line_info.old_line
range_info.start.new_line = current_line_info.new_line
range_info.start.type = type
else
local start_line_info = u.get_lines_from_hunks(data.hunks, range.start_line, is_new)
range_info.start.old_line = start_line_info.old_line
range_info.start.new_line = start_line_info.new_line
range_info.start.type = type
end
if current_line == range.end_line then
range_info["end"].old_line = current_line_info.old_line
range_info["end"].new_line = current_line_info.new_line
range_info["end"].type = type
else
local end_line_info = u.get_lines_from_hunks(data.hunks, range.end_line, is_new)
range_info["end"].old_line = end_line_info.old_line
range_info["end"].new_line = end_line_info.new_line
range_info["end"].type = type
end
reviewer_info.range_info = range_info
return reviewer_info
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
---Checks whether the lines in the two buffers are the same
---@return boolean
M.lines_are_same = function(layout, a_cursor, b_cursor)
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
---Returns whether the comment is on a deleted line, added line, or unmodified line.
---This is in order to build the payload for Gitlab correctly by setting the old line and new line.
---@param a_linenr number
---@param b_linenr number
---@param is_current_sha boolean
---@param hunks Hunk[] A list of hunks
---@param all_diff_output table The raw diff output
function M.get_modification_type(a_linenr, b_linenr, is_current_sha, hunks, all_diff_output)
for _, hunk in ipairs(hunks) do
local old_line_end = hunk.old_line + hunk.old_range
local new_line_end = hunk.new_line + hunk.new_range
if is_current_sha then
-- If it is a single line change and neither hunk has a range, then it's added
if b_linenr >= hunk.new_line and b_linenr <= new_line_end then
if hunk.new_range == 0 and hunk.old_range == 0 then
return "added"
end
-- If leaving a comment on the new window, we may be commenting on an added line
-- or on an unmodified line. To tell, we have to check whether the line itself is
-- prefixed with "+" and only return "added" if it is.
if M.line_was_added(b_linenr, hunk, all_diff_output) then
return "added"
end
end
else
-- It's a deletion if it's in the range of the hunks and the new
-- range is zero, since that is only a deletion hunk, or if we find
-- a match in another hunk with a range, and the corresponding line is prefixed
-- with a "-" only. If it is, then it's a deletion.
if a_linenr >= hunk.old_line and a_linenr <= old_line_end and hunk.old_range == 0 then
return "deleted"
end
if
(a_linenr >= hunk.old_line and a_linenr <= old_line_end)
or (a_linenr >= hunk.new_line and b_linenr <= new_line_end)
then
if M.line_was_removed(a_linenr, hunk, all_diff_output) then
return "deleted"
end
end
end
end
-- If we can't find the line, this means the user is either trying to leave
-- a comment on an unchanged line in the new or old file SHA. This is only
-- allowed in the old file
return is_current_sha and "bad_file_unmodified" or "unmodified"
end
---@param linnr number
---@param hunk Hunk
---@param all_diff_output table
M.line_was_removed = function(linnr, hunk, all_diff_output)
for matching_line_index, line in ipairs(all_diff_output) do
local found_hunk = u.parse_possible_hunk_headers(line)
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
-- We found a matching hunk, now we need to iterate over the lines from the raw diff output
-- at that hunk until we reach the line we are looking for. When the indexes match we check
-- to see if that line is deleted or not.
for hunk_line_index = found_hunk.old_line, hunk.old_line + hunk.old_range - 1, 1 do
local line_content = all_diff_output[matching_line_index + 1]
if hunk_line_index == linnr then
if string.match(line_content, "^%-") then
return "deleted"
end
end
end
end
end
end
---@param linnr number
---@param hunk Hunk
---@param all_diff_output table
M.line_was_added = function(linnr, hunk, all_diff_output)
for matching_line_index, line in ipairs(all_diff_output) do
local found_hunk = u.parse_possible_hunk_headers(line)
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
-- For added lines, we only want to iterate over the part of the diff that has has new lines,
-- so we skip over the old range. We then keep track of the increment to the original new line index,
-- and iterate until we reach the end of the total range of this hunk. If we arrive at the matching
-- index for the line number, we check to see if the line was added.
local i = 0
local old_range = (found_hunk.old_range == 0 and found_hunk.old_line ~= 0) and 1 or found_hunk.old_range
for hunk_line_index = matching_line_index + old_range + 1, matching_line_index + old_range + found_hunk.new_range, 1 do
local line_content = all_diff_output[hunk_line_index]
if (found_hunk.new_line + i) == linnr then
if string.match(line_content, "^%+") then
return "added"
end
end
i = i + 1
end
end
end
end
return M

View File

@@ -1,74 +1,265 @@
-- This Module contains all of the reviewer code. This is the code
-- that parses or interacts with diffview directly, such as opening
-- and closing, getting metadata about the current view, and registering
-- callbacks for open/close actions.
local List = require("gitlab.utils.list")
local u = require("gitlab.utils")
local state = require("gitlab.state")
local diffview = require("gitlab.reviewer.diffview")
local git = require("gitlab.git")
local hunks = require("gitlab.hunks")
local async = require("diffview.async")
local diffview_lib = require("diffview.lib")
local M = {
reviewer = nil,
}
local reviewer_map = {
diffview = diffview,
bufnr = nil,
tabnr = nil,
stored_win = nil,
}
-- Checks for legacy installations, only Diffview is supported.
M.init = function()
local reviewer = reviewer_map[state.settings.reviewer]
if reviewer == nil then
if state.settings.reviewer ~= "diffview" then
vim.notify(
string.format("gitlab.nvim could not find reviewer %s, only diffview is supported", state.settings.reviewer),
vim.log.levels.ERROR
)
end
end
-- Opens the reviewer window.
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
M.open = reviewer.open
-- Opens the reviewer window
if diff_refs.base_sha == "" or diff_refs.head_sha == "" then
u.notify("Merge request contains no changes", vim.log.levels.ERROR)
return
end
M.close = reviewer.close
-- Closes the reviewer and cleans up
local diffview_open_command = "DiffviewOpen"
local has_clean_tree = git.has_clean_tree()
if state.settings.reviewer_settings.diffview.imply_local and has_clean_tree then
diffview_open_command = diffview_open_command .. " --imply-local"
end
M.jump = reviewer.jump
-- Jumps to the location provided in the reviewer window
-- Parameters:
-- • {file_name} The name of the file to jump to
-- • {new_line} The new_line of the change
-- • {interval} The old_line of the change
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()
M.get_location = reviewer.get_location
-- Parameters:
-- • {range} LineRange if function was triggered from visual selection
-- Returns the current location (based on cursor) from the reviewer window as ReviewerInfo class
if state.settings.reviewer_settings.diffview.imply_local and not has_clean_tree then
u.notify(
"There are uncommited changes in the working tree, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
vim.log.levels.WARN
)
end
M.get_lines = reviewer.get_lines
-- Returns the content of the file in the current location in the reviewer window
if state.INFO.has_conflicts then
u.notify("This merge request has conflicts!", vim.log.levels.WARN)
end
M.get_current_file = reviewer.get_current_file
-- Get currently loaded file
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
u.notify(
"Diagnostics are now configured settings.discussion_signs, see :h gitlab.signs_and_diagnostics",
vim.log.levels.WARN
)
end
M.place_sign = reviewer.place_sign
-- Places a sign on the line for currently reviewed file.
-- Parameters:
-- • {id} The sign id
-- • {sign} The sign to place
-- • {group} The sign group to place on
-- • {new_line} The line to place the sign on
-- • {old_line} The buffer number to place the sign on
-- 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)
M.set_callback_for_file_changed = reviewer.set_callback_for_file_changed
-- Call callback whenever the file changes
-- Parameters:
-- • {callback} The callback to call
if state.settings.discussion_tree.auto_open then
local discussions = require("gitlab.actions.discussions")
discussions.close()
discussions.toggle()
end
end
M.set_callback_for_reviewer_leave = reviewer.set_callback_for_reviewer_leave
-- Call callback whenever the reviewer is left
-- Parameters:
-- • {callback} The callback to call
-- Closes the reviewer and cleans up
M.close = function()
vim.cmd("DiffviewClose")
local discussions = require("gitlab.actions.discussions")
discussions.close()
end
M.set_diagnostics = reviewer.set_diagnostics
-- Set diagnostics for currently reviewed file
-- Parameters:
-- • {namespace} The namespace for diagnostics
-- • {diagnostics} The diagnostics to set
-- • {type} "new" if diagnostic should be in file after changes else "old"
-- • {opts} see opts in :h vim.diagnostic.set
-- Jumps to the location provided in the reviewer window
---@param file_name string
---@param new_line number|nil
---@param old_line number|nil
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 file = List.new(files):find(function(file)
return file.path == file_name
end)
async.await(view:set_file(file))
local layout = view.cur_layout
if old_line == nil then
layout.b:focus()
vim.api.nvim_win_set_cursor(0, { new_line, 0 })
else
layout.a:focus()
vim.api.nvim_win_set_cursor(0, { old_line, 0 })
end
end
---Get the data from diffview, such as line information and file name. May be used by
---other modules such as the comment module to create line codes or set diagnostics
---@return DiffviewInfo | nil
M.get_reviewer_data = function()
if M.tabnr == nil then
u.notify("Diffview reviewer must be initialized first", vim.log.levels.ERROR)
return
end
-- 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 old_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
local new_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
if old_win == nil or new_win == nil then
u.notify("Error getting window IDs for current files", vim.log.levels.ERROR)
return
end
local current_file = M.get_current_file()
if current_file == nil then
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
return
end
local new_line = vim.api.nvim_win_get_cursor(new_win)[1]
local old_line = vim.api.nvim_win_get_cursor(old_win)[1]
local is_current_sha_focused = M.is_current_sha_focused()
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
if modification_type == nil then
u.notify("Error getting modification type", vim.log.levels.ERROR)
return
end
if modification_type == "bad_file_unmodified" then
u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN)
end
local current_bufnr = is_current_sha_focused and layout.b.file.bufnr or layout.a.file.bufnr
local opposite_bufnr = is_current_sha_focused and layout.a.file.bufnr or layout.b.file.bufnr
local old_sha_win_id = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
local new_sha_win_id = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
return {
file_name = layout.a.file.path,
old_line_from_buf = old_line,
new_line_from_buf = new_line,
modification_type = modification_type,
new_sha_win_id = new_sha_win_id,
current_bufnr = current_bufnr,
old_sha_win_id = old_sha_win_id,
opposite_bufnr = opposite_bufnr,
}
end
---Return whether user is focused on the new version of the file
---@return boolean
M.is_current_sha_focused = function()
local view = diffview_lib.get_current_view()
local layout = view.cur_layout
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
local current_win = vim.fn.win_getid()
-- Handle cases where user navigates tabs in the middle of making a comment
if a_win ~= current_win and b_win ~= current_win then
current_win = M.stored_win
M.stored_win = nil
end
return current_win == b_win
end
---Get currently shown file
---@return string|nil
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
---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" },
group = group,
callback = function(...)
M.stored_win = vim.api.nvim_get_current_win()
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
M.set_callback_for_reviewer_enter = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.enter", {})
vim.api.nvim_create_autocmd("User", {
pattern = { "DiffviewViewOpened" },
group = group,
callback = function(...)
callback(...)
end,
})
end
return M

223
lua/gitlab/reviewer/location.lua Executable file
View File

@@ -0,0 +1,223 @@
local u = require("gitlab.utils")
local hunks = require("gitlab.hunks")
local state = require("gitlab.state")
---@class Location
---@field location_data LocationData
---@field reviewer_data DiffviewInfo
---@field run function
---@field build_location_data function
---@class ReviewerLineInfo
---@field old_line integer|nil
---@field new_line integer|nil
---@field type "new"|"old"
---@class ReviewerRangeInfo
---@field start ReviewerLineInfo
---@field end ReviewerLineInfo
local Location = {}
Location.__index = Location
---@param reviewer_data DiffviewInfo
---@param visual_range LineRange | nil
---@return Location
function Location.new(reviewer_data, visual_range)
local location = {}
local instance = setmetatable(location, Location)
instance.reviewer_data = reviewer_data
instance.visual_range = visual_range
instance.base_sha = state.INFO.diff_refs.base_sha
instance.head_sha = state.INFO.diff_refs.head_sha
return instance
end
---Takes in information about the current changes, such as the file name, modification type of the diff, and the line numbers
---and builds the appropriate payload when creating a comment.
function Location:build_location_data()
---@type DiffviewInfo
local reviewer_data = self.reviewer_data
---@type LineRange | nil
local visual_range = self.visual_range
---@type LocationData
local location_data = {
old_line = nil,
new_line = nil,
line_range = nil,
}
-- Comment on new line: Include only new_line in payload.
-- Comment on deleted line: Include only old_line in payload.
-- The line was not found in any hunks, send both lines.
if reviewer_data.modification_type == "added" then
location_data.old_line = nil
location_data.new_line = reviewer_data.new_line_from_buf
elseif reviewer_data.modification_type == "deleted" then
location_data.old_line = reviewer_data.old_line_from_buf
location_data.new_line = nil
elseif
reviewer_data.modification_type == "unmodified" or reviewer_data.modification_type == "bad_file_unmodified"
then
location_data.old_line = reviewer_data.old_line_from_buf
location_data.new_line = reviewer_data.new_line_from_buf
end
self.location_data = location_data
if visual_range == nil then
return
else
self.location_data.line_range = {
start = {},
["end"] = {},
}
end
self:set_start_range(visual_range)
self:set_end_range(visual_range)
-- Ranged comments should always use the end of the range.
-- Otherwise they will not highlight the full comment in Gitlab.
self.location_data.old_line = self.location_data.line_range["end"].old_line
self.location_data.new_line = self.location_data.line_range["end"].new_line
end
-- Helper methods 🤝
-- Returns the matching line from the new SHA.
-- For instance, line 12 in the new SHA may be scroll-linked
-- to line 10 in the old SHA.
---@param line number
---@return number|nil
function Location:get_line_number_from_new_sha(line)
local reviewer = require("gitlab.reviewer")
local is_current_sha_focused = reviewer.is_current_sha_focused()
if is_current_sha_focused then
return line
end
-- Otherwise we want to get the matching line in the opposite buffer
return hunks.calculate_matching_line_new(self.base_sha, self.head_sha, self.reviewer_data.file_name, line)
end
-- Returns the matching line from the old SHA.
-- For instance, line 12 in the new SHA may be scroll-linked
-- to line 10 in the old SHA.
---@param line number
---@return number|nil
function Location:get_line_number_from_old_sha(line)
local reviewer = require("gitlab.reviewer")
local is_current_sha_focused = reviewer.is_current_sha_focused()
if not is_current_sha_focused then
return line
end
-- Otherwise we want to get the matching line in the opposite buffer
return hunks.calculate_matching_line_new(self.head_sha, self.base_sha, self.reviewer_data.file_name, line)
end
-- Returns the current line number from whatever SHA (new or old)
-- the reviewer is focused in.
---@return number|nil
function Location:get_current_line()
local reviewer = require("gitlab.reviewer")
local win_id = reviewer.is_current_sha_focused() and self.reviewer_data.new_sha_win_id
or self.reviewer_data.old_sha_win_id
if win_id == nil then
return
end
local current_line = vim.api.nvim_win_get_cursor(win_id)[1]
return current_line
end
-- Given a new_line and old_line from the start of a ranged comment, returns the start
-- range information for the Gitlab payload
---@param visual_range LineRange
---@return ReviewerLineInfo|nil
function Location:set_start_range(visual_range)
local current_file = require("gitlab.reviewer").get_current_file()
if current_file == nil then
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
return
end
local reviewer = require("gitlab.reviewer")
local is_current_sha_focused = reviewer.is_current_sha_focused()
local win_id = is_current_sha_focused and self.reviewer_data.new_sha_win_id or self.reviewer_data.old_sha_win_id
if win_id == nil then
u.notify("Error getting window number of SHA for start range", vim.log.levels.ERROR)
return
end
local current_line = self:get_current_line()
if current_line == nil then
u.notify("Error getting current line for start range", vim.log.levels.ERROR)
return
end
local new_line = self:get_line_number_from_new_sha(visual_range.start_line)
local old_line = self:get_line_number_from_old_sha(visual_range.start_line)
if
(new_line == nil and self.reviewer_data.modification_type ~= "deleted")
or (old_line == nil and self.reviewer_data.modification_type ~= "added")
then
u.notify("Error getting new or old line for start range", vim.log.levels.ERROR)
return
end
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
if modification_type == nil then
u.notify("Error getting modification type for start of range", vim.log.levels.ERROR)
return
end
self.location_data.line_range.start = {
new_line = modification_type ~= "deleted" and new_line or nil,
old_line = modification_type ~= "added" and old_line or nil,
type = modification_type == "added" and "new" or "old",
}
end
-- Given a modification type, a range, and the hunk data, returns the end range information
-- for the Gitlab payload
---@param visual_range LineRange
function Location:set_end_range(visual_range)
local current_file = require("gitlab.reviewer").get_current_file()
if current_file == nil then
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
return
end
local current_line = self:get_current_line()
if current_line == nil then
u.notify("Error getting current line for end range", vim.log.levels.ERROR)
return
end
local new_line = self:get_line_number_from_new_sha(visual_range.end_line)
local old_line = self:get_line_number_from_old_sha(visual_range.end_line)
if
(new_line == nil and self.reviewer_data.modification_type ~= "deleted")
or (old_line == nil and self.reviewer_data.modification_type ~= "added")
then
u.notify("Error getting new or old line for end range", vim.log.levels.ERROR)
return
end
local reviewer = require("gitlab.reviewer")
local is_current_sha_focused = reviewer.is_current_sha_focused()
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
if modification_type == nil then
u.notify("Error getting modification type for end of range", vim.log.levels.ERROR)
return
end
self.location_data.line_range["end"] = {
new_line = modification_type ~= "deleted" and new_line or nil,
old_line = modification_type ~= "added" and old_line or nil,
type = modification_type == "added" and "new" or "old",
}
end
return Location