Skip to content

Commit 0d0ed16

Browse files
Tree Refresh; Draft Note Replies (#289)
* fix: always refresh discussion tree data after choosing a new branch * fix: rebuild discussion tree without collapsing nodes after all edit/delete/create actions * feat: add command to refresh discussion tree * feat: Add support for draft note replies, e.g. replies to existing notes and comments in draft form * fix: allow backticks in comment suggestions This is a #MINOR release
1 parent cf6ccdd commit 0d0ed16

File tree

15 files changed

+439
-449
lines changed

15 files changed

+439
-449
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ require("gitlab").setup({
162162
jump_to_reviewer = "m", -- Jump to the location in the reviewer window
163163
edit_comment = "e", -- Edit comment
164164
delete_comment = "dd", -- Delete comment
165+
refresh_data = "a", -- Refreshes the data in the view by hitting Gitlab's APIs again
165166
reply = "r", -- Reply to comment
166167
toggle_node = "t", -- Opens or closes the discussion
167168
add_emoji = "Ea" -- Add an emoji to the note/comment
@@ -295,6 +296,7 @@ vim.keymap.set("n", "glo", gitlab.open_in_browser)
295296
vim.keymap.set("n", "glM", gitlab.merge)
296297
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
297298
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
299+
vim.keymap.set("n", "glD", gitlab.toggle_draft_mode)
298300
```
299301

300302
For more information about each of these commands, and about the APIs in general, run `:h gitlab.nvim.api`

cmd/draft_notes.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ as when they are creating a normal comment, but the Gitlab
1717
endpoints + resources we handle are different */
1818

1919
type PostDraftNoteRequest struct {
20-
Comment string `json:"comment"`
20+
Comment string `json:"comment"`
21+
DiscussionId string `json:"discussion_id,omitempty"`
2122
PositionData
2223
}
2324

@@ -143,9 +144,11 @@ func (a *api) postDraftNote(w http.ResponseWriter, r *http.Request) {
143144

144145
opt := gitlab.CreateDraftNoteOptions{
145146
Note: &postDraftNoteRequest.Comment,
146-
// TODO: Support posting replies as drafts and rendering draft replies in the discussion tree
147-
// instead of the notes tree
148-
// InReplyToDiscussionID *string `url:"in_reply_to_discussion_id,omitempty" json:"in_reply_to_discussion_id,omitempty"`
147+
}
148+
149+
// Draft notes can be posted in "response" to existing discussions
150+
if postDraftNoteRequest.DiscussionId != "" {
151+
opt.InReplyToDiscussionID = gitlab.Ptr(postDraftNoteRequest.DiscussionId)
149152
}
150153

151154
if postDraftNoteRequest.FileName != "" {

cmd/reply.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
type ReplyRequest struct {
1313
DiscussionId string `json:"discussion_id"`
1414
Reply string `json:"reply"`
15+
IsDraft bool `json:"is_draft"`
1516
}
1617

1718
type ReplyResponse struct {

doc/gitlab.nvim.txt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ you call this function with no values the defaults will be used:
189189
jump_to_reviewer = "m", -- Jump to the location in the reviewer window
190190
edit_comment = "e", -- Edit comment
191191
delete_comment = "dd", -- Delete comment
192+
refresh_data = "a", -- Refreshes the data in the view by hitting Gitlab's APIs again
192193
reply = "r", -- Reply to comment
193194
toggle_node = "t", -- Opens or closes the discussion
194195
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
@@ -326,13 +327,16 @@ Just like the summary, all the different kinds of comments are saved via the
326327

327328
DRAFT NOTES *gitlab.nvim.draft-comments*
328329

329-
When you publish a "draft" of any of the above resources (configurable via the
330-
`state.settings.comments.default_to_draft` setting) the comment will be added
331-
to a review. You may publish all draft comments via the `gitlab.publish_all_drafts()`
332-
function, and you can publish an individual comment or note by pressing the
330+
When you publish a "draft" of any of the above resources, the comment will be
331+
added to a review. You can configure the default commenting mode (draft vs
332+
live) via the `state.settings.discussion_tree.draft_mode` setting, and you can
333+
toggle the setting with the `state.settings.discussion_tree.toggle_draft_mode`
334+
keybinding, or by calling the `gitlab.toggle_draft_mode()` function. You may
335+
publish all draft comments via the `gitlab.publish_all_drafts()` function, and
336+
you can publish an individual comment or note by pressing the
333337
`state.settings.discussion_tree.publish_draft` keybinding.
334338

335-
Draft notes do not support editing, replying, or emojis.
339+
Draft notes do not support replying or emojis.
336340

337341
TEMPORARY REGISTERS *gitlab.nvim.temp-registers*
338342

@@ -565,6 +569,7 @@ in normal mode):
565569
vim.keymap.set("n", "glM", gitlab.merge)
566570
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
567571
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
572+
vim.keymap.set("n", "glD", gitlab.toggle_draft_mode)
568573
<
569574

570575
TROUBLESHOOTING *gitlab.nvim.troubleshooting*
@@ -769,6 +774,12 @@ comments visible.
769774
>lua
770775
require("gitlab").publish_all_drafts()
771776
<
777+
*gitlab.nvim.toggle_draft_mode*
778+
gitlab.toggle_draft_mode() ~
779+
780+
Toggles between draft mode, where comments and notes are added to a review as
781+
drafts, and regular (or live) mode, where comments are posted immediately.
782+
772783
*gitlab.nvim.add_assignee*
773784
gitlab.add_assignee() ~
774785

lua/gitlab/actions/comment.lua

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,53 @@ local M = {
1717
current_win = nil,
1818
start_line = nil,
1919
end_line = nil,
20+
draft_popup = nil,
21+
comment_popup = nil,
2022
}
2123

2224
---Fires the API that sends the comment data to the Go server, called when you "confirm" creation
2325
---via the M.settings.popup.perform_action keybinding
2426
---@param text string comment text
2527
---@param visual_range LineRange | nil range of visual selection or nil
26-
---@param unlinked boolean | nil if true, the comment is not linked to a line
27-
local confirm_create_comment = function(text, visual_range, unlinked)
28+
---@param unlinked boolean if true, the comment is not linked to a line
29+
---@param discussion_id string | nil The ID of the discussion to which the reply is responding, nil if not a reply
30+
local confirm_create_comment = function(text, visual_range, unlinked, discussion_id)
2831
if text == nil then
2932
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
3033
return
3134
end
3235

3336
local is_draft = M.draft_popup and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr))
34-
if unlinked then
37+
38+
-- Creating a normal reply to a discussion
39+
if discussion_id ~= nil and not is_draft then
40+
local body = { discussion_id = discussion_id, reply = text, draft = is_draft }
41+
job.run_job("/mr/reply", "POST", body, function()
42+
u.notify("Sent reply!", vim.log.levels.INFO)
43+
if is_draft then
44+
draft_notes.load_draft_notes(function()
45+
discussions.rebuild_view(unlinked)
46+
end)
47+
else
48+
discussions.rebuild_view(unlinked)
49+
end
50+
end)
51+
return
52+
end
53+
54+
-- Creating a note (unlinked comment)
55+
if unlinked and discussion_id == nil then
3556
local body = { comment = text }
3657
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
37-
job.run_job(endpoint, "POST", body, function(data)
58+
job.run_job(endpoint, "POST", body, function()
3859
u.notify(is_draft and "Draft note created!" or "Note created!", vim.log.levels.INFO)
3960
if is_draft then
40-
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = true })
61+
draft_notes.load_draft_notes(function()
62+
discussions.rebuild_view(unlinked)
63+
end)
4164
else
42-
discussions.add_discussion({ data = data, unlinked = true })
65+
discussions.rebuild_view(unlinked)
4366
end
44-
discussions.refresh()
4567
end)
4668
return
4769
end
@@ -61,9 +83,7 @@ local confirm_create_comment = function(text, visual_range, unlinked)
6183
end
6284

6385
local revision = state.MR_REVISIONS[1]
64-
local body = {
65-
type = "text",
66-
comment = text,
86+
local position_data = {
6787
file_name = reviewer_data.file_name,
6888
base_commit_sha = revision.base_commit_sha,
6989
start_commit_sha = revision.start_commit_sha,
@@ -73,34 +93,84 @@ local confirm_create_comment = function(text, visual_range, unlinked)
7393
line_range = location_data.line_range,
7494
}
7595

96+
-- Creating a draft reply, in response to a discussion ID
97+
if discussion_id ~= nil and is_draft then
98+
local body = { comment = text, discussion_id = discussion_id, position = position_data }
99+
job.run_job("/mr/draft_notes/", "POST", body, function()
100+
u.notify("Draft reply created!", vim.log.levels.INFO)
101+
draft_notes.load_draft_notes(function()
102+
discussions.rebuild_view(false, true)
103+
end)
104+
end)
105+
return
106+
end
107+
108+
-- Creating a new comment (linked to specific changes)
109+
local body = u.merge({ type = "text", comment = text }, position_data)
76110
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
77-
job.run_job(endpoint, "POST", body, function(data)
111+
job.run_job(endpoint, "POST", body, function()
78112
u.notify(is_draft and "Draft comment created!" or "Comment created!", vim.log.levels.INFO)
79113
if is_draft then
80-
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = false })
114+
draft_notes.load_draft_notes(function()
115+
discussions.rebuild_view(unlinked)
116+
end)
81117
else
82-
discussions.add_discussion({ data = data, has_position = true })
118+
discussions.rebuild_view(unlinked)
83119
end
84-
discussions.refresh()
85120
end)
86121
end
87122

123+
-- This function will actually send the deletion to Gitlab when you make a selection,
124+
-- and re-render the tree
125+
---@param note_id integer
126+
---@param discussion_id string
127+
---@param unlinked boolean
128+
M.confirm_delete_comment = function(note_id, discussion_id, unlinked)
129+
local body = { discussion_id = discussion_id, note_id = tonumber(note_id) }
130+
job.run_job("/mr/comment", "DELETE", body, function(data)
131+
u.notify(data.message, vim.log.levels.INFO)
132+
discussions.rebuild_view(unlinked)
133+
end)
134+
end
135+
136+
---This function sends the edited comment to the Go server
137+
---@param discussion_id string
138+
---@param note_id integer
139+
---@param unlinked boolean
140+
M.confirm_edit_comment = function(discussion_id, note_id, unlinked)
141+
return function(text)
142+
local body = {
143+
discussion_id = discussion_id,
144+
note_id = note_id,
145+
comment = text,
146+
}
147+
job.run_job("/mr/comment", "PATCH", body, function(data)
148+
u.notify(data.message, vim.log.levels.INFO)
149+
discussions.rebuild_view(unlinked)
150+
end)
151+
end
152+
end
153+
88154
---@class LayoutOpts
89155
---@field ranged boolean
156+
---@field discussion_id string|nil
90157
---@field unlinked boolean
91158

92159
---This function sets up the layout and popups needed to create a comment, note and
93160
---multi-line comment. It also sets up the basic keybindings for switching between
94161
---window panes, and for the non-primary sections.
95162
---@param opts LayoutOpts|nil
96163
---@return NuiLayout
97-
local function create_comment_layout(opts)
164+
M.create_comment_layout = function(opts)
98165
if opts == nil then
99166
opts = {}
100167
end
101168

169+
local title = opts.discussion_id and "Reply" or "Comment"
170+
local settings = opts.discussion_id ~= nil and state.settings.popup.reply or state.settings.popup.comment
171+
102172
M.current_win = vim.api.nvim_get_current_win()
103-
M.comment_popup = Popup(u.create_popup_state("Comment", state.settings.popup.comment))
173+
M.comment_popup = Popup(u.create_popup_state(title, settings))
104174
M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
105175
M.start_line, M.end_line = u.get_visual_selection_boundaries()
106176

@@ -128,14 +198,16 @@ local function create_comment_layout(opts)
128198
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
129199
local unlinked = opts.unlinked or false
130200

201+
---Keybinding for focus on text section
131202
state.set_popup_keymaps(M.draft_popup, function()
132203
local text = u.get_buffer_text(M.comment_popup.bufnr)
133-
confirm_create_comment(text, range, unlinked)
204+
confirm_create_comment(text, range, unlinked, opts.discussion_id)
134205
vim.api.nvim_set_current_win(M.current_win)
135206
end, miscellaneous.toggle_bool, popup_opts)
136207

208+
---Keybinding for focus on draft section
137209
state.set_popup_keymaps(M.comment_popup, function(text)
138-
confirm_create_comment(text, range, unlinked)
210+
confirm_create_comment(text, range, unlinked, opts.discussion_id)
139211
vim.api.nvim_set_current_win(M.current_win)
140212
end, miscellaneous.attach_file, popup_opts)
141213

@@ -144,6 +216,14 @@ local function create_comment_layout(opts)
144216
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
145217
end)
146218

219+
--Send back to previous window on close
220+
vim.api.nvim_create_autocmd("BufHidden", {
221+
buffer = M.draft_popup.bufnr,
222+
callback = function()
223+
vim.api.nvim_set_current_win(M.current_win)
224+
end,
225+
})
226+
147227
return layout
148228
end
149229

@@ -167,7 +247,7 @@ M.create_comment = function()
167247
return
168248
end
169249

170-
local layout = create_comment_layout()
250+
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
171251
layout:mount()
172252
end
173253

@@ -181,14 +261,14 @@ M.create_multiline_comment = function()
181261
return
182262
end
183263

184-
local layout = create_comment_layout({ ranged = true, unlinked = false })
264+
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
185265
layout:mount()
186266
end
187267

188268
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
189269
--- on the changed/updated line in the current MR
190270
M.create_note = function()
191-
local layout = create_comment_layout({ ranged = false, unlinked = true })
271+
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
192272
layout:mount()
193273
end
194274

@@ -204,8 +284,8 @@ local build_suggestion = function()
204284
local backticks = "```"
205285
local selected_lines = u.get_lines(M.start_line, M.end_line)
206286

207-
for line in ipairs(selected_lines) do
208-
if string.match(line, "^```$") then
287+
for _, line in ipairs(selected_lines) do
288+
if string.match(line, "^```%S*$") then
209289
backticks = "````"
210290
break
211291
end
@@ -243,7 +323,7 @@ M.create_comment_suggestion = function()
243323

244324
local suggestion_lines, range_length = build_suggestion()
245325

246-
local layout = create_comment_layout({ ranged = range_length > 0, unlinked = false })
326+
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
247327
layout:mount()
248328
vim.schedule(function()
249329
if suggestion_lines then

lua/gitlab/actions/common.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ end
207207
M.get_line_number = function(id)
208208
---@type Discussion|DraftNote|nil
209209
local d_or_n
210-
d_or_n = List.new(state.DISCUSSION_DATA.discussions or {}):find(function(d)
210+
d_or_n = List.new(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {}):find(function(d)
211211
return d.id == id
212212
end) or List.new(state.DRAFT_NOTES or {}):find(function(d)
213213
return d.id == id

0 commit comments

Comments
 (0)