diff --git a/README.md b/README.md index 063517f2..1a4be079 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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` diff --git a/cmd/draft_notes.go b/cmd/draft_notes.go index 6d8518f9..68c16954 100644 --- a/cmd/draft_notes.go +++ b/cmd/draft_notes.go @@ -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 } @@ -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 != "" { diff --git a/cmd/reply.go b/cmd/reply.go index 556184d8..4d104fdc 100644 --- a/cmd/reply.go +++ b/cmd/reply.go @@ -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 { diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 435b4837..140b94ff 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -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 @@ -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* @@ -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* @@ -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() ~ diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index d811fa1a..4620b9d0 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -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 @@ -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, @@ -73,20 +93,67 @@ 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 @@ -94,13 +161,16 @@ end ---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() @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lua/gitlab/actions/common.lua b/lua/gitlab/actions/common.lua index a5046dbd..1210e1cf 100644 --- a/lua/gitlab/actions/common.lua +++ b/lua/gitlab/actions/common.lua @@ -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 diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index b5415a58..e80c389d 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -29,18 +29,37 @@ local M = { linked_bufnr = nil, ---@type number unlinked_bufnr = nil, - ---@type number + ---@type NuiTree|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 ---@param callback function|nil 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.unlinked_discussions = u.ensure_table(data.unlinked_discussions) state.DISCUSSION_DATA.emojis = u.ensure_table(data.emojis) - if type(callback) == "function" then + if callback ~= nil then callback() end end) @@ -50,7 +69,7 @@ end M.initialize_discussions = function() signs.setup_signs() reviewer.set_callback_for_file_changed(function() - M.refresh_view() + M.refresh_diagnostics_and_winbar() M.modifiable(false) end) reviewer.set_callback_for_reviewer_enter(function() @@ -77,19 +96,8 @@ M.modifiable = function(bool) 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 -M.refresh_view = function() +M.refresh_diagnostics_and_winbar = function() if state.settings.discussion_signs.enabled then diagnostics.refresh_diagnostics() end @@ -146,7 +154,7 @@ M.toggle = function(callback) end vim.schedule(function() - M.refresh_view() + M.refresh_diagnostics_and_winbar() end) end @@ -219,74 +227,46 @@ M.reply = function(tree) u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN) return end - local reply_popup = Popup(u.create_popup_state("Reply", state.settings.popup.reply)) + local node = tree:get_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 -M.send_reply = function(tree, discussion_id) - return function(text) - 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) + if discussion_node == nil then + u.notify("Could not get discussion root", vim.log.levels.ERROR) + return 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 -- 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" }, { prompt = "Delete comment?", }, function(choice) 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 note_node = common.get_note_node(tree, current_node) - 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.root_note_id or note_node.id + local current_node = tree:get_node() + local note_node = common.get_note_node(tree, current_node) + 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 - if root_node.is_draft then - draft_notes.send_deletion(tree) - 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) + ---@type integer + if M.is_draft_note(tree) then + draft_notes.confirm_delete_draft_note(note_node.id, unlinked) else - tree:remove_node("-" .. note_id) + local note_id = note_node.is_root and root_node.root_note_id or note_node.id + local comment = require("gitlab.actions.comment") + comment.confirm_delete_comment(note_id, root_node.id, unlinked) 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 @@ -316,42 +296,24 @@ M.edit_comment = function(tree, unlinked) vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines) -- Draft notes module handles edits for draft notes - if root_node.is_draft then - state.set_popup_keymaps(edit_popup, draft_notes.send_edits(root_node.id), nil, miscellaneous.editable_popup_opts) + if M.is_draft_note(tree) then + state.set_popup_keymaps( + edit_popup, + draft_notes.confirm_edit_draft_note(note_node.id, unlinked), + nil, + miscellaneous.editable_popup_opts + ) else + local comment = require("gitlab.actions.comment") state.set_popup_keymaps( edit_popup, - M.send_edits(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked), + 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 ----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 - M.replace_text(state.DISCUSSION_DATA.discussions, discussion_id, note_id, text) - M.rebuild_discussion_tree() - end - end) - end -end - -- This function (settings.discussion_tree.toggle_discussion_resolved) will toggle the resolved status of the current discussion and send the change to the Go server M.toggle_discussion_resolved = function(tree) local note = tree:get_node() @@ -374,8 +336,61 @@ M.toggle_discussion_resolved = function(tree) job.run_job("/mr/discussions/resolve", "PUT", body, function(data) u.notify(data.message, vim.log.levels.INFO) - M.redraw_resolved_status(tree, note, not note.resolved) - M.refresh() + local unlinked = tree.bufnr == M.unlinked_bufnr + 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 @@ -383,31 +398,46 @@ end -- 🌲 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 ---linked to specific places in the code. M.rebuild_discussion_tree = function() if M.linked_bufnr == nil then return 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) + 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 draft_comment_nodes = draft_notes.add_draft_notes_to_table(false) -- Combine inline draft notes with regular comments - local all_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 all_nodes = u.join(draft_comment_nodes, existing_comment_nodes) local discussion_tree = NuiTree({ nodes = all_nodes, bufnr = M.linked_bufnr, 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() 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 return 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) vim.api.nvim_buf_set_lines(M.unlinked_bufnr, 0, -1, false, {}) 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) -- Combine draft notes with regular notes - local all_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 all_nodes = u.join(draft_comment_nodes, existing_note_nodes) local unlinked_discussion_tree = NuiTree({ nodes = all_nodes, bufnr = M.unlinked_bufnr, 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() + M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_bufnr, true) M.unlinked_discussion_tree = unlinked_discussion_tree common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr) @@ -498,6 +529,7 @@ M.is_current_node_note = function(tree) end M.set_tree_keymaps = function(tree, bufnr, unlinked) + ---Keybindings only relevant for linked (comment) view if not unlinked then vim.keymap.set("n", state.settings.discussion_tree.jump_to_file, function() 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" }) vim.keymap.set("n", state.settings.discussion_tree.jump_to_reviewer, function() 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, { buffer = bufnr, desc = "Jump to reviewer" }) vim.keymap.set("n", state.settings.discussion_tree.toggle_tree_type, function() M.toggle_tree_type() end, { buffer = bufnr, desc = "Toggle tree type between `simple` and `by_file_name`" }) 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() if M.is_current_node_note(tree) then M.edit_comment(tree, unlinked) @@ -525,12 +563,11 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) end, { buffer = bufnr, desc = "Publish draft" }) vim.keymap.set("n", state.settings.discussion_tree.delete_comment, function() if M.is_current_node_note(tree) then - M.delete_comment(tree) + M.delete_comment(tree, unlinked) end end, { buffer = bufnr, desc = "Delete comment" }) 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 - winbar.update_winbar() + M.toggle_draft_mode() end, { buffer = bufnr, desc = "Toggle between draft mode and live mode" }) 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 @@ -591,68 +628,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) emoji.init_popup(tree, bufnr) 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" M.toggle_tree_type = function() if state.settings.discussion_tree.tree_type == "simple" then @@ -663,89 +638,23 @@ M.toggle_tree_type = function() M.rebuild_discussion_tree() 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 ---@param tree NuiTree ---@return boolean M.is_draft_note = function(tree) 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) return root_node ~= nil and root_node.is_draft 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 diff --git a/lua/gitlab/actions/discussions/tree.lua b/lua/gitlab/actions/discussions/tree.lua index 024c4475..a8954e9a 100644 --- a/lua/gitlab/actions/discussions/tree.lua +++ b/lua/gitlab/actions/discussions/tree.lua @@ -2,6 +2,7 @@ -- is not used in the draft notes tree local u = require("gitlab.utils") local common = require("gitlab.actions.common") +local List = require("gitlab.utils.list") local state = require("gitlab.state") local NuiTree = require("nui.tree") local NuiLine = require("nui.line") @@ -56,8 +57,20 @@ M.add_discussions_to_table = function(items, unlinked) 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 - local body = u.spread(root_text_nodes, discussion_children) + local body = u.spread(root_text_nodes, all_children) local root_node = NuiTree.Node({ range = range, text = root_text, @@ -460,6 +473,16 @@ M.collapse_recursively = function(tree, node, current_root_node, keep_current_op 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 M.toggle_node = function(tree) local node = tree:get_node() diff --git a/lua/gitlab/actions/draft_notes/init.lua b/lua/gitlab/actions/draft_notes/init.lua index 6ea4aa34..1122b967 100755 --- a/lua/gitlab/actions/draft_notes/init.lua +++ b/lua/gitlab/actions/draft_notes/init.lua @@ -2,8 +2,6 @@ -- That includes things like editing existing draft notes in the tree, and -- and deleting them. Normal notes and comments are managed separately, -- 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 discussion_tree = require("gitlab.actions.discussions.tree") local job = require("gitlab.job") @@ -14,76 +12,31 @@ local state = require("gitlab.state") local M = {} ----@class AddDraftNoteOpts table ----@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() +---Re-fetches all draft notes (and non-draft notes) and re-renders the relevant views +---@param unlinked boolean +---@param all boolean|nil +M.rebuild_view = function(unlinked, all) + M.load_draft_notes(function() + local discussions = require("gitlab.actions.discussions") + discussions.rebuild_view(unlinked, all) + end) 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 +---Makes API call to get the discussion data, stores it in the state, and calls the callback +---@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 ----Returns a list of nodes to add to the discussion tree. Can filter and return only unlinked (note) nodes. +---Will actually send the edits to Gitlab and refresh the draft_notes tree +---@param note_id integer ---@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) - ---@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 - ----Send edits will actually send the edits to Gitlab and refresh the draft_notes tree -M.send_edits = function(note_id) +---@return function +M.confirm_edit_draft_note = function(note_id, unlinked) return function(text) local all_notes = List.new(state.DRAFT_NOTES) 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 } job.run_job(string.format("/mr/draft_notes/%d", note_id), "PATCH", body, function(data) u.notify(data.message, vim.log.levels.INFO) - local has_position = false - 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() + M.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 -M.send_deletion = function(tree) - local current_node = tree:get_node() - local note_node = common.get_note_node(tree, current_node) - 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 - +---This function will actually send the deletion to Gitlab when you make a selection, and re-render the tree +---@param note_id integer +---@param unlinked boolean +M.confirm_delete_draft_note = function(note_id, unlinked) job.run_job(string.format("/mr/draft_notes/%d", note_id), "DELETE", nil, function(data) 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") - 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() + M.rebuild_view(unlinked) end) end @@ -185,11 +89,7 @@ M.confirm_publish_all_drafts = function() u.notify(data.message, vim.log.levels.INFO) state.DRAFT_NOTES = {} local discussions = require("gitlab.actions.discussions") - discussions.refresh(function() - discussions.rebuild_discussion_tree() - discussions.rebuild_unlinked_discussion_tree() - winbar.update_winbar() - end) + discussions.rebuild_view(false, true) end) end @@ -213,27 +113,61 @@ M.confirm_publish_draft = function(tree) job.run_job("/mr/draft_notes/publish", "POST", body, function(data) 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") - discussions.refresh(function() - if has_position then - discussions.rebuild_discussion_tree() - else - discussions.rebuild_unlinked_discussion_tree() + local unlinked = tree.bufnr == discussions.unlinked_bufnr + M.rebuild_view(unlinked) + 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 - winbar.update_winbar() + return M.has_position(note) end) - 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 diff --git a/lua/gitlab/actions/summary.lua b/lua/gitlab/actions/summary.lua index 5d35dc11..1bcedc55 100644 --- a/lua/gitlab/actions/summary.lua +++ b/lua/gitlab/actions/summary.lua @@ -91,7 +91,10 @@ M.build_info_lines = function() branch = { title = "Branch", content = info.source_branch }, labels = { title = "Labels", content = table.concat(info.labels, ", ") }, 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") }, pipeline = { title = "Pipeline Status", diff --git a/lua/gitlab/hunks.lua b/lua/gitlab/hunks.lua index f90d6f1c..15e7e8b5 100644 --- a/lua/gitlab/hunks.lua +++ b/lua/gitlab/hunks.lua @@ -91,9 +91,9 @@ end ---Parse git diff hunks. ---@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 -local parse_hunks_and_diff = function(file_path, base_branch) +local parse_hunks_and_diff = function(file_path, base_sha) local hunks = {} local all_diff_output = {} @@ -101,7 +101,7 @@ local parse_hunks_and_diff = function(file_path, base_branch) local diff_job = Job:new({ 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) if return_code == 0 then all_diff_output = j:result() @@ -225,7 +225,7 @@ end ---@param is_current_sha_focused boolean ---@return string|nil 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 return end diff --git a/lua/gitlab/indicators/diagnostics.lua b/lua/gitlab/indicators/diagnostics.lua index f263df77..08741002 100644 --- a/lua/gitlab/indicators/diagnostics.lua +++ b/lua/gitlab/indicators/diagnostics.lua @@ -32,8 +32,10 @@ local function create_diagnostic(range_info, d_or_n) local message = header if d_or_n.notes then 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 + else + message = message .. "\n" .. d_or_n.note .. "\n" end local diagnostic = { diff --git a/lua/gitlab/init.lua b/lua/gitlab/init.lua index 1c9bc8ea..8e4c4d0c 100644 --- a/lua/gitlab/init.lua +++ b/lua/gitlab/init.lua @@ -71,12 +71,16 @@ return { toggle_discussions = async.sequence({ info, user, - draft_notes_dep, - discussion_data, + u.merge(draft_notes_dep, { refresh = true }), + u.merge(discussion_data, { refresh = true }), }, 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, - 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 🤷 state = state, data = data.data, diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index c58db7a3..4bf2abee 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -73,9 +73,7 @@ M.settings = { border = "rounded", opacity = 1.0, edit = nil, - reply = nil, comment = nil, - note = nil, help = nil, pipeline = nil, squash_message = nil, @@ -90,6 +88,7 @@ M.settings = { jump_to_reviewer = "m", edit_comment = "e", delete_comment = "dd", + refresh_data = "a", open_in_browser = "b", copy_node_url = "u", publish_draft = "P", @@ -377,6 +376,7 @@ M.dependencies = { refresh = false, }, discussion_data = { + -- key is missing here... endpoint = "/mr/discussions/list", state = "DISCUSSION_DATA", 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 -- to reset the plugin state when the Go server is restarted M.clear_data = function() diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua index dd9a4656..8c0e6e81 100644 --- a/lua/gitlab/utils/init.lua +++ b/lua/gitlab/utils/init.lua @@ -472,7 +472,7 @@ end ---Get the popup view_opts ---@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 height number? Override default height ---@return table