Skip to content

Commit faf2a25

Browse files
Fix: Line Comment Refactor (#180)
This MR re-implements and fixes logic that better handles single-line comments. It does this by parsing the hunk headers and examining the actual changes they contain in order to form the Gitlab payload correctly. Addresses #128
1 parent baee20b commit faf2a25

File tree

3 files changed

+223
-93
lines changed

3 files changed

+223
-93
lines changed

lua/gitlab/actions/comment.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ end
112112
---@field file_name string
113113
---@field old_line integer | nil
114114
---@field new_line integer | nil
115-
---@field range_info ReviewerRangeInfo
115+
---@field range_info ReviewerRangeInfo|nil
116116

117117
---This function (settings.popup.perform_action) will send the comment to the Go server
118118
---@param text string comment text

lua/gitlab/reviewer/diffview.lua

100644100755
Lines changed: 173 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -126,106 +126,117 @@ M.get_location = function(range)
126126
return
127127
end
128128

129-
local bufnr = vim.api.nvim_get_current_buf()
130-
131129
-- If there's a range, use the start of the visual selection, not the current line
132130
local current_line = range and range.start_line or vim.api.nvim_win_get_cursor(0)[1]
133131

134-
-- check if we are in the diffview tab
132+
-- Check if we are in the diffview tab
135133
local tabnr = vim.api.nvim_get_current_tabpage()
136134
if tabnr ~= M.tabnr then
137135
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
138136
return
139137
end
140138

141-
-- check if we are in the diffview buffer
139+
-- Check if we are in the diffview buffer
142140
local view = diffview_lib.get_current_view()
143141
if view == nil then
144142
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
145143
return
146144
end
145+
147146
local layout = view.cur_layout
148-
local result = {}
149-
local type
150-
local is_new
151-
152-
if
153-
layout.a.file.bufnr == bufnr
154-
or (M.lines_are_same(view.cur_layout) and layout.b.file.bufnr == bufnr and range == nil)
155-
then
156-
result.file_name = layout.a.file.path
157-
result.old_line = current_line
158-
type = "old"
159-
is_new = false
160-
elseif layout.b.file.bufnr == bufnr then
161-
result.file_name = layout.b.file.path
162-
result.new_line = current_line
163-
type = "new"
164-
is_new = true
165-
else
166-
u.notify("Line location can only be determined within reviewer window")
147+
148+
---@type ReviewerInfo
149+
local reviewer_info = {
150+
file_name = layout.a.file.path,
151+
new_line = nil,
152+
old_line = nil,
153+
range_info = nil,
154+
}
155+
156+
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
157+
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
158+
local current_win = vim.fn.win_getid()
159+
local is_current_sha = current_win == b_win
160+
161+
if a_win == nil or b_win == nil then
162+
u.notify("Error retrieving window IDs for current files", vim.log.levels.ERROR)
167163
return
168164
end
169165

170-
local hunks = u.parse_hunk_headers(result.file_name, state.INFO.target_branch)
171-
if hunks == nil then
172-
u.notify("Could not parse hunks", vim.log.levels.ERROR)
166+
local current_file = M.get_current_file()
167+
if current_file == nil then
168+
u.notify("Error retrieving current file from Diffview", vim.log.levels.ERROR)
173169
return
174170
end
175171

176-
local current_line_info
177-
if is_new then
178-
current_line_info = u.get_lines_from_hunks(hunks, result.new_line, is_new)
179-
else
180-
current_line_info = u.get_lines_from_hunks(hunks, result.old_line, is_new)
172+
local a_linenr = vim.api.nvim_win_get_cursor(a_win)[1]
173+
local b_linenr = vim.api.nvim_win_get_cursor(b_win)[1]
174+
175+
local data = u.parse_hunk_headers(current_file, state.INFO.target_branch)
176+
177+
if data.hunks == nil then
178+
u.notify("Could not parse hunks", vim.log.levels.ERROR)
179+
return
181180
end
182181

183-
-- If single line comment is outside of changed lines then we need to specify both new line and old line
184-
-- otherwise the API returns error.
185-
-- https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff
186-
if not current_line_info.in_hunk then
187-
result.old_line = current_line_info.old_line
188-
result.new_line = current_line_info.new_line
182+
-- Will be different depending on focused window.
183+
local modification_type =
184+
M.get_modification_type(a_linenr, b_linenr, is_current_sha, data.hunks, data.all_diff_output)
185+
186+
if modification_type == "bad_file_unmodified" then
187+
u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN)
189188
end
190189

