Skip to content

Commit d985d71

Browse files
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
1 parent a6401d9 commit d985d71

File tree

21 files changed

+1046
-773
lines changed

21 files changed

+1046
-773
lines changed

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ $ luacheck --globals vim busted --no-max-line-length -- .
4949

5050
Please provide a description of the feature, and links to any relevant issues.
5151

52-
That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it, it will be merged into the develop branc, it will be merged into the develop branch.
52+
That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it, it will be merged into the develop branch.
5353

5454
After some time, if the develop branch is found to be stable, that branch will be merged into `main` and released. When merged into `main` the pipeline will detect whether we're merging in a patch, minor, or major change, and create a new tag (e.g. 1.0.12) and release.

.github/workflows/go.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ on:
33
pull_request:
44
branches:
55
- main
6+
- develop
67
jobs:
78
go_lint:
89
name: Lint Go 💅

.github/workflows/lua.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ on:
33
pull_request:
44
branches:
55
- main
6+
- develop
67
jobs:
78
lua_lint:
89
name: Lint Lua 💅

lua/gitlab/actions/assignees_and_reviewers.lua

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
-- and assignees in Gitlab, those who must review an MR.
33
local u = require("gitlab.utils")
44
local job = require("gitlab.job")
5+
local List = require("gitlab.utils.list")
56
local state = require("gitlab.state")
67
local M = {}
78

@@ -67,13 +68,11 @@ end
6768

6869
M.filter_eligible = function(current, to_remove)
6970
local ids = u.extract(to_remove, "id")
70-
local res = {}
71-
for _, member in ipairs(current) do
71+
return List.new(current):filter(function(member)
7272
if not u.contains(ids, member.id) then
73-
table.insert(res, member)
73+
return true
7474
end
75-
end
76-
return res
75+
end)
7776
end
7877

7978
return M

lua/gitlab/actions/comment.lua

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ local Popup = require("nui.popup")
55
local state = require("gitlab.state")
66
local job = require("gitlab.job")
77
local u = require("gitlab.utils")
8+
local git = require("gitlab.git")
89
local discussions = require("gitlab.actions.discussions")
910
local miscellaneous = require("gitlab.actions.miscellaneous")
1011
local reviewer = require("gitlab.reviewer")
12+
local Location = require("gitlab.reviewer.location")
1113
local M = {}
1214

1315
-- Popup creation is wrapped in a function so that it is performed *after* user
@@ -20,6 +22,14 @@ end
2022
-- This function will open a comment popup in order to create a comment on the changed/updated
2123
-- line in the current MR
2224
M.create_comment = function()
25+
local has_clean_tree = git.has_clean_tree()
26+
if state.settings.reviewer_settings.diffview.imply_local and not has_clean_tree then
27+
u.notify(
28+
"Cannot leave comments on a dirty tree. \n Please stash all local changes or push them up.",
29+
vim.log.levels.WARN
30+
)
31+
return
32+
end
2333
local comment_popup = create_comment_popup()
2434
comment_popup:mount()
2535
state.set_popup_keymaps(comment_popup, function(text)
@@ -95,30 +105,11 @@ M.create_note = function()
95105
end, miscellaneous.attach_file)
96106
end
97107

98-
---@class LineRange
99-
---@field start_line integer
100-
---@field end_line integer
101-
102-
---@class ReviewerLineInfo
103-
---@field old_line integer
104-
---@field new_line integer
105-
---@field type string either "new" or "old"
106-
107-
---@class ReviewerRangeInfo
108-
---@field start ReviewerLineInfo
109-
---@field end ReviewerLineInfo
110-
111-
---@class ReviewerInfo
112-
---@field file_name string
113-
---@field old_line integer | nil
114-
---@field new_line integer | nil
115-
---@field range_info ReviewerRangeInfo|nil
116-
117108
---This function (settings.popup.perform_action) will send the comment to the Go server
118109
---@param text string comment text
119-
---@param range LineRange | nil range of visuel selection or nil
110+
---@param visual_range LineRange | nil range of visual selection or nil
120111
---@param unlinked boolean | nil if true, the comment is not linked to a line
121-
M.confirm_create_comment = function(text, range, unlinked)
112+
M.confirm_create_comment = function(text, visual_range, unlinked)
122113
if text == nil then
123114
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
124115
return
@@ -129,33 +120,42 @@ M.confirm_create_comment = function(text, range, unlinked)
129120
job.run_job("/mr/comment", "POST", body, function(data)
130121
u.notify("Note created!", vim.log.levels.INFO)
131122
discussions.add_discussion({ data = data, unlinked = true })
132-
discussions.refresh_discussion_data()
123+
discussions.refresh()
133124
end)
134125
return
135126
end
136127

