Skip to content

Commit

Permalink
Tree Refresh; Draft Note Replies (#289)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
harrisoncramer authored Apr 25, 2024
1 parent cf6ccdd commit 0d0ed16
Show file tree
Hide file tree
Showing 15 changed files with 439 additions and 449 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ require("gitlab").setup({
jump_to_reviewer = "m", -- Jump to the location in the reviewer window
edit_comment = "e", -- Edit comment
delete_comment = "dd", -- Delete comment
refresh_data = "a", -- Refreshes the data in the view by hitting Gitlab's APIs again
reply = "r", -- Reply to comment
toggle_node = "t", -- Opens or closes the discussion
add_emoji = "Ea" -- Add an emoji to the note/comment
Expand Down Expand Up @@ -295,6 +296,7 @@ vim.keymap.set("n", "glo", gitlab.open_in_browser)
vim.keymap.set("n", "glM", gitlab.merge)
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
vim.keymap.set("n", "glD", gitlab.toggle_draft_mode)
```

For more information about each of these commands, and about the APIs in general, run `:h gitlab.nvim.api`
11 changes: 7 additions & 4 deletions cmd/draft_notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ as when they are creating a normal comment, but the Gitlab
endpoints + resources we handle are different */

type PostDraftNoteRequest struct {
Comment string `json:"comment"`
Comment string `json:"comment"`
DiscussionId string `json:"discussion_id,omitempty"`
PositionData
}

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

opt := gitlab.CreateDraftNoteOptions{
Note: &postDraftNoteRequest.Comment,
// TODO: Support posting replies as drafts and rendering draft replies in the discussion tree
// instead of the notes tree
// InReplyToDiscussionID *string `url:"in_reply_to_discussion_id,omitempty" json:"in_reply_to_discussion_id,omitempty"`
}

// Draft notes can be posted in "response" to existing discussions
if postDraftNoteRequest.DiscussionId != "" {
opt.InReplyToDiscussionID = gitlab.Ptr(postDraftNoteRequest.DiscussionId)
}

if postDraftNoteRequest.FileName != "" {
Expand Down
1 change: 1 addition & 0 deletions cmd/reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
type ReplyRequest struct {
DiscussionId string `json:"discussion_id"`
Reply string `json:"reply"`
IsDraft bool `json:"is_draft"`
}

type ReplyResponse struct {
Expand Down
21 changes: 16 additions & 5 deletions doc/gitlab.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ you call this function with no values the defaults will be used:
jump_to_reviewer = "m", -- Jump to the location in the reviewer window
edit_comment = "e", -- Edit comment
delete_comment = "dd", -- Delete comment
refresh_data = "a", -- Refreshes the data in the view by hitting Gitlab's APIs again
reply = "r", -- Reply to comment
toggle_node = "t", -- Opens or closes the discussion
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
Expand Down Expand Up @@ -326,13 +327,16 @@ Just like the summary, all the different kinds of comments are saved via the

DRAFT NOTES *gitlab.nvim.draft-comments*

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

Draft notes do not support editing, replying, or emojis.
Draft notes do not support replying or emojis.

TEMPORARY REGISTERS *gitlab.nvim.temp-registers*

Expand Down Expand Up @@ -565,6 +569,7 @@ in normal mode):
vim.keymap.set("n", "glM", gitlab.merge)
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
vim.keymap.set("n", "glD", gitlab.toggle_draft_mode)
<

TROUBLESHOOTING *gitlab.nvim.troubleshooting*
Expand Down Expand Up @@ -769,6 +774,12 @@ comments visible.
>lua
require("gitlab").publish_all_drafts()
<
*gitlab.nvim.toggle_draft_mode*
gitlab.toggle_draft_mode() ~

Toggles between draft mode, where comments and notes are added to a review as
drafts, and regular (or live) mode, where comments are posted immediately.

*gitlab.nvim.add_assignee*
gitlab.add_assignee() ~

Expand Down
128 changes: 104 additions & 24 deletions lua/gitlab/actions/comment.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,53 @@ local M = {
current_win = nil,
start_line = nil,
end_line = nil,
draft_popup = nil,
comment_popup = nil,
}

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

local is_draft = M.draft_popup and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr))
if unlinked then

-- Creating a normal reply to a discussion
if discussion_id ~= nil and not is_draft then
local body = { discussion_id = discussion_id, reply = text, draft = is_draft }
job.run_job("/mr/reply", "POST", body, function()
u.notify("Sent reply!", vim.log.levels.INFO)
if is_draft then
draft_notes.load_draft_notes(function()
discussions.rebuild_view(unlinked)
end)
else
discussions.rebuild_view(unlinked)
end
end)
return
end

-- Creating a note (unlinked comment)
if unlinked and discussion_id == nil then
local body = { comment = text }
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
job.run_job(endpoint, "POST", body, function(data)
job.run_job(endpoint, "POST", body, function()
u.notify(is_draft and "Draft note created!" or "Note created!", vim.log.levels.INFO)
if is_draft then
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = true })
draft_notes.load_draft_notes(function()
discussions.rebuild_view(unlinked)
end)
else
discussions.add_discussion({ data = data, unlinked = true })
discussions.rebuild_view(unlinked)
end
discussions.refresh()
end)
return
end
Expand All @@ -61,9 +83,7 @@ local confirm_create_comment = function(text, visual_range, unlinked)
end

local revision = state.MR_REVISIONS[1]
local body = {
type = "text",
comment = text,
local position_data = {
file_name = reviewer_data.file_name,
base_commit_sha = revision.base_commit_sha,
start_commit_sha = revision.start_commit_sha,
Expand All @@ -73,34 +93,84 @@ local confirm_create_comment = function(text, visual_range, unlinked)
line_range = location_data.line_range,
}

-- Creating a draft reply, in response to a discussion ID
if discussion_id ~= nil and is_draft then
local body = { comment = text, discussion_id = discussion_id, position = position_data }
job.run_job("/mr/draft_notes/", "POST", body, function()
u.notify("Draft reply created!", vim.log.levels.INFO)
draft_notes.load_draft_notes(function()
discussions.rebuild_view(false, true)
end)
end)
return
end

-- Creating a new comment (linked to specific changes)
local body = u.merge({ type = "text", comment = text }, position_data)
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
job.run_job(endpoint, "POST", body, function(data)
job.run_job(endpoint, "POST", body, function()
u.notify(is_draft and "Draft comment created!" or "Comment created!", vim.log.levels.INFO)
if is_draft then
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = false })
draft_notes.load_draft_notes(function()
discussions.rebuild_view(unlinked)
end)
else
discussions.add_discussion({ data = data, has_position = true })
discussions.rebuild_view(unlinked)
end
discussions.refresh()
end)
end

-- This function will actually send the deletion to Gitlab when you make a selection,
-- and re-render the tree
---@param note_id integer
---@param discussion_id string
---@param unlinked boolean
M.confirm_delete_comment = function(note_id, discussion_id, unlinked)
local body = { discussion_id = discussion_id, note_id = tonumber(note_id) }
job.run_job("/mr/comment", "DELETE", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
discussions.rebuild_view(unlinked)
end)
end

---This function sends the edited comment to the Go server
---@param discussion_id string
---@param note_id integer
---@param unlinked boolean
M.confirm_edit_comment = function(discussion_id, note_id, unlinked)
return function(text)
local body = {
discussion_id = discussion_id,
note_id = note_id,
comment = text,
}
job.run_job("/mr/comment", "PATCH", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
discussions.rebuild_view(unlinked)
end)
end
end

---@class LayoutOpts
---@field ranged boolean
---@field discussion_id string|nil
---@field unlinked boolean

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

local title = opts.discussion_id and "Reply" or "Comment"
local settings = opts.discussion_id ~= nil and state.settings.popup.reply or state.settings.popup.comment

M.current_win = vim.api.nvim_get_current_win()
M.comment_popup = Popup(u.create_popup_state("Comment", state.settings.popup.comment))
M.comment_popup = Popup(u.create_popup_state(title, settings))
M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
M.start_line, M.end_line = u.get_visual_selection_boundaries()

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

---Keybinding for focus on text section
state.set_popup_keymaps(M.draft_popup, function()
local text = u.get_buffer_text(M.comment_popup.bufnr)
confirm_create_comment(text, range, unlinked)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
end, miscellaneous.toggle_bool, popup_opts)

---Keybinding for focus on draft section
state.set_popup_keymaps(M.comment_popup, function(text)
confirm_create_comment(text, range, unlinked)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
end, miscellaneous.attach_file, popup_opts)

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

--Send back to previous window on close
vim.api.nvim_create_autocmd("BufHidden", {
buffer = M.draft_popup.bufnr,
callback = function()
vim.api.nvim_set_current_win(M.current_win)
end,
})

return layout
end

Expand All @@ -167,7 +247,7 @@ M.create_comment = function()
return
end

local layout = create_comment_layout()
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
layout:mount()
end

Expand All @@ -181,14 +261,14 @@ M.create_multiline_comment = function()
return
end

local layout = create_comment_layout({ ranged = true, unlinked = false })
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
layout:mount()
end

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

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

for line in ipairs(selected_lines) do
if string.match(line, "^```$") then
for _, line in ipairs(selected_lines) do
if string.match(line, "^```%S*$") then
backticks = "````"
break
end
Expand Down Expand Up @@ -243,7 +323,7 @@ M.create_comment_suggestion = function()

local suggestion_lines, range_length = build_suggestion()

local layout = create_comment_layout({ ranged = range_length > 0, unlinked = false })
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
layout:mount()
vim.schedule(function()
if suggestion_lines then
Expand Down
2 changes: 1 addition & 1 deletion lua/gitlab/actions/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ end
M.get_line_number = function(id)
---@type Discussion|DraftNote|nil
local d_or_n
d_or_n = List.new(state.DISCUSSION_DATA.discussions or {}):find(function(d)
d_or_n = List.new(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {}):find(function(d)
return d.id == id
end) or List.new(state.DRAFT_NOTES or {}):find(function(d)
return d.id == id
Expand Down
Loading

0 comments on commit 0d0ed16

Please sign in to comment.