191-
-- If users leave single-line comments in the new buffer that should be in the old buffer, we can
192-
-- tell because the line will not have changed. Send the correct payload.
193-
if M.lines_are_same(view.cur_layout) and layout.b.file.bufnr == bufnr and range == nil then
194-
local a_win = u.get_win_from_buf(layout.a.file.bufnr)
195-
local a_cursor = vim.api.nvim_win_get_cursor(a_win)[1]
196-
result.old_line = a_cursor
197-
result.new_line = a_cursor
198-
type = "old"
190+
-- Comment on new line: Include only new_line in payload.
191+
if modification_type == "added" then
192+
reviewer_info.old_line = nil
193+
reviewer_info.new_line = b_linenr
194+
-- Comment on deleted line: Include only new_line in payload.
195+
elseif modification_type == "deleted" then
196+
reviewer_info.old_line = a_linenr
197+
reviewer_info.new_line = nil
198+
-- The line was not found in any hunks, only send the old line number
199+
elseif modification_type == "unmodified" or modification_type == "bad_file_unmodified" then
200+
reviewer_info.old_line = a_linenr
201+
reviewer_info.new_line = b_linenr
199202
end
200203

201204
if range == nil then
202-
return result
205+
return reviewer_info
203206
end
204207

205-
result.range_info = { start = {}, ["end"] = {} }
208+
-- If leaving a multi-line comment, we want to also add range_info to the payload.
209+
local is_new = reviewer_info.new_line ~= nil
210+
local current_line_info = is_new and u.get_lines_from_hunks(data.hunks, reviewer_info.new_line, is_new)
211+
or u.get_lines_from_hunks(data.hunks, reviewer_info.old_line, is_new)
212+
local type = is_new and "new" or "old"
213+
214+
---@type ReviewerRangeInfo
215+
local range_info = { start = {}, ["end"] = {} }
216+
206217
if current_line == range.start_line then
207-
result.range_info.start.old_line = current_line_info.old_line
208-
result.range_info.start.new_line = current_line_info.new_line
209-
result.range_info.start.type = type
218+
range_info.start.old_line = current_line_info.old_line
219+
range_info.start.new_line = current_line_info.new_line
220+
range_info.start.type = type
210221
else
211-
local start_line_info = u.get_lines_from_hunks(hunks, range.start_line, is_new)
212-
result.range_info.start.old_line = start_line_info.old_line
213-
result.range_info.start.new_line = start_line_info.new_line
214-
result.range_info.start.type = type
222+
local start_line_info = u.get_lines_from_hunks(data.hunks, range.start_line, is_new)
223+
range_info.start.old_line = start_line_info.old_line
224+
range_info.start.new_line = start_line_info.new_line
225+
range_info.start.type = type
215226
end
216-
217227
if current_line == range.end_line then
218-
result.range_info["end"].old_line = current_line_info.old_line
219-
result.range_info["end"].new_line = current_line_info.new_line
220-
result.range_info["end"].type = type
228+
range_info["end"].old_line = current_line_info.old_line
229+
range_info["end"].new_line = current_line_info.new_line
230+
range_info["end"].type = type
221231
else
222-
local end_line_info = u.get_lines_from_hunks(hunks, range.end_line, is_new)
223-
result.range_info["end"].old_line = end_line_info.old_line
224-
result.range_info["end"].new_line = end_line_info.new_line
225-
result.range_info["end"].type = type
232+
local end_line_info = u.get_lines_from_hunks(data.hunks, range.end_line, is_new)
233+
range_info["end"].old_line = end_line_info.old_line
234+
range_info["end"].new_line = end_line_info.new_line
235+
range_info["end"].type = type
226236
end
227237

228-
return result
238+
reviewer_info.range_info = range_info
239+
return reviewer_info
229240
end
230241

231242
---Return content between start_line and end_line
@@ -236,12 +247,9 @@ M.get_lines = function(start_line, end_line)
236247
return vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
237248
end
238249

250+
---Checks whether the lines in the two buffers are the same
239251
---@return boolean
240-
M.lines_are_same = function(layout)
241-
local a_win = u.get_win_from_buf(layout.a.file.bufnr)
242-
local b_win = u.get_win_from_buf(layout.b.file.bufnr)
243-
local a_cursor = vim.api.nvim_win_get_cursor(a_win)[1]
244-
local b_cursor = vim.api.nvim_win_get_cursor(b_win)[1]
252+
M.lines_are_same = function(layout, a_cursor, b_cursor)
245253
local line_a = u.get_line_content(layout.a.file.bufnr, a_cursor)
246254
local line_b = u.get_line_content(layout.b.file.bufnr, b_cursor)
247255
return line_a == line_b
@@ -324,4 +332,101 @@ M.set_callback_for_reviewer_leave = function(callback)
324332
})
325333
end
326334