137-
local reviewer_info = reviewer.get_location(range)
138-
if not reviewer_info then
128+
local reviewer_data = reviewer.get_reviewer_data()
129+
if reviewer_data == nil then
130+
u.notify("Error getting reviewer data", vim.log.levels.ERROR)
131+
return
132+
end
133+
134+
local location = Location:new(reviewer_data, visual_range)
135+
location:build_location_data()
136+
local location_data = location.location_data
137+
if location_data == nil then
138+
u.notify("Error getting location information", vim.log.levels.ERROR)
139139
return
140140
end
141141

142142
local revision = state.MR_REVISIONS[1]
143143
local body = {
144+
type = "text",
144145
comment = text,
145-
file_name = reviewer_info.file_name,
146-
old_line = reviewer_info.old_line,
147-
new_line = reviewer_info.new_line,
146+
file_name = reviewer_data.file_name,
148147
base_commit_sha = revision.base_commit_sha,
149148
start_commit_sha = revision.start_commit_sha,
150149
head_commit_sha = revision.head_commit_sha,
151-
type = "text",
152-
line_range = reviewer_info.range_info,
150+
old_line = location_data.old_line,
151+
new_line = location_data.new_line,
152+
line_range = location_data.line_range,
153153
}
154154

155155
job.run_job("/mr/comment", "POST", body, function(data)
156156
u.notify("Comment created!", vim.log.levels.INFO)
157157
discussions.add_discussion({ data = data, unlinked = false })
158-
discussions.refresh_discussion_data()
158+
discussions.refresh()
159159
end)
160160
end
161161

lua/gitlab/actions/create_mr.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ local Input = require("nui.input")
55
local Popup = require("nui.popup")
66
local job = require("gitlab.job")
77
local u = require("gitlab.utils")
8+
local git = require("gitlab.git")
89
local state = require("gitlab.state")
910
local miscellaneous = require("gitlab.actions.miscellaneous")
1011

@@ -124,7 +125,7 @@ M.pick_target = function(args)
124125
end
125126

126127
local function make_template_path(t)
127-
local base_dir = vim.fn.trim(vim.fn.system({ "git", "rev-parse", "--show-toplevel" }))
128+
local base_dir = git.base_dir()
128129
return base_dir
129130
.. state.settings.file_separator
130131
.. ".gitlab"

lua/gitlab/actions/discussions/annotations.lua

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,22 @@
101101
---@field user_data table
102102
---@field source string
103103
---@field code string?
104+
105+
---@class LineRange
106+
---@field start_line integer
107+
---@field end_line integer
108+
109+
---@class DiffviewInfo
110+
---@field modification_type string
111+
---@field file_name string
112+
---@field current_bufnr integer
113+
---@field new_sha_win_id integer
114+
---@field old_sha_win_id integer
115+
---@field opposite_bufnr integer
116+
---@field new_line_from_buf integer
117+
---@field old_line_from_buf integer
118+
119+
---@class LocationData
120+
---@field old_line integer | nil
121+
---@field new_line integer | nil
122+
---@field line_range ReviewerRangeInfo|nil

