From 9c2265f1cb598dcf23fc9a20b4687b78b3a9b7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 30 May 2025 08:01:08 +0200 Subject: [PATCH 01/78] feat: add first draft of suggestion preview --- doc/gitlab.nvim.txt | 4 + lua/gitlab/actions/common.lua | 29 ++- lua/gitlab/actions/discussions/init.lua | 29 ++- lua/gitlab/actions/discussions/tree.lua | 8 + lua/gitlab/actions/suggestion.lua | 319 ++++++++++++++++++++++++ lua/gitlab/git.lua | 35 ++- lua/gitlab/indicators/common.lua | 10 + lua/gitlab/indicators/diagnostics.lua | 14 +- lua/gitlab/state.lua | 4 + 9 files changed, 425 insertions(+), 27 deletions(-) create mode 100644 lua/gitlab/actions/suggestion.lua diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 19ae7209..497662d2 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -223,6 +223,10 @@ you call this function with no values the defaults will be used: toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again print_node = "p", -- Print the current node (for debugging) + preview_suggestion = "sp", -- Show suggestion preview in a new tab + }, + suggestion_preview = { + quit = "q", -- Close the suggestion preview tab and discard changes to local files }, reviewer = { disable_all = false, -- Disable all default mappings for the reviewer windows diff --git a/lua/gitlab/actions/common.lua b/lua/gitlab/actions/common.lua index 538d4982..1a4424e0 100644 --- a/lua/gitlab/actions/common.lua +++ b/lua/gitlab/actions/common.lua @@ -174,6 +174,27 @@ M.get_note_node = function(tree, node) end end +---Gather all lines from immediate children that aren't note nodes +---@param tree NuiTree +---@return string[] List of individual note lines +M.get_note_lines = function(tree) + local current_node = tree:get_node() + local note_node = M.get_note_node(tree, current_node) + if note_node == nil then + u.notify("Could not get note node", vim.log.levels.ERROR) + return {} + end + local lines = List.new(note_node:get_child_ids()):reduce(function(agg, child_id) + local child_node = tree:get_node(child_id) + if child_node ~= nil and not child_node:has_children() then + local line = tree:get_node(child_id).text + table.insert(agg, line) + end + return agg + end, {}) + return lines +end + ---Takes a node and returns the line where the note is positioned in the new SHA. If ---the line is not in the new SHA, returns nil ---@param node NuiTree.Node @@ -254,17 +275,19 @@ end ---@param root_node NuiTree.Node ---@return integer|nil line_number ---@return boolean is_new_sha True if line number refers to NEW SHA +---@return integer|nil end_line M.get_line_number_from_node = function(root_node) if root_node.range then - local line_number, _, is_new_sha = M.get_line_numbers_for_range( + local line_number, end_line, is_new_sha = M.get_line_numbers_for_range( root_node.old_line, root_node.new_line, root_node.range.start.line_code, root_node.range["end"].line_code ) - return line_number, is_new_sha + return line_number, is_new_sha, end_line else - return M.get_line_number(root_node.id) + local start_line, is_new_sha = M.get_line_number(root_node.id) + return start_line, is_new_sha, start_line end end diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index c22fa766..575278f4 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -11,7 +11,6 @@ local popup = require("gitlab.popup") local state = require("gitlab.state") local reviewer = require("gitlab.reviewer") local common = require("gitlab.actions.common") -local List = require("gitlab.utils.list") local tree_utils = require("gitlab.actions.discussions.tree") local discussions_tree = require("gitlab.actions.discussions.tree") local draft_notes = require("gitlab.actions.draft_notes") @@ -246,6 +245,15 @@ M.reply = function(tree) layout:mount() end +-- Preview the suggestion(s) in the current discussion tree node +M.preview_suggestion = function(tree) + local suggestion = require("gitlab.actions.suggestion") + suggestion.show_preview({ + node = tree:get_node(), + tree = tree, + }) +end + -- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment M.delete_comment = function(tree, unlinked) vim.ui.select({ "Confirm", "Cancel" }, { @@ -289,15 +297,7 @@ M.edit_comment = function(tree, unlinked) edit_popup:mount() - -- Gather all lines from immediate children that aren't note nodes - local lines = List.new(note_node:get_child_ids()):reduce(function(agg, child_id) - local child_node = tree:get_node(child_id) - if not child_node:has_children() then - local line = tree:get_node(child_id).text - table.insert(agg, line) - end - return agg - end, {}) + local lines = common.get_note_lines(tree) local currentBuffer = vim.api.nvim_get_current_buf() vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines) @@ -593,6 +593,15 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) nowait = keymaps.discussion_tree.toggle_tree_type_nowait, }) end + + if keymaps.discussion_tree.preview_suggestion then + vim.keymap.set("n", keymaps.discussion_tree.preview_suggestion, function() + if M.is_current_node_note(tree) then + M.preview_suggestion(tree) + end + end, { buffer = bufnr, desc = "Preview suggestion", nowait = keymaps.discussion_tree.preview_suggestion_nowait }) + end + end if keymaps.discussion_tree.refresh_data then diff --git a/lua/gitlab/actions/discussions/tree.lua b/lua/gitlab/actions/discussions/tree.lua index e4f192ba..7873cf36 100644 --- a/lua/gitlab/actions/discussions/tree.lua +++ b/lua/gitlab/actions/discussions/tree.lua @@ -39,6 +39,8 @@ M.add_discussions_to_table = function(items, unlinked) local resolved = false local root_new_line = nil local root_old_line = nil + local root_head_sha = nil + local root_base_sha = nil local root_url for j, note in ipairs(discussion.notes) do @@ -48,6 +50,8 @@ M.add_discussions_to_table = function(items, unlinked) root_old_file_name = (type(note.position) == "table" and note.position.old_path or nil) root_new_line = (type(note.position) == "table" and note.position.new_line or nil) root_old_line = (type(note.position) == "table" and note.position.old_line or nil) + root_head_sha = (type(note.position) == "table" and note.position.head_sha) + root_base_sha = (type(note.position) == "table" and note.position.base_sha) root_id = discussion.id root_note_id = tostring(note.id) resolvable = note.resolvable @@ -85,6 +89,8 @@ M.add_discussions_to_table = function(items, unlinked) old_file_name = root_old_file_name, new_line = root_new_line, old_line = root_old_line, + head_sha = root_head_sha, + base_sha = root_base_sha, resolvable = resolvable, resolved = resolved, url = root_url, @@ -310,6 +316,8 @@ M.build_note = function(note, resolve_info) file_name = (type(note.position) == "table" and note.position.new_path), new_line = (type(note.position) == "table" and note.position.new_line), old_line = (type(note.position) == "table" and note.position.old_line), + head_sha = (type(note.position) == "table" and note.position.head_sha), + base_sha = (type(note.position) == "table" and note.position.base_sha), url = state.INFO.web_url .. "#note_" .. note.id, type = "note", }, text_nodes) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua new file mode 100644 index 00000000..2a90c657 --- /dev/null +++ b/lua/gitlab/actions/suggestion.lua @@ -0,0 +1,319 @@ +--- This module is responsible for previewing changes suggested in comments. +--- The data required to make the API calls are drawn from the discussion nodes. + +local common = require("gitlab.actions.common") +local diffview_lib = require("diffview.lib") +local git = require("gitlab.git") +local List = require("gitlab.utils.list") +local u = require("gitlab.utils") +local indicators_common = require("gitlab.indicators.common") + +local M = {} + +vim.fn.sign_define("GitlabSuggestion", { + text = "+", + texthl = "WarningMsg", +}) + +local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note") + +local set_buffer_lines = function(bufnr, lines) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + if M.local_implied then + vim.api.nvim_buf_call(bufnr, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + end +end + +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines) + for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do + vim.keymap.set("n", "q", function() + vim.cmd.tabclose() + if original_buf ~= nil then + if vim.api.nvim_buf_is_valid(original_buf) then + vim.cmd.bwipeout(original_buf) + end + end + if suggestion_buf ~= nil then + if vim.api.nvim_buf_is_valid(suggestion_buf) then + vim.api.nvim_set_option_value("modifiable", true, { buf = suggestion_buf }) + set_buffer_lines(suggestion_buf, original_lines) + end + end + -- TODO: restore suggestion buffer if it's HEAD! + end, { buffer = bufnr, desc = "Close suggestion preview tab" }) + end +end + +local replace_range = function(full_text, start_idx, end_idx, new_lines) + -- Copy the original text + local new_tbl = {} + for _, val in ipairs(full_text) do + table.insert(new_tbl, val) + end + -- Remove old lines + for _ = start_idx, end_idx do + table.remove(new_tbl, start_idx) + end + -- Insert new lines + for i, line in ipairs(new_lines) do + table.insert(new_tbl, start_idx + i - 1, line) + end + return new_tbl +end + +local refresh_signs = function(suggestion, note_buf) + vim.fn.sign_unplace("gitlab.suggestion") + + vim.fn.sign_place( + suggestion.note_start_linenr, + "gitlab.suggestion", + "GitlabSuggestion", + note_buf, + { lnum = suggestion.note_start_linenr } + ) + vim.fn.sign_place( + suggestion.note_end_linenr, + "gitlab.suggestion", + "GitlabSuggestion", + note_buf, + { lnum = suggestion.note_end_linenr } + ) +end + +local get_temp_file_name = function(revision, node_id, file_name) + local buf_name = string.format("gitlab://%s/%s/%s", revision, node_id, file_name) + local existing_bufnr = vim.fn.bufnr(buf_name) + if existing_bufnr > -1 and vim.fn.bufexists(existing_bufnr) then + vim.cmd.bwipeout(existing_bufnr) + end + return buf_name +end + + +M.show_preview = function(opts) + local note_lines = common.get_note_lines(opts.tree) + local root_node = common.get_root_node(opts.tree, opts.node) + if root_node == nil then + u.notify("Couldn't get root node", vim.log.levels.ERROR) + return + end + local suggestions = M.get_suggestions(note_lines) + if #suggestions == 0 then + u.notify("Note doesn't contain any suggestion.", vim.log.levels.WARN) + return + end + + if root_node.is_draft then + u.notify("Previewing a draft suggestion, showing diff against current HEAD.") + root_node.head_sha = "HEAD" + end + + local _, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) + local revision + if is_new_sha then + revision = root_node.head_sha + else + revision = root_node.base_sha + end + + if not git.revision_exists(revision) then + u.notify(string.format("Revision %s for which the comment was made does not exist", revision), + vim.log.levels.WARN) + return + end + + local original_head_text = git.get_file_revision({ file_name = root_node.file_name, revision = revision }) + local head_text = git.get_file_revision({ file_name = root_node.file_name, revision = "HEAD" }) + + -- The original head_sha doesn't contain the file, the branch was possibly rebased, and the + -- original head_sha could not been found. In that case `git.get_file_revision` should have logged + -- an error. + if original_head_text == nil then + u.notify( + string.format("File %s doesn't contain any text in revision %s for which the comment was made", root_node + .file_name, revision), + vim.log.levels.WARN + ) + return + end + + local view = diffview_lib.get_current_view() + if view == nil then + u.notify("Could not find Diffview view", vim.log.levels.ERROR) + return + end + + -- TODO: Use some common function to get the current file, deal with possible renames, decide if + -- the suggestion was made for the OLD version or NEW, etc. + local files = view.panel:ordered_file_list() + local file_name = List.new(files):find(function(file) + return file.path == root_node.file_name + end) + + if file_name == nil then + u.notify("File %s not found in HEAD.", file_name) + return + end + + -- Create new tab with a temp buffer showing the original version on which the comment was + -- made. + vim.api.nvim_cmd({ cmd = "tabnew" }, {}) + local original_lines = vim.fn.split(original_head_text, "\n", true) + local original_buf = vim.api.nvim_create_buf(true, true) + vim.api.nvim_buf_set_lines(original_buf, 0, -1, false, original_lines) + vim.bo[original_buf].modifiable = false + vim.bo[original_buf].buftype = "nofile" + vim.bo[original_buf].buflisted = false + + -- TODO: Make sure a buffer with the same name does not already exist (should be instead prevented + -- by a proper cleanup when the suggestion tab is closed). Should detect that a tab is already + -- open for the given suggestion. + + local buf_name = get_temp_file_name("ORIGINAL", root_node._id, root_node.file_name) + vim.api.nvim_buf_set_name(original_buf, buf_name) + vim.api.nvim_set_current_buf(original_buf) + vim.cmd.filetype("detect") + local buf_filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 }) + + -- TODO: Don't use local version when file contains changes (reuse `lua/gitlab/actions/comment.lua` lines 336-350) + if original_head_text == head_text and is_new_sha then + -- TODO: add check that file is not modified or doesn't have local uncommitted changes + u.notify("Original head is the same as HEAD. Using local version of " .. file_name.path, + vim.log.levels.WARNING + ) + vim.api.nvim_cmd({ cmd = "vsplit", args = { file_name.path } }, {}) + M.local_implied = true + else + -- TODO: Handle renamed files + if is_new_sha then + u.notify( + "Original head differs from HEAD. Using original version of " .. file_name.path, + vim.log.levels.WARNING + ) + else + u.notify( + "Comment was made on unchanged text. Using original version of " .. file_name.path, + vim.log.levels.WARNING + ) + end + local sug_file_name = get_temp_file_name("SUGGESTION", root_node._id, root_node.file_name) + vim.fn.mkdir(vim.fn.fnamemodify(sug_file_name, ":h"), "p") + vim.api.nvim_cmd({ cmd = "vnew", args = { sug_file_name } }, {}) + vim.bo.bufhidden = "wipe" + vim.bo.buftype = "nofile" + vim.bo.filetype = buf_filetype + M.local_implied = false + end + + local suggestion_buf = vim.api.nvim_get_current_buf() + + -- Create the file texts with suggestions applied + for _, suggestion in ipairs(suggestions) do + -- subtract 1 because nvim_buf_set_lines indexing is zero-based + local start_line = end_line_number - suggestion.start_line_offset + -- don't subtract 1 because nvim_buf_set_lines indexing is end-exclusive + local end_line = end_line_number + suggestion.end_line_offset + + suggestion.full_text = replace_range(original_lines, start_line, end_line, suggestion.lines) + end + set_buffer_lines(suggestion_buf, suggestions[1].full_text) + + vim.cmd("1,2windo diffthis") + + -- Create the note window + local note_buf = vim.api.nvim_create_buf(true, true) + vim.cmd("vsplit") + vim.api.nvim_set_current_buf(note_buf) + vim.api.nvim_buf_set_lines(note_buf, 0, -1, false, note_lines) + vim.bo.buftype = "nofile" + vim.bo.bufhidden = "wipe" + vim.bo.filetype = "markdown" + vim.bo.modifiable = false + vim.bo.buflisted = false + vim.api.nvim_buf_set_name(note_buf, string.format("gitlab://note/%s", root_node._id)) + + -- Focus the note window + local note_winid = vim.fn.win_getid(3) + vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) + refresh_signs(suggestions[1], note_buf) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines) + + -- Create autocommand for showing the active suggestion buffer in window 2 + local last_line = suggestions[1].note_start_linenr + local last_suggestion = suggestions[1] + vim.api.nvim_create_autocmd({ "CursorMoved" }, { + buffer = note_buf, + callback = function() + local current_line = vim.fn.line('.') + if current_line ~= last_line then + local suggestion = List.new(suggestions):find(function(sug) + return current_line <= sug.note_end_linenr + end) + if suggestion ~= last_suggestion then + set_buffer_lines(suggestion_buf, suggestion.full_text) + last_line = current_line + last_suggestion = suggestion + refresh_signs(suggestion, note_buf) + end + end + end + }) + + -- Show diagnostics for suggestions (enables using built-in navigation) + local diagnostics_data = M.create_diagnostics(suggestions) + vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) + + -- Show the discussion heading as virtual text + local mark_opts = { virt_lines = { { { opts.node.text, "WarningMsg" } } }, virt_lines_above = true } + vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) + -- An extmark above the first line is not visible by default, so let's scroll the window: + vim.cmd("normal! ") +end + +M.create_diagnostics = function(suggestions) + local diagnostics_data = {} + for _, suggestion in ipairs(suggestions) do + local diagnostic = { + message = table.concat(suggestion.lines, "\n") .. "\n", + col = 0, + severity = vim.diagnostic.severity.INFO, + source = "gitlab", + code = "gitlab.nvim", + lnum = suggestion.note_start_linenr - 1 + } + table.insert(diagnostics_data, diagnostic) + end + return diagnostics_data +end + +M.get_suggestions = function(note_lines) + local suggestions = {} + local in_suggestion = false + local suggestion = {} + local quote + + for i, line in ipairs(note_lines) do + local start_quote = string.match(line, "^%s*(`+)suggestion:%-%d+%+%d+") + local end_quote = string.match(line, "^%s*(`+)%s*$") + + if start_quote ~= nil and not in_suggestion then + quote = start_quote + in_suggestion = true + suggestion.start_line_offset, suggestion.end_line_offset = string.match(line, "^%s*`+suggestion:%-(%d+)%+(%d+)") + suggestion.note_start_linenr = i + suggestion.lines = {} + elseif end_quote and end_quote == quote then + suggestion.note_end_linenr = i + table.insert(suggestions, suggestion) + in_suggestion = false + suggestion = {} + elseif in_suggestion then + table.insert(suggestion.lines, line) + end + end + return suggestions +end + +return M diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index ba42546e..99a5f862 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -6,9 +6,12 @@ local M = {} ---@param command table ---@return string|nil, string|nil local run_system = function(command) - local result = vim.fn.trim(vim.fn.system(command)) + -- Preserve trailing newlines when getting contents of file revisions + local result = vim.fn.join(vim.fn.systemlist(command), "\n") if vim.v.shell_error ~= 0 then - require("gitlab.utils").notify(result, vim.log.levels.ERROR) + if result ~= "" then + require("gitlab.utils").notify(result, vim.log.levels.ERROR) + end return nil, result end return result, nil @@ -214,4 +217,32 @@ M.check_mr_in_good_condition = function() end end +---@class GetFileRevisionOpts +---@field revision string The SHA of the revision to get +---@field file_name string The name of the file to get + +---Returns the contents of the file in a given revision +---@param args GetFileRevisionOpts extra arguments for `git show` +---@return string|nil, string|nil +M.get_file_revision = function(args) + if args.revision == nil or args.file_name == nil then + return + end + local object = string.format("%s:%s", args.revision, args.file_name) + return run_system({ "git", "show", object }) +end + +---Returns true if the given revision exists, false otherwise +---@param revision string The revision to check +---@return boolean +M.revision_exists = function(revision) + if revision == nil then + require("gitlab.utils").notify("Invalid nil revision", vim.log.levels.ERROR) + return false + end + local object = string.format("%s", revision) + local result = run_system({ "git", "rev-parse", "--verify", "--quiet", "--end-of-options", object }) + return result ~= nil +end + return M diff --git a/lua/gitlab/indicators/common.lua b/lua/gitlab/indicators/common.lua index 04f68acc..1a42111e 100644 --- a/lua/gitlab/indicators/common.lua +++ b/lua/gitlab/indicators/common.lua @@ -10,6 +10,16 @@ local M = {} ---@field resolved boolean|nil ---@field created_at string|nil +-- Display options for the diagnostic +M.create_display_opts = function() + return { + virtual_text = state.settings.discussion_signs.virtual_text, + severity_sort = true, + underline = false, + signs = state.settings.discussion_signs.use_diagnostic_signs, + } +end + ---Return true if discussion has a placeable diagnostic, false otherwise. ---@param note NoteWithValues ---@return boolean diff --git a/lua/gitlab/indicators/diagnostics.lua b/lua/gitlab/indicators/diagnostics.lua index ccdd9363..a9010ec9 100644 --- a/lua/gitlab/indicators/diagnostics.lua +++ b/lua/gitlab/indicators/diagnostics.lua @@ -14,16 +14,6 @@ M.clear_diagnostics = function() vim.diagnostic.reset(diagnostics_namespace) end --- Display options for the diagnostic -local create_display_opts = function() - return { - virtual_text = state.settings.discussion_signs.virtual_text, - severity_sort = true, - underline = false, - signs = state.settings.discussion_signs.use_diagnostic_signs, - } -end - ---Takes some range information and data about a discussion ---and creates a diagnostic to be placed in the reviewer ---@param range_info table @@ -140,9 +130,9 @@ M.place_diagnostics = function(bufnr) local new_diagnostics, old_diagnostics = List.new(file_discussions):partition(indicators_common.is_new_sha) if bufnr == view.cur_layout.a.file.bufnr then - set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(old_diagnostics), create_display_opts()) + set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(old_diagnostics), indicators_common.create_display_opts()) elseif bufnr == view.cur_layout.b.file.bufnr then - set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(new_diagnostics), create_display_opts()) + set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(new_diagnostics), indicators_common.create_display_opts()) end end) diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index b7e2a469..9396751f 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -125,6 +125,10 @@ M.settings = { toggle_unresolved_discussions = "U", refresh_data = "", print_node = "p", + preview_suggestion = "sp", + }, + suggestion_preview = { + quit = "q", }, reviewer = { disable_all = false, From a2b76512c2b7462fbcf508474b9564eb6a36207d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 30 May 2025 08:02:18 +0200 Subject: [PATCH 02/78] fix: don't attempt placing diagnostics on diffview NULL buffer --- lua/gitlab/indicators/diagnostics.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/gitlab/indicators/diagnostics.lua b/lua/gitlab/indicators/diagnostics.lua index a9010ec9..b33cfd37 100644 --- a/lua/gitlab/indicators/diagnostics.lua +++ b/lua/gitlab/indicators/diagnostics.lua @@ -104,6 +104,9 @@ end ---Filter and place the diagnostics for the given buffer. ---@param bufnr number The number of the buffer for placing diagnostics. M.place_diagnostics = function(bufnr) + if bufnr and vim.api.nvim_buf_get_name(bufnr) == "diffview://null" then + return + end if not state.settings.discussion_signs.enabled then return end From 09314cf08afeb3d6454bea7b5d7a96429b818da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 30 May 2025 08:03:35 +0200 Subject: [PATCH 03/78] docs: mark parameter as optional --- lua/gitlab/actions/discussions/winbar.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlab/actions/discussions/winbar.lua b/lua/gitlab/actions/discussions/winbar.lua index 7b1dc252..01136958 100644 --- a/lua/gitlab/actions/discussions/winbar.lua +++ b/lua/gitlab/actions/discussions/winbar.lua @@ -255,7 +255,7 @@ M.get_mode = function() end ---Toggles the current view type (or sets it to `override`) and then updates the view. ----@param override "discussions"|"notes" Defines the view type to select. +---@param override? "discussions"|"notes" Defines the view type to select. M.switch_view_type = function(override) if override then M.current_view_type = override From 214068afd3ea4af3f0c19d70b417a5ebd2419a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 30 May 2025 09:03:43 +0200 Subject: [PATCH 04/78] fix: go to note in existing tab --- lua/gitlab/actions/suggestion.lua | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 2a90c657..76195f82 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -91,14 +91,41 @@ local get_temp_file_name = function(revision, node_id, file_name) return buf_name end +---Check if buffer already exists and return the number of the tab it's open in +---@param bufname string The full name of the buffer to check. +---@return number|nil tabnr The tabpage number if buffer is already open or nil. +local get_tabnr_for_buf = function(bufname) + local bufnr = vim.fn.bufnr(bufname) + if bufnr == -1 then + return nil + end + for _, tabnr in ipairs(vim.api.nvim_list_tabpages()) do + for _, winnr in ipairs( vim.api.nvim_tabpage_list_wins(tabnr)) do + if vim.api.nvim_win_get_buf(winnr) == bufnr then + return tabnr + end + end + end + return nil +end M.show_preview = function(opts) - local note_lines = common.get_note_lines(opts.tree) local root_node = common.get_root_node(opts.tree, opts.node) if root_node == nil then u.notify("Couldn't get root node", vim.log.levels.ERROR) return end + + -- If preview is already open for given note, go to the tab with a warning. + local note_bufname = string.format("gitlab://NOTE/%s", root_node._id) + local tabnr = get_tabnr_for_buf(note_bufname) + if tabnr ~= nil then + vim.api.nvim_set_current_tabpage(tabnr) + u.notify("Previously created preview can be outdated", vim.log.levels.WARN) + return + end + + local note_lines = common.get_note_lines(opts.tree) local suggestions = M.get_suggestions(note_lines) if #suggestions == 0 then u.notify("Note doesn't contain any suggestion.", vim.log.levels.WARN) @@ -232,7 +259,7 @@ M.show_preview = function(opts) vim.bo.filetype = "markdown" vim.bo.modifiable = false vim.bo.buflisted = false - vim.api.nvim_buf_set_name(note_buf, string.format("gitlab://note/%s", root_node._id)) + vim.api.nvim_buf_set_name(note_buf, note_bufname) -- Focus the note window local note_winid = vim.fn.win_getid(3) From 17a69d7a0998886d7f31e428b8d2b426241d9aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sun, 1 Jun 2025 00:27:37 +0200 Subject: [PATCH 05/78] refactor: don't use plain tabnew as it creates empty buffer --- lua/gitlab/actions/suggestion.lua | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 76195f82..edb8cb30 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -186,21 +186,17 @@ M.show_preview = function(opts) -- Create new tab with a temp buffer showing the original version on which the comment was -- made. - vim.api.nvim_cmd({ cmd = "tabnew" }, {}) local original_lines = vim.fn.split(original_head_text, "\n", true) - local original_buf = vim.api.nvim_create_buf(true, true) + local original_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(original_buf, 0, -1, false, original_lines) - vim.bo[original_buf].modifiable = false - vim.bo[original_buf].buftype = "nofile" - vim.bo[original_buf].buflisted = false - - -- TODO: Make sure a buffer with the same name does not already exist (should be instead prevented - -- by a proper cleanup when the suggestion tab is closed). Should detect that a tab is already - -- open for the given suggestion. - local buf_name = get_temp_file_name("ORIGINAL", root_node._id, root_node.file_name) vim.api.nvim_buf_set_name(original_buf, buf_name) - vim.api.nvim_set_current_buf(original_buf) + vim.api.nvim_cmd({ cmd = "tabnew", args = { buf_name } }, {}) + vim.bo.bufhidden = "wipe" + vim.bo.buflisted = false + vim.bo.buftype = "nofile" + vim.bo.modifiable = false + vim.cmd.filetype("detect") local buf_filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 }) @@ -229,6 +225,7 @@ M.show_preview = function(opts) vim.fn.mkdir(vim.fn.fnamemodify(sug_file_name, ":h"), "p") vim.api.nvim_cmd({ cmd = "vnew", args = { sug_file_name } }, {}) vim.bo.bufhidden = "wipe" + vim.bo.buflisted = false vim.bo.buftype = "nofile" vim.bo.filetype = buf_filetype M.local_implied = false @@ -250,16 +247,15 @@ M.show_preview = function(opts) vim.cmd("1,2windo diffthis") -- Create the note window - local note_buf = vim.api.nvim_create_buf(true, true) - vim.cmd("vsplit") - vim.api.nvim_set_current_buf(note_buf) + local note_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(note_buf, note_bufname) + vim.api.nvim_cmd({ cmd = "vnew", args = { note_bufname } }, {}) vim.api.nvim_buf_set_lines(note_buf, 0, -1, false, note_lines) - vim.bo.buftype = "nofile" vim.bo.bufhidden = "wipe" + vim.bo.buflisted = false + vim.bo.buftype = "nofile" vim.bo.filetype = "markdown" vim.bo.modifiable = false - vim.bo.buflisted = false - vim.api.nvim_buf_set_name(note_buf, note_bufname) -- Focus the note window local note_winid = vim.fn.win_getid(3) From 6049705b2e0db406fdd54c290000ceccb2fe4db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sun, 1 Jun 2025 01:08:45 +0200 Subject: [PATCH 06/78] refactor: make functions local --- lua/gitlab/actions/suggestion.lua | 92 +++++++++++++++---------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index edb8cb30..123ad62a 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -109,6 +109,50 @@ local get_tabnr_for_buf = function(bufname) return nil end +local get_suggestions = function(note_lines) + local suggestions = {} + local in_suggestion = false + local suggestion = {} + local quote + + for i, line in ipairs(note_lines) do + local start_quote = string.match(line, "^%s*(`+)suggestion:%-%d+%+%d+") + local end_quote = string.match(line, "^%s*(`+)%s*$") + + if start_quote ~= nil and not in_suggestion then + quote = start_quote + in_suggestion = true + suggestion.start_line_offset, suggestion.end_line_offset = string.match(line, "^%s*`+suggestion:%-(%d+)%+(%d+)") + suggestion.note_start_linenr = i + suggestion.lines = {} + elseif end_quote and end_quote == quote then + suggestion.note_end_linenr = i + table.insert(suggestions, suggestion) + in_suggestion = false + suggestion = {} + elseif in_suggestion then + table.insert(suggestion.lines, line) + end + end + return suggestions +end + +local create_diagnostics = function(suggestions) + local diagnostics_data = {} + for _, suggestion in ipairs(suggestions) do + local diagnostic = { + message = table.concat(suggestion.lines, "\n") .. "\n", + col = 0, + severity = vim.diagnostic.severity.INFO, + source = "gitlab", + code = "gitlab.nvim", + lnum = suggestion.note_start_linenr - 1 + } + table.insert(diagnostics_data, diagnostic) + end + return diagnostics_data +end + M.show_preview = function(opts) local root_node = common.get_root_node(opts.tree, opts.node) if root_node == nil then @@ -126,7 +170,7 @@ M.show_preview = function(opts) end local note_lines = common.get_note_lines(opts.tree) - local suggestions = M.get_suggestions(note_lines) + local suggestions = get_suggestions(note_lines) if #suggestions == 0 then u.notify("Note doesn't contain any suggestion.", vim.log.levels.WARN) return @@ -285,7 +329,7 @@ M.show_preview = function(opts) }) -- Show diagnostics for suggestions (enables using built-in navigation) - local diagnostics_data = M.create_diagnostics(suggestions) + local diagnostics_data = create_diagnostics(suggestions) vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) -- Show the discussion heading as virtual text @@ -295,48 +339,4 @@ M.show_preview = function(opts) vim.cmd("normal! ") end -M.create_diagnostics = function(suggestions) - local diagnostics_data = {} - for _, suggestion in ipairs(suggestions) do - local diagnostic = { - message = table.concat(suggestion.lines, "\n") .. "\n", - col = 0, - severity = vim.diagnostic.severity.INFO, - source = "gitlab", - code = "gitlab.nvim", - lnum = suggestion.note_start_linenr - 1 - } - table.insert(diagnostics_data, diagnostic) - end - return diagnostics_data -end - -M.get_suggestions = function(note_lines) - local suggestions = {} - local in_suggestion = false - local suggestion = {} - local quote - - for i, line in ipairs(note_lines) do - local start_quote = string.match(line, "^%s*(`+)suggestion:%-%d+%+%d+") - local end_quote = string.match(line, "^%s*(`+)%s*$") - - if start_quote ~= nil and not in_suggestion then - quote = start_quote - in_suggestion = true - suggestion.start_line_offset, suggestion.end_line_offset = string.match(line, "^%s*`+suggestion:%-(%d+)%+(%d+)") - suggestion.note_start_linenr = i - suggestion.lines = {} - elseif end_quote and end_quote == quote then - suggestion.note_end_linenr = i - table.insert(suggestions, suggestion) - in_suggestion = false - suggestion = {} - elseif in_suggestion then - table.insert(suggestion.lines, line) - end - end - return suggestions -end - return M From 3cbdf51d2650bac19def1673657dd011347c621e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sun, 1 Jun 2025 01:16:44 +0200 Subject: [PATCH 07/78] docs: add some docstrings --- lua/gitlab/actions/suggestion.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 123ad62a..8da52375 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -153,6 +153,12 @@ local create_diagnostics = function(suggestions) return diagnostics_data end +---@class ShowPreviewOpts +---@field tree NuiTree The current discussion tree instance +---@field node NuiTreeNode The current node in the discussion tree + +---Get suggestions from the current note and preview them in a new tab +---@param opts ShowPreviewOpts M.show_preview = function(opts) local root_node = common.get_root_node(opts.tree, opts.node) if root_node == nil then @@ -160,7 +166,7 @@ M.show_preview = function(opts) return end - -- If preview is already open for given note, go to the tab with a warning. + -- If preview is already open for given note, go to the tab with a warning. local note_bufname = string.format("gitlab://NOTE/%s", root_node._id) local tabnr = get_tabnr_for_buf(note_bufname) if tabnr ~= nil then @@ -169,6 +175,7 @@ M.show_preview = function(opts) return end + -- Return early when there're no suggestions. local note_lines = common.get_note_lines(opts.tree) local suggestions = get_suggestions(note_lines) if #suggestions == 0 then From 282899f54b9a6f21d36e429db98700807fa34cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 2 Jun 2025 06:35:05 +0200 Subject: [PATCH 08/78] fix: add base_sha to draft comments --- lua/gitlab/actions/suggestion.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 8da52375..8f3ef638 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -183,9 +183,10 @@ M.show_preview = function(opts) return end + -- Hack: draft notes don't have head_sha and base_sha yet if root_node.is_draft then - u.notify("Previewing a draft suggestion, showing diff against current HEAD.") root_node.head_sha = "HEAD" + root_node.base_sha = require("gitlab.state").INFO.target_branch end local _, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) From 95474462e4b2825dcda3490dd81c6ecd64ac75aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 2 Jun 2025 09:28:58 +0200 Subject: [PATCH 09/78] fix: use old path when comment is on OLD_SHA --- lua/gitlab/actions/suggestion.lua | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 8f3ef638..b4e4dad1 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -190,20 +190,22 @@ M.show_preview = function(opts) end local _, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) - local revision + local revision, original_file_name if is_new_sha then revision = root_node.head_sha + original_file_name = root_node.file_name else revision = root_node.base_sha + original_file_name = root_node.old_file_name end if not git.revision_exists(revision) then - u.notify(string.format("Revision %s for which the comment was made does not exist", revision), + u.notify(string.format("Revision `%s` for which the comment was made does not exist", revision), vim.log.levels.WARN) return end - local original_head_text = git.get_file_revision({ file_name = root_node.file_name, revision = revision }) + local original_head_text = git.get_file_revision({ file_name = original_file_name, revision = revision }) local head_text = git.get_file_revision({ file_name = root_node.file_name, revision = "HEAD" }) -- The original head_sha doesn't contain the file, the branch was possibly rebased, and the @@ -211,8 +213,7 @@ M.show_preview = function(opts) -- an error. if original_head_text == nil then u.notify( - string.format("File %s doesn't contain any text in revision %s for which the comment was made", root_node - .file_name, revision), + string.format("File `%s` doesn't contain any text in revision `%s` for which the comment was made", original_file_name, revision), vim.log.levels.WARN ) return @@ -228,11 +229,12 @@ M.show_preview = function(opts) -- the suggestion was made for the OLD version or NEW, etc. local files = view.panel:ordered_file_list() local file_name = List.new(files):find(function(file) - return file.path == root_node.file_name + local file_name_ = is_new_sha and file.path or file.oldpath + return file_name_ == original_file_name end) if file_name == nil then - u.notify("File %s not found in HEAD.", file_name) + u.notify(string.format("File `%s` not found in revision `%s`.", revision)) return end @@ -255,8 +257,8 @@ M.show_preview = function(opts) -- TODO: Don't use local version when file contains changes (reuse `lua/gitlab/actions/comment.lua` lines 336-350) if original_head_text == head_text and is_new_sha then -- TODO: add check that file is not modified or doesn't have local uncommitted changes - u.notify("Original head is the same as HEAD. Using local version of " .. file_name.path, - vim.log.levels.WARNING + u.notify("Original head is the same as HEAD. Using local version of " .. original_file_name, + vim.log.levels.INFO ) vim.api.nvim_cmd({ cmd = "vsplit", args = { file_name.path } }, {}) M.local_implied = true From 5b11f2083e0bfa02542c4b216a23af4bd844794b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 2 Jun 2025 09:58:50 +0200 Subject: [PATCH 10/78] docs: add TODO --- lua/gitlab/actions/suggestion.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index b4e4dad1..934d7671 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -347,6 +347,7 @@ M.show_preview = function(opts) vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) -- An extmark above the first line is not visible by default, so let's scroll the window: vim.cmd("normal! ") + -- TODO: Add virtual text (or winbar?) to show the diffed revision of the ORIGINAL. end return M From 6d9f83a04026ca6477be1ed56c63a8983d49a28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 2 Jun 2025 10:06:45 +0200 Subject: [PATCH 11/78] docs: update comment --- lua/gitlab/actions/suggestion.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 934d7671..b8d4f094 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -208,8 +208,8 @@ M.show_preview = function(opts) local original_head_text = git.get_file_revision({ file_name = original_file_name, revision = revision }) local head_text = git.get_file_revision({ file_name = root_node.file_name, revision = "HEAD" }) - -- The original head_sha doesn't contain the file, the branch was possibly rebased, and the - -- original head_sha could not been found. In that case `git.get_file_revision` should have logged + -- The original revision doesn't contain the file, the branch was possibly rebased, and the + -- original revision could not been found. In that case `git.get_file_revision` should have logged -- an error. if original_head_text == nil then u.notify( From 394d5f731270a0fa26433710f3ccc45b8eaeafc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 2 Jun 2025 12:50:20 +0200 Subject: [PATCH 12/78] fix: improve checking whether local file should be used for suggestions --- lua/gitlab/actions/suggestion.lua | 129 ++++++++++++++++-------------- lua/gitlab/git.lua | 14 ++++ 2 files changed, 84 insertions(+), 59 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index b8d4f094..6a970691 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -2,7 +2,6 @@ --- The data required to make the API calls are drawn from the discussion nodes. local common = require("gitlab.actions.common") -local diffview_lib = require("diffview.lib") local git = require("gitlab.git") local List = require("gitlab.utils.list") local u = require("gitlab.utils") @@ -109,6 +108,16 @@ local get_tabnr_for_buf = function(bufname) return nil end +---@class Suggestion +---@field start_line_offset number The offset for the start of the suggestion (e.g., "2" in suggestion:-2+3) +---@field end_line_offset number The offset for the end of the suggestion (e.g., "3" in suggestion:-2+3) +---@field note_start_linenr number The line number in the note text where the suggesion begins +---@field note_end_linenr number The line number in the note text where the suggesion ends +---@field lines string[] The text of the suggesion +---@field full_text string[] The full text of the file with the suggesion applied + +--- Create the suggestion list from the note text +---@return Suggestion[] local get_suggestions = function(note_lines) local suggestions = {} local in_suggestion = false @@ -137,6 +146,8 @@ local get_suggestions = function(note_lines) return suggestions end +--- Create diagnostics data from suggesions +---@param suggestions Suggestion[] local create_diagnostics = function(suggestions) local diagnostics_data = {} for _, suggestion in ipairs(suggestions) do @@ -153,6 +164,27 @@ local create_diagnostics = function(suggestions) return diagnostics_data end +local is_modified = function(file_name) + local has_changes = git.has_changes(file_name) + local bufnr = vim.fn.bufnr(file_name, true) + if vim.bo[bufnr].modified or has_changes then + return true + end + return false +end + +--- Update suggestions with the changes applied to the original text +---@param suggestions Suggestion[] +---@param end_line_number integer The last number of the comment range +---@param original_lines string[] Array of original lines +local add_full_text_to_suggestions = function(suggestions, end_line_number, original_lines) + for _, suggestion in ipairs(suggestions) do + local start_line = end_line_number - suggestion.start_line_offset + local end_line = end_line_number + suggestion.end_line_offset + suggestion.full_text = replace_range(original_lines, start_line, end_line, suggestion.lines) + end +end + ---@class ShowPreviewOpts ---@field tree NuiTree The current discussion tree instance ---@field node NuiTreeNode The current node in the discussion tree @@ -189,6 +221,7 @@ M.show_preview = function(opts) root_node.base_sha = require("gitlab.state").INFO.target_branch end + -- Decide which revision to use for the ORIGINAL text local _, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) local revision, original_file_name if is_new_sha then @@ -198,49 +231,29 @@ M.show_preview = function(opts) revision = root_node.base_sha original_file_name = root_node.old_file_name end - if not git.revision_exists(revision) then u.notify(string.format("Revision `%s` for which the comment was made does not exist", revision), vim.log.levels.WARN) return end + -- Get the text on which the suggestion was created local original_head_text = git.get_file_revision({ file_name = original_file_name, revision = revision }) - local head_text = git.get_file_revision({ file_name = root_node.file_name, revision = "HEAD" }) - - -- The original revision doesn't contain the file, the branch was possibly rebased, and the - -- original revision could not been found. In that case `git.get_file_revision` should have logged - -- an error. + -- If the original revision doesn't contain the file, the branch was possibly rebased, and the + -- original revision could not been found. if original_head_text == nil then u.notify( - string.format("File `%s` doesn't contain any text in revision `%s` for which the comment was made", original_file_name, revision), + string.format("File `%s` doesn't contain any text in revision `%s` for which comment was made", original_file_name, revision), vim.log.levels.WARN ) return end + local original_lines = vim.fn.split(original_head_text, "\n", true) - local view = diffview_lib.get_current_view() - if view == nil then - u.notify("Could not find Diffview view", vim.log.levels.ERROR) - return - end - - -- TODO: Use some common function to get the current file, deal with possible renames, decide if - -- the suggestion was made for the OLD version or NEW, etc. - local files = view.panel:ordered_file_list() - local file_name = List.new(files):find(function(file) - local file_name_ = is_new_sha and file.path or file.oldpath - return file_name_ == original_file_name - end) - - if file_name == nil then - u.notify(string.format("File `%s` not found in revision `%s`.", revision)) - return - end + add_full_text_to_suggestions(suggestions, end_line_number, original_lines) -- Create new tab with a temp buffer showing the original version on which the comment was -- made. - local original_lines = vim.fn.split(original_head_text, "\n", true) local original_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(original_buf, 0, -1, false, original_lines) local buf_name = get_temp_file_name("ORIGINAL", root_node._id, root_node.file_name) @@ -250,31 +263,42 @@ M.show_preview = function(opts) vim.bo.buflisted = false vim.bo.buftype = "nofile" vim.bo.modifiable = false - vim.cmd.filetype("detect") local buf_filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 }) - -- TODO: Don't use local version when file contains changes (reuse `lua/gitlab/actions/comment.lua` lines 336-350) - if original_head_text == head_text and is_new_sha then - -- TODO: add check that file is not modified or doesn't have local uncommitted changes - u.notify("Original head is the same as HEAD. Using local version of " .. original_file_name, - vim.log.levels.INFO + -- Decide if local file should be used to show suggestion preview + local head_differs_from_original = git.file_differs_in_revisions({ + original_revision = revision, + head_revision = "HEAD", + old_file_name = root_node.old_file_name, + file_name = root_node.file_name + }) + if not is_new_sha then + M.local_implied = false + u.notify( + string.format("Comment on unchanged text. Using target-branch version of `%s`", original_file_name), + vim.log.levels.WARNING + ) + elseif head_differs_from_original then + M.local_implied = false + u.notify( + string.format("File changed since comment created. Using feature-branch version of `%s`", original_file_name), + vim.log.levels.WARNING + ) + elseif is_modified(original_file_name) then + M.local_implied = false + u.notify( + string.format("File has unsaved or uncommited changes. Using feature-branch version for `%s`", original_file_name), + vim.log.levels.WARNING ) - vim.api.nvim_cmd({ cmd = "vsplit", args = { file_name.path } }, {}) + else M.local_implied = true + end + + -- Create the suggestion buffer and show a diff with the original version + if M.local_implied then + vim.api.nvim_cmd({ cmd = "vsplit", args = { original_file_name } }, {}) else - -- TODO: Handle renamed files - if is_new_sha then - u.notify( - "Original head differs from HEAD. Using original version of " .. file_name.path, - vim.log.levels.WARNING - ) - else - u.notify( - "Comment was made on unchanged text. Using original version of " .. file_name.path, - vim.log.levels.WARNING - ) - end local sug_file_name = get_temp_file_name("SUGGESTION", root_node._id, root_node.file_name) vim.fn.mkdir(vim.fn.fnamemodify(sug_file_name, ":h"), "p") vim.api.nvim_cmd({ cmd = "vnew", args = { sug_file_name } }, {}) @@ -282,22 +306,9 @@ M.show_preview = function(opts) vim.bo.buflisted = false vim.bo.buftype = "nofile" vim.bo.filetype = buf_filetype - M.local_implied = false end - local suggestion_buf = vim.api.nvim_get_current_buf() - - -- Create the file texts with suggestions applied - for _, suggestion in ipairs(suggestions) do - -- subtract 1 because nvim_buf_set_lines indexing is zero-based - local start_line = end_line_number - suggestion.start_line_offset - -- don't subtract 1 because nvim_buf_set_lines indexing is end-exclusive - local end_line = end_line_number + suggestion.end_line_offset - - suggestion.full_text = replace_range(original_lines, start_line, end_line, suggestion.lines) - end set_buffer_lines(suggestion_buf, suggestions[1].full_text) - vim.cmd("1,2windo diffthis") -- Create the note window diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index 99a5f862..2ee21c0e 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -245,4 +245,18 @@ M.revision_exists = function(revision) return result ~= nil end +---@class FileDiffersInRevisionsOpts +---@field original_revision string +---@field head_revision string +---@field old_file_name string +---@field file_name string + +---Returns true if the file differs in two revisions (handles renames) +---@param opts FileDiffersInRevisionsOpts +---@return boolean +M.file_differs_in_revisions = function(opts) + local result = run_system({ "git", "diff", "-M", opts.original_revision, opts.head_revision, "--", opts.old_file_name, opts.file_name }) + return result ~= "" +end + return M From f056e0c842ce16214536f321d2f539fc3b71d0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 3 Jun 2025 11:39:04 +0200 Subject: [PATCH 13/78] refactor: simplify imply_local usage --- lua/gitlab/actions/suggestion.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 6a970691..3972593a 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -18,7 +18,7 @@ local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_no local set_buffer_lines = function(bufnr, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - if M.local_implied then + if M.imply_local then vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) @@ -273,30 +273,28 @@ M.show_preview = function(opts) old_file_name = root_node.old_file_name, file_name = root_node.file_name }) + M.imply_local = false if not is_new_sha then - M.local_implied = false u.notify( string.format("Comment on unchanged text. Using target-branch version of `%s`", original_file_name), vim.log.levels.WARNING ) elseif head_differs_from_original then - M.local_implied = false u.notify( string.format("File changed since comment created. Using feature-branch version of `%s`", original_file_name), vim.log.levels.WARNING ) elseif is_modified(original_file_name) then - M.local_implied = false u.notify( string.format("File has unsaved or uncommited changes. Using feature-branch version for `%s`", original_file_name), vim.log.levels.WARNING ) else - M.local_implied = true + M.imply_local = true end -- Create the suggestion buffer and show a diff with the original version - if M.local_implied then + if M.imply_local then vim.api.nvim_cmd({ cmd = "vsplit", args = { original_file_name } }, {}) else local sug_file_name = get_temp_file_name("SUGGESTION", root_node._id, root_node.file_name) From bc3ee7f79554b3288677fa75d56f2f0ed8ad847c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 3 Jun 2025 18:40:49 +0200 Subject: [PATCH 14/78] docs: update docs --- lua/gitlab/actions/suggestion.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 3972593a..a7283535 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -1,5 +1,5 @@ ---- This module is responsible for previewing changes suggested in comments. ---- The data required to make the API calls are drawn from the discussion nodes. +---This module is responsible for previewing changes suggested in comments. +---The data required to make the API calls are drawn from the discussion nodes. local common = require("gitlab.actions.common") local git = require("gitlab.git") @@ -116,7 +116,7 @@ end ---@field lines string[] The text of the suggesion ---@field full_text string[] The full text of the file with the suggesion applied ---- Create the suggestion list from the note text +---Create the suggestion list from the note text ---@return Suggestion[] local get_suggestions = function(note_lines) local suggestions = {} @@ -146,7 +146,7 @@ local get_suggestions = function(note_lines) return suggestions end ---- Create diagnostics data from suggesions +---Create diagnostics data from suggesions ---@param suggestions Suggestion[] local create_diagnostics = function(suggestions) local diagnostics_data = {} @@ -173,7 +173,7 @@ local is_modified = function(file_name) return false end ---- Update suggestions with the changes applied to the original text +---Update suggestions with the changes applied to the original text ---@param suggestions Suggestion[] ---@param end_line_number integer The last number of the comment range ---@param original_lines string[] Array of original lines From b586f1b7c951bf109da2464f3585bdeb7012e0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 3 Jun 2025 18:42:13 +0200 Subject: [PATCH 15/78] feat: enable updating suggestion comments from the preview --- lua/gitlab/actions/suggestion.lua | 85 +++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index a7283535..a2ff17d9 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -164,6 +164,18 @@ local create_diagnostics = function(suggestions) return diagnostics_data end +---Show diagnostics for suggestions (enables using built-in navigation) +---@param suggestions Suggestion[] The list of suggestions for which diagnostics should be created. +---@param note_buf integer The number of the note buffer +local refresh_diagnostics = function(suggestions, note_buf) + local diagnostics_data = create_diagnostics(suggestions) + vim.diagnostic.reset(suggestion_namespace, note_buf) + vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) +end + +---Return true if the file has uncommitted or unsaved changes. +---@param file_name string Name of file to check. +---@return boolean local is_modified = function(file_name) local has_changes = git.has_changes(file_name) local bufnr = vim.fn.bufnr(file_name, true) @@ -198,14 +210,15 @@ M.show_preview = function(opts) return end - -- If preview is already open for given note, go to the tab with a warning. - local note_bufname = string.format("gitlab://NOTE/%s", root_node._id) - local tabnr = get_tabnr_for_buf(note_bufname) - if tabnr ~= nil then - vim.api.nvim_set_current_tabpage(tabnr) - u.notify("Previously created preview can be outdated", vim.log.levels.WARN) - return - end + -- -- If preview is already open for given note, go to the tab with a warning. + -- -- TODO: fix checking that note is already being edited. + -- local note_bufname = string.format("gitlab://NOTE/%s", root_node._id) + -- local tabnr = get_tabnr_for_buf(note_bufname) + -- if tabnr ~= nil then + -- vim.api.nvim_set_current_tabpage(tabnr) + -- u.notify("Previously created preview can be outdated", vim.log.levels.WARN) + -- return + -- end -- Return early when there're no suggestions. local note_lines = common.get_note_lines(opts.tree) @@ -310,15 +323,15 @@ M.show_preview = function(opts) vim.cmd("1,2windo diffthis") -- Create the note window - local note_buf = vim.api.nvim_create_buf(false, true) + local note_buf = vim.api.nvim_create_buf(false, false) + local note_bufname = vim.fn.tempname() vim.api.nvim_buf_set_name(note_buf, note_bufname) vim.api.nvim_cmd({ cmd = "vnew", args = { note_bufname } }, {}) vim.api.nvim_buf_set_lines(note_buf, 0, -1, false, note_lines) vim.bo.bufhidden = "wipe" vim.bo.buflisted = false - vim.bo.buftype = "nofile" vim.bo.filetype = "markdown" - vim.bo.modifiable = false + vim.bo.modified = false -- Focus the note window local note_winid = vim.fn.win_getid(3) @@ -337,7 +350,7 @@ M.show_preview = function(opts) local suggestion = List.new(suggestions):find(function(sug) return current_line <= sug.note_end_linenr end) - if suggestion ~= last_suggestion then + if suggestion and suggestion ~= last_suggestion then set_buffer_lines(suggestion_buf, suggestion.full_text) last_line = current_line last_suggestion = suggestion @@ -347,9 +360,51 @@ M.show_preview = function(opts) end }) - -- Show diagnostics for suggestions (enables using built-in navigation) - local diagnostics_data = create_diagnostics(suggestions) - vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) + -- Create autocommand to update suggestions list based on the note buffer content. + vim.api.nvim_create_autocmd({ "BufWritePost" }, { + buffer = note_buf, + callback = function() + local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) + suggestions = get_suggestions(updated_note_lines) + add_full_text_to_suggestions(suggestions, end_line_number, original_lines) + vim.api.nvim_exec_autocmds('CursorMoved', { buffer = note_buf }) + refresh_diagnostics(suggestions, note_buf) + end + }) + + -- Set keymap for posting updated note buffer to the server. + vim.keymap.set("n", "ZZ", function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + local text = u.get_buffer_text(note_buf) + local root_id = tostring(root_node.id) + + local current_node = opts.tree:get_node() + local note_node = common.get_note_node(opts.tree, current_node) + if note_node == nil then + u.notify("Couldn't get note node", vim.log.levels.ERROR) + return + end + local note_id = tonumber(note_node.root_note_id or note_node.id) + + if root_node.is_draft then + require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false)(text) + else + require("gitlab.actions.comment").confirm_edit_comment(root_id, note_id, false)(text) + end + + + if suggestion_buf ~= nil then + if vim.api.nvim_buf_is_valid(suggestion_buf) then + vim.api.nvim_set_option_value("modifiable", true, { buf = suggestion_buf }) + set_buffer_lines(suggestion_buf, original_lines) + end + end + vim.cmd.tabclose() + end, { buffer = note_buf, desc = "Send the suggestion note to the server." }) + + refresh_diagnostics(suggestions, note_buf) -- Show the discussion heading as virtual text local mark_opts = { virt_lines = { { { opts.node.text, "WarningMsg" } } }, virt_lines_above = true } From 478baa45ffcaa3615ad37c26ae91b46b283656a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 4 Jun 2025 00:34:48 +0200 Subject: [PATCH 16/78] refactor: move more keymap definitions to set_keymaps function --- lua/gitlab/actions/suggestion.lua | 88 ++++++++++++++----------------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index a2ff17d9..d65e4e64 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -16,7 +16,13 @@ vim.fn.sign_define("GitlabSuggestion", { local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note") +---Reset the contents of the suggestion buffer +---@param bufnr integer +---@param lines string[] local set_buffer_lines = function(bufnr, lines) + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) if M.imply_local then vim.api.nvim_buf_call(bufnr, function() @@ -25,24 +31,42 @@ local set_buffer_lines = function(bufnr, lines) end end -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines) +---Set keymaps for the suggestion tab buffers +---@param note_buf integer Number of the note buffer +---@param original_buf integer Number of the buffer with the original contents of the file +---@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch) +---@param original_lines string[] The list of lines in the original (commented on) version of the file +---@param root_node NuiTree.Node The root node of the comment in the discussion tree +---@param tree NuiTree The discussion tree instance +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, tree) + local keymaps = require("gitlab.state").settings.keymaps + + -- Reset suggestion buffer to original state and close preview tab for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do - vim.keymap.set("n", "q", function() + vim.keymap.set("n", keymaps.popup.discard_changes, function() + set_buffer_lines(suggestion_buf, original_lines) vim.cmd.tabclose() - if original_buf ~= nil then - if vim.api.nvim_buf_is_valid(original_buf) then - vim.cmd.bwipeout(original_buf) - end - end - if suggestion_buf ~= nil then - if vim.api.nvim_buf_is_valid(suggestion_buf) then - vim.api.nvim_set_option_value("modifiable", true, { buf = suggestion_buf }) - set_buffer_lines(suggestion_buf, original_lines) - end - end - -- TODO: restore suggestion buffer if it's HEAD! - end, { buffer = bufnr, desc = "Close suggestion preview tab" }) + end, { buffer = bufnr, desc = "Close preview tab discarding changes" }) end + + -- Post updated suggestion note buffer to the server. + vim.keymap.set("n", keymaps.popup.perform_action, function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + local note_node = common.get_note_node(tree, tree:get_node()) + if note_node == nil then + u.notify("Couldn't get note node", vim.log.levels.ERROR) + return + end + local note_id = note_node.is_root and note_node.root_note_id or note_node.id + local edit_action = root_node.is_draft + and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) + or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) + edit_action(u.get_buffer_text(note_buf)) + set_buffer_lines(suggestion_buf, original_lines) + vim.cmd.tabclose() + end, { buffer = note_buf, desc = "Update suggestion note on Gitlab" }) end local replace_range = function(full_text, start_idx, end_idx, new_lines) @@ -337,7 +361,7 @@ M.show_preview = function(opts) local note_winid = vim.fn.win_getid(3) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, opts.tree) -- Create autocommand for showing the active suggestion buffer in window 2 local last_line = suggestions[1].note_start_linenr @@ -372,38 +396,6 @@ M.show_preview = function(opts) end }) - -- Set keymap for posting updated note buffer to the server. - vim.keymap.set("n", "ZZ", function() - vim.api.nvim_buf_call(note_buf, function() - vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) - end) - local text = u.get_buffer_text(note_buf) - local root_id = tostring(root_node.id) - - local current_node = opts.tree:get_node() - local note_node = common.get_note_node(opts.tree, current_node) - if note_node == nil then - u.notify("Couldn't get note node", vim.log.levels.ERROR) - return - end - local note_id = tonumber(note_node.root_note_id or note_node.id) - - if root_node.is_draft then - require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false)(text) - else - require("gitlab.actions.comment").confirm_edit_comment(root_id, note_id, false)(text) - end - - - if suggestion_buf ~= nil then - if vim.api.nvim_buf_is_valid(suggestion_buf) then - vim.api.nvim_set_option_value("modifiable", true, { buf = suggestion_buf }) - set_buffer_lines(suggestion_buf, original_lines) - end - end - vim.cmd.tabclose() - end, { buffer = note_buf, desc = "Send the suggestion note to the server." }) - refresh_diagnostics(suggestions, note_buf) -- Show the discussion heading as virtual text From 844fffa7b22cf0cb73549c19823608982d12ec32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 4 Jun 2025 00:35:58 +0200 Subject: [PATCH 17/78] style: format file --- lua/gitlab/actions/suggestion.lua | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index d65e4e64..040394ea 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -123,7 +123,7 @@ local get_tabnr_for_buf = function(bufname) return nil end for _, tabnr in ipairs(vim.api.nvim_list_tabpages()) do - for _, winnr in ipairs( vim.api.nvim_tabpage_list_wins(tabnr)) do + for _, winnr in ipairs(vim.api.nvim_tabpage_list_wins(tabnr)) do if vim.api.nvim_win_get_buf(winnr) == bufnr then return tabnr end @@ -181,7 +181,7 @@ local create_diagnostics = function(suggestions) severity = vim.diagnostic.severity.INFO, source = "gitlab", code = "gitlab.nvim", - lnum = suggestion.note_start_linenr - 1 + lnum = suggestion.note_start_linenr - 1, } table.insert(diagnostics_data, diagnostic) end @@ -269,8 +269,10 @@ M.show_preview = function(opts) original_file_name = root_node.old_file_name end if not git.revision_exists(revision) then - u.notify(string.format("Revision `%s` for which the comment was made does not exist", revision), - vim.log.levels.WARN) + u.notify( + string.format("Revision `%s` for which the comment was made does not exist", revision), + vim.log.levels.WARN + ) return end @@ -280,7 +282,11 @@ M.show_preview = function(opts) -- original revision could not been found. if original_head_text == nil then u.notify( - string.format("File `%s` doesn't contain any text in revision `%s` for which comment was made", original_file_name, revision), + string.format( + "File `%s` doesn't contain any text in revision `%s` for which comment was made", + original_file_name, + revision + ), vim.log.levels.WARN ) return @@ -301,14 +307,14 @@ M.show_preview = function(opts) vim.bo.buftype = "nofile" vim.bo.modifiable = false vim.cmd.filetype("detect") - local buf_filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 }) + local buf_filetype = vim.api.nvim_get_option_value("filetype", { buf = 0 }) -- Decide if local file should be used to show suggestion preview local head_differs_from_original = git.file_differs_in_revisions({ original_revision = revision, head_revision = "HEAD", old_file_name = root_node.old_file_name, - file_name = root_node.file_name + file_name = root_node.file_name, }) M.imply_local = false if not is_new_sha then @@ -369,7 +375,7 @@ M.show_preview = function(opts) vim.api.nvim_create_autocmd({ "CursorMoved" }, { buffer = note_buf, callback = function() - local current_line = vim.fn.line('.') + local current_line = vim.fn.line(".") if current_line ~= last_line then local suggestion = List.new(suggestions):find(function(sug) return current_line <= sug.note_end_linenr @@ -381,7 +387,7 @@ M.show_preview = function(opts) refresh_signs(suggestion, note_buf) end end - end + end, }) -- Create autocommand to update suggestions list based on the note buffer content. @@ -391,9 +397,9 @@ M.show_preview = function(opts) local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) suggestions = get_suggestions(updated_note_lines) add_full_text_to_suggestions(suggestions, end_line_number, original_lines) - vim.api.nvim_exec_autocmds('CursorMoved', { buffer = note_buf }) + vim.api.nvim_exec_autocmds("CursorMoved", { buffer = note_buf }) refresh_diagnostics(suggestions, note_buf) - end + end, }) refresh_diagnostics(suggestions, note_buf) From 01dcfc0a0ddd655327380d1c4feca9318a905edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 4 Jun 2025 10:32:13 +0200 Subject: [PATCH 18/78] refactor: create autocommands in a separate function --- lua/gitlab/actions/suggestion.lua | 77 +++++++++++++++++-------------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 040394ea..d84a0878 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -221,6 +221,48 @@ local add_full_text_to_suggestions = function(suggestions, end_line_number, orig end end +---Create autocommands for the note buffer +---@param note_buf integer Note buffer number +---@param suggestion_buf integer Suggestion buffer number +---@param suggestions Suggestion[] +---@param end_line_number integer The last number of the comment range +---@param original_lines string[] Array of original lines +local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) + -- Create autocommand for showing the active suggestion buffer in window 2 + local last_line = suggestions[1].note_start_linenr + local last_suggestion = suggestions[1] + vim.api.nvim_create_autocmd({ "CursorMoved" }, { + buffer = note_buf, + callback = function() + local current_line = vim.fn.line(".") + if current_line ~= last_line then + local suggestion = List.new(suggestions):find(function(sug) + return current_line <= sug.note_end_linenr + end) + if suggestion and suggestion ~= last_suggestion then + set_buffer_lines(suggestion_buf, suggestion.full_text) + last_line = current_line + last_suggestion = suggestion + refresh_signs(suggestion, note_buf) + end + end + end, + }) + + -- Create autocommand to update suggestions list based on the note buffer content. + vim.api.nvim_create_autocmd({ "BufWritePost" }, { + buffer = note_buf, + callback = function() + local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) + suggestions = get_suggestions(updated_note_lines) + add_full_text_to_suggestions(suggestions, end_line_number, original_lines) + last_line = 0 + vim.api.nvim_exec_autocmds("CursorMoved", { buffer = note_buf }) + refresh_diagnostics(suggestions, note_buf) + end, + }) +end + ---@class ShowPreviewOpts ---@field tree NuiTree The current discussion tree instance ---@field node NuiTreeNode The current node in the discussion tree @@ -368,41 +410,8 @@ M.show_preview = function(opts) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, opts.tree) - - -- Create autocommand for showing the active suggestion buffer in window 2 - local last_line = suggestions[1].note_start_linenr - local last_suggestion = suggestions[1] - vim.api.nvim_create_autocmd({ "CursorMoved" }, { - buffer = note_buf, - callback = function() - local current_line = vim.fn.line(".") - if current_line ~= last_line then - local suggestion = List.new(suggestions):find(function(sug) - return current_line <= sug.note_end_linenr - end) - if suggestion and suggestion ~= last_suggestion then - set_buffer_lines(suggestion_buf, suggestion.full_text) - last_line = current_line - last_suggestion = suggestion - refresh_signs(suggestion, note_buf) - end - end - end, - }) - - -- Create autocommand to update suggestions list based on the note buffer content. - vim.api.nvim_create_autocmd({ "BufWritePost" }, { - buffer = note_buf, - callback = function() - local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) - suggestions = get_suggestions(updated_note_lines) - add_full_text_to_suggestions(suggestions, end_line_number, original_lines) - vim.api.nvim_exec_autocmds("CursorMoved", { buffer = note_buf }) - refresh_diagnostics(suggestions, note_buf) - end, - }) - refresh_diagnostics(suggestions, note_buf) + create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) -- Show the discussion heading as virtual text local mark_opts = { virt_lines = { { { opts.node.text, "WarningMsg" } } }, virt_lines_above = true } From 9e1bdf52e76da18026e990fa71c6bddb76db9885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 4 Jun 2025 10:43:20 +0200 Subject: [PATCH 19/78] fix: make note buffer nomodified when discarding changes --- lua/gitlab/actions/suggestion.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index d84a0878..f7655de2 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -45,6 +45,7 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do vim.keymap.set("n", keymaps.popup.discard_changes, function() set_buffer_lines(suggestion_buf, original_lines) + vim.bo[note_buf].modified = false vim.cmd.tabclose() end, { buffer = bufnr, desc = "Close preview tab discarding changes" }) end From 69b884abd1396d31043152fa6d1d5af0691a16a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 5 Jun 2025 17:44:15 +0200 Subject: [PATCH 20/78] fix: validate buffer number before accessing it --- lua/gitlab/actions/suggestion.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index f7655de2..1840adb9 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -45,7 +45,9 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do vim.keymap.set("n", keymaps.popup.discard_changes, function() set_buffer_lines(suggestion_buf, original_lines) - vim.bo[note_buf].modified = false + if vim.api.nvim_buf_is_valid(note_buf) then + vim.bo[note_buf].modified = false + end vim.cmd.tabclose() end, { buffer = bufnr, desc = "Close preview tab discarding changes" }) end From c14ea7e36245dc5963c202e0efec4dec8e08d51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 5 Jun 2025 17:45:40 +0200 Subject: [PATCH 21/78] fix: split horizontally on narrow screen --- lua/gitlab/actions/suggestion.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 1840adb9..078783c6 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -382,12 +382,13 @@ M.show_preview = function(opts) end -- Create the suggestion buffer and show a diff with the original version + local split_cmd = vim.o.columns > 240 and "vsplit" or "split" if M.imply_local then - vim.api.nvim_cmd({ cmd = "vsplit", args = { original_file_name } }, {}) + vim.api.nvim_cmd({ cmd = split_cmd, args = { original_file_name } }, {}) else local sug_file_name = get_temp_file_name("SUGGESTION", root_node._id, root_node.file_name) vim.fn.mkdir(vim.fn.fnamemodify(sug_file_name, ":h"), "p") - vim.api.nvim_cmd({ cmd = "vnew", args = { sug_file_name } }, {}) + vim.api.nvim_cmd({ cmd = split_cmd, args = { sug_file_name } }, {}) vim.bo.bufhidden = "wipe" vim.bo.buflisted = false vim.bo.buftype = "nofile" @@ -401,7 +402,7 @@ M.show_preview = function(opts) local note_buf = vim.api.nvim_create_buf(false, false) local note_bufname = vim.fn.tempname() vim.api.nvim_buf_set_name(note_buf, note_bufname) - vim.api.nvim_cmd({ cmd = "vnew", args = { note_bufname } }, {}) + vim.api.nvim_cmd({ cmd = "vnew", mods = { split = "botright" }, args = { note_bufname } }, {}) vim.api.nvim_buf_set_lines(note_buf, 0, -1, false, note_lines) vim.bo.bufhidden = "wipe" vim.bo.buflisted = false From 3d4162438b61983fdc0c368f4e86dff8a0dffd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 5 Jun 2025 17:47:03 +0200 Subject: [PATCH 22/78] fix: move virtual lines left (and up) --- lua/gitlab/actions/suggestion.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 078783c6..54d91426 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -418,7 +418,11 @@ M.show_preview = function(opts) create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) -- Show the discussion heading as virtual text - local mark_opts = { virt_lines = { { { opts.node.text, "WarningMsg" } } }, virt_lines_above = true } + local mark_opts = { + virt_lines = { { { opts.node.text, "WarningMsg" } } }, + virt_lines_above = true, + right_gravity = false, + } vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) -- An extmark above the first line is not visible by default, so let's scroll the window: vim.cmd("normal! ") From f02c60812fd874dfb8e4944b70370e179c5bcfec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 5 Jun 2025 22:51:08 +0200 Subject: [PATCH 23/78] refactor: pass only tree to show_preview() --- lua/gitlab/actions/discussions/init.lua | 5 +-- lua/gitlab/actions/suggestion.lua | 56 ++++++++++++------------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 575278f4..9c3f0ced 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -248,10 +248,7 @@ end -- Preview the suggestion(s) in the current discussion tree node M.preview_suggestion = function(tree) local suggestion = require("gitlab.actions.suggestion") - suggestion.show_preview({ - node = tree:get_node(), - tree = tree, - }) + suggestion.show_preview(tree) end -- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 54d91426..d9320812 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -36,9 +36,9 @@ end ---@param original_buf integer Number of the buffer with the original contents of the file ---@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch) ---@param original_lines string[] The list of lines in the original (commented on) version of the file ----@param root_node NuiTree.Node The root node of the comment in the discussion tree ----@param tree NuiTree The discussion tree instance -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, tree) +---@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment) +---@param note_node NuiTreeNode The first node of a comment or reply +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node) local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab @@ -57,11 +57,6 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.api.nvim_buf_call(note_buf, function() vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) - local note_node = common.get_note_node(tree, tree:get_node()) - if note_node == nil then - u.notify("Couldn't get note node", vim.log.levels.ERROR) - return - end local note_id = note_node.is_root and note_node.root_note_id or note_node.id local edit_action = root_node.is_draft and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) @@ -266,16 +261,29 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ }) end ----@class ShowPreviewOpts ----@field tree NuiTree The current discussion tree instance ----@field node NuiTreeNode The current node in the discussion tree +---Show the note header as virtual text +---@param text string The text to show in the header +---@param note_buf integer The number of the note buffer +local add_window_header = function(text, note_buf) + local mark_opts = { + virt_lines = { { { text, "WarningMsg" } } }, + virt_lines_above = true, + right_gravity = false, + } + vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) + -- An extmark above the first line is not visible by default, so let's scroll the window: + vim.cmd("normal! ") + -- TODO: Add virtual text (or winbar?) to show the diffed revision of the ORIGINAL. +end ---Get suggestions from the current note and preview them in a new tab ----@param opts ShowPreviewOpts -M.show_preview = function(opts) - local root_node = common.get_root_node(opts.tree, opts.node) - if root_node == nil then - u.notify("Couldn't get root node", vim.log.levels.ERROR) +---@param tree NuiTree The current discussion tree instance +M.show_preview = function(tree) + local current_node = tree:get_node() + local root_node = common.get_root_node(tree, current_node) + local note_node = common.get_note_node(tree, current_node) + if root_node == nil or note_node == nil then + u.notify("Couldn't get root node or note node", vim.log.levels.ERROR) return end @@ -290,7 +298,7 @@ M.show_preview = function(opts) -- end -- Return early when there're no suggestions. - local note_lines = common.get_note_lines(opts.tree) + local note_lines = common.get_note_lines(tree) local suggestions = get_suggestions(note_lines) if #suggestions == 0 then u.notify("Note doesn't contain any suggestion.", vim.log.levels.WARN) @@ -413,20 +421,10 @@ M.show_preview = function(opts) local note_winid = vim.fn.win_getid(3) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, opts.tree) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node) refresh_diagnostics(suggestions, note_buf) create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) - - -- Show the discussion heading as virtual text - local mark_opts = { - virt_lines = { { { opts.node.text, "WarningMsg" } } }, - virt_lines_above = true, - right_gravity = false, - } - vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) - -- An extmark above the first line is not visible by default, so let's scroll the window: - vim.cmd("normal! ") - -- TODO: Add virtual text (or winbar?) to show the diffed revision of the ORIGINAL. + add_window_header(note_node.text, note_buf) end return M From 02e352268ee8e34d13f66c2693ed87267f16b8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 6 Jun 2025 09:28:09 +0200 Subject: [PATCH 24/78] fix: check if suggestion preview already exists for given note --- lua/gitlab/actions/suggestion.lua | 73 +++++++++++++++++-------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index d9320812..1db2e0ce 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -67,7 +67,13 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li end, { buffer = note_buf, desc = "Update suggestion note on Gitlab" }) end -local replace_range = function(full_text, start_idx, end_idx, new_lines) +---Replace a range of items in a list with items fromanother list +---@param full_text string[] The full list of lines +---@param start_idx integer The beginning of the range to be replaced +---@param end_idx integer The end of the range to be replaced +---@param new_lines string[] The lines of text that should replace the original range +---@return string[] The new list of lines after replacing +local replace_line_range = function(full_text, start_idx, end_idx, new_lines) -- Copy the original text local new_tbl = {} for _, val in ipairs(full_text) do @@ -84,9 +90,11 @@ local replace_range = function(full_text, start_idx, end_idx, new_lines) return new_tbl end +---Refresh the signs in the note buffer +---@param suggestion Suggestion The data for an individual suggestion. +---@param note_buf integer The number of the note buffer local refresh_signs = function(suggestion, note_buf) vim.fn.sign_unplace("gitlab.suggestion") - vim.fn.sign_place( suggestion.note_start_linenr, "gitlab.suggestion", @@ -103,23 +111,22 @@ local refresh_signs = function(suggestion, note_buf) ) end +---Create the name for a temporary file. +---@param revision string The revision of the file for which the comment was made. +---@param node_id any The id of the note node containing the suggestion. +---@param file_name string The name of the commented file. +---@return string buf_name The full name of the new buffer. +---@return integer bufnr The number of the buffer associated with the new name (-1 if buffer doesn't exist). local get_temp_file_name = function(revision, node_id, file_name) - local buf_name = string.format("gitlab://%s/%s/%s", revision, node_id, file_name) - local existing_bufnr = vim.fn.bufnr(buf_name) - if existing_bufnr > -1 and vim.fn.bufexists(existing_bufnr) then - vim.cmd.bwipeout(existing_bufnr) - end - return buf_name + local buf_name = string.format("gitlab::%s/%s::%s", revision, node_id, file_name) + local bufnr = vim.fn.bufnr(buf_name) + return buf_name, bufnr end ----Check if buffer already exists and return the number of the tab it's open in ----@param bufname string The full name of the buffer to check. ----@return number|nil tabnr The tabpage number if buffer is already open or nil. -local get_tabnr_for_buf = function(bufname) - local bufnr = vim.fn.bufnr(bufname) - if bufnr == -1 then - return nil - end +---Check if buffer already exists and return the number of the tab it's open in. +---@param bufnr integer The buffer number to check. +---@return number|nil tabnr The tabpage number if buffer is already open, or nil. +local get_tabnr_for_buf = function(bufnr) for _, tabnr in ipairs(vim.api.nvim_list_tabpages()) do for _, winnr in ipairs(vim.api.nvim_tabpage_list_wins(tabnr)) do if vim.api.nvim_win_get_buf(winnr) == bufnr then @@ -215,7 +222,7 @@ local add_full_text_to_suggestions = function(suggestions, end_line_number, orig for _, suggestion in ipairs(suggestions) do local start_line = end_line_number - suggestion.start_line_offset local end_line = end_line_number + suggestion.end_line_offset - suggestion.full_text = replace_range(original_lines, start_line, end_line, suggestion.lines) + suggestion.full_text = replace_line_range(original_lines, start_line, end_line, suggestion.lines) end end @@ -287,16 +294,6 @@ M.show_preview = function(tree) return end - -- -- If preview is already open for given note, go to the tab with a warning. - -- -- TODO: fix checking that note is already being edited. - -- local note_bufname = string.format("gitlab://NOTE/%s", root_node._id) - -- local tabnr = get_tabnr_for_buf(note_bufname) - -- if tabnr ~= nil then - -- vim.api.nvim_set_current_tabpage(tabnr) - -- u.notify("Previously created preview can be outdated", vim.log.levels.WARN) - -- return - -- end - -- Return early when there're no suggestions. local note_lines = common.get_note_lines(tree) local suggestions = get_suggestions(note_lines) @@ -329,6 +326,15 @@ M.show_preview = function(tree) return end + -- If preview is already open for given note, go to the tab with a warning. + local original_buf_name, original_bufnr = get_temp_file_name("ORIGINAL", note_node.id, original_file_name) + local tabnr = get_tabnr_for_buf(original_bufnr) + if tabnr ~= nil then + vim.api.nvim_set_current_tabpage(tabnr) + u.notify("Previously created preview can be outdated", vim.log.levels.WARN) + return + end + -- Get the text on which the suggestion was created local original_head_text = git.get_file_revision({ file_name = original_file_name, revision = revision }) -- If the original revision doesn't contain the file, the branch was possibly rebased, and the @@ -350,11 +356,10 @@ M.show_preview = function(tree) -- Create new tab with a temp buffer showing the original version on which the comment was -- made. - local original_buf = vim.api.nvim_create_buf(false, true) + vim.fn.mkdir(vim.fn.fnamemodify(original_buf_name, ":h"), "p") + vim.api.nvim_cmd({ cmd = "tabnew", args = { original_buf_name } }, {}) + local original_buf = vim.api.nvim_get_current_buf() vim.api.nvim_buf_set_lines(original_buf, 0, -1, false, original_lines) - local buf_name = get_temp_file_name("ORIGINAL", root_node._id, root_node.file_name) - vim.api.nvim_buf_set_name(original_buf, buf_name) - vim.api.nvim_cmd({ cmd = "tabnew", args = { buf_name } }, {}) vim.bo.bufhidden = "wipe" vim.bo.buflisted = false vim.bo.buftype = "nofile" @@ -394,9 +399,9 @@ M.show_preview = function(tree) if M.imply_local then vim.api.nvim_cmd({ cmd = split_cmd, args = { original_file_name } }, {}) else - local sug_file_name = get_temp_file_name("SUGGESTION", root_node._id, root_node.file_name) - vim.fn.mkdir(vim.fn.fnamemodify(sug_file_name, ":h"), "p") - vim.api.nvim_cmd({ cmd = split_cmd, args = { sug_file_name } }, {}) + local sug_buf_name = get_temp_file_name("SUGGESTION", note_node.id, root_node.file_name) + vim.fn.mkdir(vim.fn.fnamemodify(sug_buf_name, ":h"), "p") + vim.api.nvim_cmd({ cmd = split_cmd, args = { sug_buf_name } }, {}) vim.bo.bufhidden = "wipe" vim.bo.buflisted = false vim.bo.buftype = "nofile" From 078236fbcca7687c8ba3aca0ba3ca9cec18bb108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 6 Jun 2025 10:36:52 +0200 Subject: [PATCH 25/78] docs: update function annotations --- lua/gitlab/actions/suggestion.lua | 91 ++++++++++++++++--------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 1db2e0ce..d0d65c3e 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -16,9 +16,9 @@ vim.fn.sign_define("GitlabSuggestion", { local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note") ----Reset the contents of the suggestion buffer ----@param bufnr integer ----@param lines string[] +---Reset the contents of the suggestion buffer. +---@param bufnr integer The number of the suggestion buffer. +---@param lines string[] Lines of text to put into the buffer. local set_buffer_lines = function(bufnr, lines) if not vim.api.nvim_buf_is_valid(bufnr) then return @@ -31,13 +31,13 @@ local set_buffer_lines = function(bufnr, lines) end end ----Set keymaps for the suggestion tab buffers ----@param note_buf integer Number of the note buffer ----@param original_buf integer Number of the buffer with the original contents of the file ----@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch) ----@param original_lines string[] The list of lines in the original (commented on) version of the file ----@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment) ----@param note_node NuiTreeNode The first node of a comment or reply +---Set keymaps for the suggestion tab buffers. +---@param note_buf integer Number of the note buffer. +---@param original_buf integer Number of the buffer with the original contents of the file. +---@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch). +---@param original_lines string[] The list of lines in the original (commented on) version of the file. +---@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment). +---@param note_node NuiTreeNode The first node of a comment or reply. local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node) local keymaps = require("gitlab.state").settings.keymaps @@ -67,12 +67,12 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li end, { buffer = note_buf, desc = "Update suggestion note on Gitlab" }) end ----Replace a range of items in a list with items fromanother list ----@param full_text string[] The full list of lines ----@param start_idx integer The beginning of the range to be replaced ----@param end_idx integer The end of the range to be replaced ----@param new_lines string[] The lines of text that should replace the original range ----@return string[] The new list of lines after replacing +---Replace a range of items in a list with items fromanother list. +---@param full_text string[] The full list of lines. +---@param start_idx integer The beginning of the range to be replaced. +---@param end_idx integer The end of the range to be replaced. +---@param new_lines string[] The lines of text that should replace the original range. +---@return string[] new_tbl The new list of lines after replacing. local replace_line_range = function(full_text, start_idx, end_idx, new_lines) -- Copy the original text local new_tbl = {} @@ -90,9 +90,9 @@ local replace_line_range = function(full_text, start_idx, end_idx, new_lines) return new_tbl end ----Refresh the signs in the note buffer +---Refresh the signs in the note buffer. ---@param suggestion Suggestion The data for an individual suggestion. ----@param note_buf integer The number of the note buffer +---@param note_buf integer The number of the note buffer. local refresh_signs = function(suggestion, note_buf) vim.fn.sign_unplace("gitlab.suggestion") vim.fn.sign_place( @@ -145,8 +145,9 @@ end ---@field lines string[] The text of the suggesion ---@field full_text string[] The full text of the file with the suggesion applied ----Create the suggestion list from the note text ----@return Suggestion[] +---Create the suggestion list from the note text. +---@param note_lines string[] The content of the comment. +---@return Suggestion[] suggestions List of suggestion data. local get_suggestions = function(note_lines) local suggestions = {} local in_suggestion = false @@ -156,7 +157,6 @@ local get_suggestions = function(note_lines) for i, line in ipairs(note_lines) do local start_quote = string.match(line, "^%s*(`+)suggestion:%-%d+%+%d+") local end_quote = string.match(line, "^%s*(`+)%s*$") - if start_quote ~= nil and not in_suggestion then quote = start_quote in_suggestion = true @@ -172,11 +172,13 @@ local get_suggestions = function(note_lines) table.insert(suggestion.lines, line) end end + return suggestions end ----Create diagnostics data from suggesions ----@param suggestions Suggestion[] +---Create diagnostics data from suggesions. +---@param suggestions Suggestion[] The list of suggestions data for the current note. +---@return vim.Diagnostic[] diagnostics_data List of diagnostic data for vim.diagnostic.set. local create_diagnostics = function(suggestions) local diagnostics_data = {} for _, suggestion in ipairs(suggestions) do @@ -193,7 +195,7 @@ local create_diagnostics = function(suggestions) return diagnostics_data end ----Show diagnostics for suggestions (enables using built-in navigation) +---Show diagnostics for suggestions (enables using built-in navigation with `]d` and `[d`). ---@param suggestions Suggestion[] The list of suggestions for which diagnostics should be created. ---@param note_buf integer The number of the note buffer local refresh_diagnostics = function(suggestions, note_buf) @@ -214,10 +216,10 @@ local is_modified = function(file_name) return false end ----Update suggestions with the changes applied to the original text ----@param suggestions Suggestion[] ----@param end_line_number integer The last number of the comment range ----@param original_lines string[] Array of original lines +---Update suggestions with the changes applied to the original text. +---@param suggestions Suggestion[] List of existing partial suggestion data. +---@param end_line_number integer The last number of the comment range. +---@param original_lines string[] Array of original lines. local add_full_text_to_suggestions = function(suggestions, end_line_number, original_lines) for _, suggestion in ipairs(suggestions) do local start_line = end_line_number - suggestion.start_line_offset @@ -226,12 +228,12 @@ local add_full_text_to_suggestions = function(suggestions, end_line_number, orig end end ----Create autocommands for the note buffer ----@param note_buf integer Note buffer number ----@param suggestion_buf integer Suggestion buffer number ----@param suggestions Suggestion[] ----@param end_line_number integer The last number of the comment range ----@param original_lines string[] Array of original lines +---Create autocommands for the note buffer. +---@param note_buf integer Note buffer number. +---@param suggestion_buf integer Suggestion buffer number. +---@param suggestions Suggestion[] List of suggestion data. +---@param end_line_number integer The last number of the comment range. +---@param original_lines string[] Array of original lines. local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) -- Create autocommand for showing the active suggestion buffer in window 2 local last_line = suggestions[1].note_start_linenr @@ -268,9 +270,9 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ }) end ----Show the note header as virtual text ----@param text string The text to show in the header ----@param note_buf integer The number of the note buffer +---Show the note header as virtual text. +---@param text string The text to show in the header. +---@param note_buf integer The number of the note buffer. local add_window_header = function(text, note_buf) local mark_opts = { virt_lines = { { { text, "WarningMsg" } } }, @@ -280,11 +282,12 @@ local add_window_header = function(text, note_buf) vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) -- An extmark above the first line is not visible by default, so let's scroll the window: vim.cmd("normal! ") - -- TODO: Add virtual text (or winbar?) to show the diffed revision of the ORIGINAL. + -- TODO: Add virtual text (or winbar?) to show the diffed revision of the ORIGINAL. This doesn't + -- work well because of the diff scrollbind makes the extmark above line 1 disappear. end ----Get suggestions from the current note and preview them in a new tab ----@param tree NuiTree The current discussion tree instance +---Get suggestions from the current note and preview them in a new tab. +---@param tree NuiTree The current discussion tree instance. M.show_preview = function(tree) local current_node = tree:get_node() local root_node = common.get_root_node(tree, current_node) @@ -422,13 +425,15 @@ M.show_preview = function(tree) vim.bo.filetype = "markdown" vim.bo.modified = false - -- Focus the note window + -- Set up keymaps and autocommands + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node) + create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) + + -- Focus the note window on the first suggestion local note_winid = vim.fn.win_getid(3) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node) refresh_diagnostics(suggestions, note_buf) - create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) add_window_header(note_node.text, note_buf) end From db0ef3941354dba640bd4491a6bc7122d40cffa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 6 Jun 2025 15:34:48 +0200 Subject: [PATCH 26/78] refactor: add full text to suggestions --- lua/gitlab/actions/suggestion.lua | 41 +++++++++++++------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index d0d65c3e..b937e41b 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -147,8 +147,10 @@ end ---Create the suggestion list from the note text. ---@param note_lines string[] The content of the comment. +---@param end_line_number integer The last number of the comment range. +---@param original_lines string[] Array of original lines. ---@return Suggestion[] suggestions List of suggestion data. -local get_suggestions = function(note_lines) +local get_suggestions = function(note_lines, end_line_number, original_lines) local suggestions = {} local in_suggestion = false local suggestion = {} @@ -165,6 +167,12 @@ local get_suggestions = function(note_lines) suggestion.lines = {} elseif end_quote and end_quote == quote then suggestion.note_end_linenr = i + + -- Add the full text with the changes applied to the original text. + local start_line = end_line_number - suggestion.start_line_offset + local end_line = end_line_number + suggestion.end_line_offset + suggestion.full_text = replace_line_range(original_lines, start_line, end_line, suggestion.lines) + table.insert(suggestions, suggestion) in_suggestion = false suggestion = {} @@ -216,18 +224,6 @@ local is_modified = function(file_name) return false end ----Update suggestions with the changes applied to the original text. ----@param suggestions Suggestion[] List of existing partial suggestion data. ----@param end_line_number integer The last number of the comment range. ----@param original_lines string[] Array of original lines. -local add_full_text_to_suggestions = function(suggestions, end_line_number, original_lines) - for _, suggestion in ipairs(suggestions) do - local start_line = end_line_number - suggestion.start_line_offset - local end_line = end_line_number + suggestion.end_line_offset - suggestion.full_text = replace_line_range(original_lines, start_line, end_line, suggestion.lines) - end -end - ---Create autocommands for the note buffer. ---@param note_buf integer Note buffer number. ---@param suggestion_buf integer Suggestion buffer number. @@ -261,8 +257,7 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ buffer = note_buf, callback = function() local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) - suggestions = get_suggestions(updated_note_lines) - add_full_text_to_suggestions(suggestions, end_line_number, original_lines) + suggestions = get_suggestions(updated_note_lines, end_line_number, original_lines) last_line = 0 vim.api.nvim_exec_autocmds("CursorMoved", { buffer = note_buf }) refresh_diagnostics(suggestions, note_buf) @@ -297,14 +292,6 @@ M.show_preview = function(tree) return end - -- Return early when there're no suggestions. - local note_lines = common.get_note_lines(tree) - local suggestions = get_suggestions(note_lines) - if #suggestions == 0 then - u.notify("Note doesn't contain any suggestion.", vim.log.levels.WARN) - return - end - -- Hack: draft notes don't have head_sha and base_sha yet if root_node.is_draft then root_node.head_sha = "HEAD" @@ -355,7 +342,13 @@ M.show_preview = function(tree) end local original_lines = vim.fn.split(original_head_text, "\n", true) - add_full_text_to_suggestions(suggestions, end_line_number, original_lines) + -- Return early when there're no suggestions. + local note_lines = common.get_note_lines(tree) + local suggestions = get_suggestions(note_lines, end_line_number, original_lines) + if #suggestions == 0 then + u.notify("Note doesn't contain any suggestion.", vim.log.levels.WARN) + return + end -- Create new tab with a temp buffer showing the original version on which the comment was -- made. From 5734b10ff3a651ace2bc7b97528aaf6c41d923d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 6 Jun 2025 15:46:05 +0200 Subject: [PATCH 27/78] fix: make imply_local local --- lua/gitlab/actions/suggestion.lua | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index b937e41b..b98cf737 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -19,12 +19,13 @@ local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_no ---Reset the contents of the suggestion buffer. ---@param bufnr integer The number of the suggestion buffer. ---@param lines string[] Lines of text to put into the buffer. -local set_buffer_lines = function(bufnr, lines) +---@param imply_local boolean True if buffer is local file and should be written. +local set_buffer_lines = function(bufnr, lines, imply_local) if not vim.api.nvim_buf_is_valid(bufnr) then return end vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - if M.imply_local then + if imply_local then vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) @@ -38,13 +39,14 @@ end ---@param original_lines string[] The list of lines in the original (commented on) version of the file. ---@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment). ---@param note_node NuiTreeNode The first node of a comment or reply. -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node) +---@param imply_local boolean True if suggestion buffer is local file and should be written. +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local) local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do vim.keymap.set("n", keymaps.popup.discard_changes, function() - set_buffer_lines(suggestion_buf, original_lines) + set_buffer_lines(suggestion_buf, original_lines, imply_local) if vim.api.nvim_buf_is_valid(note_buf) then vim.bo[note_buf].modified = false end @@ -62,7 +64,7 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) edit_action(u.get_buffer_text(note_buf)) - set_buffer_lines(suggestion_buf, original_lines) + set_buffer_lines(suggestion_buf, original_lines, imply_local) vim.cmd.tabclose() end, { buffer = note_buf, desc = "Update suggestion note on Gitlab" }) end @@ -230,7 +232,8 @@ end ---@param suggestions Suggestion[] List of suggestion data. ---@param end_line_number integer The last number of the comment range. ---@param original_lines string[] Array of original lines. -local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) +---@param imply_local boolean True if suggestion buffer is local file and should be written. +local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local) -- Create autocommand for showing the active suggestion buffer in window 2 local last_line = suggestions[1].note_start_linenr local last_suggestion = suggestions[1] @@ -243,7 +246,7 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ return current_line <= sug.note_end_linenr end) if suggestion and suggestion ~= last_suggestion then - set_buffer_lines(suggestion_buf, suggestion.full_text) + set_buffer_lines(suggestion_buf, suggestion.full_text, imply_local) last_line = current_line last_suggestion = suggestion refresh_signs(suggestion, note_buf) @@ -370,7 +373,7 @@ M.show_preview = function(tree) old_file_name = root_node.old_file_name, file_name = root_node.file_name, }) - M.imply_local = false + local imply_local = false if not is_new_sha then u.notify( string.format("Comment on unchanged text. Using target-branch version of `%s`", original_file_name), @@ -387,12 +390,12 @@ M.show_preview = function(tree) vim.log.levels.WARNING ) else - M.imply_local = true + imply_local = true end -- Create the suggestion buffer and show a diff with the original version local split_cmd = vim.o.columns > 240 and "vsplit" or "split" - if M.imply_local then + if imply_local then vim.api.nvim_cmd({ cmd = split_cmd, args = { original_file_name } }, {}) else local sug_buf_name = get_temp_file_name("SUGGESTION", note_node.id, root_node.file_name) @@ -404,7 +407,7 @@ M.show_preview = function(tree) vim.bo.filetype = buf_filetype end local suggestion_buf = vim.api.nvim_get_current_buf() - set_buffer_lines(suggestion_buf, suggestions[1].full_text) + set_buffer_lines(suggestion_buf, suggestions[1].full_text, imply_local) vim.cmd("1,2windo diffthis") -- Create the note window @@ -419,8 +422,8 @@ M.show_preview = function(tree) vim.bo.modified = false -- Set up keymaps and autocommands - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node) - create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local) + create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local) -- Focus the note window on the first suggestion local note_winid = vim.fn.win_getid(3) From 926ae2eee1301c13c973aa803c9037c89b1c60da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 10:12:58 +0200 Subject: [PATCH 28/78] feat: edit suggestions for comments without suggestions --- lua/gitlab/actions/suggestion.lua | 41 +++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index b98cf737..e0859047 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -97,6 +97,9 @@ end ---@param note_buf integer The number of the note buffer. local refresh_signs = function(suggestion, note_buf) vim.fn.sign_unplace("gitlab.suggestion") + if suggestion.is_default then + return + end vim.fn.sign_place( suggestion.note_start_linenr, "gitlab.suggestion", @@ -146,6 +149,7 @@ end ---@field note_end_linenr number The line number in the note text where the suggesion ends ---@field lines string[] The text of the suggesion ---@field full_text string[] The full text of the file with the suggesion applied +---@field is_default boolean If true, the "suggestion" is a placeholder for comments without actual suggestions. ---Create the suggestion list from the note text. ---@param note_lines string[] The content of the comment. @@ -183,6 +187,19 @@ local get_suggestions = function(note_lines, end_line_number, original_lines) end end + if #suggestions == 0 then + suggestions = { + { + start_line_offset = 0, + end_line_offset = 0, + note_start_linenr = 1, + note_end_linenr = 1, + lines = {}, + full_text = original_lines, + is_default = true, + } + } + end return suggestions end @@ -192,15 +209,17 @@ end local create_diagnostics = function(suggestions) local diagnostics_data = {} for _, suggestion in ipairs(suggestions) do - local diagnostic = { - message = table.concat(suggestion.lines, "\n") .. "\n", - col = 0, - severity = vim.diagnostic.severity.INFO, - source = "gitlab", - code = "gitlab.nvim", - lnum = suggestion.note_start_linenr - 1, - } - table.insert(diagnostics_data, diagnostic) + if not suggestion.is_default then + local diagnostic = { + message = table.concat(suggestion.lines, "\n") .. "\n", + col = 0, + severity = vim.diagnostic.severity.INFO, + source = "gitlab", + code = "gitlab.nvim", + lnum = suggestion.note_start_linenr - 1, + } + table.insert(diagnostics_data, diagnostic) + end end return diagnostics_data end @@ -348,10 +367,6 @@ M.show_preview = function(tree) -- Return early when there're no suggestions. local note_lines = common.get_note_lines(tree) local suggestions = get_suggestions(note_lines, end_line_number, original_lines) - if #suggestions == 0 then - u.notify("Note doesn't contain any suggestion.", vim.log.levels.WARN) - return - end -- Create new tab with a temp buffer showing the original version on which the comment was -- made. From 10e14677cd0cfe5af17a509351e670a378893792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 13:05:04 +0200 Subject: [PATCH 29/78] refactor: determine imply_local in separate function --- lua/gitlab/actions/suggestion.lua | 84 +++++++++++++++++-------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index e0859047..15e401ba 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -203,6 +203,51 @@ local get_suggestions = function(note_lines, end_line_number, original_lines) return suggestions end +---Return true if the file has uncommitted or unsaved changes. +---@param file_name string Name of file to check. +---@return boolean +local is_modified = function(file_name) + local has_changes = git.has_changes(file_name) + local bufnr = vim.fn.bufnr(file_name, true) + if vim.bo[bufnr].modified or has_changes then + return true + end + return false +end + +---Decide if local file should be used to show suggestion preview +---@param revision string The revision of the file for which the comment was made. +---@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment). +---@param is_new_sha boolean True if line number refers to NEW SHA +---@param original_file_name string The name of the file on which the comment was made. +local determine_imply_local = function(revision, root_node, is_new_sha, original_file_name) + local head_differs_from_original = git.file_differs_in_revisions({ + original_revision = revision, + head_revision = "HEAD", + old_file_name = root_node.old_file_name, + file_name = root_node.file_name, + }) + if not is_new_sha then + u.notify( + string.format("Comment on unchanged text. Using target-branch version of `%s`", original_file_name), + vim.log.levels.INFO + ) + elseif head_differs_from_original then + u.notify( + string.format("File changed since comment created. Using feature-branch version of `%s`", original_file_name), + vim.log.levels.INFO + ) + elseif is_modified(original_file_name) then + u.notify( + string.format("File has unsaved or uncommited changes. Using feature-branch version for `%s`", original_file_name), + vim.log.levels.WARN + ) + else + return true + end + return false +end + ---Create diagnostics data from suggesions. ---@param suggestions Suggestion[] The list of suggestions data for the current note. ---@return vim.Diagnostic[] diagnostics_data List of diagnostic data for vim.diagnostic.set. @@ -233,18 +278,6 @@ local refresh_diagnostics = function(suggestions, note_buf) vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) end ----Return true if the file has uncommitted or unsaved changes. ----@param file_name string Name of file to check. ----@return boolean -local is_modified = function(file_name) - local has_changes = git.has_changes(file_name) - local bufnr = vim.fn.bufnr(file_name, true) - if vim.bo[bufnr].modified or has_changes then - return true - end - return false -end - ---Create autocommands for the note buffer. ---@param note_buf integer Note buffer number. ---@param suggestion_buf integer Suggestion buffer number. @@ -381,32 +414,7 @@ M.show_preview = function(tree) vim.cmd.filetype("detect") local buf_filetype = vim.api.nvim_get_option_value("filetype", { buf = 0 }) - -- Decide if local file should be used to show suggestion preview - local head_differs_from_original = git.file_differs_in_revisions({ - original_revision = revision, - head_revision = "HEAD", - old_file_name = root_node.old_file_name, - file_name = root_node.file_name, - }) - local imply_local = false - if not is_new_sha then - u.notify( - string.format("Comment on unchanged text. Using target-branch version of `%s`", original_file_name), - vim.log.levels.WARNING - ) - elseif head_differs_from_original then - u.notify( - string.format("File changed since comment created. Using feature-branch version of `%s`", original_file_name), - vim.log.levels.WARNING - ) - elseif is_modified(original_file_name) then - u.notify( - string.format("File has unsaved or uncommited changes. Using feature-branch version for `%s`", original_file_name), - vim.log.levels.WARNING - ) - else - imply_local = true - end + local imply_local = determine_imply_local(revision, root_node, is_new_sha, original_file_name) -- Create the suggestion buffer and show a diff with the original version local split_cmd = vim.o.columns > 240 and "vsplit" or "split" From 833c741b4a9d5a4035ebd0c83afdf476c3029214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 14:23:56 +0200 Subject: [PATCH 30/78] fix: prevent error when there are multiple endquotes without a corresponding start_quote --- lua/gitlab/actions/suggestion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 15e401ba..3f14a09e 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -171,7 +171,7 @@ local get_suggestions = function(note_lines, end_line_number, original_lines) suggestion.start_line_offset, suggestion.end_line_offset = string.match(line, "^%s*`+suggestion:%-(%d+)%+(%d+)") suggestion.note_start_linenr = i suggestion.lines = {} - elseif end_quote and end_quote == quote then + elseif in_suggestion and end_quote and end_quote == quote then suggestion.note_end_linenr = i -- Add the full text with the changes applied to the original text. From 33e0b02de0941c5a8fa221d6c021ef7171858b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 15:03:26 +0200 Subject: [PATCH 31/78] refactor: get original lines in seprate function --- lua/gitlab/actions/suggestion.lua | 38 +++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 3f14a09e..749b4c6c 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -128,6 +128,28 @@ local get_temp_file_name = function(revision, node_id, file_name) return buf_name, bufnr end +---Get the text on which the suggestion was created. +---@param original_file_name string The name of the file on which the comment was made. +---@param revision string The revision of the file for which the comment was made. +---@return string[]|nil original_lines The list of original lines. +local get_original_lines = function(original_file_name, revision) + local original_head_text = git.get_file_revision({ file_name = original_file_name, revision = revision }) + -- If the original revision doesn't contain the file, the branch was possibly rebased, and the + -- original revision could not been found. + if original_head_text == nil then + u.notify( + string.format( + "File `%s` doesn't contain any text in revision `%s` for which comment was made", + original_file_name, + revision + ), + vim.log.levels.WARN + ) + return + end + return vim.fn.split(original_head_text, "\n", true) +end + ---Check if buffer already exists and return the number of the tab it's open in. ---@param bufnr integer The buffer number to check. ---@return number|nil tabnr The tabpage number if buffer is already open, or nil. @@ -380,22 +402,10 @@ M.show_preview = function(tree) return end - -- Get the text on which the suggestion was created - local original_head_text = git.get_file_revision({ file_name = original_file_name, revision = revision }) - -- If the original revision doesn't contain the file, the branch was possibly rebased, and the - -- original revision could not been found. - if original_head_text == nil then - u.notify( - string.format( - "File `%s` doesn't contain any text in revision `%s` for which comment was made", - original_file_name, - revision - ), - vim.log.levels.WARN - ) + local original_lines = get_original_lines(original_file_name, revision) + if original_lines == nil then return end - local original_lines = vim.fn.split(original_head_text, "\n", true) -- Return early when there're no suggestions. local note_lines = common.get_note_lines(tree) From a661059909951bf01505dd5ddeb92cc01781bd39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 21:57:45 +0200 Subject: [PATCH 32/78] fix: show error when suggestion start is before first line of file --- lua/gitlab/actions/suggestion.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index 749b4c6c..a02c046a 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -69,13 +69,18 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li end, { buffer = note_buf, desc = "Update suggestion note on Gitlab" }) end ----Replace a range of items in a list with items fromanother list. +---Replace a range of items in a list with items from another list. ---@param full_text string[] The full list of lines. ---@param start_idx integer The beginning of the range to be replaced. ---@param end_idx integer The end of the range to be replaced. ---@param new_lines string[] The lines of text that should replace the original range. +---@param note_start_linenr number The line number in the note text where the suggesion begins ---@return string[] new_tbl The new list of lines after replacing. -local replace_line_range = function(full_text, start_idx, end_idx, new_lines) +local replace_line_range = function(full_text, start_idx, end_idx, new_lines, note_start_linenr) + if start_idx < 1 then + u.notify(string.format("Can't apply suggestion at line %d, invalid start of range.", note_start_linenr), vim.log.levels.ERROR) + return full_text + end -- Copy the original text local new_tbl = {} for _, val in ipairs(full_text) do @@ -199,7 +204,7 @@ local get_suggestions = function(note_lines, end_line_number, original_lines) -- Add the full text with the changes applied to the original text. local start_line = end_line_number - suggestion.start_line_offset local end_line = end_line_number + suggestion.end_line_offset - suggestion.full_text = replace_line_range(original_lines, start_line, end_line, suggestion.lines) + suggestion.full_text = replace_line_range(original_lines, start_line, end_line, suggestion.lines, suggestion.note_start_linenr) table.insert(suggestions, suggestion) in_suggestion = false From 5b38506cf8549302ad96bb59a1ea4d5ee2e1527e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 22:10:06 +0200 Subject: [PATCH 33/78] fix: convert string to number when editing root node --- lua/gitlab/actions/suggestion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestion.lua index a02c046a..3f7882af 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestion.lua @@ -59,7 +59,7 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.api.nvim_buf_call(note_buf, function() vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) - local note_id = note_node.is_root and note_node.root_note_id or note_node.id + local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) local edit_action = root_node.is_draft and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) From 2475ce3f41829dc8d72dd85cc1f9db3a71cf393a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 22:26:52 +0200 Subject: [PATCH 34/78] refactor rename preview suggestion to edit suggestion --- doc/gitlab.nvim.txt | 2 +- lua/gitlab/actions/discussions/init.lua | 14 +++++++------- .../actions/{suggestion.lua => suggestions.lua} | 3 ++- lua/gitlab/state.lua | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) rename lua/gitlab/actions/{suggestion.lua => suggestions.lua} (99%) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 497662d2..d0cb7d28 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -223,7 +223,7 @@ you call this function with no values the defaults will be used: toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again print_node = "p", -- Print the current node (for debugging) - preview_suggestion = "sp", -- Show suggestion preview in a new tab + edit_suggestion = "se", -- Edit suggestion comment in a new tab }, suggestion_preview = { quit = "q", -- Close the suggestion preview tab and discard changes to local files diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 9c3f0ced..3ab68e04 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -246,9 +246,9 @@ M.reply = function(tree) end -- Preview the suggestion(s) in the current discussion tree node -M.preview_suggestion = function(tree) - local suggestion = require("gitlab.actions.suggestion") - suggestion.show_preview(tree) +M.edit_suggestion = function(tree) + local suggestions = require("gitlab.actions.suggestions") + suggestions.show_preview(tree) end -- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment @@ -591,12 +591,12 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) }) end - if keymaps.discussion_tree.preview_suggestion then - vim.keymap.set("n", keymaps.discussion_tree.preview_suggestion, function() + if keymaps.discussion_tree.edit_suggestion then + vim.keymap.set("n", keymaps.discussion_tree.edit_suggestion, function() if M.is_current_node_note(tree) then - M.preview_suggestion(tree) + M.edit_suggestion(tree) end - end, { buffer = bufnr, desc = "Preview suggestion", nowait = keymaps.discussion_tree.preview_suggestion_nowait }) + end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_nowait }) end end diff --git a/lua/gitlab/actions/suggestion.lua b/lua/gitlab/actions/suggestions.lua similarity index 99% rename from lua/gitlab/actions/suggestion.lua rename to lua/gitlab/actions/suggestions.lua index 3f7882af..4a321f54 100644 --- a/lua/gitlab/actions/suggestion.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -335,7 +335,8 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ }) -- Create autocommand to update suggestions list based on the note buffer content. - vim.api.nvim_create_autocmd({ "BufWritePost" }, { + -- vim.api.nvim_create_autocmd({ "BufWritePost", "CursorHold", "CursorHoldI" }, { + vim.api.nvim_create_autocmd({ "BufWritePost", }, { buffer = note_buf, callback = function() local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 9396751f..0bd28142 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -125,7 +125,7 @@ M.settings = { toggle_unresolved_discussions = "U", refresh_data = "", print_node = "p", - preview_suggestion = "sp", + edit_suggestion = "se", }, suggestion_preview = { quit = "q", From 3c48880df2bd0abb924fe334acb883739b30f018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 22:32:34 +0200 Subject: [PATCH 35/78] fix: unify keymap setting pattern with popups --- doc/gitlab.nvim.txt | 3 +- lua/gitlab/actions/suggestions.lua | 44 ++++++++++++++++-------------- lua/gitlab/state.lua | 3 +- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index d0cb7d28..c7246135 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -226,7 +226,8 @@ you call this function with no values the defaults will be used: edit_suggestion = "se", -- Edit suggestion comment in a new tab }, suggestion_preview = { - quit = "q", -- Close the suggestion preview tab and discard changes to local files + apply_changes = "ZZ", -- Post updated suggestion comment to Gitlab, close the suggestion preview tab and discard changes to local files + discard_changes = "ZQ", -- Close the suggestion preview tab and discard changes to local files }, reviewer = { disable_all = false, -- Disable all default mappings for the reviewer windows diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 4a321f54..9ebe37e1 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -44,29 +44,33 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab - for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do - vim.keymap.set("n", keymaps.popup.discard_changes, function() - set_buffer_lines(suggestion_buf, original_lines, imply_local) - if vim.api.nvim_buf_is_valid(note_buf) then - vim.bo[note_buf].modified = false - end - vim.cmd.tabclose() - end, { buffer = bufnr, desc = "Close preview tab discarding changes" }) + if keymaps.suggestion_preview.discard_changes then + for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do + vim.keymap.set("n", keymaps.suggestion_preview.discard_changes, function() + set_buffer_lines(suggestion_buf, original_lines, imply_local) + if vim.api.nvim_buf_is_valid(note_buf) then + vim.bo[note_buf].modified = false + end + vim.cmd.tabclose() + end, { buffer = bufnr, desc = "Close preview tab discarding changes", nowait = keymaps.suggestion_preview.discard_changes_nowait }) + end end -- Post updated suggestion note buffer to the server. - vim.keymap.set("n", keymaps.popup.perform_action, function() - vim.api.nvim_buf_call(note_buf, function() - vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) - end) - local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) - local edit_action = root_node.is_draft - and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) - or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) - edit_action(u.get_buffer_text(note_buf)) - set_buffer_lines(suggestion_buf, original_lines, imply_local) - vim.cmd.tabclose() - end, { buffer = note_buf, desc = "Update suggestion note on Gitlab" }) + if keymaps.suggestion_preview.apply_changes then + vim.keymap.set("n", keymaps.suggestion_preview.apply_changes, function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) + local edit_action = root_node.is_draft + and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) + or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) + edit_action(u.get_buffer_text(note_buf)) + set_buffer_lines(suggestion_buf, original_lines, imply_local) + vim.cmd.tabclose() + end, { buffer = note_buf, desc = "Update suggestion note on Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait }) + end end ---Replace a range of items in a list with items from another list. diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 0bd28142..40c21d78 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -128,7 +128,8 @@ M.settings = { edit_suggestion = "se", }, suggestion_preview = { - quit = "q", + apply_changes = "ZZ", + discard_changes = "ZQ", }, reviewer = { disable_all = false, From 3992cef0ba53a91e44cc957d635ff840e407a297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 22:34:24 +0200 Subject: [PATCH 36/78] docs: remove outdated comment --- lua/gitlab/actions/suggestions.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 9ebe37e1..4da07bca 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -417,7 +417,6 @@ M.show_preview = function(tree) return end - -- Return early when there're no suggestions. local note_lines = common.get_note_lines(tree) local suggestions = get_suggestions(note_lines, end_line_number, original_lines) From 5d8f7928a860c511d2fa1de1bd412a966ab29de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 7 Jun 2025 23:50:43 +0200 Subject: [PATCH 37/78] feat: add keymap for pasting default suggestion --- doc/gitlab.nvim.txt | 1 + lua/gitlab/actions/suggestions.lua | 36 ++++++++++++++++++++++++++---- lua/gitlab/state.lua | 1 + 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index c7246135..d277e3f7 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -228,6 +228,7 @@ you call this function with no values the defaults will be used: suggestion_preview = { apply_changes = "ZZ", -- Post updated suggestion comment to Gitlab, close the suggestion preview tab and discard changes to local files discard_changes = "ZQ", -- Close the suggestion preview tab and discard changes to local files + paste_default_suggestion = "glS", -- Paste the default suggestion linewise after the cursor (this overrides the "Start review" keybinding only for the "Comment" buffer) }, reviewer = { disable_all = false, -- Disable all default mappings for the reviewer windows diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 4da07bca..7e56a94e 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -40,7 +40,8 @@ end ---@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment). ---@param note_node NuiTreeNode The first node of a comment or reply. ---@param imply_local boolean True if suggestion buffer is local file and should be written. -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local) +---@param default_suggestion_lines string[] The default suggestion lines with backticks. +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines) local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab @@ -71,6 +72,12 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.cmd.tabclose() end, { buffer = note_buf, desc = "Update suggestion note on Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait }) end + + if keymaps.suggestion_preview.paste_default_suggestion then + vim.keymap.set("n", keymaps.suggestion_preview.paste_default_suggestion, function() + vim.api.nvim_put(default_suggestion_lines, "l", true, false) + end, { buffer = note_buf, desc = "Paste default suggestion", nowait = keymaps.suggestion_preview.paste_default_suggestion_nowait }) + end end ---Replace a range of items in a list with items from another list. @@ -159,6 +166,26 @@ local get_original_lines = function(original_file_name, revision) return vim.fn.split(original_head_text, "\n", true) end +---Create the default suggestion lines for given comment range. +---@param original_lines string[] The list of lines in the original (commented on) version of the file. +---@param start_line_number integer The start line of the range of the comment (1-based indexing). +---@param end_line_number integer The end line of the range of the comment. +---@return string[] suggestion_lines +local get_default_suggestion = function(original_lines, start_line_number, end_line_number) + local backticks = "```" + local selected_lines = {unpack(original_lines, start_line_number, end_line_number)} + for _, line in ipairs(selected_lines) do + local match = string.match(line, "^%s*(`+)%s*$") + if match and #match >= #backticks then + backticks = match .. "`" + end + end + local suggestion_lines = {backticks .. "suggestion:-" .. (end_line_number - start_line_number) .. "+0"} + vim.list_extend(suggestion_lines, selected_lines) + table.insert(suggestion_lines, backticks) + return suggestion_lines +end + ---Check if buffer already exists and return the number of the tab it's open in. ---@param bufnr integer The buffer number to check. ---@return number|nil tabnr The tabpage number if buffer is already open, or nil. @@ -246,7 +273,7 @@ local is_modified = function(file_name) return false end ----Decide if local file should be used to show suggestion preview +---Decide if local file should be used to show suggestion preview. ---@param revision string The revision of the file for which the comment was made. ---@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment). ---@param is_new_sha boolean True if line number refers to NEW SHA @@ -386,7 +413,7 @@ M.show_preview = function(tree) end -- Decide which revision to use for the ORIGINAL text - local _, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) + local start_line_number, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) local revision, original_file_name if is_new_sha then revision = root_node.head_sha @@ -464,7 +491,8 @@ M.show_preview = function(tree) vim.bo.modified = false -- Set up keymaps and autocommands - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local) + local default_suggestion_lines = get_default_suggestion(original_lines, start_line_number, end_line_number) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines) create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local) -- Focus the note window on the first suggestion diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 40c21d78..1fb9ca05 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -130,6 +130,7 @@ M.settings = { suggestion_preview = { apply_changes = "ZZ", discard_changes = "ZQ", + paste_default_suggestion = "glS", }, reviewer = { disable_all = false, From 6fccdbf2b33097ebdcc76dd5188fd99ef000796f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sun, 8 Jun 2025 01:25:05 +0200 Subject: [PATCH 38/78] fix: update suggestions on CursorMoved and CursorMovedI --- lua/gitlab/actions/suggestions.lua | 43 +++++++++++++++++------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 7e56a94e..62540b34 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -344,36 +344,41 @@ end ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local) - -- Create autocommand for showing the active suggestion buffer in window 2 - local last_line = suggestions[1].note_start_linenr - local last_suggestion = suggestions[1] - vim.api.nvim_create_autocmd({ "CursorMoved" }, { + local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] + + ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. + local update_suggestion_buffer = function() + local current_line = vim.fn.line(".") + if current_line == last_line then + return + end + local suggestion = List.new(suggestions):find(function(sug) + return current_line <= sug.note_end_linenr + end) + if not suggestion or suggestion == last_suggestion then + return + end + set_buffer_lines(suggestion_buf, suggestion.full_text, imply_local) + last_line, last_suggestion = current_line, suggestion + refresh_signs(suggestion, note_buf) + end + + -- Create autocommand to update the Suggestion buffer when the cursor moves in the Comment buffer. + vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { buffer = note_buf, callback = function() - local current_line = vim.fn.line(".") - if current_line ~= last_line then - local suggestion = List.new(suggestions):find(function(sug) - return current_line <= sug.note_end_linenr - end) - if suggestion and suggestion ~= last_suggestion then - set_buffer_lines(suggestion_buf, suggestion.full_text, imply_local) - last_line = current_line - last_suggestion = suggestion - refresh_signs(suggestion, note_buf) - end - end + update_suggestion_buffer() end, }) -- Create autocommand to update suggestions list based on the note buffer content. - -- vim.api.nvim_create_autocmd({ "BufWritePost", "CursorHold", "CursorHoldI" }, { - vim.api.nvim_create_autocmd({ "BufWritePost", }, { + vim.api.nvim_create_autocmd({"TextChanged", "TextChangedI"}, { buffer = note_buf, callback = function() local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) suggestions = get_suggestions(updated_note_lines, end_line_number, original_lines) last_line = 0 - vim.api.nvim_exec_autocmds("CursorMoved", { buffer = note_buf }) + update_suggestion_buffer() refresh_diagnostics(suggestions, note_buf) end, }) From 17cc2754ca034892e552023b4959133ce1deb7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sun, 8 Jun 2025 01:26:17 +0200 Subject: [PATCH 39/78] docs: update comment about winbar --- lua/gitlab/actions/suggestions.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 62540b34..91fa7d8f 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -396,8 +396,8 @@ local add_window_header = function(text, note_buf) vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) -- An extmark above the first line is not visible by default, so let's scroll the window: vim.cmd("normal! ") - -- TODO: Add virtual text (or winbar?) to show the diffed revision of the ORIGINAL. This doesn't - -- work well because of the diff scrollbind makes the extmark above line 1 disappear. + -- TODO: Replace with winbar, possibly also show the diffed revision of the ORIGINAL. + -- Extmarks are not ideal for this because of scrolling issues. end ---Get suggestions from the current note and preview them in a new tab. From fba03953a238efd0f9f6fa4de3cd1e134cc5a840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sun, 8 Jun 2025 20:41:20 +0200 Subject: [PATCH 40/78] docs: add TODOs --- lua/gitlab/actions/discussions/init.lua | 1 + lua/gitlab/actions/suggestions.lua | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 3ab68e04..7facde4c 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -239,6 +239,7 @@ M.reply = function(tree) discussion_id = discussion_id, unlinked = unlinked, reply = true, + -- TODO: use discussion_node.old_file_name for comments on unchanged lines in renamed files file_name = discussion_node.file_name, }) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 91fa7d8f..b243c160 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -78,6 +78,10 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.api.nvim_put(default_suggestion_lines, "l", true, false) end, { buffer = note_buf, desc = "Paste default suggestion", nowait = keymaps.suggestion_preview.paste_default_suggestion_nowait }) end + + -- TODO: Keymap for applying changes to the Suggestion buffer. + -- TODO: Keymap for showing help on keymaps in the Comment buffer and Suggestion buffer. + -- TODO: Keymap for uploading files. end ---Replace a range of items in a list with items from another list. @@ -285,11 +289,14 @@ local determine_imply_local = function(revision, root_node, is_new_sha, original old_file_name = root_node.old_file_name, file_name = root_node.file_name, }) + -- TODO: Find out if this condition is not too restrictive. if not is_new_sha then u.notify( string.format("Comment on unchanged text. Using target-branch version of `%s`", original_file_name), vim.log.levels.INFO ) + -- TODO: Find out if this condition is not too restrictive (maybe instead check if a later comment in the thread matches "^changed this line in [version %d+ of the diff]"). + -- TODO: Rework to be able to switch between diffing against current head and original head. elseif head_differs_from_original then u.notify( string.format("File changed since comment created. Using feature-branch version of `%s`", original_file_name), @@ -400,6 +407,8 @@ local add_window_header = function(text, note_buf) -- Extmarks are not ideal for this because of scrolling issues. end +---TODO: Enable "reply_with_suggestion" from discussion tree. +---TODO: Enable "create_comment_with_suggestion" from reviewe.r ---Get suggestions from the current note and preview them in a new tab. ---@param tree NuiTree The current discussion tree instance. M.show_preview = function(tree) From 98f692fd97369b01aaa2c413e82267f131a956ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 9 Jun 2025 09:40:05 +0200 Subject: [PATCH 41/78] feat: enable replying to comments in the suggestion preview --- doc/gitlab.nvim.txt | 1 + lua/gitlab/actions/comment.lua | 6 ++--- lua/gitlab/actions/discussions/init.lua | 20 +++++++++++++++- lua/gitlab/actions/suggestions.lua | 32 ++++++++++++++++--------- lua/gitlab/state.lua | 1 + 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index d277e3f7..17040bd7 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -224,6 +224,7 @@ you call this function with no values the defaults will be used: refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again print_node = "p", -- Print the current node (for debugging) edit_suggestion = "se", -- Edit suggestion comment in a new tab + reply_with_suggestion = "sr", -- Reply to comment with a suggestion in a new tab }, suggestion_preview = { apply_changes = "ZZ", -- Post updated suggestion comment to Gitlab, close the suggestion preview tab and discard changes to local files diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index 03202a2b..bdea557e 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -26,7 +26,7 @@ local M = { ---@param text string comment text ---@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, unlinked, discussion_id) +M.confirm_create_comment = function(text, unlinked, discussion_id) if text == nil then u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR) return @@ -188,13 +188,13 @@ M.create_comment_layout = function(opts) ---Keybinding for focus on draft section popup.set_popup_keymaps(M.draft_popup, function() local text = u.get_buffer_text(M.comment_popup.bufnr) - confirm_create_comment(text, unlinked, opts.discussion_id) + M.confirm_create_comment(text, unlinked, opts.discussion_id) vim.api.nvim_set_current_win(current_win) end, miscellaneous.toggle_bool, popup.non_editable_popup_opts) ---Keybinding for focus on text section popup.set_popup_keymaps(M.comment_popup, function(text) - confirm_create_comment(text, unlinked, opts.discussion_id) + M.confirm_create_comment(text, unlinked, opts.discussion_id) vim.api.nvim_set_current_win(current_win) end, miscellaneous.attach_file, popup.editable_popup_opts) diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 7facde4c..c64b78de 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -246,7 +246,17 @@ M.reply = function(tree) layout:mount() end --- Preview the suggestion(s) in the current discussion tree node +-- Reply to the current thread in a new tab with a default suggestion based on the original text. +M.reply_with_suggestion = function(tree) + if M.is_draft_note(tree) then + u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN) + return + end + local suggestions = require("gitlab.actions.suggestions") + suggestions.show_preview(tree, true) +end + +-- Edit the current comment in a new tab with a suggestion preview. M.edit_suggestion = function(tree) local suggestions = require("gitlab.actions.suggestions") suggestions.show_preview(tree) @@ -600,6 +610,14 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_nowait }) end + if keymaps.discussion_tree.reply_with_suggestion then + vim.keymap.set("n", keymaps.discussion_tree.reply_with_suggestion, function() + if M.is_current_node_note(tree) then + M.reply_with_suggestion(tree) + end + end, { buffer = bufnr, desc = "Reply with suggestion", nowait = keymaps.discussion_tree.reply_with_suggestion_nowait }) + end + end if keymaps.discussion_tree.refresh_data then diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index b243c160..fc20c0df 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -41,7 +41,8 @@ end ---@param note_node NuiTreeNode The first node of a comment or reply. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param default_suggestion_lines string[] The default suggestion lines with backticks. -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines) +---@param is_reply boolean True if the suggestion comment is a reply to a thread. +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply) local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab @@ -63,11 +64,18 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.api.nvim_buf_call(note_buf, function() vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) + local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) - local edit_action = root_node.is_draft - and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) - or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) - edit_action(u.get_buffer_text(note_buf)) + if root_node.is_draft then + require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false)(u.get_buffer_text(note_buf)) + elseif is_reply then + -- TODO: enable creating drafts (will have to modify lua/gitlab/actions/comment.lua 35 and + -- swtich from extmark to winbar for the window header). + require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false, root_node.id) + else + require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false)(u.get_buffer_text(note_buf)) + end + set_buffer_lines(suggestion_buf, original_lines, imply_local) vim.cmd.tabclose() end, { buffer = note_buf, desc = "Update suggestion note on Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait }) @@ -394,9 +402,10 @@ end ---Show the note header as virtual text. ---@param text string The text to show in the header. ---@param note_buf integer The number of the note buffer. -local add_window_header = function(text, note_buf) +---@param is_reply boolean True if the suggestion comment is a reply to a thread. +local add_window_header = function(text, note_buf, is_reply) local mark_opts = { - virt_lines = { { { text, "WarningMsg" } } }, + virt_lines = { { { is_reply and "Reply to: " or "Edit: ", "Normal" }, { text, "WarningMsg" } } }, virt_lines_above = true, right_gravity = false, } @@ -411,7 +420,8 @@ end ---TODO: Enable "create_comment_with_suggestion" from reviewe.r ---Get suggestions from the current note and preview them in a new tab. ---@param tree NuiTree The current discussion tree instance. -M.show_preview = function(tree) +---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. +M.show_preview = function(tree, is_reply) local current_node = tree:get_node() local root_node = common.get_root_node(tree, current_node) local note_node = common.get_note_node(tree, current_node) @@ -458,7 +468,7 @@ M.show_preview = function(tree) return end - local note_lines = common.get_note_lines(tree) + local note_lines = is_reply and get_default_suggestion(original_lines, start_line_number, end_line_number) or common.get_note_lines(tree) local suggestions = get_suggestions(note_lines, end_line_number, original_lines) -- Create new tab with a temp buffer showing the original version on which the comment was @@ -506,7 +516,7 @@ M.show_preview = function(tree) -- Set up keymaps and autocommands local default_suggestion_lines = get_default_suggestion(original_lines, start_line_number, end_line_number) - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply) create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local) -- Focus the note window on the first suggestion @@ -514,7 +524,7 @@ M.show_preview = function(tree) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) refresh_diagnostics(suggestions, note_buf) - add_window_header(note_node.text, note_buf) + add_window_header(note_node.text, note_buf, is_reply) end return M diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 1fb9ca05..3ef81d94 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -126,6 +126,7 @@ M.settings = { refresh_data = "", print_node = "p", edit_suggestion = "se", + reply_with_suggestion = "sr", }, suggestion_preview = { apply_changes = "ZZ", From 21bde2d8a6df71ded88966fc7a0439799fa79ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 9 Jun 2025 10:31:40 +0200 Subject: [PATCH 42/78] feat: show draft mode in note header --- lua/gitlab/actions/comment.lua | 2 +- lua/gitlab/actions/discussions/init.lua | 4 ++ lua/gitlab/actions/suggestions.lua | 69 ++++++++++++++++++------- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index bdea557e..87cadb05 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -32,7 +32,7 @@ M.confirm_create_comment = function(text, unlinked, discussion_id) return 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)) or state.settings.discussion_tree.draft_mode -- Creating a normal reply to a discussion if discussion_id ~= nil and not is_draft then diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index c64b78de..dca8649e 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -830,6 +830,10 @@ 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 + vim.api.nvim_exec_autocmds("User", { + pattern = "GitlabDraftModeToggled", + data = { draft_mode = state.settings.discussion_tree.draft_mode } + }) end ---Toggle between sorting by "original comment" (oldest at the top) or "latest reply" (newest at the diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index fc20c0df..c158669a 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -15,6 +15,7 @@ vim.fn.sign_define("GitlabSuggestion", { }) local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note") +local note_header_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note_header") ---Reset the contents of the suggestion buffer. ---@param bufnr integer The number of the suggestion buffer. @@ -69,8 +70,6 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li if root_node.is_draft then require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false)(u.get_buffer_text(note_buf)) elseif is_reply then - -- TODO: enable creating drafts (will have to modify lua/gitlab/actions/comment.lua 35 and - -- swtich from extmark to winbar for the window header). require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false, root_node.id) else require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false)(u.get_buffer_text(note_buf)) @@ -351,6 +350,35 @@ local refresh_diagnostics = function(suggestions, note_buf) vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) end +local get_mode = function(is_reply) + if not is_reply then + return + end + if require("gitlab.state").settings.discussion_tree.draft_mode then + return { " Draft", "GitlabDraftMode" } + else + return { " Live", "GitlabLiveMode" } + end +end + +---Show the note header as virtual text. +---@param text string The text to show in the header. +---@param note_buf integer The number of the note buffer. +---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. +local add_window_header = function(text, note_buf, is_reply) + vim.api.nvim_buf_clear_namespace(note_buf, note_header_namespace, 0, -1) + local mark_opts = { + virt_lines = { { { is_reply and "Reply to: " or "Edit: ", "Normal" }, { text, "GitlabUserName" }, get_mode(is_reply) } }, + virt_lines_above = true, + right_gravity = false, + } + vim.api.nvim_buf_set_extmark(note_buf, note_header_namespace, 0, 0, mark_opts) + -- An extmark above the first line is not visible by default, so let's scroll the window: + vim.cmd("normal! ") + -- TODO: Replace with winbar, possibly also show the diffed revision of the ORIGINAL. + -- Extmarks are not ideal for this because of scrolling issues. +end + ---Create autocommands for the note buffer. ---@param note_buf integer Note buffer number. ---@param suggestion_buf integer Suggestion buffer number. @@ -358,7 +386,7 @@ end ---@param end_line_number integer The last number of the comment range. ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. -local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local) +local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_header, is_reply) local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. @@ -397,23 +425,24 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ refresh_diagnostics(suggestions, note_buf) end, }) -end ----Show the note header as virtual text. ----@param text string The text to show in the header. ----@param note_buf integer The number of the note buffer. ----@param is_reply boolean True if the suggestion comment is a reply to a thread. -local add_window_header = function(text, note_buf, is_reply) - local mark_opts = { - virt_lines = { { { is_reply and "Reply to: " or "Edit: ", "Normal" }, { text, "WarningMsg" } } }, - virt_lines_above = true, - right_gravity = false, - } - vim.api.nvim_buf_set_extmark(note_buf, suggestion_namespace, 0, 0, mark_opts) - -- An extmark above the first line is not visible by default, so let's scroll the window: - vim.cmd("normal! ") - -- TODO: Replace with winbar, possibly also show the diffed revision of the ORIGINAL. - -- Extmarks are not ideal for this because of scrolling issues. + -- Update the note buffer header when draft mode is toggled. + local group = vim.api.nvim_create_augroup("GitlabDraftModeToggled" .. note_buf, { clear = true }) + vim.api.nvim_create_autocmd("User", { + group = group, + pattern = "GitlabDraftModeToggled", + callback = function() + add_window_header(note_header, note_buf, is_reply) + end, + }) + -- Auto-delete the group when the buffer is unloaded. + vim.api.nvim_create_autocmd("BufUnload", { + buffer = note_buf, + group = group, + callback = function() + vim.api.nvim_del_augroup_by_id(group) + end, + }) end ---TODO: Enable "reply_with_suggestion" from discussion tree. @@ -517,7 +546,7 @@ M.show_preview = function(tree, is_reply) -- Set up keymaps and autocommands local default_suggestion_lines = get_default_suggestion(original_lines, start_line_number, end_line_number) set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply) - create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local) + create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_node.text, is_reply) -- Focus the note window on the first suggestion local note_winid = vim.fn.win_getid(3) From 7bb39acac49cd307772538811fc487859fe3c8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 9 Jun 2025 10:35:25 +0200 Subject: [PATCH 43/78] fix: enable updating draft replies --- lua/gitlab/actions/suggestions.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index c158669a..592afdbd 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -67,7 +67,7 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li end) local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) - if root_node.is_draft then + if note_node.is_draft then require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false)(u.get_buffer_text(note_buf)) elseif is_reply then require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false, root_node.id) From 0add185d8961b481eee8c10215cfa04d0acdeafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 10 Jun 2025 14:44:02 +0200 Subject: [PATCH 44/78] feat: add possibility to create suggestions with preview from the reviewer --- doc/gitlab.nvim.txt | 1 + lua/gitlab/actions/comment.lua | 20 +++- lua/gitlab/actions/suggestions.lua | 145 +++++++++++++++++++---------- lua/gitlab/init.lua | 1 + lua/gitlab/reviewer/init.lua | 33 +++++++ lua/gitlab/state.lua | 1 + 6 files changed, 151 insertions(+), 50 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 17040bd7..04c009eb 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -235,6 +235,7 @@ you call this function with no values the defaults will be used: disable_all = false, -- Disable all default mappings for the reviewer windows create_comment = "c", -- Create a comment for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line create_suggestion = "s", -- Create a suggestion for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line + create_suggestion_with_preview = "s", -- In a new tab create a suggestion with a diff preview for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line move_to_discussion_tree = "a", -- Jump to the comment in the discussion tree }, }, diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index 87cadb05..a7553a4b 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -21,6 +21,13 @@ local M = { comment_popup = nil, } +---Decide if the comment is a draft based on the draft popup field. +---@return boolean|nil is_draft True if the draft popup exists and the string it contains converts to `true`. +local get_draft_value_from_popup = function() + local buf_is_valid = M.draft_popup and M.draft_popup.bufnr and vim.api.nvim_buf_is_valid(M.draft_popup.bufnr) + return buf_is_valid and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr)) +end + ---Fires the API that sends the comment data to the Go server, called when you "confirm" creation ---via the M.settings.keymaps.popup.perform_action keybinding ---@param text string comment text @@ -32,7 +39,7 @@ M.confirm_create_comment = function(text, unlinked, discussion_id) return end - local is_draft = M.draft_popup and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr)) or state.settings.discussion_tree.draft_mode + local is_draft = get_draft_value_from_popup() or state.settings.discussion_tree.draft_mode -- Creating a normal reply to a discussion if discussion_id ~= nil and not is_draft then @@ -295,6 +302,17 @@ M.create_comment_suggestion = function() end) end +--- This function will create a new tab with a suggestion preview for the changed/updated line in +--- the current MR. +M.create_comment_with_suggestion = function() + M.location = Location.new() + if not M.can_create_comment(true) then + u.press_escape() + return + end + require("gitlab.actions.suggestions").show_preview(nil, false, M.location) +end + ---Returns true if it's possible to create an Inline Comment ---@param must_be_visual boolean True if current mode must be visual ---@return boolean diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 592afdbd..15c5aca3 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -38,12 +38,13 @@ end ---@param original_buf integer Number of the buffer with the original contents of the file. ---@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch). ---@param original_lines string[] The list of lines in the original (commented on) version of the file. ----@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment). ----@param note_node NuiTreeNode The first node of a comment or reply. +---@param root_node NuiTreeNode|nil The first comment in the discussion thread (can be a draft comment), nil if a new comment is created. +---@param note_node NuiTreeNode|nil The first node of a comment or reply, nil if a new comment is created. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param default_suggestion_lines string[] The default suggestion lines with backticks. ----@param is_reply boolean True if the suggestion comment is a reply to a thread. -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply) +---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. +---@param is_new_comment boolean True if the suggestion is a new comment. +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply, is_new_comment) local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab @@ -66,18 +67,24 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) - local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) - if note_node.is_draft then - require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false)(u.get_buffer_text(note_buf)) - elseif is_reply then + if note_node and root_node then + local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) + local edit_action = note_node.is_draft + and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) + or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) + edit_action(u.get_buffer_text(note_buf)) + elseif root_node and is_reply then require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false, root_node.id) + elseif is_new_comment then + require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false) else - require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false)(u.get_buffer_text(note_buf)) + -- This should not really happen. + u.notify("Cannot create comment", vim.log.levels.ERROR) end set_buffer_lines(suggestion_buf, original_lines, imply_local) vim.cmd.tabclose() - end, { buffer = note_buf, desc = "Update suggestion note on Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait }) + end, { buffer = note_buf, desc = "Post suggestion comment to Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait }) end if keymaps.suggestion_preview.paste_default_suggestion then @@ -286,15 +293,15 @@ end ---Decide if local file should be used to show suggestion preview. ---@param revision string The revision of the file for which the comment was made. ----@param root_node NuiTreeNode The first comment in the discussion thread (can be a draft comment). ---@param is_new_sha boolean True if line number refers to NEW SHA ---@param original_file_name string The name of the file on which the comment was made. -local determine_imply_local = function(revision, root_node, is_new_sha, original_file_name) +---@param new_file_name string The new name of the file on which the comment was made. +local determine_imply_local = function(revision, is_new_sha, original_file_name, new_file_name) local head_differs_from_original = git.file_differs_in_revisions({ original_revision = revision, head_revision = "HEAD", - old_file_name = root_node.old_file_name, - file_name = root_node.file_name, + old_file_name = original_file_name, + file_name = new_file_name, }) -- TODO: Find out if this condition is not too restrictive. if not is_new_sha then @@ -350,8 +357,12 @@ local refresh_diagnostics = function(suggestions, note_buf) vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) end -local get_mode = function(is_reply) - if not is_reply then +---Get the text for the draft mode +---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. +---@param is_new_comment boolean True if the suggestion is a new comment. +---@return string[]|nil +local get_mode = function(is_reply, is_new_comment) + if not is_reply and not is_new_comment then return end if require("gitlab.state").settings.discussion_tree.draft_mode then @@ -365,10 +376,11 @@ end ---@param text string The text to show in the header. ---@param note_buf integer The number of the note buffer. ---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. -local add_window_header = function(text, note_buf, is_reply) +---@param is_new_comment boolean True if the suggestion is a new comment. +local add_window_header = function(text, note_buf, is_reply, is_new_comment) vim.api.nvim_buf_clear_namespace(note_buf, note_header_namespace, 0, -1) local mark_opts = { - virt_lines = { { { is_reply and "Reply to: " or "Edit: ", "Normal" }, { text, "GitlabUserName" }, get_mode(is_reply) } }, + virt_lines = { { { is_reply and "Reply to: " or is_new_comment and "Create: " or "Edit: ", "Normal" }, { text, "GitlabUserName" }, get_mode(is_reply, is_new_comment) } }, virt_lines_above = true, right_gravity = false, } @@ -386,7 +398,9 @@ end ---@param end_line_number integer The last number of the comment range. ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. -local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_header, is_reply) +---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. +---@param is_new_comment boolean True if the suggestion is a new comment. +local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_header, is_reply, is_new_comment) local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. @@ -432,7 +446,7 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ group = group, pattern = "GitlabDraftModeToggled", callback = function() - add_window_header(note_header, note_buf, is_reply) + add_window_header(note_header, note_buf, is_reply, is_new_comment) end, }) -- Auto-delete the group when the buffer is unloaded. @@ -448,33 +462,61 @@ end ---TODO: Enable "reply_with_suggestion" from discussion tree. ---TODO: Enable "create_comment_with_suggestion" from reviewe.r ---Get suggestions from the current note and preview them in a new tab. ----@param tree NuiTree The current discussion tree instance. +---@param tree NuiTree|nil The current discussion tree instance. ---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. -M.show_preview = function(tree, is_reply) - local current_node = tree:get_node() - local root_node = common.get_root_node(tree, current_node) - local note_node = common.get_note_node(tree, current_node) - if root_node == nil or note_node == nil then - u.notify("Couldn't get root node or note node", vim.log.levels.ERROR) - return - end +---@param location Location|nil The location of the visual selection in the reviewer. +M.show_preview = function(tree, is_reply, location) + + local start_line_number, end_line_number, is_new_sha, revision + local root_node, note_node + local note_buf_header_text, comment_id + local original_file_name, new_file_name + local is_new_comment = false + -- Populate necessary variables from the discussion tree + if tree ~= nil then + local current_node = tree:get_node() + root_node = common.get_root_node(tree, current_node) + note_node = common.get_note_node(tree, current_node) + note_buf_header_text = note_node.text + comment_id = note_node.id + if root_node == nil or note_node == nil then + u.notify("Couldn't get root node or note node", vim.log.levels.ERROR) + return + end - -- Hack: draft notes don't have head_sha and base_sha yet - if root_node.is_draft then - root_node.head_sha = "HEAD" - root_node.base_sha = require("gitlab.state").INFO.target_branch - end + -- Hack: draft notes don't have head_sha and base_sha yet + if root_node.is_draft then + root_node.head_sha = "HEAD" + root_node.base_sha = require("gitlab.state").INFO.target_branch + end - -- Decide which revision to use for the ORIGINAL text - local start_line_number, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) - local revision, original_file_name - if is_new_sha then - revision = root_node.head_sha - original_file_name = root_node.file_name + -- Decide which revision to use for the ORIGINAL text + start_line_number, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) + if is_new_sha then + revision = root_node.head_sha + original_file_name = root_node.file_name + else + revision = root_node.base_sha + original_file_name = root_node.old_file_name + end + new_file_name = root_node.file_name + + -- Populate necessary variables from the reviewer location data + elseif location ~= nil then + note_buf_header_text = "New comment" + comment_id = "HEAD" + start_line_number = location.visual_range.start_line + end_line_number = location.visual_range.end_line + is_new_sha = location.reviewer_data.new_sha_focused + revision = is_new_sha and "HEAD" or require("gitlab.state").INFO.target_branch + original_file_name = location.reviewer_data.file_name or location.reviewer_data.old_file_name + new_file_name = location.reviewer_data.file_name + is_new_comment = true else - revision = root_node.base_sha - original_file_name = root_node.old_file_name + u.notify("Cannot create comment", vim.log.levels.ERROR) + return end + if not git.revision_exists(revision) then u.notify( string.format("Revision `%s` for which the comment was made does not exist", revision), @@ -484,7 +526,7 @@ M.show_preview = function(tree, is_reply) end -- If preview is already open for given note, go to the tab with a warning. - local original_buf_name, original_bufnr = get_temp_file_name("ORIGINAL", note_node.id, original_file_name) + local original_buf_name, original_bufnr = get_temp_file_name("ORIGINAL", comment_id, original_file_name) local tabnr = get_tabnr_for_buf(original_bufnr) if tabnr ~= nil then vim.api.nvim_set_current_tabpage(tabnr) @@ -497,7 +539,12 @@ M.show_preview = function(tree, is_reply) return end - local note_lines = is_reply and get_default_suggestion(original_lines, start_line_number, end_line_number) or common.get_note_lines(tree) + local note_lines + if tree and not is_reply then + note_lines = common.get_note_lines(tree) + else + note_lines = get_default_suggestion(original_lines, start_line_number, end_line_number) + end local suggestions = get_suggestions(note_lines, end_line_number, original_lines) -- Create new tab with a temp buffer showing the original version on which the comment was @@ -513,14 +560,14 @@ M.show_preview = function(tree, is_reply) vim.cmd.filetype("detect") local buf_filetype = vim.api.nvim_get_option_value("filetype", { buf = 0 }) - local imply_local = determine_imply_local(revision, root_node, is_new_sha, original_file_name) + local imply_local = determine_imply_local(revision, is_new_sha, original_file_name, new_file_name) -- Create the suggestion buffer and show a diff with the original version local split_cmd = vim.o.columns > 240 and "vsplit" or "split" if imply_local then vim.api.nvim_cmd({ cmd = split_cmd, args = { original_file_name } }, {}) else - local sug_buf_name = get_temp_file_name("SUGGESTION", note_node.id, root_node.file_name) + local sug_buf_name = get_temp_file_name("SUGGESTION", comment_id, new_file_name) vim.fn.mkdir(vim.fn.fnamemodify(sug_buf_name, ":h"), "p") vim.api.nvim_cmd({ cmd = split_cmd, args = { sug_buf_name } }, {}) vim.bo.bufhidden = "wipe" @@ -545,15 +592,15 @@ M.show_preview = function(tree, is_reply) -- Set up keymaps and autocommands local default_suggestion_lines = get_default_suggestion(original_lines, start_line_number, end_line_number) - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply) - create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_node.text, is_reply) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply, is_new_comment) + create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_buf_header_text, is_reply, is_new_comment) -- Focus the note window on the first suggestion local note_winid = vim.fn.win_getid(3) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) refresh_diagnostics(suggestions, note_buf) - add_window_header(note_node.text, note_buf, is_reply) + add_window_header(note_buf_header_text, note_buf, is_reply, is_new_comment) end return M diff --git a/lua/gitlab/init.lua b/lua/gitlab/init.lua index f17c6320..a3b0a0e4 100644 --- a/lua/gitlab/init.lua +++ b/lua/gitlab/init.lua @@ -67,6 +67,7 @@ return { create_comment = async.sequence({ info, revisions }, comment.create_comment), create_multiline_comment = async.sequence({ info, revisions }, comment.create_multiline_comment), create_comment_suggestion = async.sequence({ info, revisions }, comment.create_comment_suggestion), + create_comment_with_suggestion = async.sequence({ info, revisions }, comment.create_comment_with_suggestion), move_to_discussion_tree_from_diagnostic = async.sequence({}, discussions.move_to_discussion_tree), create_note = async.sequence({ info }, comment.create_note), create_mr = async.sequence({}, create_mr.start), diff --git a/lua/gitlab/reviewer/init.lua b/lua/gitlab/reviewer/init.lua index 46a6034f..170bf7d4 100644 --- a/lua/gitlab/reviewer/init.lua +++ b/lua/gitlab/reviewer/init.lua @@ -430,6 +430,39 @@ M.set_keymaps = function(bufnr) }) end + -- Set mappings for creating suggestions with a preview in a new tab + if keymaps.reviewer.create_suggestion_with_preview ~= false then + -- Set keymap for repeated operator keybinding + vim.keymap.set("o", keymaps.reviewer.create_suggestion_with_preview, function() + -- The "V" in "V%d$" forces linewise motion, see `:h o_V` + vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { string.format("V%d$", vim.v.count1) } }, {}) + end, { + buffer = bufnr, + desc = "Create suggestion with preview for [count] lines", + nowait = keymaps.reviewer.create_suggestion_with_preview_nowait, + }) + + -- Set operator keybinding + vim.keymap.set("n", keymaps.reviewer.create_suggestion_with_preview, function() + M.operator_count = vim.v.count + M.operator = keymaps.reviewer.create_suggestion_with_preview + execute_operatorfunc("create_comment_with_suggestion") + end, { + buffer = bufnr, + desc = "Create suggestion with preview for range of motion", + nowait = keymaps.reviewer.create_suggestion_with_preview_nowait, + }) + + -- Set visual mode keybinding + vim.keymap.set("v", keymaps.reviewer.create_suggestion_with_preview, function() + require("gitlab").create_comment_with_suggestion() + end, { + buffer = bufnr, + desc = "Create suggestion with preview for selected text", + nowait = keymaps.reviewer.create_suggestion_with_preview_nowait, + }) + end + -- Set mapping for moving to discussion tree if keymaps.reviewer.move_to_discussion_tree ~= false then vim.keymap.set("n", keymaps.reviewer.move_to_discussion_tree, function() diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 3ef81d94..d14bc0c4 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -137,6 +137,7 @@ M.settings = { disable_all = false, create_comment = "c", create_suggestion = "s", + create_suggestion_with_preview = "S", move_to_discussion_tree = "a", }, }, From 63306b5fb3c56e0ee2e573b4975397af5ee65e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 11 Jun 2025 09:27:32 +0200 Subject: [PATCH 45/78] docs: fix info about using feature branch --- lua/gitlab/actions/suggestions.lua | 8 +++++--- lua/gitlab/git.lua | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 15c5aca3..0012ad6f 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -298,8 +298,8 @@ end ---@param new_file_name string The new name of the file on which the comment was made. local determine_imply_local = function(revision, is_new_sha, original_file_name, new_file_name) local head_differs_from_original = git.file_differs_in_revisions({ - original_revision = revision, - head_revision = "HEAD", + revision_1 = revision, + revision_2 = "HEAD", old_file_name = original_file_name, file_name = new_file_name, }) @@ -312,8 +312,10 @@ local determine_imply_local = function(revision, is_new_sha, original_file_name, -- TODO: Find out if this condition is not too restrictive (maybe instead check if a later comment in the thread matches "^changed this line in [version %d+ of the diff]"). -- TODO: Rework to be able to switch between diffing against current head and original head. elseif head_differs_from_original then + -- TODO: Fix the logic of determining what version is used to create the diff, whether the local + -- file used and when this log message is shown. u.notify( - string.format("File changed since comment created. Using feature-branch version of `%s`", original_file_name), + string.format("File changed since comment created. Using version of `%s` on which comment was made", original_file_name), vim.log.levels.INFO ) elseif is_modified(original_file_name) then diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index 2ee21c0e..12433d66 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -246,8 +246,8 @@ M.revision_exists = function(revision) end ---@class FileDiffersInRevisionsOpts ----@field original_revision string ----@field head_revision string +---@field revision_1 string +---@field revision_2 string ---@field old_file_name string ---@field file_name string @@ -255,7 +255,7 @@ end ---@param opts FileDiffersInRevisionsOpts ---@return boolean M.file_differs_in_revisions = function(opts) - local result = run_system({ "git", "diff", "-M", opts.original_revision, opts.head_revision, "--", opts.old_file_name, opts.file_name }) + local result = run_system({ "git", "diff", "-M", opts.revision_1, opts.revision_2, "--", opts.old_file_name, opts.file_name }) return result ~= "" end From 79c8caff5eb8f1e4c6f6bba4cfae7c218dcd7893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 13 Jun 2025 11:59:18 +0200 Subject: [PATCH 46/78] docs: use simpler info messages --- lua/gitlab/actions/suggestions.lua | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 0012ad6f..3f4f0d93 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -305,24 +305,15 @@ local determine_imply_local = function(revision, is_new_sha, original_file_name, }) -- TODO: Find out if this condition is not too restrictive. if not is_new_sha then - u.notify( - string.format("Comment on unchanged text. Using target-branch version of `%s`", original_file_name), - vim.log.levels.INFO - ) + u.notify("Comment on unchanged text. Using target-branch version", vim.log.levels.INFO) -- TODO: Find out if this condition is not too restrictive (maybe instead check if a later comment in the thread matches "^changed this line in [version %d+ of the diff]"). -- TODO: Rework to be able to switch between diffing against current head and original head. elseif head_differs_from_original then -- TODO: Fix the logic of determining what version is used to create the diff, whether the local -- file used and when this log message is shown. - u.notify( - string.format("File changed since comment created. Using version of `%s` on which comment was made", original_file_name), - vim.log.levels.INFO - ) + u.notify("File changed since comment created. Using version on which comment was made", vim.log.levels.INFO) elseif is_modified(original_file_name) then - u.notify( - string.format("File has unsaved or uncommited changes. Using feature-branch version for `%s`", original_file_name), - vim.log.levels.WARN - ) + u.notify("File has unsaved or uncommited changes. Using feature-branch version", vim.log.levels.WARN) else return true end From 37952010f3afb2be991169b232e6fb4de8f4d822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 13 Jun 2025 14:07:19 +0200 Subject: [PATCH 47/78] fix: check is_reply first --- lua/gitlab/actions/suggestions.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 3f4f0d93..1e9f4ca4 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -67,14 +67,14 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) - if note_node and root_node then + if root_node and is_reply then + require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false, root_node.id) + elseif note_node and root_node then local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) local edit_action = note_node.is_draft and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) edit_action(u.get_buffer_text(note_buf)) - elseif root_node and is_reply then - require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false, root_node.id) elseif is_new_comment then require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false) else From 983eb59ac439ccd6c3625b2ae4cec0b2af5864f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 13 Jun 2025 20:37:36 +0200 Subject: [PATCH 48/78] refactor: simplify variable names --- lua/gitlab/actions/suggestions.lua | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 1e9f4ca4..61d662fe 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -186,19 +186,19 @@ end ---Create the default suggestion lines for given comment range. ---@param original_lines string[] The list of lines in the original (commented on) version of the file. ----@param start_line_number integer The start line of the range of the comment (1-based indexing). ----@param end_line_number integer The end line of the range of the comment. +---@param start_line integer The start line number of the range of the comment (1-based indexing). +---@param end_line integer The end line number of the range of the comment. ---@return string[] suggestion_lines -local get_default_suggestion = function(original_lines, start_line_number, end_line_number) +local get_default_suggestion = function(original_lines, start_line, end_line) local backticks = "```" - local selected_lines = {unpack(original_lines, start_line_number, end_line_number)} + local selected_lines = {unpack(original_lines, start_line, end_line)} for _, line in ipairs(selected_lines) do local match = string.match(line, "^%s*(`+)%s*$") if match and #match >= #backticks then backticks = match .. "`" end end - local suggestion_lines = {backticks .. "suggestion:-" .. (end_line_number - start_line_number) .. "+0"} + local suggestion_lines = {backticks .. "suggestion:-" .. (end_line - start_line) .. "+0"} vim.list_extend(suggestion_lines, selected_lines) table.insert(suggestion_lines, backticks) return suggestion_lines @@ -229,10 +229,10 @@ end ---Create the suggestion list from the note text. ---@param note_lines string[] The content of the comment. ----@param end_line_number integer The last number of the comment range. +---@param end_line integer The last line number of the comment range. ---@param original_lines string[] Array of original lines. ---@return Suggestion[] suggestions List of suggestion data. -local get_suggestions = function(note_lines, end_line_number, original_lines) +local get_suggestions = function(note_lines, end_line, original_lines) local suggestions = {} local in_suggestion = false local suggestion = {} @@ -251,9 +251,9 @@ local get_suggestions = function(note_lines, end_line_number, original_lines) suggestion.note_end_linenr = i -- Add the full text with the changes applied to the original text. - local start_line = end_line_number - suggestion.start_line_offset - local end_line = end_line_number + suggestion.end_line_offset - suggestion.full_text = replace_line_range(original_lines, start_line, end_line, suggestion.lines, suggestion.note_start_linenr) + local start_line = end_line - suggestion.start_line_offset + local end_line_number = end_line + suggestion.end_line_offset + suggestion.full_text = replace_line_range(original_lines, start_line, end_line_number, suggestion.lines, suggestion.note_start_linenr) table.insert(suggestions, suggestion) in_suggestion = false @@ -388,12 +388,12 @@ end ---@param note_buf integer Note buffer number. ---@param suggestion_buf integer Suggestion buffer number. ---@param suggestions Suggestion[] List of suggestion data. ----@param end_line_number integer The last number of the comment range. +---@param end_line integer The last line number of the comment range. ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. ---@param is_new_comment boolean True if the suggestion is a new comment. -local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_header, is_reply, is_new_comment) +local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line, original_lines, imply_local, note_header, is_reply, is_new_comment) local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. @@ -460,7 +460,7 @@ end ---@param location Location|nil The location of the visual selection in the reviewer. M.show_preview = function(tree, is_reply, location) - local start_line_number, end_line_number, is_new_sha, revision + local start_line, end_line, is_new_sha, revision local root_node, note_node local note_buf_header_text, comment_id local original_file_name, new_file_name @@ -484,7 +484,7 @@ M.show_preview = function(tree, is_reply, location) end -- Decide which revision to use for the ORIGINAL text - start_line_number, is_new_sha, end_line_number = common.get_line_number_from_node(root_node) + start_line, is_new_sha, end_line = common.get_line_number_from_node(root_node) if is_new_sha then revision = root_node.head_sha original_file_name = root_node.file_name @@ -498,8 +498,8 @@ M.show_preview = function(tree, is_reply, location) elseif location ~= nil then note_buf_header_text = "New comment" comment_id = "HEAD" - start_line_number = location.visual_range.start_line - end_line_number = location.visual_range.end_line + start_line = location.visual_range.start_line + end_line = location.visual_range.end_line is_new_sha = location.reviewer_data.new_sha_focused revision = is_new_sha and "HEAD" or require("gitlab.state").INFO.target_branch original_file_name = location.reviewer_data.file_name or location.reviewer_data.old_file_name @@ -536,9 +536,9 @@ M.show_preview = function(tree, is_reply, location) if tree and not is_reply then note_lines = common.get_note_lines(tree) else - note_lines = get_default_suggestion(original_lines, start_line_number, end_line_number) + note_lines = get_default_suggestion(original_lines, start_line, end_line) end - local suggestions = get_suggestions(note_lines, end_line_number, original_lines) + local suggestions = get_suggestions(note_lines, end_line, original_lines) -- Create new tab with a temp buffer showing the original version on which the comment was -- made. @@ -584,9 +584,9 @@ M.show_preview = function(tree, is_reply, location) vim.bo.modified = false -- Set up keymaps and autocommands - local default_suggestion_lines = get_default_suggestion(original_lines, start_line_number, end_line_number) + local default_suggestion_lines = get_default_suggestion(original_lines, start_line, end_line) set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply, is_new_comment) - create_autocommands(note_buf, suggestion_buf, suggestions, end_line_number, original_lines, imply_local, note_buf_header_text, is_reply, is_new_comment) + create_autocommands(note_buf, suggestion_buf, suggestions, end_line, original_lines, imply_local, note_buf_header_text, is_reply, is_new_comment) -- Focus the note window on the first suggestion local note_winid = vim.fn.win_getid(3) From 3f778f0b04893d18208da9e9a14368888cfebf5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 14 Jun 2025 00:41:25 +0200 Subject: [PATCH 49/78] refactor: use ShowPreviewOpts --- lua/gitlab/actions/comment.lua | 18 ++- lua/gitlab/actions/discussions/init.lua | 63 ++++++-- lua/gitlab/actions/suggestions.lua | 204 +++++++++--------------- 3 files changed, 142 insertions(+), 143 deletions(-) diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index a7553a4b..4e0abef5 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -310,7 +310,23 @@ M.create_comment_with_suggestion = function() u.press_escape() return end - require("gitlab.actions.suggestions").show_preview(nil, false, M.location) + + local original_file_name = M.location.reviewer_data.old_file_name ~= "" and M.location.reviewer_data.old_file_name + or M.location.reviewer_data.file_name + local is_new_sha = M.location.reviewer_data.new_sha_focused + + ---@type ShowPreviewOpts + local opts = { + original_file_name = original_file_name, + new_file_name = M.location.reviewer_data.file_name, + start_line = M.location.visual_range.start_line, + end_line = M.location.visual_range.end_line, + is_new_sha = is_new_sha, + revision = is_new_sha and "HEAD" or require("gitlab.state").INFO.target_branch, + note_header = "comment", + comment_type = "new", + } + require("gitlab.actions.suggestions").show_preview(opts) end ---Returns true if it's possible to create an Inline Comment diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index dca8649e..e795291e 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -246,20 +246,59 @@ M.reply = function(tree) layout:mount() end --- Reply to the current thread in a new tab with a default suggestion based on the original text. -M.reply_with_suggestion = function(tree) - if M.is_draft_note(tree) then +---Open a new tab with a suggestion preview. +---@param tree NuiTree The current discussion tree instance. +---@param action "reply"|"edit" Reply to the current thread or edit the current comment. +M.suggestion_preview = function(tree, action) + local is_draft = M.is_draft_note(tree) + if action == "reply" and is_draft then u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN) return end - local suggestions = require("gitlab.actions.suggestions") - suggestions.show_preview(tree, true) -end --- Edit the current comment in a new tab with a suggestion preview. -M.edit_suggestion = function(tree) - local suggestions = require("gitlab.actions.suggestions") - suggestions.show_preview(tree) + local current_node = tree:get_node() + local root_node = common.get_root_node(tree, current_node) + local note_node = common.get_note_node(tree, current_node) + + if root_node == nil or note_node == nil then + u.notify("Couldn't get root node or note node", vim.log.levels.ERROR) + return + end + + -- Hack: draft notes don't have head_sha and base_sha yet + if root_node.is_draft then + root_node.head_sha = "HEAD" + root_node.base_sha = require("gitlab.state").INFO.target_branch + end + + local start_line, is_new_sha, end_line = common.get_line_number_from_node(root_node) + + if start_line == nil or end_line == nil then + u.notify("Couldn't get comment range. Can't build suggestion preview", vim.log.levels.ERROR) + return + end + + local note_node_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) + if note_node_id == nil then + u.notify("Couldn't get comment id", vim.log.levels.ERROR) + return + end + + ---@type ShowPreviewOpts + local opts = { + original_file_name = is_new_sha and root_node.file_name or root_node.old_file_name, + new_file_name = root_node.file_name, + start_line = start_line, + end_line = end_line, + is_new_sha = is_new_sha, + revision = is_new_sha and "HEAD" or require("gitlab.state").INFO.target_branch, + note_header = note_node.text, + comment_type = action == "reply" and action or is_draft and "draft" or "edit", + note_lines = action ~= "reply" and common.get_note_lines(tree) or nil, + root_node_id = root_node.id, + note_node_id = note_node_id, + } + require("gitlab.actions.suggestions").show_preview(opts) end -- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment @@ -605,7 +644,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) if keymaps.discussion_tree.edit_suggestion then vim.keymap.set("n", keymaps.discussion_tree.edit_suggestion, function() if M.is_current_node_note(tree) then - M.edit_suggestion(tree) + M.suggestion_preview(tree, "edit") end end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_nowait }) end @@ -613,7 +652,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) if keymaps.discussion_tree.reply_with_suggestion then vim.keymap.set("n", keymaps.discussion_tree.reply_with_suggestion, function() if M.is_current_node_note(tree) then - M.reply_with_suggestion(tree) + M.suggestion_preview(tree, "reply") end end, { buffer = bufnr, desc = "Reply with suggestion", nowait = keymaps.discussion_tree.reply_with_suggestion_nowait }) end diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 61d662fe..07a336d9 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -1,7 +1,6 @@ ---This module is responsible for previewing changes suggested in comments. ---The data required to make the API calls are drawn from the discussion nodes. -local common = require("gitlab.actions.common") local git = require("gitlab.git") local List = require("gitlab.utils.list") local u = require("gitlab.utils") @@ -38,13 +37,10 @@ end ---@param original_buf integer Number of the buffer with the original contents of the file. ---@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch). ---@param original_lines string[] The list of lines in the original (commented on) version of the file. ----@param root_node NuiTreeNode|nil The first comment in the discussion thread (can be a draft comment), nil if a new comment is created. ----@param note_node NuiTreeNode|nil The first node of a comment or reply, nil if a new comment is created. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param default_suggestion_lines string[] The default suggestion lines with backticks. ----@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. ----@param is_new_comment boolean True if the suggestion is a new comment. -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply, is_new_comment) +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. +local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, imply_local, default_suggestion_lines, opts) local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab @@ -67,16 +63,15 @@ local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_li vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) - if root_node and is_reply then - require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false, root_node.id) - elseif note_node and root_node then - local note_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) - local edit_action = note_node.is_draft - and require("gitlab.actions.draft_notes").confirm_edit_draft_note(note_id, false) - or require("gitlab.actions.comment").confirm_edit_comment(root_node.id, note_id, false) - edit_action(u.get_buffer_text(note_buf)) - elseif is_new_comment then - require("gitlab.actions.comment").confirm_create_comment(u.get_buffer_text(note_buf), false) + local buf_text = u.get_buffer_text(note_buf) + if opts.comment_type == "reply" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false, opts.root_node_id) + elseif opts.comment_type == "draft" then + require("gitlab.actions.draft_notes").confirm_edit_draft_note(opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "edit" then + require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "new" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false) else -- This should not really happen. u.notify("Cannot create comment", vim.log.levels.ERROR) @@ -152,30 +147,33 @@ end ---Create the name for a temporary file. ---@param revision string The revision of the file for which the comment was made. ----@param node_id any The id of the note node containing the suggestion. +---@param node_id string|integer The id of the note node containing the suggestion. ---@param file_name string The name of the commented file. ---@return string buf_name The full name of the new buffer. ---@return integer bufnr The number of the buffer associated with the new name (-1 if buffer doesn't exist). local get_temp_file_name = function(revision, node_id, file_name) + -- TODO: Come up with a nicer naming convention. local buf_name = string.format("gitlab::%s/%s::%s", revision, node_id, file_name) local bufnr = vim.fn.bufnr(buf_name) return buf_name, bufnr end ---Get the text on which the suggestion was created. ----@param original_file_name string The name of the file on which the comment was made. ----@param revision string The revision of the file for which the comment was made. +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. ---@return string[]|nil original_lines The list of original lines. -local get_original_lines = function(original_file_name, revision) - local original_head_text = git.get_file_revision({ file_name = original_file_name, revision = revision }) +local get_original_lines = function(opts) + local original_head_text = git.get_file_revision({ + file_name = opts.is_new_sha and opts.new_file_name or opts.original_file_name, + revision = opts.revision, + }) -- If the original revision doesn't contain the file, the branch was possibly rebased, and the -- original revision could not been found. if original_head_text == nil then u.notify( string.format( "File `%s` doesn't contain any text in revision `%s` for which comment was made", - original_file_name, - revision + opts.original_file_name, + opts.revision ), vim.log.levels.WARN ) @@ -186,19 +184,18 @@ end ---Create the default suggestion lines for given comment range. ---@param original_lines string[] The list of lines in the original (commented on) version of the file. ----@param start_line integer The start line number of the range of the comment (1-based indexing). ----@param end_line integer The end line number of the range of the comment. +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. ---@return string[] suggestion_lines -local get_default_suggestion = function(original_lines, start_line, end_line) +local get_default_suggestion = function(original_lines, opts) local backticks = "```" - local selected_lines = {unpack(original_lines, start_line, end_line)} + local selected_lines = {unpack(original_lines, opts.start_line, opts.end_line)} for _, line in ipairs(selected_lines) do local match = string.match(line, "^%s*(`+)%s*$") if match and #match >= #backticks then backticks = match .. "`" end end - local suggestion_lines = {backticks .. "suggestion:-" .. (end_line - start_line) .. "+0"} + local suggestion_lines = {backticks .. "suggestion:-" .. (opts.end_line - opts.start_line) .. "+0"} vim.list_extend(suggestion_lines, selected_lines) table.insert(suggestion_lines, backticks) return suggestion_lines @@ -292,27 +289,24 @@ local is_modified = function(file_name) end ---Decide if local file should be used to show suggestion preview. ----@param revision string The revision of the file for which the comment was made. ----@param is_new_sha boolean True if line number refers to NEW SHA ----@param original_file_name string The name of the file on which the comment was made. ----@param new_file_name string The new name of the file on which the comment was made. -local determine_imply_local = function(revision, is_new_sha, original_file_name, new_file_name) +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. +local determine_imply_local = function(opts) local head_differs_from_original = git.file_differs_in_revisions({ - revision_1 = revision, + revision_1 = opts.revision, revision_2 = "HEAD", - old_file_name = original_file_name, - file_name = new_file_name, + old_file_name = opts.original_file_name, + file_name = opts.new_file_name, }) -- TODO: Find out if this condition is not too restrictive. - if not is_new_sha then - u.notify("Comment on unchanged text. Using target-branch version", vim.log.levels.INFO) + if not opts.is_new_sha then + u.notify("Comment on old text. Using target-branch version", vim.log.levels.INFO) -- TODO: Find out if this condition is not too restrictive (maybe instead check if a later comment in the thread matches "^changed this line in [version %d+ of the diff]"). -- TODO: Rework to be able to switch between diffing against current head and original head. elseif head_differs_from_original then -- TODO: Fix the logic of determining what version is used to create the diff, whether the local -- file used and when this log message is shown. u.notify("File changed since comment created. Using version on which comment was made", vim.log.levels.INFO) - elseif is_modified(original_file_name) then + elseif is_modified(opts.new_file_name) then u.notify("File has unsaved or uncommited changes. Using feature-branch version", vim.log.levels.WARN) else return true @@ -351,11 +345,10 @@ local refresh_diagnostics = function(suggestions, note_buf) end ---Get the text for the draft mode ----@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. ----@param is_new_comment boolean True if the suggestion is a new comment. +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. ---@return string[]|nil -local get_mode = function(is_reply, is_new_comment) - if not is_reply and not is_new_comment then +local get_mode = function(opts) + if opts.comment_type == "draft" or opts.comment_type == "edit" then return end if require("gitlab.state").settings.discussion_tree.draft_mode then @@ -366,14 +359,12 @@ local get_mode = function(is_reply, is_new_comment) end ---Show the note header as virtual text. ----@param text string The text to show in the header. ---@param note_buf integer The number of the note buffer. ----@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. ----@param is_new_comment boolean True if the suggestion is a new comment. -local add_window_header = function(text, note_buf, is_reply, is_new_comment) +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. +local add_window_header = function(note_buf, opts) vim.api.nvim_buf_clear_namespace(note_buf, note_header_namespace, 0, -1) local mark_opts = { - virt_lines = { { { is_reply and "Reply to: " or is_new_comment and "Create: " or "Edit: ", "Normal" }, { text, "GitlabUserName" }, get_mode(is_reply, is_new_comment) } }, + virt_lines = { { { opts.comment_type .. ": ", "Normal" }, { opts.note_header, "GitlabUserName" }, get_mode(opts) } }, virt_lines_above = true, right_gravity = false, } @@ -388,12 +379,10 @@ end ---@param note_buf integer Note buffer number. ---@param suggestion_buf integer Suggestion buffer number. ---@param suggestions Suggestion[] List of suggestion data. ----@param end_line integer The last line number of the comment range. ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ----@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. ----@param is_new_comment boolean True if the suggestion is a new comment. -local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_line, original_lines, imply_local, note_header, is_reply, is_new_comment) +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. +local create_autocommands = function(note_buf, suggestion_buf, suggestions, original_lines, imply_local, opts) local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. @@ -426,7 +415,7 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ buffer = note_buf, callback = function() local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) - suggestions = get_suggestions(updated_note_lines, end_line_number, original_lines) + suggestions = get_suggestions(updated_note_lines, opts.end_line, original_lines) last_line = 0 update_suggestion_buffer() refresh_diagnostics(suggestions, note_buf) @@ -439,7 +428,7 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ group = group, pattern = "GitlabDraftModeToggled", callback = function() - add_window_header(note_header, note_buf, is_reply, is_new_comment) + add_window_header(note_buf, opts) end, }) -- Auto-delete the group when the buffer is unloaded. @@ -452,74 +441,34 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, end_ }) end ----TODO: Enable "reply_with_suggestion" from discussion tree. ----TODO: Enable "create_comment_with_suggestion" from reviewe.r ----Get suggestions from the current note and preview them in a new tab. ----@param tree NuiTree|nil The current discussion tree instance. ----@param is_reply boolean|nil True if the suggestion comment is a reply to a thread. ----@param location Location|nil The location of the visual selection in the reviewer. -M.show_preview = function(tree, is_reply, location) - - local start_line, end_line, is_new_sha, revision - local root_node, note_node - local note_buf_header_text, comment_id - local original_file_name, new_file_name - local is_new_comment = false - -- Populate necessary variables from the discussion tree - if tree ~= nil then - local current_node = tree:get_node() - root_node = common.get_root_node(tree, current_node) - note_node = common.get_note_node(tree, current_node) - note_buf_header_text = note_node.text - comment_id = note_node.id - if root_node == nil or note_node == nil then - u.notify("Couldn't get root node or note node", vim.log.levels.ERROR) - return - end - - -- Hack: draft notes don't have head_sha and base_sha yet - if root_node.is_draft then - root_node.head_sha = "HEAD" - root_node.base_sha = require("gitlab.state").INFO.target_branch - end +---@class ShowPreviewOpts The options passed to the M.show_preview function. +---@field original_file_name string +---@field new_file_name string +---@field start_line integer +---@field end_line integer +---@field is_new_sha boolean +---@field revision string +---@field note_header string +---@field comment_type "reply"|"draft"|"edit"|"new" The type of comment ("reply", "draft" and "edit" come from the discussion tree, "new" from the reviewer) +---@field note_lines string[]|nil +---@field root_node_id string +---@field note_node_id integer - -- Decide which revision to use for the ORIGINAL text - start_line, is_new_sha, end_line = common.get_line_number_from_node(root_node) - if is_new_sha then - revision = root_node.head_sha - original_file_name = root_node.file_name - else - revision = root_node.base_sha - original_file_name = root_node.old_file_name - end - new_file_name = root_node.file_name - - -- Populate necessary variables from the reviewer location data - elseif location ~= nil then - note_buf_header_text = "New comment" - comment_id = "HEAD" - start_line = location.visual_range.start_line - end_line = location.visual_range.end_line - is_new_sha = location.reviewer_data.new_sha_focused - revision = is_new_sha and "HEAD" or require("gitlab.state").INFO.target_branch - original_file_name = location.reviewer_data.file_name or location.reviewer_data.old_file_name - new_file_name = location.reviewer_data.file_name - is_new_comment = true - else - u.notify("Cannot create comment", vim.log.levels.ERROR) - return - end - - if not git.revision_exists(revision) then +---Get suggestions from the current note and preview them in a new tab. +---@param opts ShowPreviewOpts The options passed to the M.show_preview function. +M.show_preview = function(opts) + if not git.revision_exists(opts.revision) then u.notify( - string.format("Revision `%s` for which the comment was made does not exist", revision), - vim.log.levels.WARN + string.format("Revision `%s` for which the comment was made does not exist", opts.revision), + vim.log.levels.ERROR ) return end + local commented_file_name = opts.is_new_sha and opts.new_file_name or opts.original_file_name + local original_buf_name, original_bufnr = get_temp_file_name("ORIGINAL", opts.note_node_id or "NEW_COMMENT", commented_file_name) + -- If preview is already open for given note, go to the tab with a warning. - local original_buf_name, original_bufnr = get_temp_file_name("ORIGINAL", comment_id, original_file_name) local tabnr = get_tabnr_for_buf(original_bufnr) if tabnr ~= nil then vim.api.nvim_set_current_tabpage(tabnr) @@ -527,18 +476,13 @@ M.show_preview = function(tree, is_reply, location) return end - local original_lines = get_original_lines(original_file_name, revision) + local original_lines = get_original_lines(opts) if original_lines == nil then return end - local note_lines - if tree and not is_reply then - note_lines = common.get_note_lines(tree) - else - note_lines = get_default_suggestion(original_lines, start_line, end_line) - end - local suggestions = get_suggestions(note_lines, end_line, original_lines) + local note_lines = opts.note_lines or get_default_suggestion(original_lines, opts) + local suggestions = get_suggestions(note_lines, opts.end_line, original_lines) -- Create new tab with a temp buffer showing the original version on which the comment was -- made. @@ -553,14 +497,14 @@ M.show_preview = function(tree, is_reply, location) vim.cmd.filetype("detect") local buf_filetype = vim.api.nvim_get_option_value("filetype", { buf = 0 }) - local imply_local = determine_imply_local(revision, is_new_sha, original_file_name, new_file_name) + local imply_local = determine_imply_local(opts) -- Create the suggestion buffer and show a diff with the original version local split_cmd = vim.o.columns > 240 and "vsplit" or "split" if imply_local then - vim.api.nvim_cmd({ cmd = split_cmd, args = { original_file_name } }, {}) + vim.api.nvim_cmd({ cmd = split_cmd, args = { opts.new_file_name } }, {}) else - local sug_buf_name = get_temp_file_name("SUGGESTION", comment_id, new_file_name) + local sug_buf_name = get_temp_file_name("SUGGESTION", opts.note_node_id or "NEW_COMMENT", commented_file_name) vim.fn.mkdir(vim.fn.fnamemodify(sug_buf_name, ":h"), "p") vim.api.nvim_cmd({ cmd = split_cmd, args = { sug_buf_name } }, {}) vim.bo.bufhidden = "wipe" @@ -584,16 +528,16 @@ M.show_preview = function(tree, is_reply, location) vim.bo.modified = false -- Set up keymaps and autocommands - local default_suggestion_lines = get_default_suggestion(original_lines, start_line, end_line) - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, root_node, note_node, imply_local, default_suggestion_lines, is_reply, is_new_comment) - create_autocommands(note_buf, suggestion_buf, suggestions, end_line, original_lines, imply_local, note_buf_header_text, is_reply, is_new_comment) + local default_suggestion_lines = get_default_suggestion(original_lines, opts) + set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, imply_local, default_suggestion_lines, opts) + create_autocommands(note_buf, suggestion_buf, suggestions, original_lines, imply_local, opts) -- Focus the note window on the first suggestion local note_winid = vim.fn.win_getid(3) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) refresh_diagnostics(suggestions, note_buf) - add_window_header(note_buf_header_text, note_buf, is_reply, is_new_comment) + add_window_header(note_buf, opts) end return M From 6ed9e3fcff9c518bfc81d79fbebf0f6a81cbfe19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 19 Jun 2025 01:08:38 +0200 Subject: [PATCH 50/78] feat: add mapping for previewing suggestion with head_sha revision --- doc/gitlab.nvim.txt | 1 + lua/gitlab/actions/discussions/init.lua | 14 ++++++++++++-- lua/gitlab/state.lua | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 04c009eb..ea1dc340 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -224,6 +224,7 @@ you call this function with no values the defaults will be used: refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again print_node = "p", -- Print the current node (for debugging) edit_suggestion = "se", -- Edit suggestion comment in a new tab + edit_suggestion_at_comment_revision = "sE", -- Edit suggestion comment in a new tab, use the revision of the file for which the comment was made (useful when commented line was changed later). reply_with_suggestion = "sr", -- Reply to comment with a suggestion in a new tab }, suggestion_preview = { diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index e795291e..27859ea3 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -249,7 +249,8 @@ end ---Open a new tab with a suggestion preview. ---@param tree NuiTree The current discussion tree instance. ---@param action "reply"|"edit" Reply to the current thread or edit the current comment. -M.suggestion_preview = function(tree, action) +---@param use_head_sha boolean|nil Use the head_sha of the root_node as revision or the current HEAD by default. +M.suggestion_preview = function(tree, action, use_head_sha) local is_draft = M.is_draft_note(tree) if action == "reply" and is_draft then u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN) @@ -272,6 +273,7 @@ M.suggestion_preview = function(tree, action) end local start_line, is_new_sha, end_line = common.get_line_number_from_node(root_node) + local head_ref = use_head_sha and root_node.head_sha or "HEAD" if start_line == nil or end_line == nil then u.notify("Couldn't get comment range. Can't build suggestion preview", vim.log.levels.ERROR) @@ -291,7 +293,7 @@ M.suggestion_preview = function(tree, action) start_line = start_line, end_line = end_line, is_new_sha = is_new_sha, - revision = is_new_sha and "HEAD" or require("gitlab.state").INFO.target_branch, + revision = is_new_sha and head_ref or require("gitlab.state").INFO.target_branch, note_header = note_node.text, comment_type = action == "reply" and action or is_draft and "draft" or "edit", note_lines = action ~= "reply" and common.get_note_lines(tree) or nil, @@ -649,6 +651,14 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_nowait }) end + if keymaps.discussion_tree.edit_suggestion_at_comment_revision then + vim.keymap.set("n", keymaps.discussion_tree.edit_suggestion_at_comment_revision, function() + if M.is_current_node_note(tree) then + M.suggestion_preview(tree, "edit", true) + end + end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_at_comment_revision_nowait }) + end + if keymaps.discussion_tree.reply_with_suggestion then vim.keymap.set("n", keymaps.discussion_tree.reply_with_suggestion, function() if M.is_current_node_note(tree) then diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index d14bc0c4..01f914ac 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -126,6 +126,7 @@ M.settings = { refresh_data = "", print_node = "p", edit_suggestion = "se", + edit_suggestion_at_comment_revision = "sE", reply_with_suggestion = "sr", }, suggestion_preview = { From 9dbc478cef2eec48dfa57d5e6e3f18f09b3dc7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 20 Jun 2025 07:54:50 +0200 Subject: [PATCH 51/78] fix: add head_sha to root_node of draft notes --- lua/gitlab/actions/discussions/init.lua | 6 ------ lua/gitlab/actions/draft_notes/init.lua | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 27859ea3..3b9270cf 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -266,12 +266,6 @@ M.suggestion_preview = function(tree, action, use_head_sha) return end - -- Hack: draft notes don't have head_sha and base_sha yet - if root_node.is_draft then - root_node.head_sha = "HEAD" - root_node.base_sha = require("gitlab.state").INFO.target_branch - end - local start_line, is_new_sha, end_line = common.get_line_number_from_node(root_node) local head_ref = use_head_sha and root_node.head_sha or "HEAD" diff --git a/lua/gitlab/actions/draft_notes/init.lua b/lua/gitlab/actions/draft_notes/init.lua index 1f0e0e1d..40aca960 100755 --- a/lua/gitlab/actions/draft_notes/init.lua +++ b/lua/gitlab/actions/draft_notes/init.lua @@ -158,6 +158,7 @@ M.build_root_draft_note = function(note) old_file_name = (type(note.position) == "table" and note.position.old_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), + head_sha = (type(note.position) == "table" and note.position.head_sha or nil), resolvable = false, resolved = false, url = state.INFO.web_url .. "#note_" .. note.id, From 13a9fd22fb8e2ff1a37216ee08904b52536b2e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 20 Jun 2025 11:33:59 +0200 Subject: [PATCH 52/78] feat: replace extmarks by winbar --- lua/gitlab/actions/suggestions.lua | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 07a336d9..873072e9 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -14,7 +14,6 @@ vim.fn.sign_define("GitlabSuggestion", { }) local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note") -local note_header_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note_header") ---Reset the contents of the suggestion buffer. ---@param bufnr integer The number of the suggestion buffer. @@ -344,35 +343,36 @@ local refresh_diagnostics = function(suggestions, note_buf) vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) end ----Get the text for the draft mode +---Get the highlighted text for the draft mode. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. ----@return string[]|nil +---@return string local get_mode = function(opts) if opts.comment_type == "draft" or opts.comment_type == "edit" then - return + return "" end if require("gitlab.state").settings.discussion_tree.draft_mode then - return { " Draft", "GitlabDraftMode" } + return "%#GitlabDraftMode#Draft" else - return { " Live", "GitlabLiveMode" } + return "%#GitlabLiveMode#Live" end end ----Show the note header as virtual text. +---Update the winbar on top of the suggestion preview windows. ---@param note_buf integer The number of the note buffer. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. -local add_window_header = function(note_buf, opts) - vim.api.nvim_buf_clear_namespace(note_buf, note_header_namespace, 0, -1) - local mark_opts = { - virt_lines = { { { opts.comment_type .. ": ", "Normal" }, { opts.note_header, "GitlabUserName" }, get_mode(opts) } }, - virt_lines_above = true, - right_gravity = false, - } - vim.api.nvim_buf_set_extmark(note_buf, note_header_namespace, 0, 0, mark_opts) - -- An extmark above the first line is not visible by default, so let's scroll the window: - vim.cmd("normal! ") - -- TODO: Replace with winbar, possibly also show the diffed revision of the ORIGINAL. - -- Extmarks are not ideal for this because of scrolling issues. +local update_winbar = function(note_buf, opts) + local win_id = vim.fn.bufwinid(note_buf) + if win_id == -1 then + return -- Buffer not displayed in any window + end + + local note_content = string.format( + " %s: %s %s ", + "%#Normal#" .. opts.comment_type, + "%#GitlabUserName#" .. opts.note_header, + get_mode(opts) + ) + vim.api.nvim_set_option_value("winbar", note_content, { scope = "local", win = win_id }) end ---Create autocommands for the note buffer. @@ -428,7 +428,7 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, orig group = group, pattern = "GitlabDraftModeToggled", callback = function() - add_window_header(note_buf, opts) + update_winbar(note_buf, opts) end, }) -- Auto-delete the group when the buffer is unloaded. @@ -537,7 +537,7 @@ M.show_preview = function(opts) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) refresh_diagnostics(suggestions, note_buf) - add_window_header(note_buf, opts) + update_winbar(note_buf, opts) end return M From 1ad7e7dc3113a282b18e11aa553fb0dd775b9c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 20 Jun 2025 11:50:23 +0200 Subject: [PATCH 53/78] feat: add winbar to suggestion window --- lua/gitlab/actions/suggestions.lua | 46 +++++++++++++++++++----------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 873072e9..e1c5f885 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -358,31 +358,42 @@ local get_mode = function(opts) end ---Update the winbar on top of the suggestion preview windows. ----@param note_buf integer The number of the note buffer. +---@param note_winid integer Note window number. +---@param suggestion_winid integer Suggestion window number in the preview tab. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. -local update_winbar = function(note_buf, opts) - local win_id = vim.fn.bufwinid(note_buf) - if win_id == -1 then - return -- Buffer not displayed in any window +local update_winbar = function(note_winid, suggestion_winid, opts) + print('DEBUGPRINT[474]: suggestions.lua:364: opts=' .. vim.inspect(opts)) + + if note_winid ~= -1 then + local note_content = string.format( + " %s: %s %s ", + "%#Normal#" .. opts.comment_type, + "%#GitlabUserName#" .. opts.note_header, + get_mode(opts) + ) + vim.api.nvim_set_option_value("winbar", note_content, { scope = "local", win = note_winid }) end - local note_content = string.format( - " %s: %s %s ", - "%#Normal#" .. opts.comment_type, - "%#GitlabUserName#" .. opts.note_header, - get_mode(opts) - ) - vim.api.nvim_set_option_value("winbar", note_content, { scope = "local", win = win_id }) + if suggestion_winid ~= -1 then + local note_content = string.format( + " %s: %s ", + "%#Normal#revision", + "%#GitlabUserName#" .. opts.revision + ) + vim.api.nvim_set_option_value("winbar", note_content, { scope = "local", win = suggestion_winid }) + end end ---Create autocommands for the note buffer. ---@param note_buf integer Note buffer number. +---@param note_winid integer Note window number. ---@param suggestion_buf integer Suggestion buffer number. +---@param suggestion_winid integer Suggestion window number in the preview tab. ---@param suggestions Suggestion[] List of suggestion data. ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. -local create_autocommands = function(note_buf, suggestion_buf, suggestions, original_lines, imply_local, opts) +local create_autocommands = function(note_buf, note_winid, suggestion_buf, suggestion_winid, suggestions, original_lines, imply_local, opts) local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. @@ -428,7 +439,7 @@ local create_autocommands = function(note_buf, suggestion_buf, suggestions, orig group = group, pattern = "GitlabDraftModeToggled", callback = function() - update_winbar(note_buf, opts) + update_winbar(note_winid, suggestion_winid, opts) end, }) -- Auto-delete the group when the buffer is unloaded. @@ -513,11 +524,13 @@ M.show_preview = function(opts) vim.bo.filetype = buf_filetype end local suggestion_buf = vim.api.nvim_get_current_buf() + local suggestion_winid = vim.api.nvim_get_current_win() set_buffer_lines(suggestion_buf, suggestions[1].full_text, imply_local) vim.cmd("1,2windo diffthis") -- Create the note window local note_buf = vim.api.nvim_create_buf(false, false) + local note_winid = vim.fn.win_getid(3) local note_bufname = vim.fn.tempname() vim.api.nvim_buf_set_name(note_buf, note_bufname) vim.api.nvim_cmd({ cmd = "vnew", mods = { split = "botright" }, args = { note_bufname } }, {}) @@ -530,14 +543,13 @@ M.show_preview = function(opts) -- Set up keymaps and autocommands local default_suggestion_lines = get_default_suggestion(original_lines, opts) set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, imply_local, default_suggestion_lines, opts) - create_autocommands(note_buf, suggestion_buf, suggestions, original_lines, imply_local, opts) + create_autocommands(note_buf, note_winid, suggestion_buf, suggestion_winid, suggestions, original_lines, imply_local, opts) -- Focus the note window on the first suggestion - local note_winid = vim.fn.win_getid(3) vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) refresh_diagnostics(suggestions, note_buf) - update_winbar(note_buf, opts) + update_winbar(note_winid, suggestion_winid, opts) end return M From 381373234b2576f1e573eaf7ec6e2a398ad67544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 20 Jun 2025 12:05:35 +0200 Subject: [PATCH 54/78] feat: add winbar to orignial buffer --- lua/gitlab/actions/suggestions.lua | 60 ++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index e1c5f885..a24f94b7 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -343,10 +343,21 @@ local refresh_diagnostics = function(suggestions, note_buf) vim.diagnostic.set(suggestion_namespace, note_buf, diagnostics_data, indicators_common.create_display_opts()) end +---Get the highlighted text for the edit mode of the suggestion buffer. +---@param imply_local boolean True if suggestion buffer is local file and should be written. +---@return string +local get_edit_mode = function(imply_local) + if imply_local then + return "%#GitlabLiveMode#Local file" + else + return "%#GitlabDraftMode#Temp file" + end +end + ---Get the highlighted text for the draft mode. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. ---@return string -local get_mode = function(opts) +local get_draft_mode = function(opts) if opts.comment_type == "draft" or opts.comment_type == "edit" then return "" end @@ -360,27 +371,36 @@ end ---Update the winbar on top of the suggestion preview windows. ---@param note_winid integer Note window number. ---@param suggestion_winid integer Suggestion window number in the preview tab. +---@param original_winid integer Original text window number in the preview tab. +---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. -local update_winbar = function(note_winid, suggestion_winid, opts) - print('DEBUGPRINT[474]: suggestions.lua:364: opts=' .. vim.inspect(opts)) - - if note_winid ~= -1 then - local note_content = string.format( - " %s: %s %s ", - "%#Normal#" .. opts.comment_type, - "%#GitlabUserName#" .. opts.note_header, - get_mode(opts) +local update_winbar = function(note_winid, suggestion_winid, original_winid, imply_local, opts) + if original_winid ~= -1 then + local content = string.format( + " %s: %s ", + "%#Normal#original", + "%#GitlabUserName#" .. opts.revision ) - vim.api.nvim_set_option_value("winbar", note_content, { scope = "local", win = note_winid }) + vim.api.nvim_set_option_value("winbar", content, { scope = "local", win = original_winid }) end if suggestion_winid ~= -1 then - local note_content = string.format( + local content = string.format( " %s: %s ", - "%#Normal#revision", - "%#GitlabUserName#" .. opts.revision + "%#Normal#mode", + get_edit_mode(imply_local) + ) + vim.api.nvim_set_option_value("winbar", content, { scope = "local", win = suggestion_winid }) + end + + if note_winid ~= -1 then + local content = string.format( + " %s: %s %s ", + "%#Normal#" .. opts.comment_type, + "%#GitlabUserName#" .. opts.note_header, + get_draft_mode(opts) ) - vim.api.nvim_set_option_value("winbar", note_content, { scope = "local", win = suggestion_winid }) + vim.api.nvim_set_option_value("winbar", content, { scope = "local", win = note_winid }) end end @@ -389,11 +409,12 @@ end ---@param note_winid integer Note window number. ---@param suggestion_buf integer Suggestion buffer number. ---@param suggestion_winid integer Suggestion window number in the preview tab. +---@param original_winid integer Original text window number in the preview tab. ---@param suggestions Suggestion[] List of suggestion data. ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. -local create_autocommands = function(note_buf, note_winid, suggestion_buf, suggestion_winid, suggestions, original_lines, imply_local, opts) +local create_autocommands = function(note_buf, note_winid, suggestion_buf, suggestion_winid, original_winid, suggestions, original_lines, imply_local, opts) local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. @@ -439,7 +460,7 @@ local create_autocommands = function(note_buf, note_winid, suggestion_buf, sugge group = group, pattern = "GitlabDraftModeToggled", callback = function() - update_winbar(note_winid, suggestion_winid, opts) + update_winbar(note_winid, suggestion_winid, original_winid, imply_local, opts) end, }) -- Auto-delete the group when the buffer is unloaded. @@ -500,6 +521,7 @@ M.show_preview = function(opts) vim.fn.mkdir(vim.fn.fnamemodify(original_buf_name, ":h"), "p") vim.api.nvim_cmd({ cmd = "tabnew", args = { original_buf_name } }, {}) local original_buf = vim.api.nvim_get_current_buf() + local original_winid = vim.api.nvim_get_current_win() vim.api.nvim_buf_set_lines(original_buf, 0, -1, false, original_lines) vim.bo.bufhidden = "wipe" vim.bo.buflisted = false @@ -543,13 +565,13 @@ M.show_preview = function(opts) -- Set up keymaps and autocommands local default_suggestion_lines = get_default_suggestion(original_lines, opts) set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, imply_local, default_suggestion_lines, opts) - create_autocommands(note_buf, note_winid, suggestion_buf, suggestion_winid, suggestions, original_lines, imply_local, opts) + create_autocommands(note_buf, note_winid, suggestion_buf, suggestion_winid, original_winid, suggestions, original_lines, imply_local, opts) -- Focus the note window on the first suggestion vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) refresh_signs(suggestions[1], note_buf) refresh_diagnostics(suggestions, note_buf) - update_winbar(note_winid, suggestion_winid, opts) + update_winbar(note_winid, suggestion_winid, original_winid, imply_local, opts) end return M From 826428649a616571cd64892c94bf80a4940d1c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 20 Jun 2025 12:07:59 +0200 Subject: [PATCH 55/78] style: apply stylua --- lua/gitlab/actions/discussions/init.lua | 28 ++-- lua/gitlab/actions/suggestions.lua | 164 ++++++++++++++++-------- lua/gitlab/git.lua | 3 +- lua/gitlab/indicators/diagnostics.lua | 14 +- 4 files changed, 142 insertions(+), 67 deletions(-) diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 3b9270cf..4eea7ebd 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -650,17 +650,29 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) if M.is_current_node_note(tree) then M.suggestion_preview(tree, "edit", true) end - end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_at_comment_revision_nowait }) + end, { + buffer = bufnr, + desc = "Edit suggestion", + nowait = keymaps.discussion_tree.edit_suggestion_at_comment_revision_nowait, + }) end if keymaps.discussion_tree.reply_with_suggestion then - vim.keymap.set("n", keymaps.discussion_tree.reply_with_suggestion, function() - if M.is_current_node_note(tree) then - M.suggestion_preview(tree, "reply") - end - end, { buffer = bufnr, desc = "Reply with suggestion", nowait = keymaps.discussion_tree.reply_with_suggestion_nowait }) + vim.keymap.set( + "n", + keymaps.discussion_tree.reply_with_suggestion, + function() + if M.is_current_node_note(tree) then + M.suggestion_preview(tree, "reply") + end + end, + { + buffer = bufnr, + desc = "Reply with suggestion", + nowait = keymaps.discussion_tree.reply_with_suggestion_nowait, + } + ) end - end if keymaps.discussion_tree.refresh_data then @@ -875,7 +887,7 @@ M.toggle_draft_mode = function() state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode vim.api.nvim_exec_autocmds("User", { pattern = "GitlabDraftModeToggled", - data = { draft_mode = state.settings.discussion_tree.draft_mode } + data = { draft_mode = state.settings.discussion_tree.draft_mode }, }) end diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index a24f94b7..cf693bef 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -39,52 +39,87 @@ end ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param default_suggestion_lines string[] The default suggestion lines with backticks. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. -local set_keymaps = function(note_buf, original_buf, suggestion_buf, original_lines, imply_local, default_suggestion_lines, opts) +local set_keymaps = function( + note_buf, + original_buf, + suggestion_buf, + original_lines, + imply_local, + default_suggestion_lines, + opts +) local keymaps = require("gitlab.state").settings.keymaps -- Reset suggestion buffer to original state and close preview tab if keymaps.suggestion_preview.discard_changes then for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do - vim.keymap.set("n", keymaps.suggestion_preview.discard_changes, function() - set_buffer_lines(suggestion_buf, original_lines, imply_local) - if vim.api.nvim_buf_is_valid(note_buf) then - vim.bo[note_buf].modified = false - end - vim.cmd.tabclose() - end, { buffer = bufnr, desc = "Close preview tab discarding changes", nowait = keymaps.suggestion_preview.discard_changes_nowait }) + vim.keymap.set( + "n", + keymaps.suggestion_preview.discard_changes, + function() + set_buffer_lines(suggestion_buf, original_lines, imply_local) + if vim.api.nvim_buf_is_valid(note_buf) then + vim.bo[note_buf].modified = false + end + vim.cmd.tabclose() + end, + { + buffer = bufnr, + desc = "Close preview tab discarding changes", + nowait = keymaps.suggestion_preview.discard_changes_nowait, + } + ) end end -- Post updated suggestion note buffer to the server. if keymaps.suggestion_preview.apply_changes then - vim.keymap.set("n", keymaps.suggestion_preview.apply_changes, function() - vim.api.nvim_buf_call(note_buf, function() - vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) - end) - - local buf_text = u.get_buffer_text(note_buf) - if opts.comment_type == "reply" then - require("gitlab.actions.comment").confirm_create_comment(buf_text, false, opts.root_node_id) - elseif opts.comment_type == "draft" then - require("gitlab.actions.draft_notes").confirm_edit_draft_note(opts.note_node_id, false)(buf_text) - elseif opts.comment_type == "edit" then - require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) - elseif opts.comment_type == "new" then - require("gitlab.actions.comment").confirm_create_comment(buf_text, false) - else - -- This should not really happen. - u.notify("Cannot create comment", vim.log.levels.ERROR) - end + vim.keymap.set( + "n", + keymaps.suggestion_preview.apply_changes, + function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + + local buf_text = u.get_buffer_text(note_buf) + if opts.comment_type == "reply" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false, opts.root_node_id) + elseif opts.comment_type == "draft" then + require("gitlab.actions.draft_notes").confirm_edit_draft_note(opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "edit" then + require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "new" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false) + else + -- This should not really happen. + u.notify("Cannot create comment", vim.log.levels.ERROR) + end - set_buffer_lines(suggestion_buf, original_lines, imply_local) - vim.cmd.tabclose() - end, { buffer = note_buf, desc = "Post suggestion comment to Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait }) + set_buffer_lines(suggestion_buf, original_lines, imply_local) + vim.cmd.tabclose() + end, + { + buffer = note_buf, + desc = "Post suggestion comment to Gitlab", + nowait = keymaps.suggestion_preview.apply_changes_nowait, + } + ) end if keymaps.suggestion_preview.paste_default_suggestion then - vim.keymap.set("n", keymaps.suggestion_preview.paste_default_suggestion, function() - vim.api.nvim_put(default_suggestion_lines, "l", true, false) - end, { buffer = note_buf, desc = "Paste default suggestion", nowait = keymaps.suggestion_preview.paste_default_suggestion_nowait }) + vim.keymap.set( + "n", + keymaps.suggestion_preview.paste_default_suggestion, + function() + vim.api.nvim_put(default_suggestion_lines, "l", true, false) + end, + { + buffer = note_buf, + desc = "Paste default suggestion", + nowait = keymaps.suggestion_preview.paste_default_suggestion_nowait, + } + ) end -- TODO: Keymap for applying changes to the Suggestion buffer. @@ -101,7 +136,10 @@ end ---@return string[] new_tbl The new list of lines after replacing. local replace_line_range = function(full_text, start_idx, end_idx, new_lines, note_start_linenr) if start_idx < 1 then - u.notify(string.format("Can't apply suggestion at line %d, invalid start of range.", note_start_linenr), vim.log.levels.ERROR) + u.notify( + string.format("Can't apply suggestion at line %d, invalid start of range.", note_start_linenr), + vim.log.levels.ERROR + ) return full_text end -- Copy the original text @@ -187,14 +225,14 @@ end ---@return string[] suggestion_lines local get_default_suggestion = function(original_lines, opts) local backticks = "```" - local selected_lines = {unpack(original_lines, opts.start_line, opts.end_line)} + local selected_lines = { unpack(original_lines, opts.start_line, opts.end_line) } for _, line in ipairs(selected_lines) do local match = string.match(line, "^%s*(`+)%s*$") if match and #match >= #backticks then backticks = match .. "`" end end - local suggestion_lines = {backticks .. "suggestion:-" .. (opts.end_line - opts.start_line) .. "+0"} + local suggestion_lines = { backticks .. "suggestion:-" .. (opts.end_line - opts.start_line) .. "+0" } vim.list_extend(suggestion_lines, selected_lines) table.insert(suggestion_lines, backticks) return suggestion_lines @@ -249,7 +287,8 @@ local get_suggestions = function(note_lines, end_line, original_lines) -- Add the full text with the changes applied to the original text. local start_line = end_line - suggestion.start_line_offset local end_line_number = end_line + suggestion.end_line_offset - suggestion.full_text = replace_line_range(original_lines, start_line, end_line_number, suggestion.lines, suggestion.note_start_linenr) + suggestion.full_text = + replace_line_range(original_lines, start_line, end_line_number, suggestion.lines, suggestion.note_start_linenr) table.insert(suggestions, suggestion) in_suggestion = false @@ -269,7 +308,7 @@ local get_suggestions = function(note_lines, end_line, original_lines) lines = {}, full_text = original_lines, is_default = true, - } + }, } end return suggestions @@ -348,9 +387,9 @@ end ---@return string local get_edit_mode = function(imply_local) if imply_local then - return "%#GitlabLiveMode#Local file" + return "%#GitlabLiveMode#Local file" else - return "%#GitlabDraftMode#Temp file" + return "%#GitlabDraftMode#Temp file" end end @@ -362,9 +401,9 @@ local get_draft_mode = function(opts) return "" end if require("gitlab.state").settings.discussion_tree.draft_mode then - return "%#GitlabDraftMode#Draft" + return "%#GitlabDraftMode#Draft" else - return "%#GitlabLiveMode#Live" + return "%#GitlabLiveMode#Live" end end @@ -376,20 +415,12 @@ end ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. local update_winbar = function(note_winid, suggestion_winid, original_winid, imply_local, opts) if original_winid ~= -1 then - local content = string.format( - " %s: %s ", - "%#Normal#original", - "%#GitlabUserName#" .. opts.revision - ) + local content = string.format(" %s: %s ", "%#Normal#original", "%#GitlabUserName#" .. opts.revision) vim.api.nvim_set_option_value("winbar", content, { scope = "local", win = original_winid }) end if suggestion_winid ~= -1 then - local content = string.format( - " %s: %s ", - "%#Normal#mode", - get_edit_mode(imply_local) - ) + local content = string.format(" %s: %s ", "%#Normal#mode", get_edit_mode(imply_local)) vim.api.nvim_set_option_value("winbar", content, { scope = "local", win = suggestion_winid }) end @@ -414,7 +445,17 @@ end ---@param original_lines string[] Array of original lines. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. -local create_autocommands = function(note_buf, note_winid, suggestion_buf, suggestion_winid, original_winid, suggestions, original_lines, imply_local, opts) +local create_autocommands = function( + note_buf, + note_winid, + suggestion_buf, + suggestion_winid, + original_winid, + suggestions, + original_lines, + imply_local, + opts +) local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. @@ -435,7 +476,7 @@ local create_autocommands = function(note_buf, note_winid, suggestion_buf, sugge end -- Create autocommand to update the Suggestion buffer when the cursor moves in the Comment buffer. - vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { + vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { buffer = note_buf, callback = function() update_suggestion_buffer() @@ -443,7 +484,7 @@ local create_autocommands = function(note_buf, note_winid, suggestion_buf, sugge }) -- Create autocommand to update suggestions list based on the note buffer content. - vim.api.nvim_create_autocmd({"TextChanged", "TextChangedI"}, { + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { buffer = note_buf, callback = function() local updated_note_lines = vim.api.nvim_buf_get_lines(note_buf, 0, -1, false) @@ -498,7 +539,8 @@ M.show_preview = function(opts) end local commented_file_name = opts.is_new_sha and opts.new_file_name or opts.original_file_name - local original_buf_name, original_bufnr = get_temp_file_name("ORIGINAL", opts.note_node_id or "NEW_COMMENT", commented_file_name) + local original_buf_name, original_bufnr = + get_temp_file_name("ORIGINAL", opts.note_node_id or "NEW_COMMENT", commented_file_name) -- If preview is already open for given note, go to the tab with a warning. local tabnr = get_tabnr_for_buf(original_bufnr) @@ -565,7 +607,17 @@ M.show_preview = function(opts) -- Set up keymaps and autocommands local default_suggestion_lines = get_default_suggestion(original_lines, opts) set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, imply_local, default_suggestion_lines, opts) - create_autocommands(note_buf, note_winid, suggestion_buf, suggestion_winid, original_winid, suggestions, original_lines, imply_local, opts) + create_autocommands( + note_buf, + note_winid, + suggestion_buf, + suggestion_winid, + original_winid, + suggestions, + original_lines, + imply_local, + opts + ) -- Focus the note window on the first suggestion vim.api.nvim_win_set_cursor(note_winid, { suggestions[1].note_start_linenr, 0 }) diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index 12433d66..42d58277 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -255,7 +255,8 @@ end ---@param opts FileDiffersInRevisionsOpts ---@return boolean M.file_differs_in_revisions = function(opts) - local result = run_system({ "git", "diff", "-M", opts.revision_1, opts.revision_2, "--", opts.old_file_name, opts.file_name }) + local result = + run_system({ "git", "diff", "-M", opts.revision_1, opts.revision_2, "--", opts.old_file_name, opts.file_name }) return result ~= "" end diff --git a/lua/gitlab/indicators/diagnostics.lua b/lua/gitlab/indicators/diagnostics.lua index b33cfd37..2a220470 100644 --- a/lua/gitlab/indicators/diagnostics.lua +++ b/lua/gitlab/indicators/diagnostics.lua @@ -133,9 +133,19 @@ M.place_diagnostics = function(bufnr) local new_diagnostics, old_diagnostics = List.new(file_discussions):partition(indicators_common.is_new_sha) if bufnr == view.cur_layout.a.file.bufnr then - set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(old_diagnostics), indicators_common.create_display_opts()) + set_diagnostics( + diagnostics_namespace, + bufnr, + M.parse_diagnostics(old_diagnostics), + indicators_common.create_display_opts() + ) elseif bufnr == view.cur_layout.b.file.bufnr then - set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(new_diagnostics), indicators_common.create_display_opts()) + set_diagnostics( + diagnostics_namespace, + bufnr, + M.parse_diagnostics(new_diagnostics), + indicators_common.create_display_opts() + ) end end) From 821791fedbccfb79cc906137c992db137a323103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 11 Jul 2025 14:31:01 +0200 Subject: [PATCH 56/78] fix: refresh LSP diagnostics in suggestion buffer hen settings buffer lines --- lua/gitlab/actions/suggestions.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index cf693bef..e83eeb91 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -15,6 +15,19 @@ vim.fn.sign_define("GitlabSuggestion", { local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_note") +---Refresh the diagnostics from LSP in the suggestions buffer if there are any clients that support +---diagnostics. +---@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch). +local refresh_lsp_diagnostics = function(suggestion_buf) + for _, client in ipairs(vim.lsp.get_clients({ bufnr = suggestion_buf })) do + if client:supports_method('textDocument/diagnostic', suggestion_buf) then + vim.lsp.buf_request(suggestion_buf, 'textDocument/diagnostic', { + textDocument = vim.lsp.util.make_text_document_params(suggestion_buf) + }) + end + end +end + ---Reset the contents of the suggestion buffer. ---@param bufnr integer The number of the suggestion buffer. ---@param lines string[] Lines of text to put into the buffer. @@ -28,6 +41,7 @@ local set_buffer_lines = function(bufnr, lines, imply_local) vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) end) + refresh_lsp_diagnostics(bufnr) end end From 8e487a8ae06cd5fbd0e0e9fe029f64a58c79aeb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 11 Jul 2025 14:32:14 +0200 Subject: [PATCH 57/78] docs: make error message more informative --- lua/gitlab/actions/suggestions.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index e83eeb91..5a32ce5c 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -107,7 +107,7 @@ local set_keymaps = function( require("gitlab.actions.comment").confirm_create_comment(buf_text, false) else -- This should not really happen. - u.notify("Cannot create comment", vim.log.levels.ERROR) + u.notify(string.format("Cannot create comment with unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) end set_buffer_lines(suggestion_buf, original_lines, imply_local) From 6a689226d4b2007b077204d04ee95973c02cef6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 11 Jul 2025 14:42:26 +0200 Subject: [PATCH 58/78] docs: add note why changing modified option --- lua/gitlab/actions/suggestions.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 5a32ce5c..6888e293 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -73,6 +73,7 @@ local set_keymaps = function( function() set_buffer_lines(suggestion_buf, original_lines, imply_local) if vim.api.nvim_buf_is_valid(note_buf) then + -- Set nomodified to enable safely closing the buffer vim.bo[note_buf].modified = false end vim.cmd.tabclose() From 675bc549b2ec50cb16847d005f402435b268622d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 11 Jul 2025 16:45:23 +0200 Subject: [PATCH 59/78] fix: reset suggestion buffer before closing --- lua/gitlab/actions/suggestions.lua | 148 +++++++++++++++++------------ 1 file changed, 88 insertions(+), 60 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 6888e293..0a542475 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -36,6 +36,7 @@ local set_buffer_lines = function(bufnr, lines, imply_local) if not vim.api.nvim_buf_is_valid(bufnr) then return end + vim.bo[bufnr].modifiable = true vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) if imply_local then vim.api.nvim_buf_call(bufnr, function() @@ -45,6 +46,27 @@ local set_buffer_lines = function(bufnr, lines, imply_local) end end +---Reset suggestion buffer options and keymaps before closing the preview. +---@param imply_local boolean True if suggestion buffer is local file and should be written. +---@param suggestion_buf integer Suggestion buffer number. +---@param original_lines string[] The list of lines in the original (commented on) version of the file. +---@param original_suggestion_winbar string The original suggestion buffer/window 'winbar'. +---@param suggestion_winid integer Suggestion window number in the preview tab. +local reset_suggestion_buf = function( + imply_local, + suggestion_buf, + original_lines, + original_suggestion_winbar, + suggestion_winid +) + local keymaps = require("gitlab.state").settings.keymaps + set_buffer_lines(suggestion_buf, original_lines, imply_local) + if imply_local then + pcall(vim.api.nvim_buf_del_keymap, suggestion_buf, "n", keymaps.suggestion_preview.discard_changes) + vim.api.nvim_set_option_value("winbar", original_suggestion_winbar, { scope = "local", win = suggestion_winid }) + end +end + ---Set keymaps for the suggestion tab buffers. ---@param note_buf integer Number of the note buffer. ---@param original_buf integer Number of the buffer with the original contents of the file. @@ -52,6 +74,8 @@ end ---@param original_lines string[] The list of lines in the original (commented on) version of the file. ---@param imply_local boolean True if suggestion buffer is local file and should be written. ---@param default_suggestion_lines string[] The default suggestion lines with backticks. +---@param original_suggestion_winbar string The original suggestion buffer/window 'winbar'. +---@param suggestion_winid integer Suggestion window number in the preview tab. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. local set_keymaps = function( note_buf, @@ -60,6 +84,8 @@ local set_keymaps = function( original_lines, imply_local, default_suggestion_lines, + original_suggestion_winbar, + suggestion_winid, opts ) local keymaps = require("gitlab.state").settings.keymaps @@ -67,74 +93,61 @@ local set_keymaps = function( -- Reset suggestion buffer to original state and close preview tab if keymaps.suggestion_preview.discard_changes then for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do - vim.keymap.set( - "n", - keymaps.suggestion_preview.discard_changes, - function() - set_buffer_lines(suggestion_buf, original_lines, imply_local) - if vim.api.nvim_buf_is_valid(note_buf) then - -- Set nomodified to enable safely closing the buffer - vim.bo[note_buf].modified = false - end - vim.cmd.tabclose() - end, - { - buffer = bufnr, - desc = "Close preview tab discarding changes", - nowait = keymaps.suggestion_preview.discard_changes_nowait, - } - ) + vim.keymap.set("n", keymaps.suggestion_preview.discard_changes, function() + if vim.api.nvim_buf_is_valid(note_buf) then + vim.bo[note_buf].modified = false + end + reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) + vim.cmd.tabclose() + end, { + buffer = bufnr, + desc = "Close preview tab discarding changes", + nowait = keymaps.suggestion_preview.discard_changes_nowait, + }) end end -- Post updated suggestion note buffer to the server. if keymaps.suggestion_preview.apply_changes then - vim.keymap.set( - "n", - keymaps.suggestion_preview.apply_changes, - function() - vim.api.nvim_buf_call(note_buf, function() - vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) - end) - - local buf_text = u.get_buffer_text(note_buf) - if opts.comment_type == "reply" then - require("gitlab.actions.comment").confirm_create_comment(buf_text, false, opts.root_node_id) - elseif opts.comment_type == "draft" then - require("gitlab.actions.draft_notes").confirm_edit_draft_note(opts.note_node_id, false)(buf_text) - elseif opts.comment_type == "edit" then - require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) - elseif opts.comment_type == "new" then - require("gitlab.actions.comment").confirm_create_comment(buf_text, false) - else - -- This should not really happen. - u.notify(string.format("Cannot create comment with unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) - end + vim.keymap.set("n", keymaps.suggestion_preview.apply_changes, function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + + local buf_text = u.get_buffer_text(note_buf) + if opts.comment_type == "reply" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false, opts.root_node_id) + elseif opts.comment_type == "draft" then + require("gitlab.actions.draft_notes").confirm_edit_draft_note(opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "edit" then + require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "new" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false) + else + -- This should not really happen. + u.notify( + string.format("Cannot create comment with unsupported action `%s`", opts.comment_type), + vim.log.levels.ERROR + ) + end - set_buffer_lines(suggestion_buf, original_lines, imply_local) - vim.cmd.tabclose() - end, - { - buffer = note_buf, - desc = "Post suggestion comment to Gitlab", - nowait = keymaps.suggestion_preview.apply_changes_nowait, - } - ) + reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) + vim.cmd.tabclose() + end, { + buffer = note_buf, + desc = "Post suggestion comment to Gitlab", + nowait = keymaps.suggestion_preview.apply_changes_nowait, + }) end if keymaps.suggestion_preview.paste_default_suggestion then - vim.keymap.set( - "n", - keymaps.suggestion_preview.paste_default_suggestion, - function() - vim.api.nvim_put(default_suggestion_lines, "l", true, false) - end, - { - buffer = note_buf, - desc = "Paste default suggestion", - nowait = keymaps.suggestion_preview.paste_default_suggestion_nowait, - } - ) + vim.keymap.set("n", keymaps.suggestion_preview.paste_default_suggestion, function() + vim.api.nvim_put(default_suggestion_lines, "l", true, false) + end, { + buffer = note_buf, + desc = "Paste default suggestion", + nowait = keymaps.suggestion_preview.paste_default_suggestion_nowait, + }) end -- TODO: Keymap for applying changes to the Suggestion buffer. @@ -607,6 +620,11 @@ M.show_preview = function(opts) set_buffer_lines(suggestion_buf, suggestions[1].full_text, imply_local) vim.cmd("1,2windo diffthis") + -- Backup the suggestion buffer winbar to reset it when suggestion preview is closed. Despite the + -- option being "window-local", it's carried over to the buffer even after closing the preview. + -- See https://github.com/neovim/neovim/issues/11525 + local suggestion_winbar = vim.api.nvim_get_option_value("winbar", { scope = "local", win = suggestion_winid }) + -- Create the note window local note_buf = vim.api.nvim_create_buf(false, false) local note_winid = vim.fn.win_getid(3) @@ -621,7 +639,17 @@ M.show_preview = function(opts) -- Set up keymaps and autocommands local default_suggestion_lines = get_default_suggestion(original_lines, opts) - set_keymaps(note_buf, original_buf, suggestion_buf, original_lines, imply_local, default_suggestion_lines, opts) + set_keymaps( + note_buf, + original_buf, + suggestion_buf, + original_lines, + imply_local, + default_suggestion_lines, + suggestion_winbar, + suggestion_winid, + opts + ) create_autocommands( note_buf, note_winid, From 97be236cc8e9e4ea38c698d2551f38446462646e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 11 Jul 2025 16:46:09 +0200 Subject: [PATCH 60/78] style: apply stylua --- lua/gitlab/actions/suggestions.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 0a542475..b5bd1fd5 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -20,9 +20,9 @@ local suggestion_namespace = vim.api.nvim_create_namespace("gitlab_suggestion_no ---@param suggestion_buf integer Number of the buffer with applied suggestions (can be local or scratch). local refresh_lsp_diagnostics = function(suggestion_buf) for _, client in ipairs(vim.lsp.get_clients({ bufnr = suggestion_buf })) do - if client:supports_method('textDocument/diagnostic', suggestion_buf) then - vim.lsp.buf_request(suggestion_buf, 'textDocument/diagnostic', { - textDocument = vim.lsp.util.make_text_document_params(suggestion_buf) + if client:supports_method("textDocument/diagnostic", suggestion_buf) then + vim.lsp.buf_request(suggestion_buf, "textDocument/diagnostic", { + textDocument = vim.lsp.util.make_text_document_params(suggestion_buf), }) end end From 9f67c5843a2e3d42bce3c79daddf892da256f40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 12 Jul 2025 07:15:14 +0200 Subject: [PATCH 61/78] fix: automatically choose head_sha if file has changed --- doc/gitlab.nvim.txt | 3 +- lua/gitlab/actions/common.lua | 27 ++++++++++++++++ lua/gitlab/actions/discussions/init.lua | 42 +++++++++++-------------- lua/gitlab/actions/discussions/tree.lua | 4 +++ lua/gitlab/state.lua | 1 - 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index ea1dc340..409d3aa1 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -224,8 +224,7 @@ you call this function with no values the defaults will be used: refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again print_node = "p", -- Print the current node (for debugging) edit_suggestion = "se", -- Edit suggestion comment in a new tab - edit_suggestion_at_comment_revision = "sE", -- Edit suggestion comment in a new tab, use the revision of the file for which the comment was made (useful when commented line was changed later). - reply_with_suggestion = "sr", -- Reply to comment with a suggestion in a new tab + reply_with_suggestion = "sr", -- Reply to comment with a suggestion preview in a new tab }, suggestion_preview = { apply_changes = "ZZ", -- Post updated suggestion comment to Gitlab, close the suggestion preview tab and discard changes to local files diff --git a/lua/gitlab/actions/common.lua b/lua/gitlab/actions/common.lua index 1a4424e0..46675d6e 100644 --- a/lua/gitlab/actions/common.lua +++ b/lua/gitlab/actions/common.lua @@ -343,4 +343,31 @@ M.jump_to_file = function(tree) vim.api.nvim_win_set_cursor(0, { line_number, 0 }) end +---Determine whether commented line has changed since making the comment. +---@param tree NuiTree The current discussion tree instance. +---@param note_node NuiTree.Node The main node of the note containing the note author etc. +---@return boolean line_changed True if any of the notes in the thread is a system note starting with "changed this line". +M.commented_line_has_changed = function(tree, note_node) + local line_changed = List.new(note_node:get_child_ids()):includes(function(child_id) + local child_node = tree:get_node(child_id) + if child_node == nil then + return false + end + + -- Inspect note bodies or recourse to child notes. + if child_node.type == "note_body" then + local line = tree:get_node(child_id).text + if string.match(line, "^changed this line") and note_node.system then + return true + end + elseif child_node.type == "note" and M.commented_line_has_changed(tree, child_node) then + return true + end + + return false + end) + + return line_changed +end + return M diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 4eea7ebd..8880706a 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -249,8 +249,7 @@ end ---Open a new tab with a suggestion preview. ---@param tree NuiTree The current discussion tree instance. ---@param action "reply"|"edit" Reply to the current thread or edit the current comment. ----@param use_head_sha boolean|nil Use the head_sha of the root_node as revision or the current HEAD by default. -M.suggestion_preview = function(tree, action, use_head_sha) +M.suggestion_preview = function(tree, action) local is_draft = M.is_draft_note(tree) if action == "reply" and is_draft then u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN) @@ -261,35 +260,44 @@ M.suggestion_preview = function(tree, action, use_head_sha) local root_node = common.get_root_node(tree, current_node) local note_node = common.get_note_node(tree, current_node) + -- Return early if note info is missing if root_node == nil or note_node == nil then u.notify("Couldn't get root node or note node", vim.log.levels.ERROR) return end + local note_node_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) + if note_node_id == nil then + u.notify("Couldn't get comment id", vim.log.levels.ERROR) + return + end + -- Return early if comment position is missing local start_line, is_new_sha, end_line = common.get_line_number_from_node(root_node) - local head_ref = use_head_sha and root_node.head_sha or "HEAD" - if start_line == nil or end_line == nil then u.notify("Couldn't get comment range. Can't build suggestion preview", vim.log.levels.ERROR) return end - local note_node_id = tonumber(note_node.is_root and note_node.root_note_id or note_node.id) - if note_node_id == nil then - u.notify("Couldn't get comment id", vim.log.levels.ERROR) - return + -- Get values for preview depending on whether comment is on OLD or NEW version + local original_file_name, revision + if is_new_sha then + original_file_name = root_node.file_name + revision = common.commented_line_has_changed(tree, root_node) and root_node.head_sha or "HEAD" + else + original_file_name = root_node.old_file_name + revision = root_node.base_sha end ---@type ShowPreviewOpts local opts = { - original_file_name = is_new_sha and root_node.file_name or root_node.old_file_name, + original_file_name = original_file_name, new_file_name = root_node.file_name, start_line = start_line, end_line = end_line, is_new_sha = is_new_sha, - revision = is_new_sha and head_ref or require("gitlab.state").INFO.target_branch, + revision = revision, note_header = note_node.text, - comment_type = action == "reply" and action or is_draft and "draft" or "edit", + comment_type = is_draft and "draft" or action, note_lines = action ~= "reply" and common.get_note_lines(tree) or nil, root_node_id = root_node.id, note_node_id = note_node_id, @@ -645,18 +653,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_nowait }) end - if keymaps.discussion_tree.edit_suggestion_at_comment_revision then - vim.keymap.set("n", keymaps.discussion_tree.edit_suggestion_at_comment_revision, function() - if M.is_current_node_note(tree) then - M.suggestion_preview(tree, "edit", true) - end - end, { - buffer = bufnr, - desc = "Edit suggestion", - nowait = keymaps.discussion_tree.edit_suggestion_at_comment_revision_nowait, - }) - end - if keymaps.discussion_tree.reply_with_suggestion then vim.keymap.set( "n", diff --git a/lua/gitlab/actions/discussions/tree.lua b/lua/gitlab/actions/discussions/tree.lua index 7873cf36..35a4816f 100644 --- a/lua/gitlab/actions/discussions/tree.lua +++ b/lua/gitlab/actions/discussions/tree.lua @@ -42,6 +42,7 @@ M.add_discussions_to_table = function(items, unlinked) local root_head_sha = nil local root_base_sha = nil local root_url + local system = false for j, note in ipairs(discussion.notes) do if j == 1 then @@ -58,6 +59,7 @@ M.add_discussions_to_table = function(items, unlinked) resolved = note.resolved root_url = state.INFO.web_url .. "#note_" .. note.id range = (type(note.position) == "table" and note.position.line_range or nil) + system = note.system else -- Otherwise insert it as a child node... local note_node = M.build_note(note) table.insert(discussion_children, note_node) @@ -93,6 +95,7 @@ M.add_discussions_to_table = function(items, unlinked) base_sha = root_base_sha, resolvable = resolvable, resolved = resolved, + system = system, url = root_url, }, body) @@ -319,6 +322,7 @@ M.build_note = function(note, resolve_info) head_sha = (type(note.position) == "table" and note.position.head_sha), base_sha = (type(note.position) == "table" and note.position.base_sha), url = state.INFO.web_url .. "#note_" .. note.id, + system = note.system, type = "note", }, text_nodes) diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 01f914ac..d14bc0c4 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -126,7 +126,6 @@ M.settings = { refresh_data = "", print_node = "p", edit_suggestion = "se", - edit_suggestion_at_comment_revision = "sE", reply_with_suggestion = "sr", }, suggestion_preview = { From b753bb12dbac995fb02a5f04a9b8e6e9c5d583a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 12 Jul 2025 21:39:22 +0200 Subject: [PATCH 62/78] fix: remove unnecessary check --- lua/gitlab/indicators/diagnostics.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/lua/gitlab/indicators/diagnostics.lua b/lua/gitlab/indicators/diagnostics.lua index 2a220470..602d57d2 100644 --- a/lua/gitlab/indicators/diagnostics.lua +++ b/lua/gitlab/indicators/diagnostics.lua @@ -115,9 +115,6 @@ M.place_diagnostics = function(bufnr) u.notify("Could not find Diffview view", vim.log.levels.ERROR) return end - if vim.api.nvim_buf_get_name(bufnr) == "diffview://null" then - return - end local ok, err = pcall(function() local file_discussions = List.new(M.placeable_discussions):filter(function(discussion_or_note) From 8120680ac2cafcbb1100ae8bf3dfcdf5e98fa7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 12 Jul 2025 22:00:20 +0200 Subject: [PATCH 63/78] fix: don't reset temporary suggestion buffer before closing preview --- lua/gitlab/actions/suggestions.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index b5bd1fd5..a5d175c6 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -97,7 +97,16 @@ local set_keymaps = function( if vim.api.nvim_buf_is_valid(note_buf) then vim.bo[note_buf].modified = false end - reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) + -- Resetting can cause invalid-buffer errors for temporary (non-local) suggestion buffer + if imply_local then + reset_suggestion_buf( + imply_local, + suggestion_buf, + original_lines, + original_suggestion_winbar, + suggestion_winid + ) + end vim.cmd.tabclose() end, { buffer = bufnr, From 2964a000a54573a71139573458f5c0e5a83229d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 12 Jul 2025 22:25:59 +0200 Subject: [PATCH 64/78] fix: recompute folds in suggestion buffer on TextChangedI --- lua/gitlab/actions/suggestions.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index a5d175c6..e3a48f99 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -38,6 +38,13 @@ local set_buffer_lines = function(bufnr, lines, imply_local) end vim.bo[bufnr].modifiable = true vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + -- Recompute and re-apply folds (Otherwise folds are messed up when TextChangedI is triggered). + -- TODO: Find out if it's a (Neo)vim bug. + vim.api.nvim_buf_call(bufnr, function() + vim.cmd("normal! zX") + end) + if imply_local then vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) From 01d1e23350f9eb9e6d6e04ad49cafcb2c1291d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 14 Jul 2025 08:46:41 +0200 Subject: [PATCH 65/78] docs: improve messages to user --- lua/gitlab/actions/discussions/init.lua | 23 +++++++++-------------- lua/gitlab/actions/suggestions.lua | 17 ++++++----------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 8880706a..c90787f8 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -654,20 +654,15 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) end if keymaps.discussion_tree.reply_with_suggestion then - vim.keymap.set( - "n", - keymaps.discussion_tree.reply_with_suggestion, - function() - if M.is_current_node_note(tree) then - M.suggestion_preview(tree, "reply") - end - end, - { - buffer = bufnr, - desc = "Reply with suggestion", - nowait = keymaps.discussion_tree.reply_with_suggestion_nowait, - } - ) + vim.keymap.set("n", keymaps.discussion_tree.reply_with_suggestion, function() + if M.is_current_node_note(tree) then + M.suggestion_preview(tree, "reply") + end + end, { + buffer = bufnr, + desc = "Reply with suggestion", + nowait = keymaps.discussion_tree.reply_with_suggestion_nowait, + }) end end diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index e3a48f99..32d712a1 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -141,10 +141,7 @@ local set_keymaps = function( require("gitlab.actions.comment").confirm_create_comment(buf_text, false) else -- This should not really happen. - u.notify( - string.format("Cannot create comment with unsupported action `%s`", opts.comment_type), - vim.log.levels.ERROR - ) + u.notify(string.format("Cannot perform unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) end reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) @@ -379,17 +376,15 @@ local determine_imply_local = function(opts) old_file_name = opts.original_file_name, file_name = opts.new_file_name, }) - -- TODO: Find out if this condition is not too restrictive. + -- TODO: Find out if this condition is not too restrictive (comment on unchanged lines could be + -- shown in local file just fine). Ideally, change logic of showing comments on unchanged lines + -- from OLD to NEW version (to enable more local-file diffing). if not opts.is_new_sha then u.notify("Comment on old text. Using target-branch version", vim.log.levels.INFO) - -- TODO: Find out if this condition is not too restrictive (maybe instead check if a later comment in the thread matches "^changed this line in [version %d+ of the diff]"). - -- TODO: Rework to be able to switch between diffing against current head and original head. elseif head_differs_from_original then - -- TODO: Fix the logic of determining what version is used to create the diff, whether the local - -- file used and when this log message is shown. - u.notify("File changed since comment created. Using version on which comment was made", vim.log.levels.INFO) + u.notify("Line changed. Using version for which comment was made", vim.log.levels.INFO) elseif is_modified(opts.new_file_name) then - u.notify("File has unsaved or uncommited changes. Using feature-branch version", vim.log.levels.WARN) + u.notify("File has unsaved or uncommited changes", vim.log.levels.WARN) else return true end From 0b1c55bd8ae34049f3786976d5cab7a5c62cb335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 14 Jul 2025 09:41:00 +0200 Subject: [PATCH 66/78] refactor: rename var --- lua/gitlab/actions/comment.lua | 4 ++-- lua/gitlab/actions/discussions/init.lua | 6 ++---- lua/gitlab/actions/suggestions.lua | 10 +++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index 4e0abef5..71666737 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -311,13 +311,13 @@ M.create_comment_with_suggestion = function() return end - local original_file_name = M.location.reviewer_data.old_file_name ~= "" and M.location.reviewer_data.old_file_name + local old_file_name = M.location.reviewer_data.old_file_name ~= "" and M.location.reviewer_data.old_file_name or M.location.reviewer_data.file_name local is_new_sha = M.location.reviewer_data.new_sha_focused ---@type ShowPreviewOpts local opts = { - original_file_name = original_file_name, + old_file_name = old_file_name, new_file_name = M.location.reviewer_data.file_name, start_line = M.location.visual_range.start_line, end_line = M.location.visual_range.end_line, diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index c90787f8..07a0269c 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -279,18 +279,16 @@ M.suggestion_preview = function(tree, action) end -- Get values for preview depending on whether comment is on OLD or NEW version - local original_file_name, revision + local revision if is_new_sha then - original_file_name = root_node.file_name revision = common.commented_line_has_changed(tree, root_node) and root_node.head_sha or "HEAD" else - original_file_name = root_node.old_file_name revision = root_node.base_sha end ---@type ShowPreviewOpts local opts = { - original_file_name = original_file_name, + old_file_name = root_node.old_file_name, new_file_name = root_node.file_name, start_line = start_line, end_line = end_line, diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 32d712a1..c7b537ad 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -241,7 +241,7 @@ end ---@return string[]|nil original_lines The list of original lines. local get_original_lines = function(opts) local original_head_text = git.get_file_revision({ - file_name = opts.is_new_sha and opts.new_file_name or opts.original_file_name, + file_name = opts.is_new_sha and opts.new_file_name or opts.old_file_name, revision = opts.revision, }) -- If the original revision doesn't contain the file, the branch was possibly rebased, and the @@ -250,7 +250,7 @@ local get_original_lines = function(opts) u.notify( string.format( "File `%s` doesn't contain any text in revision `%s` for which comment was made", - opts.original_file_name, + opts.old_file_name, opts.revision ), vim.log.levels.WARN @@ -373,7 +373,7 @@ local determine_imply_local = function(opts) local head_differs_from_original = git.file_differs_in_revisions({ revision_1 = opts.revision, revision_2 = "HEAD", - old_file_name = opts.original_file_name, + old_file_name = opts.old_file_name, file_name = opts.new_file_name, }) -- TODO: Find out if this condition is not too restrictive (comment on unchanged lines could be @@ -554,7 +554,7 @@ local create_autocommands = function( end ---@class ShowPreviewOpts The options passed to the M.show_preview function. ----@field original_file_name string +---@field old_file_name string ---@field new_file_name string ---@field start_line integer ---@field end_line integer @@ -577,7 +577,7 @@ M.show_preview = function(opts) return end - local commented_file_name = opts.is_new_sha and opts.new_file_name or opts.original_file_name + local commented_file_name = opts.is_new_sha and opts.new_file_name or opts.old_file_name local original_buf_name, original_bufnr = get_temp_file_name("ORIGINAL", opts.note_node_id or "NEW_COMMENT", commented_file_name) From 2b27933f7723ef5cf164977dd3e4ddf87d075dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 16 Jul 2025 01:48:22 +0200 Subject: [PATCH 67/78] feat: add ability to apply suggestion to local file --- doc/gitlab.nvim.txt | 9 +++++---- lua/gitlab/actions/common.lua | 4 ++-- lua/gitlab/actions/discussions/init.lua | 26 +++++++++++++++++++++++-- lua/gitlab/actions/suggestions.lua | 14 +++++++++---- lua/gitlab/state.lua | 1 + 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 409d3aa1..9c6a2ef9 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -223,13 +223,14 @@ you call this function with no values the defaults will be used: toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again print_node = "p", -- Print the current node (for debugging) - edit_suggestion = "se", -- Edit suggestion comment in a new tab + edit_suggestion = "se", -- Edit comment with suggestion preview in a new tab reply_with_suggestion = "sr", -- Reply to comment with a suggestion preview in a new tab + apply_suggestion = "sa", -- Apply the suggestion to the local file with a preview in a new tab }, suggestion_preview = { - apply_changes = "ZZ", -- Post updated suggestion comment to Gitlab, close the suggestion preview tab and discard changes to local files - discard_changes = "ZQ", -- Close the suggestion preview tab and discard changes to local files - paste_default_suggestion = "glS", -- Paste the default suggestion linewise after the cursor (this overrides the "Start review" keybinding only for the "Comment" buffer) + apply_changes = "ZZ", -- Close suggestion preview tab, and post suggestion comment to Gitlab (and discard changes to local file) or "apply" changes to local file + discard_changes = "ZQ", -- Close suggestion preview tab and discard changes to local file + paste_default_suggestion = "glS", -- Paste the default suggestion below the cursor (overrides default "glS" (start review) keybinding for the "Note" buffer) }, reviewer = { disable_all = false, -- Disable all default mappings for the reviewer windows diff --git a/lua/gitlab/actions/common.lua b/lua/gitlab/actions/common.lua index 46675d6e..7810c347 100644 --- a/lua/gitlab/actions/common.lua +++ b/lua/gitlab/actions/common.lua @@ -199,7 +199,7 @@ end ---the line is not in the new SHA, returns nil ---@param node NuiTree.Node ---@return number|nil -local function get_new_line(node) +M.get_new_line = function(node) ---@type GitlabLineRange|nil local range = node.range if range == nil then @@ -327,7 +327,7 @@ M.jump_to_file = function(tree) return end vim.cmd.tabnew() - local line_number = get_new_line(root_node) or get_old_line(root_node) + local line_number = M.get_new_line(root_node) or get_old_line(root_node) if line_number == nil or line_number == 0 then line_number = 1 end diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 07a0269c..d2c1991a 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -248,7 +248,7 @@ end ---Open a new tab with a suggestion preview. ---@param tree NuiTree The current discussion tree instance. ----@param action "reply"|"edit" Reply to the current thread or edit the current comment. +---@param action "reply"|"edit"|"apply" Reply to the current thread, edit the current comment or apply the suggestion to local file. M.suggestion_preview = function(tree, action) local is_draft = M.is_draft_note(tree) if action == "reply" and is_draft then @@ -274,10 +274,24 @@ M.suggestion_preview = function(tree, action) -- Return early if comment position is missing local start_line, is_new_sha, end_line = common.get_line_number_from_node(root_node) if start_line == nil or end_line == nil then - u.notify("Couldn't get comment range. Can't build suggestion preview", vim.log.levels.ERROR) + u.notify("Couldn't get comment range. Can't create suggestion preview", vim.log.levels.ERROR) return end + -- Override reviewer values when local-applying a suggestion that was made on the OLD version + if action == "apply" and not is_new_sha then + local range = end_line - start_line + start_line = common.get_new_line(root_node) + + if start_line == nil then + u.notify("Couldn't get position in new version. Can't create suggestion preview", vim.log.levels.ERROR) + return + end + + end_line = start_line + range + is_new_sha = true + end + -- Get values for preview depending on whether comment is on OLD or NEW version local revision if is_new_sha then @@ -651,6 +665,14 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) end, { buffer = bufnr, desc = "Edit suggestion", nowait = keymaps.discussion_tree.edit_suggestion_nowait }) end + if keymaps.discussion_tree.apply_suggestion then + vim.keymap.set("n", keymaps.discussion_tree.apply_suggestion, function() + if M.is_current_node_note(tree) then + M.suggestion_preview(tree, "apply") + end + end, { buffer = bufnr, desc = "Apply suggestion", nowait = keymaps.discussion_tree.apply_suggestion_nowait }) + end + if keymaps.discussion_tree.reply_with_suggestion then vim.keymap.set("n", keymaps.discussion_tree.reply_with_suggestion, function() if M.is_current_node_note(tree) then diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index c7b537ad..e46ac189 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -139,6 +139,13 @@ local set_keymaps = function( require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) elseif opts.comment_type == "new" then require("gitlab.actions.comment").confirm_create_comment(buf_text, false) + elseif opts.comment_type == "apply" then + if imply_local then + -- Override original with current buffer contents + original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) + else + u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) + end else -- This should not really happen. u.notify(string.format("Cannot perform unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) @@ -163,7 +170,6 @@ local set_keymaps = function( }) end - -- TODO: Keymap for applying changes to the Suggestion buffer. -- TODO: Keymap for showing help on keymaps in the Comment buffer and Suggestion buffer. -- TODO: Keymap for uploading files. end @@ -380,9 +386,9 @@ local determine_imply_local = function(opts) -- shown in local file just fine). Ideally, change logic of showing comments on unchanged lines -- from OLD to NEW version (to enable more local-file diffing). if not opts.is_new_sha then - u.notify("Comment on old text. Using target-branch version", vim.log.levels.INFO) + u.notify("Comment on old text. Using target-branch version", vim.log.levels.WARN) elseif head_differs_from_original then - u.notify("Line changed. Using version for which comment was made", vim.log.levels.INFO) + u.notify("Line changed. Using version for which comment was made", vim.log.levels.WARN) elseif is_modified(opts.new_file_name) then u.notify("File has unsaved or uncommited changes", vim.log.levels.WARN) else @@ -561,7 +567,7 @@ end ---@field is_new_sha boolean ---@field revision string ---@field note_header string ----@field comment_type "reply"|"draft"|"edit"|"new" The type of comment ("reply", "draft" and "edit" come from the discussion tree, "new" from the reviewer) +---@field comment_type "apply"|"reply"|"draft"|"edit"|"new" The type of comment ("apply", "reply", "draft" and "edit" come from the discussion tree, "new" from the reviewer) ---@field note_lines string[]|nil ---@field root_node_id string ---@field note_node_id integer diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index d14bc0c4..adeff6b6 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -127,6 +127,7 @@ M.settings = { print_node = "p", edit_suggestion = "se", reply_with_suggestion = "sr", + apply_suggestion = "sa", }, suggestion_preview = { apply_changes = "ZZ", From 53c3bb38ebdffc5b14da7297272b949b1bdc48f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 17 Jul 2025 10:49:06 +0200 Subject: [PATCH 68/78] docs: use better mapping description --- lua/gitlab/actions/suggestions.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index e46ac189..515d4da6 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -155,7 +155,7 @@ local set_keymaps = function( vim.cmd.tabclose() end, { buffer = note_buf, - desc = "Post suggestion comment to Gitlab", + desc = opts.comment_type == "apply" and "Write changes to local file" or "Post suggestion comment to Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait, }) end From e9cbed177db39b39f55b2581c87929ea26df11b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 17 Jul 2025 10:50:28 +0200 Subject: [PATCH 69/78] docs: add help keymap --- lua/gitlab/actions/suggestions.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 515d4da6..def9cde5 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -97,9 +97,9 @@ local set_keymaps = function( ) local keymaps = require("gitlab.state").settings.keymaps - -- Reset suggestion buffer to original state and close preview tab - if keymaps.suggestion_preview.discard_changes then - for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do + for _, bufnr in ipairs({ note_buf, original_buf, suggestion_buf }) do + -- Reset suggestion buffer to original state and close preview tab + if keymaps.suggestion_preview.discard_changes then vim.keymap.set("n", keymaps.suggestion_preview.discard_changes, function() if vim.api.nvim_buf_is_valid(note_buf) then vim.bo[note_buf].modified = false @@ -121,6 +121,13 @@ local set_keymaps = function( nowait = keymaps.suggestion_preview.discard_changes_nowait, }) end + + if keymaps.help then + vim.keymap.set("n", keymaps.help, function() + local help = require("gitlab.actions.help") + help.open() + end, { buffer = bufnr, desc = "Open help", nowait = keymaps.help_nowait }) + end end -- Post updated suggestion note buffer to the server. @@ -170,7 +177,6 @@ local set_keymaps = function( }) end - -- TODO: Keymap for showing help on keymaps in the Comment buffer and Suggestion buffer. -- TODO: Keymap for uploading files. end From 2136a0e3ca322edb2d2499800cfdf8cc7f910536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 17 Jul 2025 10:55:12 +0200 Subject: [PATCH 70/78] fix: use mappings in all preview windows --- lua/gitlab/actions/suggestions.lua | 75 +++++++++++++++--------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index def9cde5..a0a2ce94 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -70,6 +70,7 @@ local reset_suggestion_buf = function( set_buffer_lines(suggestion_buf, original_lines, imply_local) if imply_local then pcall(vim.api.nvim_buf_del_keymap, suggestion_buf, "n", keymaps.suggestion_preview.discard_changes) + pcall(vim.api.nvim_buf_del_keymap, suggestion_buf, "n", keymaps.suggestion_preview.apply_changes) vim.api.nvim_set_option_value("winbar", original_suggestion_winbar, { scope = "local", win = suggestion_winid }) end end @@ -122,6 +123,43 @@ local set_keymaps = function( }) end + -- Post updated suggestion note buffer to the server. + if keymaps.suggestion_preview.apply_changes then + vim.keymap.set("n", keymaps.suggestion_preview.apply_changes, function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + + local buf_text = u.get_buffer_text(note_buf) + if opts.comment_type == "reply" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false, opts.root_node_id) + elseif opts.comment_type == "draft" then + require("gitlab.actions.draft_notes").confirm_edit_draft_note(opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "edit" then + require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) + elseif opts.comment_type == "new" then + require("gitlab.actions.comment").confirm_create_comment(buf_text, false) + elseif opts.comment_type == "apply" then + if imply_local then + -- Override original with current buffer contents + original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) + else + u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) + end + else + -- This should not really happen. + u.notify(string.format("Cannot perform unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) + end + + reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) + vim.cmd.tabclose() + end, { + buffer = bufnr, + desc = opts.comment_type == "apply" and "Write changes to local file" or "Post suggestion comment to Gitlab", + nowait = keymaps.suggestion_preview.apply_changes_nowait, + }) + end + if keymaps.help then vim.keymap.set("n", keymaps.help, function() local help = require("gitlab.actions.help") @@ -130,43 +168,6 @@ local set_keymaps = function( end end - -- Post updated suggestion note buffer to the server. - if keymaps.suggestion_preview.apply_changes then - vim.keymap.set("n", keymaps.suggestion_preview.apply_changes, function() - vim.api.nvim_buf_call(note_buf, function() - vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) - end) - - local buf_text = u.get_buffer_text(note_buf) - if opts.comment_type == "reply" then - require("gitlab.actions.comment").confirm_create_comment(buf_text, false, opts.root_node_id) - elseif opts.comment_type == "draft" then - require("gitlab.actions.draft_notes").confirm_edit_draft_note(opts.note_node_id, false)(buf_text) - elseif opts.comment_type == "edit" then - require("gitlab.actions.comment").confirm_edit_comment(opts.root_node_id, opts.note_node_id, false)(buf_text) - elseif opts.comment_type == "new" then - require("gitlab.actions.comment").confirm_create_comment(buf_text, false) - elseif opts.comment_type == "apply" then - if imply_local then - -- Override original with current buffer contents - original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) - else - u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) - end - else - -- This should not really happen. - u.notify(string.format("Cannot perform unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) - end - - reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) - vim.cmd.tabclose() - end, { - buffer = note_buf, - desc = opts.comment_type == "apply" and "Write changes to local file" or "Post suggestion comment to Gitlab", - nowait = keymaps.suggestion_preview.apply_changes_nowait, - }) - end - if keymaps.suggestion_preview.paste_default_suggestion then vim.keymap.set("n", keymaps.suggestion_preview.paste_default_suggestion, function() vim.api.nvim_put(default_suggestion_lines, "l", true, false) From e2872e4312ea67e01697d6bb451bf3ba9adedea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 17 Jul 2025 13:00:22 +0200 Subject: [PATCH 71/78] feat: add attach_file keybinding --- doc/gitlab.nvim.txt | 8 +++++--- lua/gitlab/actions/suggestions.lua | 10 +++++++++- lua/gitlab/state.lua | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 9c6a2ef9..c02948f9 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -548,9 +548,11 @@ emojis that you have responded with. UPLOADING FILES *gitlab.nvim.uploading-files* To attach a file to an MR description, reply, comment, and so forth use the -`keymaps.popup.perform_linewise_action` keybinding when the popup is open. -This will open a picker that will look for files in the directory you specify -in the `settings.attachment_dir` folder (this must be an absolute path). +`keymaps.popup.perform_linewise_action` keybinding when the popup is open (or +the `keymaps.suggestion_preview.attach_file` in the comment buffer of the +suggestion preview). This will open a picker that will look for files in the +directory you specify in the `settings.attachment_dir` folder (this must be an +absolute path). When you have picked the file, it will be added to the current buffer at the current line. diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index a0a2ce94..0e60f039 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -178,7 +178,15 @@ local set_keymaps = function( }) end - -- TODO: Keymap for uploading files. + if keymaps.suggestion_preview.attach_file and opts.comment_type ~= "apply" then + vim.keymap.set("n", keymaps.suggestion_preview.attach_file, function() + require("gitlab.actions.miscellaneous").attach_file() + end, { + buffer = note_buf, + desc = "Attach file", + nowait = keymaps.suggestion_preview.attach_file_nowait, + }) + end end ---Replace a range of items in a list with items from another list. diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index adeff6b6..ba46a75e 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -132,6 +132,7 @@ M.settings = { suggestion_preview = { apply_changes = "ZZ", discard_changes = "ZQ", + attach_file = "ZA", paste_default_suggestion = "glS", }, reviewer = { From cceebc8b640c449c1aed2d55576030f440a14de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Thu, 17 Jul 2025 17:40:35 +0200 Subject: [PATCH 72/78] fix: don't create directories for temp files --- lua/gitlab/actions/suggestions.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 0e60f039..baa64537 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -620,7 +620,6 @@ M.show_preview = function(opts) -- Create new tab with a temp buffer showing the original version on which the comment was -- made. - vim.fn.mkdir(vim.fn.fnamemodify(original_buf_name, ":h"), "p") vim.api.nvim_cmd({ cmd = "tabnew", args = { original_buf_name } }, {}) local original_buf = vim.api.nvim_get_current_buf() local original_winid = vim.api.nvim_get_current_win() @@ -640,7 +639,6 @@ M.show_preview = function(opts) vim.api.nvim_cmd({ cmd = split_cmd, args = { opts.new_file_name } }, {}) else local sug_buf_name = get_temp_file_name("SUGGESTION", opts.note_node_id or "NEW_COMMENT", commented_file_name) - vim.fn.mkdir(vim.fn.fnamemodify(sug_buf_name, ":h"), "p") vim.api.nvim_cmd({ cmd = split_cmd, args = { sug_buf_name } }, {}) vim.bo.bufhidden = "wipe" vim.bo.buflisted = false From ef7da2e6b82371eb10a904f355a5901f04fae45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 6 Aug 2025 20:50:43 +0200 Subject: [PATCH 73/78] docs: fix keybinding --- doc/gitlab.nvim.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index c02948f9..44d0c7ef 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -236,7 +236,7 @@ you call this function with no values the defaults will be used: disable_all = false, -- Disable all default mappings for the reviewer windows create_comment = "c", -- Create a comment for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line create_suggestion = "s", -- Create a suggestion for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line - create_suggestion_with_preview = "s", -- In a new tab create a suggestion with a diff preview for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line + create_suggestion_with_preview = "S", -- In a new tab create a suggestion with a diff preview for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line move_to_discussion_tree = "a", -- Jump to the comment in the discussion tree }, }, From 3dec6ee59fa9e62333e5286bdde6c762c1f49328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Wed, 6 Aug 2025 20:55:26 +0200 Subject: [PATCH 74/78] docs: add keybinding description --- doc/gitlab.nvim.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 44d0c7ef..5c54206c 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -230,6 +230,7 @@ you call this function with no values the defaults will be used: suggestion_preview = { apply_changes = "ZZ", -- Close suggestion preview tab, and post suggestion comment to Gitlab (and discard changes to local file) or "apply" changes to local file discard_changes = "ZQ", -- Close suggestion preview tab and discard changes to local file + attach_file = "ZA", -- Attach a file from the `settings.attachment_dir` paste_default_suggestion = "glS", -- Paste the default suggestion below the cursor (overrides default "glS" (start review) keybinding for the "Note" buffer) }, reviewer = { From e6931e0bb3bfd5566e1ddfbf94d4d9cdd7d8ed21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 23 Sep 2025 06:50:20 +0200 Subject: [PATCH 75/78] fix: don't update suggestion buffer if the text doesn't change This prevents the folding in the suggestion buffer to get off when the suggestion doesn't actually modify the original and the user types some text outside of the tripple-quoted suggestion segment. --- lua/gitlab/actions/suggestions.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index baa64537..263bdce1 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -527,7 +527,12 @@ local create_autocommands = function( local suggestion = List.new(suggestions):find(function(sug) return current_line <= sug.note_end_linenr end) - if not suggestion or suggestion == last_suggestion then + local old_buffer_text = u.get_buffer_text(suggestion_buf) + if + not suggestion + or suggestion == last_suggestion + or old_buffer_text == table.concat(suggestion.full_text, "\n") + then return end set_buffer_lines(suggestion_buf, suggestion.full_text, imply_local) From efdf5b76f1740fb4d1635fabbd1fb4f6077f534f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 23 Sep 2025 07:21:25 +0200 Subject: [PATCH 76/78] refactor: simplify checking if suggestion has changed --- lua/gitlab/actions/suggestions.lua | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 263bdce1..b5b864ed 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -516,7 +516,7 @@ local create_autocommands = function( imply_local, opts ) - local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] + local last_line = suggestions[1].note_start_linenr ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. local update_suggestion_buffer = function() @@ -527,16 +527,11 @@ local create_autocommands = function( local suggestion = List.new(suggestions):find(function(sug) return current_line <= sug.note_end_linenr end) - local old_buffer_text = u.get_buffer_text(suggestion_buf) - if - not suggestion - or suggestion == last_suggestion - or old_buffer_text == table.concat(suggestion.full_text, "\n") - then + if not suggestion or u.get_buffer_text(suggestion_buf) == table.concat(suggestion.full_text, "\n") then return end set_buffer_lines(suggestion_buf, suggestion.full_text, imply_local) - last_line, last_suggestion = current_line, suggestion + last_line = current_line refresh_signs(suggestion, note_buf) end From 7f4f61fae273d7e3a6289b200c4b5d752f0553c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 6 Oct 2025 17:24:53 +0200 Subject: [PATCH 77/78] feat: apply suggestion with new commit and resolve thread --- doc/gitlab.nvim.txt | 3 ++- lua/gitlab/actions/discussions/init.lua | 14 ++++++++-- lua/gitlab/actions/suggestions.lua | 36 ++++++++++++++++++++++--- lua/gitlab/git.lua | 32 ++++++++++++++++++++++ lua/gitlab/state.lua | 1 + 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 5c54206c..60b6bafb 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -228,9 +228,10 @@ you call this function with no values the defaults will be used: apply_suggestion = "sa", -- Apply the suggestion to the local file with a preview in a new tab }, suggestion_preview = { - apply_changes = "ZZ", -- Close suggestion preview tab, and post suggestion comment to Gitlab (and discard changes to local file) or "apply" changes to local file + apply_changes = "ZZ", -- Close suggestion preview tab, and post comment to Gitlab (discarding changes to local file). In "apply mode", accept suggestion, commit changes, then push to remote and resolve thread discard_changes = "ZQ", -- Close suggestion preview tab and discard changes to local file attach_file = "ZA", -- Attach a file from the `settings.attachment_dir` + apply_changes_locally = "Zz", -- Only in "apply mode", write suggestion buffer to local file paste_default_suggestion = "glS", -- Paste the default suggestion below the cursor (overrides default "glS" (start review) keybinding for the "Note" buffer) }, reviewer = { diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index d2c1991a..9e059249 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -313,6 +313,7 @@ M.suggestion_preview = function(tree, action) note_lines = action ~= "reply" and common.get_note_lines(tree) or nil, root_node_id = root_node.id, note_node_id = note_node_id, + tree = tree, } require("gitlab.actions.suggestions").show_preview(opts) end @@ -385,7 +386,9 @@ M.edit_comment = function(tree, unlinked) end -- This function (settings.keymaps.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) +---@param tree NuiTree +---@param override boolean|nil If not nil, set resolved to `override` value instead of toggling. +M.toggle_discussion_resolved = function(tree, override) local note = tree:get_node() if note == nil then return @@ -399,9 +402,16 @@ M.toggle_discussion_resolved = function(tree) return end + local resolved + if override ~= nil then + resolved = override + else + resolved = not note.resolved + end + local body = { discussion_id = note.id, - resolved = not note.resolved, + resolved = resolved, } job.run_job("/mr/discussions/resolve", "PUT", body, function(data) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index b5b864ed..fc3f0913 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -123,7 +123,7 @@ local set_keymaps = function( }) end - -- Post updated suggestion note buffer to the server. + -- Post suggestion note to the server. if keymaps.suggestion_preview.apply_changes then vim.keymap.set("n", keymaps.suggestion_preview.apply_changes, function() vim.api.nvim_buf_call(note_buf, function() @@ -141,10 +141,18 @@ local set_keymaps = function( require("gitlab.actions.comment").confirm_create_comment(buf_text, false) elseif opts.comment_type == "apply" then if imply_local then - -- Override original with current buffer contents original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) + if + not git.add({ filename = vim.fn.bufname(suggestion_buf) }) + or not git.commit({ commit_message = "Apply 1 suggestion to 1 file" }) + or not git.push() + then + return + end + require("gitlab.actions.discussions").toggle_discussion_resolved(opts.tree, true) else u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) + return end else -- This should not really happen. @@ -155,11 +163,32 @@ local set_keymaps = function( vim.cmd.tabclose() end, { buffer = bufnr, - desc = opts.comment_type == "apply" and "Write changes to local file" or "Post suggestion comment to Gitlab", + desc = opts.comment_type == "apply" and "Apply suggestion and resolve thread" + or "Post suggestion comment to Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait, }) end + if opts.comment_type == "apply" and keymaps.suggestion_preview.apply_changes_locally then + vim.keymap.set("n", keymaps.suggestion_preview.apply_changes_locally, function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + if imply_local then + original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) + else + u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) + return + end + reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) + vim.cmd.tabclose() + end, { + buffer = bufnr, + desc = "Write changes to local file", + nowait = keymaps.suggestion_preview.apply_changes_locally_nowait, + }) + end + if keymaps.help then vim.keymap.set("n", keymaps.help, function() local help = require("gitlab.actions.help") @@ -586,6 +615,7 @@ end ---@field note_lines string[]|nil ---@field root_node_id string ---@field note_node_id integer +---@field tree NuiTree ---Get suggestions from the current note and preview them in a new tab. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index 42d58277..73536180 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -260,4 +260,36 @@ M.file_differs_in_revisions = function(opts) return result ~= "" end +M.add = function(opts) + local _, add_err = run_system({ "git", "add", opts.filename }) + if add_err ~= nil then + require("gitlab.utils").notify("Adding changes failed: " .. add_err, vim.log.levels.ERROR) + return false + end + return true +end + +M.commit = function(opts) + local _, commit_err = run_system({ "git", "commit", "-m", opts.commit_message, "-q" }) + if commit_err ~= nil then + require("gitlab.utils").notify("Committing changes failed: " .. commit_err, vim.log.levels.ERROR) + return false + end + return true +end + +M.push = function() + local remote_branch = M.get_remote_branch() + if remote_branch == nil then + return false + end + local remote, branch = string.match(remote_branch, "([^/]+)/(.*)") + local _, push_err = run_system({ "git", "push", remote, branch }) + if push_err ~= nil then + require("gitlab.utils").notify("Pushing remote-tracking branch failed: " .. push_err, vim.log.levels.ERROR) + return false + end + return true +end + return M diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index ba46a75e..e3040f5a 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -133,6 +133,7 @@ M.settings = { apply_changes = "ZZ", discard_changes = "ZQ", attach_file = "ZA", + apply_changes_locally = "Zz", paste_default_suggestion = "glS", }, reviewer = { From 8ac996e097bc6994025f7bb09294858844ba8d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 7 Oct 2025 00:07:50 +0200 Subject: [PATCH 78/78] fix: add check that there are no staged changes --- lua/gitlab/actions/suggestions.lua | 25 ++++++++++++++----------- lua/gitlab/git.lua | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index fc3f0913..0cbba441 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -140,20 +140,23 @@ local set_keymaps = function( elseif opts.comment_type == "new" then require("gitlab.actions.comment").confirm_create_comment(buf_text, false) elseif opts.comment_type == "apply" then - if imply_local then - original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) - if - not git.add({ filename = vim.fn.bufname(suggestion_buf) }) - or not git.commit({ commit_message = "Apply 1 suggestion to 1 file" }) - or not git.push() - then - return - end - require("gitlab.actions.discussions").toggle_discussion_resolved(opts.tree, true) - else + if not imply_local then u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) return end + if git.has_staged_changes() then + u.notify("Cannot commit suggestion when there are staged changes", vim.log.levels.ERROR) + return + end + if + not git.add({ filename = vim.fn.bufname(suggestion_buf) }) + or not git.commit({ commit_message = "Apply 1 suggestion to 1 file" }) + or not git.push() + then + return + end + require("gitlab.actions.discussions").toggle_discussion_resolved(opts.tree, true) + original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) else -- This should not really happen. u.notify(string.format("Cannot perform unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index 73536180..c5c8676e 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -260,6 +260,12 @@ M.file_differs_in_revisions = function(opts) return result ~= "" end +---@class AddOpts +---@field filename string The file to stage + +---Returns true if staging succeeds, false otherwise +---@param opts AddOpts +---@return boolean M.add = function(opts) local _, add_err = run_system({ "git", "add", opts.filename }) if add_err ~= nil then @@ -269,6 +275,12 @@ M.add = function(opts) return true end +---@class CommitOpts +---@field commit_message string The commit message to include in the commit + +---Returns true if the commit succeeds, false otherwise +---@param opts CommitOpts +---@return boolean M.commit = function(opts) local _, commit_err = run_system({ "git", "commit", "-m", opts.commit_message, "-q" }) if commit_err ~= nil then @@ -278,6 +290,15 @@ M.commit = function(opts) return true end +---Returns true if there are staged changes +---@return boolean +M.has_staged_changes = function() + local result = run_system({ "git", "diff", "--staged" }) + return result ~= "" +end + +---Returns true if the push succeeds, false otherwise +---@return boolean M.push = function() local remote_branch = M.get_remote_branch() if remote_branch == nil then