335+
---Returns whether the comment is on a deleted line, added line, or unmodified line.
336+
---This is in order to build the payload for Gitlab correctly by setting the old line and new line.
337+
---@param a_linenr number
338+
---@param b_linenr number
339+
---@param is_current_sha boolean
340+
---@param hunks Hunk[] A list of hunks
341+
---@param all_diff_output table The raw diff output
342+
function M.get_modification_type(a_linenr, b_linenr, is_current_sha, hunks, all_diff_output)
343+
for _, hunk in ipairs(hunks) do
344+
local old_line_end = hunk.old_line + hunk.old_range
345+
local new_line_end = hunk.new_line + hunk.new_range
346+
347+
if is_current_sha then
348+
-- If it is a single line change and neither hunk has a range, then it's added
349+
if b_linenr >= hunk.new_line and b_linenr <= new_line_end then
350+
if hunk.new_range == 0 and hunk.old_range == 0 then
351+
return "added"
352+
end
353+
-- If leaving a comment on the new window, we may be commenting on an added line
354+
-- or on an unmodified line. To tell, we have to check whether the line itself is
355+
-- prefixed with "+" and only return "added" if it is.
356+
if M.line_was_added(b_linenr, hunk, all_diff_output) then
357+
return "added"
358+
end
359+
end
360+
else
361+
-- It's a deletion if it's in the range of the hunks and the new
362+
-- range is zero, since that is only a deletion hunk, or if we find
363+
-- a match in another hunk with a range, and the corresponding line is prefixed
364+
-- with a "-" only. If it is, then it's a deletion.
365+
if a_linenr >= hunk.old_line and a_linenr <= old_line_end and hunk.old_range == 0 then
366+
return "deleted"
367+
end
368+
if
369+
(a_linenr >= hunk.old_line and a_linenr <= old_line_end)
370+
or (a_linenr >= hunk.new_line and b_linenr <= new_line_end)
371+
then
372+
if M.line_was_removed(a_linenr, hunk, all_diff_output) then
373+
return "deleted"
374+
end
375+
end
376+
end
377+
end
378+
379+
-- If we can't find the line, this means the user is either trying to leave
380+
-- a comment on an unchanged line in the new or old file SHA. This is only
381+
-- allowed in the old file
382+
return is_current_sha and "bad_file_unmodified" or "unmodified"
383+
end
384+
385+
---@param linnr number
386+
---@param hunk Hunk
387+
---@param all_diff_output table
388+
M.line_was_removed = function(linnr, hunk, all_diff_output)
389+
for matching_line_index, line in ipairs(all_diff_output) do
390+
local found_hunk = u.parse_possible_hunk_headers(line)
391+
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
392+
-- We found a matching hunk, now we need to iterate over the lines from the raw diff output
393+
-- at that hunk until we reach the line we are looking for. When the indexes match we check
394+
-- to see if that line is deleted or not.
395+
for hunk_line_index = found_hunk.old_line, hunk.old_line + hunk.old_range - 1, 1 do
396+
local line_content = all_diff_output[matching_line_index + 1]
397+
if hunk_line_index == linnr then
398+
if string.match(line_content, "^%-") then
399+
return "deleted"
400+
end
401+
end
402+
end
403+
end
404+
end
405+
end
406+
407+
---@param linnr number
408+
---@param hunk Hunk
409+
---@param all_diff_output table
410+
M.line_was_added = function(linnr, hunk, all_diff_output)
411+
for matching_line_index, line in ipairs(all_diff_output) do
412+
local found_hunk = u.parse_possible_hunk_headers(line)
413+
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
414+
-- For added lines, we only want to iterate over the part of the diff that has has new lines,
415+
-- so we skip over the old range. We then keep track of the increment to the original new line index,
416+
-- and iterate until we reach the end of the total range of this hunk. If we arrive at the matching
417+
-- index for the line number, we check to see if the line was added.
418+
local i = 0
419+
local old_range = (found_hunk.old_range == 0 and found_hunk.old_line ~= 0) and 1 or found_hunk.old_range
420+
for hunk_line_index = matching_line_index + old_range + 1, matching_line_index + old_range + found_hunk.new_range, 1 do
421+
local line_content = all_diff_output[hunk_line_index]
422+
if (found_hunk.new_line + i) == linnr then
423+
if string.match(line_content, "^%+") then
424+
return "added"
425+
end
426+
end
427+
i = i + 1
428+
end
429+
end
430+
end
431+
end
327432
return M

0 commit comments

Comments
 (0)