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
This commit is contained in:
committed by
GitHub
parent
cf6ccddce3
commit
0d0ed1639a
@@ -162,6 +162,7 @@ require("gitlab").setup({
|
|||||||
jump_to_reviewer = "m", -- Jump to the location in the reviewer window
|
jump_to_reviewer = "m", -- Jump to the location in the reviewer window
|
||||||
edit_comment = "e", -- Edit comment
|
edit_comment = "e", -- Edit comment
|
||||||
delete_comment = "dd", -- Delete 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
|
reply = "r", -- Reply to comment
|
||||||
toggle_node = "t", -- Opens or closes the discussion
|
toggle_node = "t", -- Opens or closes the discussion
|
||||||
add_emoji = "Ea" -- Add an emoji to the note/comment
|
add_emoji = "Ea" -- Add an emoji to the note/comment
|
||||||
@@ -295,6 +296,7 @@ vim.keymap.set("n", "glo", gitlab.open_in_browser)
|
|||||||
vim.keymap.set("n", "glM", gitlab.merge)
|
vim.keymap.set("n", "glM", gitlab.merge)
|
||||||
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
|
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
|
||||||
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
|
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`
|
For more information about each of these commands, and about the APIs in general, run `:h gitlab.nvim.api`
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ endpoints + resources we handle are different */
|
|||||||
|
|
||||||
type PostDraftNoteRequest struct {
|
type PostDraftNoteRequest struct {
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
|
DiscussionId string `json:"discussion_id,omitempty"`
|
||||||
PositionData
|
PositionData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +144,11 @@ func (a *api) postDraftNote(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
opt := gitlab.CreateDraftNoteOptions{
|
opt := gitlab.CreateDraftNoteOptions{
|
||||||
Note: &postDraftNoteRequest.Comment,
|
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 != "" {
|
if postDraftNoteRequest.FileName != "" {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
type ReplyRequest struct {
|
type ReplyRequest struct {
|
||||||
DiscussionId string `json:"discussion_id"`
|
DiscussionId string `json:"discussion_id"`
|
||||||
Reply string `json:"reply"`
|
Reply string `json:"reply"`
|
||||||
|
IsDraft bool `json:"is_draft"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplyResponse struct {
|
type ReplyResponse struct {
|
||||||
|
|||||||
@@ -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
|
jump_to_reviewer = "m", -- Jump to the location in the reviewer window
|
||||||
edit_comment = "e", -- Edit comment
|
edit_comment = "e", -- Edit comment
|
||||||
delete_comment = "dd", -- Delete 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
|
reply = "r", -- Reply to comment
|
||||||
toggle_node = "t", -- Opens or closes the discussion
|
toggle_node = "t", -- Opens or closes the discussion
|
||||||
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
|
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
|
|||||||
|
|
||||||
DRAFT NOTES *gitlab.nvim.draft-comments*
|
DRAFT NOTES *gitlab.nvim.draft-comments*
|
||||||
|
|
||||||
When you publish a "draft" of any of the above resources (configurable via the
|
When you publish a "draft" of any of the above resources, the comment will be
|
||||||
`state.settings.comments.default_to_draft` setting) the comment will be added
|
added to a review. You can configure the default commenting mode (draft vs
|
||||||
to a review. You may publish all draft comments via the `gitlab.publish_all_drafts()`
|
live) via the `state.settings.discussion_tree.draft_mode` setting, and you can
|
||||||
function, and you can publish an individual comment or note by pressing the
|
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.
|
`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*
|
TEMPORARY REGISTERS *gitlab.nvim.temp-registers*
|
||||||
|
|
||||||
@@ -565,6 +569,7 @@ in normal mode):
|
|||||||
vim.keymap.set("n", "glM", gitlab.merge)
|
vim.keymap.set("n", "glM", gitlab.merge)
|
||||||
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
|
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
|
||||||
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
|
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
|
||||||
|
vim.keymap.set("n", "glD", gitlab.toggle_draft_mode)
|
||||||
<
|
<
|
||||||
|
|
||||||
TROUBLESHOOTING *gitlab.nvim.troubleshooting*
|
TROUBLESHOOTING *gitlab.nvim.troubleshooting*
|
||||||
@@ -769,6 +774,12 @@ comments visible.
|
|||||||
>lua
|
>lua
|
||||||
require("gitlab").publish_all_drafts()
|
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.nvim.add_assignee*
|
||||||
gitlab.add_assignee() ~
|
gitlab.add_assignee() ~
|
||||||
|
|
||||||
|
|||||||
@@ -17,31 +17,53 @@ local M = {
|
|||||||
current_win = nil,
|
current_win = nil,
|
||||||
start_line = nil,
|
start_line = nil,
|
||||||
end_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
|
---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
|
---via the M.settings.popup.perform_action keybinding
|
||||||
---@param text string comment text
|
---@param text string comment text
|
||||||
---@param visual_range LineRange | nil range of visual selection or nil
|
---@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
|
---@param unlinked boolean if true, the comment is not linked to a line
|
||||||
local confirm_create_comment = function(text, visual_range, unlinked)
|
---@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
|
if text == nil then
|
||||||
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
|
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local is_draft = M.draft_popup and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr))
|
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 body = { comment = text }
|
||||||
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
|
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)
|
u.notify(is_draft and "Draft note created!" or "Note created!", vim.log.levels.INFO)
|
||||||
if is_draft then
|
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
|
else
|
||||||
discussions.add_discussion({ data = data, unlinked = true })
|
discussions.rebuild_view(unlinked)
|
||||||
end
|
end
|
||||||
discussions.refresh()
|
|
||||||
end)
|
end)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -61,9 +83,7 @@ local confirm_create_comment = function(text, visual_range, unlinked)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local revision = state.MR_REVISIONS[1]
|
local revision = state.MR_REVISIONS[1]
|
||||||
local body = {
|
local position_data = {
|
||||||
type = "text",
|
|
||||||
comment = text,
|
|
||||||
file_name = reviewer_data.file_name,
|
file_name = reviewer_data.file_name,
|
||||||
base_commit_sha = revision.base_commit_sha,
|
base_commit_sha = revision.base_commit_sha,
|
||||||
start_commit_sha = revision.start_commit_sha,
|
start_commit_sha = revision.start_commit_sha,
|
||||||
@@ -73,20 +93,67 @@ local confirm_create_comment = function(text, visual_range, unlinked)
|
|||||||
line_range = location_data.line_range,
|
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"
|
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)
|
u.notify(is_draft and "Draft comment created!" or "Comment created!", vim.log.levels.INFO)
|
||||||
if is_draft then
|
if is_draft then
|
||||||
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = false })
|
draft_notes.load_draft_notes(function()
|
||||||
else
|
discussions.rebuild_view(unlinked)
|
||||||
discussions.add_discussion({ data = data, has_position = true })
|
|
||||||
end
|
|
||||||
discussions.refresh()
|
|
||||||
end)
|
end)
|
||||||
|
else
|
||||||
|
discussions.rebuild_view(unlinked)
|
||||||
|
end
|
||||||
|
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
|
end
|
||||||
|
|
||||||
---@class LayoutOpts
|
---@class LayoutOpts
|
||||||
---@field ranged boolean
|
---@field ranged boolean
|
||||||
|
---@field discussion_id string|nil
|
||||||
---@field unlinked boolean
|
---@field unlinked boolean
|
||||||
|
|
||||||
---This function sets up the layout and popups needed to create a comment, note and
|
---This function sets up the layout and popups needed to create a comment, note and
|
||||||
@@ -94,13 +161,16 @@ end
|
|||||||
---window panes, and for the non-primary sections.
|
---window panes, and for the non-primary sections.
|
||||||
---@param opts LayoutOpts|nil
|
---@param opts LayoutOpts|nil
|
||||||
---@return NuiLayout
|
---@return NuiLayout
|
||||||
local function create_comment_layout(opts)
|
M.create_comment_layout = function(opts)
|
||||||
if opts == nil then
|
if opts == nil then
|
||||||
opts = {}
|
opts = {}
|
||||||
end
|
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.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.draft_popup = Popup(u.create_box_popup_state("Draft", false))
|
||||||
M.start_line, M.end_line = u.get_visual_selection_boundaries()
|
M.start_line, M.end_line = u.get_visual_selection_boundaries()
|
||||||
|
|
||||||
@@ -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 range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
|
||||||
local unlinked = opts.unlinked or false
|
local unlinked = opts.unlinked or false
|
||||||
|
|
||||||
|
---Keybinding for focus on text section
|
||||||
state.set_popup_keymaps(M.draft_popup, function()
|
state.set_popup_keymaps(M.draft_popup, function()
|
||||||
local text = u.get_buffer_text(M.comment_popup.bufnr)
|
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)
|
vim.api.nvim_set_current_win(M.current_win)
|
||||||
end, miscellaneous.toggle_bool, popup_opts)
|
end, miscellaneous.toggle_bool, popup_opts)
|
||||||
|
|
||||||
|
---Keybinding for focus on draft section
|
||||||
state.set_popup_keymaps(M.comment_popup, function(text)
|
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)
|
vim.api.nvim_set_current_win(M.current_win)
|
||||||
end, miscellaneous.attach_file, popup_opts)
|
end, miscellaneous.attach_file, popup_opts)
|
||||||
|
|
||||||
@@ -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) })
|
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
|
||||||
end)
|
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
|
return layout
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -167,7 +247,7 @@ M.create_comment = function()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local layout = create_comment_layout()
|
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
|
||||||
layout:mount()
|
layout:mount()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -181,14 +261,14 @@ M.create_multiline_comment = function()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local layout = create_comment_layout({ ranged = true, unlinked = false })
|
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
|
||||||
layout:mount()
|
layout:mount()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
|
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
|
||||||
--- on the changed/updated line in the current MR
|
--- on the changed/updated line in the current MR
|
||||||
M.create_note = function()
|
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()
|
layout:mount()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -204,8 +284,8 @@ local build_suggestion = function()
|
|||||||
local backticks = "```"
|
local backticks = "```"
|
||||||
local selected_lines = u.get_lines(M.start_line, M.end_line)
|
local selected_lines = u.get_lines(M.start_line, M.end_line)
|
||||||
|
|
||||||
for line in ipairs(selected_lines) do
|
for _, line in ipairs(selected_lines) do
|
||||||
if string.match(line, "^```$") then
|
if string.match(line, "^```%S*$") then
|
||||||
backticks = "````"
|
backticks = "````"
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -243,7 +323,7 @@ M.create_comment_suggestion = function()
|
|||||||
|
|
||||||
local suggestion_lines, range_length = build_suggestion()
|
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()
|
layout:mount()
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
if suggestion_lines then
|
if suggestion_lines then
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ end
|
|||||||
M.get_line_number = function(id)
|
M.get_line_number = function(id)
|
||||||
---@type Discussion|DraftNote|nil
|
---@type Discussion|DraftNote|nil
|
||||||
local d_or_n
|
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
|
return d.id == id
|
||||||
end) or List.new(state.DRAFT_NOTES or {}):find(function(d)
|
end) or List.new(state.DRAFT_NOTES or {}):find(function(d)
|
||||||
return d.id == id
|
return d.id == id
|
||||||
|
|||||||
@@ -29,18 +29,37 @@ local M = {
|
|||||||
linked_bufnr = nil,
|
linked_bufnr = nil,
|
||||||
---@type number
|
---@type number
|
||||||
unlinked_bufnr = nil,
|
unlinked_bufnr = nil,
|
||||||
---@type number
|
---@type NuiTree|nil
|
||||||
discussion_tree = nil,
|
discussion_tree = nil,
|
||||||
|
---@type NuiTree|nil
|
||||||
|
unlinked_discussion_tree = nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
---Re-fetches all discussions and re-renders the relevant view
|
||||||
|
---@param unlinked boolean
|
||||||
|
---@param all boolean|nil
|
||||||
|
M.rebuild_view = function(unlinked, all)
|
||||||
|
M.load_discussions(function()
|
||||||
|
if all then
|
||||||
|
M.rebuild_unlinked_discussion_tree()
|
||||||
|
M.rebuild_discussion_tree()
|
||||||
|
elseif unlinked then
|
||||||
|
M.rebuild_unlinked_discussion_tree()
|
||||||
|
else
|
||||||
|
M.rebuild_discussion_tree()
|
||||||
|
end
|
||||||
|
M.refresh_diagnostics_and_winbar()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
---Makes API call to get the discussion data, stores it in the state, and calls the callback
|
---Makes API call to get the discussion data, stores it in the state, and calls the callback
|
||||||
---@param callback function|nil
|
---@param callback function|nil
|
||||||
M.load_discussions = function(callback)
|
M.load_discussions = function(callback)
|
||||||
job.run_job("/mr/discussions/list", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
|
state.load_new_state("discussion_data", function(data)
|
||||||
state.DISCUSSION_DATA.discussions = u.ensure_table(data.discussions)
|
state.DISCUSSION_DATA.discussions = u.ensure_table(data.discussions)
|
||||||
state.DISCUSSION_DATA.unlinked_discussions = u.ensure_table(data.unlinked_discussions)
|
state.DISCUSSION_DATA.unlinked_discussions = u.ensure_table(data.unlinked_discussions)
|
||||||
state.DISCUSSION_DATA.emojis = u.ensure_table(data.emojis)
|
state.DISCUSSION_DATA.emojis = u.ensure_table(data.emojis)
|
||||||
if type(callback) == "function" then
|
if callback ~= nil then
|
||||||
callback()
|
callback()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -50,7 +69,7 @@ end
|
|||||||
M.initialize_discussions = function()
|
M.initialize_discussions = function()
|
||||||
signs.setup_signs()
|
signs.setup_signs()
|
||||||
reviewer.set_callback_for_file_changed(function()
|
reviewer.set_callback_for_file_changed(function()
|
||||||
M.refresh_view()
|
M.refresh_diagnostics_and_winbar()
|
||||||
M.modifiable(false)
|
M.modifiable(false)
|
||||||
end)
|
end)
|
||||||
reviewer.set_callback_for_reviewer_enter(function()
|
reviewer.set_callback_for_reviewer_enter(function()
|
||||||
@@ -77,19 +96,8 @@ M.modifiable = function(bool)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Refresh discussion data, signs, diagnostics, and winbar with new data from API
|
|
||||||
--- and rebuild the entire view
|
|
||||||
M.refresh = function(cb)
|
|
||||||
M.load_discussions(function()
|
|
||||||
M.refresh_view()
|
|
||||||
if cb ~= nil then
|
|
||||||
cb()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Take existing data and refresh the diagnostics, the winbar, and the signs
|
--- Take existing data and refresh the diagnostics, the winbar, and the signs
|
||||||
M.refresh_view = function()
|
M.refresh_diagnostics_and_winbar = function()
|
||||||
if state.settings.discussion_signs.enabled then
|
if state.settings.discussion_signs.enabled then
|
||||||
diagnostics.refresh_diagnostics()
|
diagnostics.refresh_diagnostics()
|
||||||
end
|
end
|
||||||
@@ -146,7 +154,7 @@ M.toggle = function(callback)
|
|||||||
end
|
end
|
||||||
|
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
M.refresh_view()
|
M.refresh_diagnostics_and_winbar()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -219,48 +227,29 @@ M.reply = function(tree)
|
|||||||
u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN)
|
u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local reply_popup = Popup(u.create_popup_state("Reply", state.settings.popup.reply))
|
|
||||||
local node = tree:get_node()
|
local node = tree:get_node()
|
||||||
local discussion_node = common.get_root_node(tree, node)
|
local discussion_node = common.get_root_node(tree, node)
|
||||||
local id = tostring(discussion_node.id)
|
|
||||||
reply_popup:mount()
|
|
||||||
state.set_popup_keymaps(
|
|
||||||
reply_popup,
|
|
||||||
M.send_reply(tree, id),
|
|
||||||
miscellaneous.attach_file,
|
|
||||||
miscellaneous.editable_popup_opts
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- This function will send the reply to the Go API
|
if discussion_node == nil then
|
||||||
M.send_reply = function(tree, discussion_id)
|
u.notify("Could not get discussion root", vim.log.levels.ERROR)
|
||||||
return function(text)
|
return
|
||||||
local body = { discussion_id = discussion_id, reply = text }
|
|
||||||
|
|
||||||
job.run_job("/mr/reply", "POST", body, function(data)
|
|
||||||
u.notify("Sent reply!", vim.log.levels.INFO)
|
|
||||||
M.add_reply_to_tree(tree, data.note, discussion_id)
|
|
||||||
M.load_discussions()
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local discussion_id = tostring(discussion_node.id)
|
||||||
|
local comment = require("gitlab.actions.comment")
|
||||||
|
local unlinked = tree.bufnr == M.unlinked_bufnr
|
||||||
|
local layout = comment.create_comment_layout({ ranged = false, discussion_id = discussion_id, unlinked = unlinked })
|
||||||
|
layout:mount()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- This function (settings.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment
|
-- This function (settings.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment
|
||||||
M.delete_comment = function(tree)
|
M.delete_comment = function(tree, unlinked)
|
||||||
vim.ui.select({ "Confirm", "Cancel" }, {
|
vim.ui.select({ "Confirm", "Cancel" }, {
|
||||||
prompt = "Delete comment?",
|
prompt = "Delete comment?",
|
||||||
}, function(choice)
|
}, function(choice)
|
||||||
if choice == "Confirm" then
|
if choice == "Confirm" then
|
||||||
M.send_deletion(tree)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- This function will actually send the deletion to Gitlab
|
|
||||||
-- when you make a selection, and re-render the tree
|
|
||||||
M.send_deletion = function(tree)
|
|
||||||
local current_node = tree:get_node()
|
local current_node = tree:get_node()
|
||||||
|
|
||||||
local note_node = common.get_note_node(tree, current_node)
|
local note_node = common.get_note_node(tree, current_node)
|
||||||
local root_node = common.get_root_node(tree, current_node)
|
local root_node = common.get_root_node(tree, current_node)
|
||||||
if note_node == nil or root_node == nil then
|
if note_node == nil or root_node == nil then
|
||||||
@@ -269,24 +258,15 @@ M.send_deletion = function(tree)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@type integer
|
---@type integer
|
||||||
|
if M.is_draft_note(tree) then
|
||||||
|
draft_notes.confirm_delete_draft_note(note_node.id, unlinked)
|
||||||
|
else
|
||||||
local note_id = note_node.is_root and root_node.root_note_id or note_node.id
|
local note_id = note_node.is_root and root_node.root_note_id or note_node.id
|
||||||
|
local comment = require("gitlab.actions.comment")
|
||||||
if root_node.is_draft then
|
comment.confirm_delete_comment(note_id, root_node.id, unlinked)
|
||||||
draft_notes.send_deletion(tree)
|
end
|
||||||
else
|
|
||||||
local body = { discussion_id = root_node.id, note_id = tonumber(note_id) }
|
|
||||||
job.run_job("/mr/comment", "DELETE", body, function(data)
|
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
|
||||||
if note_node.is_root then
|
|
||||||
-- Replace root node w/ current node's contents...
|
|
||||||
tree:remove_node("-" .. root_node.id)
|
|
||||||
else
|
|
||||||
tree:remove_node("-" .. note_id)
|
|
||||||
end
|
end
|
||||||
tree:render()
|
|
||||||
M.refresh()
|
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- This function (settings.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
|
-- This function (settings.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
|
||||||
@@ -316,39 +296,21 @@ M.edit_comment = function(tree, unlinked)
|
|||||||
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
|
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
|
||||||
|
|
||||||
-- Draft notes module handles edits for draft notes
|
-- Draft notes module handles edits for draft notes
|
||||||
if root_node.is_draft then
|
if M.is_draft_note(tree) then
|
||||||
state.set_popup_keymaps(edit_popup, draft_notes.send_edits(root_node.id), nil, miscellaneous.editable_popup_opts)
|
|
||||||
else
|
|
||||||
state.set_popup_keymaps(
|
state.set_popup_keymaps(
|
||||||
edit_popup,
|
edit_popup,
|
||||||
M.send_edits(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
|
draft_notes.confirm_edit_draft_note(note_node.id, unlinked),
|
||||||
nil,
|
nil,
|
||||||
miscellaneous.editable_popup_opts
|
miscellaneous.editable_popup_opts
|
||||||
)
|
)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---This function sends the edited comment to the Go server
|
|
||||||
---@param discussion_id string
|
|
||||||
---@param note_id integer
|
|
||||||
---@param unlinked boolean
|
|
||||||
M.send_edits = 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)
|
|
||||||
if unlinked then
|
|
||||||
M.replace_text(state.DISCUSSION_DATA.unlinked_discussions, discussion_id, note_id, text)
|
|
||||||
M.rebuild_unlinked_discussion_tree()
|
|
||||||
else
|
else
|
||||||
M.replace_text(state.DISCUSSION_DATA.discussions, discussion_id, note_id, text)
|
local comment = require("gitlab.actions.comment")
|
||||||
M.rebuild_discussion_tree()
|
state.set_popup_keymaps(
|
||||||
end
|
edit_popup,
|
||||||
end)
|
comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
|
||||||
|
nil,
|
||||||
|
miscellaneous.editable_popup_opts
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -374,8 +336,61 @@ M.toggle_discussion_resolved = function(tree)
|
|||||||
|
|
||||||
job.run_job("/mr/discussions/resolve", "PUT", body, function(data)
|
job.run_job("/mr/discussions/resolve", "PUT", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
M.redraw_resolved_status(tree, note, not note.resolved)
|
local unlinked = tree.bufnr == M.unlinked_bufnr
|
||||||
M.refresh()
|
M.rebuild_view(unlinked)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Opens a popup prompting the user to choose an emoji to attach to the current node
|
||||||
|
---@param tree any
|
||||||
|
---@param unlinked boolean
|
||||||
|
M.add_emoji_to_note = function(tree, unlinked)
|
||||||
|
local node = tree:get_node()
|
||||||
|
local note_node = common.get_note_node(tree, node)
|
||||||
|
local root_node = common.get_root_node(tree, node)
|
||||||
|
local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
|
||||||
|
local emojis = require("gitlab.emoji").emoji_list
|
||||||
|
emoji.pick_emoji(emojis, function(name)
|
||||||
|
local body = { emoji = name, note_id = note_id }
|
||||||
|
job.run_job("/mr/awardable/note/", "POST", body, function()
|
||||||
|
u.notify("Emoji added", vim.log.levels.INFO)
|
||||||
|
M.rebuild_view(unlinked)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Opens a popup prompting the user to choose an emoji to remove from the current node
|
||||||
|
---@param tree any
|
||||||
|
---@param unlinked boolean
|
||||||
|
M.delete_emoji_from_note = function(tree, unlinked)
|
||||||
|
local node = tree:get_node()
|
||||||
|
local note_node = common.get_note_node(tree, node)
|
||||||
|
local root_node = common.get_root_node(tree, node)
|
||||||
|
local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
|
||||||
|
local note_id_str = tostring(note_id)
|
||||||
|
|
||||||
|
local e = require("gitlab.emoji")
|
||||||
|
|
||||||
|
local emojis = {}
|
||||||
|
local current_emojis = state.DISCUSSION_DATA.emojis[note_id_str]
|
||||||
|
for _, current_emoji in ipairs(current_emojis) do
|
||||||
|
if state.USER.id == current_emoji.user.id then
|
||||||
|
table.insert(emojis, e.emoji_map[current_emoji.name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
emoji.pick_emoji(emojis, function(name)
|
||||||
|
local awardable_id
|
||||||
|
for _, current_emoji in ipairs(current_emojis) do
|
||||||
|
if current_emoji.name == name and current_emoji.user.id == state.USER.id then
|
||||||
|
awardable_id = current_emoji.id
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
job.run_job(string.format("/mr/awardable/note/%d/%d", note_id, awardable_id), "DELETE", nil, function()
|
||||||
|
u.notify("Emoji removed", vim.log.levels.INFO)
|
||||||
|
M.rebuild_view(unlinked)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -383,31 +398,46 @@ end
|
|||||||
-- 🌲 Helper Functions
|
-- 🌲 Helper Functions
|
||||||
--
|
--
|
||||||
|
|
||||||
|
---Used to collect all nodes in a tree prior to rebuilding it, so that they
|
||||||
|
---can be re-expanded before render
|
||||||
|
---@param tree any
|
||||||
|
---@return table
|
||||||
|
M.gather_expanded_node_ids = function(tree)
|
||||||
|
-- Gather all nodes for later expansion, after rebuild
|
||||||
|
local ids = {}
|
||||||
|
for id, node in pairs(tree and tree.nodes.by_id or {}) do
|
||||||
|
if node._is_expanded then
|
||||||
|
table.insert(ids, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ids
|
||||||
|
end
|
||||||
|
|
||||||
---Rebuilds the discussion tree, which contains all comments and draft comments
|
---Rebuilds the discussion tree, which contains all comments and draft comments
|
||||||
---linked to specific places in the code.
|
---linked to specific places in the code.
|
||||||
M.rebuild_discussion_tree = function()
|
M.rebuild_discussion_tree = function()
|
||||||
if M.linked_bufnr == nil then
|
if M.linked_bufnr == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local expanded_node_ids = M.gather_expanded_node_ids(M.discussion_tree)
|
||||||
common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr)
|
common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr)
|
||||||
|
|
||||||
vim.api.nvim_buf_set_lines(M.linked_bufnr, 0, -1, false, {})
|
vim.api.nvim_buf_set_lines(M.linked_bufnr, 0, -1, false, {})
|
||||||
local existing_comment_nodes = discussions_tree.add_discussions_to_table(state.DISCUSSION_DATA.discussions, false)
|
local existing_comment_nodes = discussions_tree.add_discussions_to_table(state.DISCUSSION_DATA.discussions, false)
|
||||||
local draft_comment_nodes = draft_notes.add_draft_notes_to_table(false)
|
local draft_comment_nodes = draft_notes.add_draft_notes_to_table(false)
|
||||||
|
|
||||||
-- Combine inline draft notes with regular comments
|
-- Combine inline draft notes with regular comments
|
||||||
local all_nodes = {}
|
local all_nodes = u.join(draft_comment_nodes, existing_comment_nodes)
|
||||||
for _, draft_node in ipairs(draft_comment_nodes) do
|
|
||||||
table.insert(all_nodes, draft_node)
|
|
||||||
end
|
|
||||||
for _, node in ipairs(existing_comment_nodes) do
|
|
||||||
table.insert(all_nodes, node)
|
|
||||||
end
|
|
||||||
|
|
||||||
local discussion_tree = NuiTree({
|
local discussion_tree = NuiTree({
|
||||||
nodes = all_nodes,
|
nodes = all_nodes,
|
||||||
bufnr = M.linked_bufnr,
|
bufnr = M.linked_bufnr,
|
||||||
prepare_node = tree_utils.nui_tree_prepare_node,
|
prepare_node = tree_utils.nui_tree_prepare_node,
|
||||||
})
|
})
|
||||||
|
-- Re-expand already expanded nodes
|
||||||
|
for _, id in ipairs(expanded_node_ids) do
|
||||||
|
tree_utils.open_node_by_id(discussion_tree, id)
|
||||||
|
end
|
||||||
|
|
||||||
discussion_tree:render()
|
discussion_tree:render()
|
||||||
M.set_tree_keymaps(discussion_tree, M.linked_bufnr, false)
|
M.set_tree_keymaps(discussion_tree, M.linked_bufnr, false)
|
||||||
@@ -423,6 +453,7 @@ M.rebuild_unlinked_discussion_tree = function()
|
|||||||
if M.unlinked_bufnr == nil then
|
if M.unlinked_bufnr == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local expanded_node_ids = M.gather_expanded_node_ids(M.unlinked_discussion_tree)
|
||||||
common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr)
|
common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr)
|
||||||
vim.api.nvim_buf_set_lines(M.unlinked_bufnr, 0, -1, false, {})
|
vim.api.nvim_buf_set_lines(M.unlinked_bufnr, 0, -1, false, {})
|
||||||
local existing_note_nodes =
|
local existing_note_nodes =
|
||||||
@@ -430,20 +461,20 @@ M.rebuild_unlinked_discussion_tree = function()
|
|||||||
local draft_comment_nodes = draft_notes.add_draft_notes_to_table(true)
|
local draft_comment_nodes = draft_notes.add_draft_notes_to_table(true)
|
||||||
|
|
||||||
-- Combine draft notes with regular notes
|
-- Combine draft notes with regular notes
|
||||||
local all_nodes = {}
|
local all_nodes = u.join(draft_comment_nodes, existing_note_nodes)
|
||||||
for _, draft_node in ipairs(draft_comment_nodes) do
|
|
||||||
table.insert(all_nodes, draft_node)
|
|
||||||
end
|
|
||||||
for _, node in ipairs(existing_note_nodes) do
|
|
||||||
table.insert(all_nodes, node)
|
|
||||||
end
|
|
||||||
|
|
||||||
local unlinked_discussion_tree = NuiTree({
|
local unlinked_discussion_tree = NuiTree({
|
||||||
nodes = all_nodes,
|
nodes = all_nodes,
|
||||||
bufnr = M.unlinked_bufnr,
|
bufnr = M.unlinked_bufnr,
|
||||||
prepare_node = tree_utils.nui_tree_prepare_node,
|
prepare_node = tree_utils.nui_tree_prepare_node,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
-- Re-expand already expanded nodes
|
||||||
|
for _, id in ipairs(expanded_node_ids) do
|
||||||
|
tree_utils.open_node_by_id(unlinked_discussion_tree, id)
|
||||||
|
end
|
||||||
unlinked_discussion_tree:render()
|
unlinked_discussion_tree:render()
|
||||||
|
|
||||||
M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_bufnr, true)
|
M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_bufnr, true)
|
||||||
M.unlinked_discussion_tree = unlinked_discussion_tree
|
M.unlinked_discussion_tree = unlinked_discussion_tree
|
||||||
common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr)
|
common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr)
|
||||||
@@ -498,6 +529,7 @@ M.is_current_node_note = function(tree)
|
|||||||
end
|
end
|
||||||
|
|
||||||
M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||||
|
---Keybindings only relevant for linked (comment) view
|
||||||
if not unlinked then
|
if not unlinked then
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.jump_to_file, function()
|
vim.keymap.set("n", state.settings.discussion_tree.jump_to_file, function()
|
||||||
if M.is_current_node_note(tree) then
|
if M.is_current_node_note(tree) then
|
||||||
@@ -506,13 +538,19 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
|||||||
end, { buffer = bufnr, desc = "Jump to file" })
|
end, { buffer = bufnr, desc = "Jump to file" })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.jump_to_reviewer, function()
|
vim.keymap.set("n", state.settings.discussion_tree.jump_to_reviewer, function()
|
||||||
if M.is_current_node_note(tree) then
|
if M.is_current_node_note(tree) then
|
||||||
common.jump_to_reviewer(tree, M.refresh_view)
|
common.jump_to_reviewer(tree, M.refresh_diagnostics_and_winbar)
|
||||||
end
|
end
|
||||||
end, { buffer = bufnr, desc = "Jump to reviewer" })
|
end, { buffer = bufnr, desc = "Jump to reviewer" })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.toggle_tree_type, function()
|
vim.keymap.set("n", state.settings.discussion_tree.toggle_tree_type, function()
|
||||||
M.toggle_tree_type()
|
M.toggle_tree_type()
|
||||||
end, { buffer = bufnr, desc = "Toggle tree type between `simple` and `by_file_name`" })
|
end, { buffer = bufnr, desc = "Toggle tree type between `simple` and `by_file_name`" })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
vim.keymap.set("n", state.settings.discussion_tree.refresh_data, function()
|
||||||
|
u.notify("Refreshing data...", vim.log.levels.INFO)
|
||||||
|
draft_notes.rebuild_view(unlinked, false)
|
||||||
|
end, { buffer = bufnr, desc = "Refreshes the view with Gitlab's APIs" })
|
||||||
|
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.edit_comment, function()
|
vim.keymap.set("n", state.settings.discussion_tree.edit_comment, function()
|
||||||
if M.is_current_node_note(tree) then
|
if M.is_current_node_note(tree) then
|
||||||
M.edit_comment(tree, unlinked)
|
M.edit_comment(tree, unlinked)
|
||||||
@@ -525,12 +563,11 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
|||||||
end, { buffer = bufnr, desc = "Publish draft" })
|
end, { buffer = bufnr, desc = "Publish draft" })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.delete_comment, function()
|
vim.keymap.set("n", state.settings.discussion_tree.delete_comment, function()
|
||||||
if M.is_current_node_note(tree) then
|
if M.is_current_node_note(tree) then
|
||||||
M.delete_comment(tree)
|
M.delete_comment(tree, unlinked)
|
||||||
end
|
end
|
||||||
end, { buffer = bufnr, desc = "Delete comment" })
|
end, { buffer = bufnr, desc = "Delete comment" })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.toggle_draft_mode, function()
|
vim.keymap.set("n", state.settings.discussion_tree.toggle_draft_mode, function()
|
||||||
state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode
|
M.toggle_draft_mode()
|
||||||
winbar.update_winbar()
|
|
||||||
end, { buffer = bufnr, desc = "Toggle between draft mode and live mode" })
|
end, { buffer = bufnr, desc = "Toggle between draft mode and live mode" })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.toggle_resolved, function()
|
vim.keymap.set("n", state.settings.discussion_tree.toggle_resolved, function()
|
||||||
if M.is_current_node_note(tree) and not M.is_draft_note(tree) then
|
if M.is_current_node_note(tree) and not M.is_draft_note(tree) then
|
||||||
@@ -591,68 +628,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
|||||||
emoji.init_popup(tree, bufnr)
|
emoji.init_popup(tree, bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
---Redraws the header of a node in a tree when it's been toggled to resolved/unresolved
|
|
||||||
---@param tree NuiTree
|
|
||||||
---@param note NuiTree.Node
|
|
||||||
---@param mark_resolved boolean
|
|
||||||
M.redraw_resolved_status = function(tree, note, mark_resolved)
|
|
||||||
local current_text = tree.nodes.by_id["-" .. note.id].text
|
|
||||||
local target = mark_resolved and "resolved" or "unresolved"
|
|
||||||
local current = mark_resolved and "unresolved" or "resolved"
|
|
||||||
|
|
||||||
local function set_property(key, val)
|
|
||||||
tree.nodes.by_id["-" .. note.id][key] = val
|
|
||||||
end
|
|
||||||
|
|
||||||
local has_symbol = function(s)
|
|
||||||
return state.settings.discussion_tree[s] ~= nil and state.settings.discussion_tree[s] ~= ""
|
|
||||||
end
|
|
||||||
|
|
||||||
set_property("resolved", mark_resolved)
|
|
||||||
|
|
||||||
if not has_symbol(current) and not has_symbol(target) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not has_symbol(current) and has_symbol(target) then
|
|
||||||
set_property("text", (current_text .. " " .. state.settings.discussion_tree[target]))
|
|
||||||
elseif has_symbol(current) and not has_symbol(target) then
|
|
||||||
set_property("text", u.remove_last_chunk(current_text))
|
|
||||||
else
|
|
||||||
set_property("text", (u.remove_last_chunk(current_text) .. " " .. state.settings.discussion_tree[target]))
|
|
||||||
end
|
|
||||||
|
|
||||||
tree:render()
|
|
||||||
end
|
|
||||||
|
|
||||||
---Replace text in discussion after note update.
|
|
||||||
---@param data Discussion[]|UnlinkedDiscussion[]
|
|
||||||
---@param discussion_id string
|
|
||||||
---@param note_id integer
|
|
||||||
---@param text string
|
|
||||||
M.replace_text = function(data, discussion_id, note_id, text)
|
|
||||||
for i, discussion in ipairs(data) do
|
|
||||||
if discussion.id == discussion_id then
|
|
||||||
for j, note in ipairs(discussion.notes) do
|
|
||||||
if note.id == note_id then
|
|
||||||
data[i].notes[j].body = text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Given some note data, adds it to the tree and re-renders the tree
|
|
||||||
---@param tree any
|
|
||||||
---@param note any
|
|
||||||
---@param discussion_id any
|
|
||||||
M.add_reply_to_tree = function(tree, note, discussion_id)
|
|
||||||
local note_node = tree_utils.build_note(note)
|
|
||||||
note_node:expand()
|
|
||||||
tree:add_node(note_node, discussion_id and ("-" .. discussion_id) or nil)
|
|
||||||
tree:render()
|
|
||||||
end
|
|
||||||
|
|
||||||
---Toggle comments tree type between "simple" and "by_file_name"
|
---Toggle comments tree type between "simple" and "by_file_name"
|
||||||
M.toggle_tree_type = function()
|
M.toggle_tree_type = function()
|
||||||
if state.settings.discussion_tree.tree_type == "simple" then
|
if state.settings.discussion_tree.tree_type == "simple" then
|
||||||
@@ -663,89 +638,23 @@ M.toggle_tree_type = function()
|
|||||||
M.rebuild_discussion_tree()
|
M.rebuild_discussion_tree()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
|
||||||
|
M.toggle_draft_mode = function()
|
||||||
|
state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode
|
||||||
|
winbar.update_winbar()
|
||||||
|
end
|
||||||
|
|
||||||
---Indicates whether the node under the cursor is a draft note or not
|
---Indicates whether the node under the cursor is a draft note or not
|
||||||
---@param tree NuiTree
|
---@param tree NuiTree
|
||||||
---@return boolean
|
---@return boolean
|
||||||
M.is_draft_note = function(tree)
|
M.is_draft_note = function(tree)
|
||||||
local current_node = tree:get_node()
|
local current_node = tree:get_node()
|
||||||
|
local note_node = common.get_note_node(tree, current_node)
|
||||||
|
if note_node and note_node.is_draft then
|
||||||
|
return true
|
||||||
|
end
|
||||||
local root_node = common.get_root_node(tree, current_node)
|
local root_node = common.get_root_node(tree, current_node)
|
||||||
return root_node ~= nil and root_node.is_draft
|
return root_node ~= nil and root_node.is_draft
|
||||||
end
|
end
|
||||||
|
|
||||||
---Opens a popup prompting the user to choose an emoji to attach to the current node
|
|
||||||
---@param tree any
|
|
||||||
---@param unlinked boolean
|
|
||||||
M.add_emoji_to_note = function(tree, unlinked)
|
|
||||||
local node = tree:get_node()
|
|
||||||
local note_node = common.get_note_node(tree, node)
|
|
||||||
local root_node = common.get_root_node(tree, node)
|
|
||||||
local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
|
|
||||||
local note_id_str = tostring(note_id)
|
|
||||||
local emojis = require("gitlab.emoji").emoji_list
|
|
||||||
emoji.pick_emoji(emojis, function(name)
|
|
||||||
local body = { emoji = name, note_id = note_id }
|
|
||||||
job.run_job("/mr/awardable/note/", "POST", body, function(data)
|
|
||||||
if state.DISCUSSION_DATA.emojis[note_id_str] == nil then
|
|
||||||
state.DISCUSSION_DATA.emojis[note_id_str] = {}
|
|
||||||
table.insert(state.DISCUSSION_DATA.emojis[note_id_str], data.Emoji)
|
|
||||||
else
|
|
||||||
table.insert(state.DISCUSSION_DATA.emojis[note_id_str], data.Emoji)
|
|
||||||
end
|
|
||||||
if unlinked then
|
|
||||||
M.rebuild_unlinked_discussion_tree()
|
|
||||||
else
|
|
||||||
M.rebuild_discussion_tree()
|
|
||||||
end
|
|
||||||
u.notify("Emoji added", vim.log.levels.INFO)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Opens a popup prompting the user to choose an emoji to remove from the current node
|
|
||||||
---@param tree any
|
|
||||||
---@param unlinked boolean
|
|
||||||
M.delete_emoji_from_note = function(tree, unlinked)
|
|
||||||
local node = tree:get_node()
|
|
||||||
local note_node = common.get_note_node(tree, node)
|
|
||||||
local root_node = common.get_root_node(tree, node)
|
|
||||||
local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
|
|
||||||
local note_id_str = tostring(note_id)
|
|
||||||
|
|
||||||
local e = require("gitlab.emoji")
|
|
||||||
|
|
||||||
local emojis = {}
|
|
||||||
local current_emojis = state.DISCUSSION_DATA.emojis[note_id_str]
|
|
||||||
for _, current_emoji in ipairs(current_emojis) do
|
|
||||||
if state.USER.id == current_emoji.user.id then
|
|
||||||
table.insert(emojis, e.emoji_map[current_emoji.name])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
emoji.pick_emoji(emojis, function(name)
|
|
||||||
local awardable_id
|
|
||||||
for _, current_emoji in ipairs(current_emojis) do
|
|
||||||
if current_emoji.name == name and current_emoji.user.id == state.USER.id then
|
|
||||||
awardable_id = current_emoji.id
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
job.run_job(string.format("/mr/awardable/note/%d/%d", note_id, awardable_id), "DELETE", nil, function(_)
|
|
||||||
local keep = {} -- Emojis to keep after deletion in the UI
|
|
||||||
for _, saved in ipairs(state.DISCUSSION_DATA.emojis[note_id_str]) do
|
|
||||||
if saved.name ~= name or saved.user.id ~= state.USER.id then
|
|
||||||
table.insert(keep, saved)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
state.DISCUSSION_DATA.emojis[note_id_str] = keep
|
|
||||||
if unlinked then
|
|
||||||
M.rebuild_unlinked_discussion_tree()
|
|
||||||
else
|
|
||||||
M.rebuild_discussion_tree()
|
|
||||||
end
|
|
||||||
e.init_popup(tree, unlinked and M.unlinked_bufnr or M.linked_bufnr)
|
|
||||||
u.notify("Emoji removed", vim.log.levels.INFO)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
-- is not used in the draft notes tree
|
-- is not used in the draft notes tree
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local common = require("gitlab.actions.common")
|
local common = require("gitlab.actions.common")
|
||||||
|
local List = require("gitlab.utils.list")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local NuiTree = require("nui.tree")
|
local NuiTree = require("nui.tree")
|
||||||
local NuiLine = require("nui.line")
|
local NuiLine = require("nui.line")
|
||||||
@@ -56,8 +57,20 @@ M.add_discussions_to_table = function(items, unlinked)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Attaches draft notes that are replies to their parent discussions
|
||||||
|
local draft_replies = List.new(state.DRAFT_NOTES or {})
|
||||||
|
:filter(function(note)
|
||||||
|
return note.discussion_id == discussion.id
|
||||||
|
end)
|
||||||
|
:map(function(note)
|
||||||
|
local result = M.build_note(note)
|
||||||
|
return result
|
||||||
|
end)
|
||||||
|
|
||||||
|
local all_children = u.join(discussion_children, draft_replies)
|
||||||
|
|
||||||
-- Creates the first node in the discussion, and attaches children
|
-- Creates the first node in the discussion, and attaches children
|
||||||
local body = u.spread(root_text_nodes, discussion_children)
|
local body = u.spread(root_text_nodes, all_children)
|
||||||
local root_node = NuiTree.Node({
|
local root_node = NuiTree.Node({
|
||||||
range = range,
|
range = range,
|
||||||
text = root_text,
|
text = root_text,
|
||||||
@@ -460,6 +473,16 @@ M.collapse_recursively = function(tree, node, current_root_node, keep_current_op
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Expands a given node in a given tree by it's ID
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param id string
|
||||||
|
M.open_node_by_id = function(tree, id)
|
||||||
|
local node = tree:get_node(id)
|
||||||
|
if node then
|
||||||
|
node:expand()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- This function (settings.discussion_tree.toggle_node) expands/collapses the current node and its children
|
-- This function (settings.discussion_tree.toggle_node) expands/collapses the current node and its children
|
||||||
M.toggle_node = function(tree)
|
M.toggle_node = function(tree)
|
||||||
local node = tree:get_node()
|
local node = tree:get_node()
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
-- That includes things like editing existing draft notes in the tree, and
|
-- That includes things like editing existing draft notes in the tree, and
|
||||||
-- and deleting them. Normal notes and comments are managed separately,
|
-- and deleting them. Normal notes and comments are managed separately,
|
||||||
-- under lua/gitlab/actions/discussions/init.lua
|
-- under lua/gitlab/actions/discussions/init.lua
|
||||||
local winbar = require("gitlab.actions.discussions.winbar")
|
|
||||||
local diagnostics = require("gitlab.indicators.diagnostics")
|
|
||||||
local common = require("gitlab.actions.common")
|
local common = require("gitlab.actions.common")
|
||||||
local discussion_tree = require("gitlab.actions.discussions.tree")
|
local discussion_tree = require("gitlab.actions.discussions.tree")
|
||||||
local job = require("gitlab.job")
|
local job = require("gitlab.job")
|
||||||
@@ -14,76 +12,31 @@ local state = require("gitlab.state")
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---@class AddDraftNoteOpts table
|
---Re-fetches all draft notes (and non-draft notes) and re-renders the relevant views
|
||||||
---@field draft_note DraftNote
|
|
||||||
---@field unlinked boolean
|
|
||||||
|
|
||||||
---Adds a draft note to the draft notes state, then rebuilds the view
|
|
||||||
---@param opts AddDraftNoteOpts
|
|
||||||
M.add_draft_note = function(opts)
|
|
||||||
local new_draft_notes = u.ensure_table(state.DRAFT_NOTES)
|
|
||||||
table.insert(new_draft_notes, opts.draft_note)
|
|
||||||
state.DRAFT_NOTES = new_draft_notes
|
|
||||||
local discussions = require("gitlab.actions.discussions")
|
|
||||||
if opts.unlinked then
|
|
||||||
discussions.rebuild_unlinked_discussion_tree()
|
|
||||||
else
|
|
||||||
discussions.rebuild_discussion_tree()
|
|
||||||
end
|
|
||||||
winbar.update_winbar()
|
|
||||||
end
|
|
||||||
|
|
||||||
---Tells whether a draft note was left on a particular diff or is an unlinked note
|
|
||||||
---@param note DraftNote
|
|
||||||
M.has_position = function(note)
|
|
||||||
return note.position.new_path ~= nil or note.position.old_path ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---Returns a list of nodes to add to the discussion tree. Can filter and return only unlinked (note) nodes.
|
|
||||||
---@param unlinked boolean
|
---@param unlinked boolean
|
||||||
---@return NuiTree.Node[]
|
---@param all boolean|nil
|
||||||
M.add_draft_notes_to_table = function(unlinked)
|
M.rebuild_view = function(unlinked, all)
|
||||||
local draft_notes = List.new(state.DRAFT_NOTES)
|
M.load_draft_notes(function()
|
||||||
|
local discussions = require("gitlab.actions.discussions")
|
||||||
local draft_note_nodes = draft_notes
|
discussions.rebuild_view(unlinked, all)
|
||||||
---@param note DraftNote
|
|
||||||
:filter(function(note)
|
|
||||||
if unlinked then
|
|
||||||
return not M.has_position(note)
|
|
||||||
end
|
|
||||||
return M.has_position(note)
|
|
||||||
end)
|
end)
|
||||||
---@param note DraftNote
|
|
||||||
:map(function(note)
|
|
||||||
local _, root_text, root_text_nodes = discussion_tree.build_note(note)
|
|
||||||
return NuiTree.Node({
|
|
||||||
range = (type(note.position) == "table" and note.position.line_range or nil),
|
|
||||||
text = root_text,
|
|
||||||
type = "note",
|
|
||||||
is_root = true,
|
|
||||||
is_draft = true,
|
|
||||||
id = note.id,
|
|
||||||
root_note_id = note.id,
|
|
||||||
file_name = (type(note.position) == "table" and note.position.new_path or nil),
|
|
||||||
new_line = (type(note.position) == "table" and note.position.new_line or nil),
|
|
||||||
old_line = (type(note.position) == "table" and note.position.old_line or nil),
|
|
||||||
resolvable = false,
|
|
||||||
resolved = false,
|
|
||||||
url = state.INFO.web_url .. "#note_" .. note.id,
|
|
||||||
}, root_text_nodes)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return draft_note_nodes
|
|
||||||
|
|
||||||
-- TODO: Combine draft_notes and normal discussion nodes in the complex discussion
|
|
||||||
-- tree. The code for that feature is a clusterfuck so this is difficult
|
|
||||||
-- if state.settings.discussion_tree.tree_type == "simple" then
|
|
||||||
-- return draft_note_nodes
|
|
||||||
-- end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---Send edits will actually send the edits to Gitlab and refresh the draft_notes tree
|
---Makes API call to get the discussion data, stores it in the state, and calls the callback
|
||||||
M.send_edits = function(note_id)
|
---@param callback function|nil
|
||||||
|
M.load_draft_notes = function(callback)
|
||||||
|
state.load_new_state("draft_notes", function()
|
||||||
|
if callback ~= nil then
|
||||||
|
callback()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Will actually send the edits to Gitlab and refresh the draft_notes tree
|
||||||
|
---@param note_id integer
|
||||||
|
---@param unlinked boolean
|
||||||
|
---@return function
|
||||||
|
M.confirm_edit_draft_note = function(note_id, unlinked)
|
||||||
return function(text)
|
return function(text)
|
||||||
local all_notes = List.new(state.DRAFT_NOTES)
|
local all_notes = List.new(state.DRAFT_NOTES)
|
||||||
local the_note = all_notes:find(function(note)
|
local the_note = all_notes:find(function(note)
|
||||||
@@ -92,67 +45,18 @@ M.send_edits = function(note_id)
|
|||||||
local body = { note = text, position = the_note.position }
|
local body = { note = text, position = the_note.position }
|
||||||
job.run_job(string.format("/mr/draft_notes/%d", note_id), "PATCH", body, function(data)
|
job.run_job(string.format("/mr/draft_notes/%d", note_id), "PATCH", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
local has_position = false
|
M.rebuild_view(unlinked)
|
||||||
local new_draft_notes = all_notes:map(function(note)
|
|
||||||
if note.id == note_id then
|
|
||||||
has_position = M.has_position(note)
|
|
||||||
note.note = text
|
|
||||||
end
|
|
||||||
return note
|
|
||||||
end)
|
|
||||||
state.DRAFT_NOTES = new_draft_notes
|
|
||||||
local discussions = require("gitlab.actions.discussions")
|
|
||||||
if has_position then
|
|
||||||
discussions.rebuild_discussion_tree()
|
|
||||||
else
|
|
||||||
discussions.rebuild_unlinked_discussion_tree()
|
|
||||||
end
|
|
||||||
winbar.update_winbar()
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- This function will actually send the deletion to Gitlab when you make a selection, and re-render the tree
|
---This function will actually send the deletion to Gitlab when you make a selection, and re-render the tree
|
||||||
M.send_deletion = function(tree)
|
---@param note_id integer
|
||||||
local current_node = tree:get_node()
|
---@param unlinked boolean
|
||||||
local note_node = common.get_note_node(tree, current_node)
|
M.confirm_delete_draft_note = function(note_id, unlinked)
|
||||||
local root_node = common.get_root_node(tree, current_node)
|
|
||||||
|
|
||||||
if note_node == nil or root_node == nil then
|
|
||||||
u.notify("Could not get note or root node", vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type integer
|
|
||||||
local note_id = note_node.is_root and root_node.id or note_node.id
|
|
||||||
|
|
||||||
job.run_job(string.format("/mr/draft_notes/%d", note_id), "DELETE", nil, function(data)
|
job.run_job(string.format("/mr/draft_notes/%d", note_id), "DELETE", nil, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
M.rebuild_view(unlinked)
|
||||||
local has_position = false
|
|
||||||
local new_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
|
||||||
if note.id ~= note_id then
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
has_position = M.has_position(note)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
state.DRAFT_NOTES = new_draft_notes
|
|
||||||
local discussions = require("gitlab.actions.discussions")
|
|
||||||
if has_position then
|
|
||||||
discussions.rebuild_discussion_tree()
|
|
||||||
else
|
|
||||||
discussions.rebuild_unlinked_discussion_tree()
|
|
||||||
end
|
|
||||||
|
|
||||||
if state.settings.discussion_signs.enabled and state.DISCUSSION_DATA then
|
|
||||||
diagnostics.refresh_diagnostics()
|
|
||||||
end
|
|
||||||
|
|
||||||
winbar.update_winbar()
|
|
||||||
common.add_empty_titles()
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -185,11 +89,7 @@ M.confirm_publish_all_drafts = function()
|
|||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
state.DRAFT_NOTES = {}
|
state.DRAFT_NOTES = {}
|
||||||
local discussions = require("gitlab.actions.discussions")
|
local discussions = require("gitlab.actions.discussions")
|
||||||
discussions.refresh(function()
|
discussions.rebuild_view(false, true)
|
||||||
discussions.rebuild_discussion_tree()
|
|
||||||
discussions.rebuild_unlinked_discussion_tree()
|
|
||||||
winbar.update_winbar()
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -213,27 +113,61 @@ M.confirm_publish_draft = function(tree)
|
|||||||
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
|
||||||
local has_position = false
|
|
||||||
local new_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
|
||||||
if note.id ~= note_id then
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
has_position = M.has_position(note)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
state.DRAFT_NOTES = new_draft_notes
|
|
||||||
local discussions = require("gitlab.actions.discussions")
|
local discussions = require("gitlab.actions.discussions")
|
||||||
discussions.refresh(function()
|
local unlinked = tree.bufnr == discussions.unlinked_bufnr
|
||||||
if has_position then
|
M.rebuild_view(unlinked)
|
||||||
discussions.rebuild_discussion_tree()
|
|
||||||
else
|
|
||||||
discussions.rebuild_unlinked_discussion_tree()
|
|
||||||
end
|
|
||||||
winbar.update_winbar()
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Helper functions
|
||||||
|
---Tells whether a draft note was left on a particular diff or is an unlinked note
|
||||||
|
---@param note DraftNote
|
||||||
|
M.has_position = function(note)
|
||||||
|
return note.position.new_path ~= nil or note.position.old_path ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Builds a note for the discussion tree for draft notes that are roots
|
||||||
|
---of their own discussions, e.g. not replies
|
||||||
|
---@param note DraftNote
|
||||||
|
---@return NuiTree.Node
|
||||||
|
M.build_root_draft_note = function(note)
|
||||||
|
local _, root_text, root_text_nodes = discussion_tree.build_note(note)
|
||||||
|
return NuiTree.Node({
|
||||||
|
range = (type(note.position) == "table" and note.position.line_range or nil),
|
||||||
|
text = root_text,
|
||||||
|
type = "note",
|
||||||
|
is_root = true,
|
||||||
|
is_draft = true,
|
||||||
|
id = note.id,
|
||||||
|
root_note_id = note.id,
|
||||||
|
file_name = (type(note.position) == "table" and note.position.new_path or nil),
|
||||||
|
new_line = (type(note.position) == "table" and note.position.new_line or nil),
|
||||||
|
old_line = (type(note.position) == "table" and note.position.old_line or nil),
|
||||||
|
resolvable = false,
|
||||||
|
resolved = false,
|
||||||
|
url = state.INFO.web_url .. "#note_" .. note.id,
|
||||||
|
}, root_text_nodes)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns a list of nodes to add to the discussion tree. Can filter and return only unlinked (note) nodes.
|
||||||
|
---@param unlinked boolean
|
||||||
|
---@return NuiTree.Node[]
|
||||||
|
M.add_draft_notes_to_table = function(unlinked)
|
||||||
|
local draft_notes = List.new(state.DRAFT_NOTES)
|
||||||
|
local draft_note_nodes = draft_notes
|
||||||
|
---@param note DraftNote
|
||||||
|
:filter(function(note)
|
||||||
|
if unlinked then
|
||||||
|
return not M.has_position(note)
|
||||||
|
end
|
||||||
|
return M.has_position(note)
|
||||||
|
end)
|
||||||
|
:filter(function(note)
|
||||||
|
return note.discussion_id == "" -- Do not include draft replies
|
||||||
|
end)
|
||||||
|
:map(M.build_root_draft_note)
|
||||||
|
|
||||||
|
return draft_note_nodes
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ M.build_info_lines = function()
|
|||||||
branch = { title = "Branch", content = info.source_branch },
|
branch = { title = "Branch", content = info.source_branch },
|
||||||
labels = { title = "Labels", content = table.concat(info.labels, ", ") },
|
labels = { title = "Labels", content = table.concat(info.labels, ", ") },
|
||||||
target_branch = { title = "Target Branch", content = info.target_branch },
|
target_branch = { title = "Target Branch", content = info.target_branch },
|
||||||
delete_branch = { title = "Delete Source Branch", content = (info.force_remove_source_branch and "Yes" or "No") },
|
delete_branch = {
|
||||||
|
title = "Delete Source Branch",
|
||||||
|
content = (info.force_remove_source_branch and "Yes" or "No"),
|
||||||
|
},
|
||||||
squash = { title = "Squash Commits", content = (info.squash and "Yes" or "No") },
|
squash = { title = "Squash Commits", content = (info.squash and "Yes" or "No") },
|
||||||
pipeline = {
|
pipeline = {
|
||||||
title = "Pipeline Status",
|
title = "Pipeline Status",
|
||||||
|
|||||||
@@ -91,9 +91,9 @@ end
|
|||||||
|
|
||||||
---Parse git diff hunks.
|
---Parse git diff hunks.
|
||||||
---@param file_path string Path to file.
|
---@param file_path string Path to file.
|
||||||
---@param base_branch string Git base branch of merge request.
|
---@param base_sha string Git base SHA of merge request.
|
||||||
---@return HunksAndDiff
|
---@return HunksAndDiff
|
||||||
local parse_hunks_and_diff = function(file_path, base_branch)
|
local parse_hunks_and_diff = function(file_path, base_sha)
|
||||||
local hunks = {}
|
local hunks = {}
|
||||||
local all_diff_output = {}
|
local all_diff_output = {}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ local parse_hunks_and_diff = function(file_path, base_branch)
|
|||||||
|
|
||||||
local diff_job = Job:new({
|
local diff_job = Job:new({
|
||||||
command = "git",
|
command = "git",
|
||||||
args = { "diff", "--minimal", "--unified=0", "--no-color", base_branch, "--", file_path },
|
args = { "diff", "--minimal", "--unified=0", "--no-color", base_sha, "--", file_path },
|
||||||
on_exit = function(j, return_code)
|
on_exit = function(j, return_code)
|
||||||
if return_code == 0 then
|
if return_code == 0 then
|
||||||
all_diff_output = j:result()
|
all_diff_output = j:result()
|
||||||
@@ -225,7 +225,7 @@ end
|
|||||||
---@param is_current_sha_focused boolean
|
---@param is_current_sha_focused boolean
|
||||||
---@return string|nil
|
---@return string|nil
|
||||||
function M.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
function M.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
||||||
local hunk_and_diff_data = parse_hunks_and_diff(current_file, state.INFO.target_branch)
|
local hunk_and_diff_data = parse_hunks_and_diff(current_file, state.INFO.diff_refs.base_sha)
|
||||||
if hunk_and_diff_data.hunks == nil then
|
if hunk_and_diff_data.hunks == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ local function create_diagnostic(range_info, d_or_n)
|
|||||||
local message = header
|
local message = header
|
||||||
if d_or_n.notes then
|
if d_or_n.notes then
|
||||||
for _, note in ipairs(d_or_n.notes or {}) do
|
for _, note in ipairs(d_or_n.notes or {}) do
|
||||||
message = message .. actions_common.build_note_header(note) .. "\n" .. note.body .. "\n"
|
message = message .. "\n" .. note.body .. "\n"
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
message = message .. "\n" .. d_or_n.note .. "\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
local diagnostic = {
|
local diagnostic = {
|
||||||
|
|||||||
@@ -71,12 +71,16 @@ return {
|
|||||||
toggle_discussions = async.sequence({
|
toggle_discussions = async.sequence({
|
||||||
info,
|
info,
|
||||||
user,
|
user,
|
||||||
draft_notes_dep,
|
u.merge(draft_notes_dep, { refresh = true }),
|
||||||
discussion_data,
|
u.merge(discussion_data, { refresh = true }),
|
||||||
}, discussions.toggle),
|
}, discussions.toggle),
|
||||||
toggle_resolved = async.sequence({ info }, discussions.toggle_discussion_resolved),
|
toggle_draft_mode = discussions.toggle_draft_mode,
|
||||||
publish_all_drafts = draft_notes.publish_all_drafts,
|
publish_all_drafts = draft_notes.publish_all_drafts,
|
||||||
reply = async.sequence({ info }, discussions.reply),
|
refresh_data = function()
|
||||||
|
-- This also rebuilds the regular views
|
||||||
|
u.notify("Refreshing data...", vim.log.levels.INFO)
|
||||||
|
draft_notes.rebuild_view(false, true)
|
||||||
|
end,
|
||||||
-- Other functions 🤷
|
-- Other functions 🤷
|
||||||
state = state,
|
state = state,
|
||||||
data = data.data,
|
data = data.data,
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ M.settings = {
|
|||||||
border = "rounded",
|
border = "rounded",
|
||||||
opacity = 1.0,
|
opacity = 1.0,
|
||||||
edit = nil,
|
edit = nil,
|
||||||
reply = nil,
|
|
||||||
comment = nil,
|
comment = nil,
|
||||||
note = nil,
|
|
||||||
help = nil,
|
help = nil,
|
||||||
pipeline = nil,
|
pipeline = nil,
|
||||||
squash_message = nil,
|
squash_message = nil,
|
||||||
@@ -90,6 +88,7 @@ M.settings = {
|
|||||||
jump_to_reviewer = "m",
|
jump_to_reviewer = "m",
|
||||||
edit_comment = "e",
|
edit_comment = "e",
|
||||||
delete_comment = "dd",
|
delete_comment = "dd",
|
||||||
|
refresh_data = "a",
|
||||||
open_in_browser = "b",
|
open_in_browser = "b",
|
||||||
copy_node_url = "u",
|
copy_node_url = "u",
|
||||||
publish_draft = "P",
|
publish_draft = "P",
|
||||||
@@ -377,6 +376,7 @@ M.dependencies = {
|
|||||||
refresh = false,
|
refresh = false,
|
||||||
},
|
},
|
||||||
discussion_data = {
|
discussion_data = {
|
||||||
|
-- key is missing here...
|
||||||
endpoint = "/mr/discussions/list",
|
endpoint = "/mr/discussions/list",
|
||||||
state = "DISCUSSION_DATA",
|
state = "DISCUSSION_DATA",
|
||||||
refresh = false,
|
refresh = false,
|
||||||
@@ -389,6 +389,24 @@ M.dependencies = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
M.load_new_state = function(dep, cb)
|
||||||
|
local job = require("gitlab.job")
|
||||||
|
local dependency = M.dependencies[dep]
|
||||||
|
job.run_job(
|
||||||
|
dependency.endpoint,
|
||||||
|
dependency.method or "GET",
|
||||||
|
dependency.body and dependency.body() or nil,
|
||||||
|
function(data)
|
||||||
|
if dependency.key then
|
||||||
|
M[dependency.state] = u.ensure_table(data[dependency.key])
|
||||||
|
end
|
||||||
|
if type(cb) == "function" then
|
||||||
|
cb(data) -- To set data manually...
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
-- This function clears out all of the previously fetched data. It's used
|
-- This function clears out all of the previously fetched data. It's used
|
||||||
-- to reset the plugin state when the Go server is restarted
|
-- to reset the plugin state when the Go server is restarted
|
||||||
M.clear_data = function()
|
M.clear_data = function()
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ end
|
|||||||
|
|
||||||
---Get the popup view_opts
|
---Get the popup view_opts
|
||||||
---@param title string The string to appear on top of the popup
|
---@param title string The string to appear on top of the popup
|
||||||
---@param settings table User defined popup settings
|
---@param settings table|nil User defined popup settings
|
||||||
---@param width number? Override default width
|
---@param width number? Override default width
|
||||||
---@param height number? Override default height
|
---@param height number? Override default height
|
||||||
---@return table
|
---@return table
|
||||||
|
|||||||
Reference in New Issue
Block a user