lua/gitlab/actions/discussions/init.lua

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ local job = require("gitlab.job")
99
local u = require("gitlab.utils")
1010
local state = require("gitlab.state")
1111
local reviewer = require("gitlab.reviewer")
12+
local List = require("gitlab.utils.list")
1213
local miscellaneous = require("gitlab.actions.miscellaneous")
1314
local discussions_tree = require("gitlab.actions.discussions.tree")
15+
local diffview_lib = require("diffview.lib")
1416
local signs = require("gitlab.actions.discussions.signs")
1517
local winbar = require("gitlab.actions.discussions.winbar")
1618
local help = require("gitlab.actions.help")
@@ -53,28 +55,55 @@ end
5355
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
5456
M.initialize_discussions = function()
5557
signs.setup_signs()
56-
-- Setup callback to refresh discussion data, discussion signs and diagnostics whenever the reviewed file changes.
57-
reviewer.set_callback_for_file_changed(M.refresh_discussion_data)
58-
-- Setup callback to clear signs and diagnostics whenever reviewer is left.
59-
reviewer.set_callback_for_reviewer_leave(signs.clear_signs_and_diagnostics)
58+
reviewer.set_callback_for_file_changed(function()
59+
M.refresh_view()
60+
M.modifiable(false)
61+
end)
62+
reviewer.set_callback_for_reviewer_enter(function()
63+
M.modifiable(false)
64+
end)
65+
reviewer.set_callback_for_reviewer_leave(function()
66+
signs.clear_signs_and_diagnostics()
67+
M.modifiable(true)
68+
end)
69+
end
70+
71+
--- Ensures that the both buffers in the reviewer are/not modifiable. Relevant if the user is using
72+
--- the --imply-local setting
73+
M.modifiable = function(bool)
74+
local view = diffview_lib.get_current_view()
75+
local a = view.cur_layout.a.file.bufnr
76+
local b = view.cur_layout.b.file.bufnr
77+
if vim.api.nvim_buf_is_loaded(a) then
78+
vim.api.nvim_buf_set_option(a, "modifiable", bool)
79+
end
80+
if vim.api.nvim_buf_is_loaded(b) then
81+
vim.api.nvim_buf_set_option(b, "modifiable", bool)
82+
end
6083
end
6184

6285
---Refresh discussion data, signs, diagnostics, and winbar with new data from API
63-
M.refresh_discussion_data = function()
86+
--- and rebuild the entire view
87+
M.refresh = function()
6488
M.load_discussions(function()
65-
if state.settings.discussion_sign.enabled then
66-
signs.refresh_signs(M.discussions)
67-
end
68-
if state.settings.discussion_diagnostic.enabled then
69-
signs.refresh_diagnostics(M.discussions)
70-
end
71-
if M.split_visible then
72-
local linked_is_focused = M.linked_bufnr == M.focused_bufnr
73-
winbar.update_winbar(M.discussions, M.unlinked_discussions, linked_is_focused and "Discussions" or "Notes")
74-
end
89+
M.refresh_view()
7590
end)
7691
end
7792

93+
--- Take existing data and refresh the diagnostics, the winbar, and the signs
94+
M.refresh_view = function()
95+
if state.settings.discussion_sign.enabled then
96+
signs.refresh_signs(M.discussions)
97+
end
98+
if state.settings.discussion_diagnostic.enabled then
99+
signs.refresh_diagnostics(M.discussions)
100+
end
101+
if M.split_visible then
102+
local linked_is_focused = M.linked_bufnr == M.focused_bufnr
103+
winbar.update_winbar(M.discussions, M.unlinked_discussions, linked_is_focused and "Discussions" or "Notes")
104+
end
105+
end
106+
78107
---Toggle Discussions tree type between "simple" and "by_file_name"
79108
---@param unlinked boolean True if selected view type is Notes (unlinked discussions)
80109
M.toggle_tree_type = function(unlinked)
@@ -148,6 +177,7 @@ M.toggle = function(callback)
148177
end)
149178
end
150179

180+
-- Change between views in the discussion panel, either notes or discussions
151181
local switch_view_type = function()
152182
local change_to_unlinked = M.linked_bufnr == M.focused_bufnr
153183
local new_bufnr = change_to_unlinked and M.unlinked_bufnr or M.linked_bufnr
@@ -254,36 +284,23 @@ end
254284

255285
-- This function will actually send the deletion to Gitlab
256286
-- when you make a selection, and re-render the tree
257-
M.send_deletion = function(tree, unlinked)
287+
M.send_deletion = function(tree)
258288
local current_node = tree:get_node()
259289

260290
local note_node = M.get_note_node(tree, current_node)
261291
local root_node = M.get_root_node(tree, current_node)
262292
local note_id = note_node.is_root and root_node.root_note_id or note_node.id
263-
264293
local body = { discussion_id = root_node.id, note_id = tonumber(note_id) }
265-
266294
job.run_job("/mr/comment", "DELETE", body, function(data)
267295
u.notify(data.message, vim.log.levels.INFO)
268-
if not note_node.is_root then
269-
tree:remove_node("-" .. note_id) -- Note is not a discussion root, safe to remove
270-
tree:render()
296+
if note_node.is_root then
297+
-- Replace root node w/ current node's contents...
298+
tree:remove_node("-" .. root_node.id)
271299
else
272-
if unlinked then
273-
M.unlinked_discussions = u.remove_first_value(M.unlinked_discussions)
274-
M.rebuild_unlinked_discussion_tree()
275-
else
276-
M.discussions = u.remove_first_value(M.discussions)
277-
M.rebuild_discussion_tree()
278-
end
279-
M.add_empty_titles({
280-
{ M.linked_bufnr, M.discussions, "No Discussions for this MR" },
281-
{ M.unlinked_bufnr, M.unlinked_discussions, "No Notes (Unlinked Discussions) for this MR" },
282-
})
283-
M.switch_can_edit_bufs(false)
300+
tree:remove_node("-" .. note_id)
284301
end
285-
286-
M.refresh_discussion_data()
302+
tree:render()
303+
M.refresh()
287304
end)
288305
end
289306

@@ -293,18 +310,22 @@ M.edit_comment = function(tree, unlinked)
293310
local current_node = tree:get_node()
294311
local note_node = M.get_note_node(tree, current_node)
295312
local root_node = M.get_root_node(tree, current_node)
313+
if note_node == nil or root_node == nil then
314+
u.notify("Could not get root or note node", vim.log.levels.ERROR)
315+
return
316+
end
296317

297318
edit_popup:mount()
298319

299-
local lines = {} -- Gather all lines from immediate children that aren't note nodes
300-
local children_ids = note_node:get_child_ids()
301-
for _, child_id in ipairs(children_ids) do
320+
-- Gather all lines from immediate children that aren't note nodes
321+
local lines = List.new(note_node:get_child_ids()):reduce(function(agg, child_id)
302322
local child_node = tree:get_node(child_id)
303323
if not child_node:has_children() then
304324
local line = tree:get_node(child_id).text
305-
table.insert(lines, line)
325+
table.insert(agg, line)
306326
end
307-
end
327+
return agg
328+
end, {})
308329

309330
local currentBuffer = vim.api.nvim_get_current_buf()
310331
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
@@ -362,7 +383,7 @@ M.toggle_discussion_resolved = function(tree)
362383
job.run_job("/mr/discussions/resolve", "PUT", body, function(data)
363384
u.notify(data.message, vim.log.levels.INFO)
364385
M.redraw_resolved_status(tree, note, not note.resolved)
365-
M.refresh_discussion_data()
386+
M.refresh()
366387
end)
367388
end
368389

@@ -373,7 +394,17 @@ M.jump_to_reviewer = function(tree)
373394
u.notify(error, vim.log.levels.ERROR)
374395
return
375396
end
376-
reviewer.jump(file_name, new_line, old_line, { is_undefined_type = is_undefined_type })
397+
398+
local new_line_int = tonumber(new_line)
399+
local old_line_int = tonumber(old_line)
400+
401+
if new_line_int == nil and old_line_int == nil then
402+
u.notify("Could not get new or old line", vim.log.levels.ERROR)
403+
return
404+
end
405+
406+
reviewer.jump(file_name, new_line_int, old_line_int, { is_undefined_type = is_undefined_type })
407+
M.refresh_view()
377408
end
378409

379410
-- This function (settings.discussion_tree.jump_to_file) will jump to the file changed in a new tab

0 commit comments

Comments
 (0)