From 7be16ca41d74bb16fd76eccafef6c574a5632f0a 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/74] 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 d393e519a218fd3de9b7885e3050494fdf8c2947 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/74] 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 afe311bb5785018d120b97f671bfd2d64e46a485 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/74] 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 bf52dd3a636c5ae6f02f5f223d7ecfd261789840 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/74] 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 e53695769489978c424fdefc7044a792e0264f9c 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/74] 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 9014f54ca60ddefda04fc132fc6d55788fbd3782 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/74] 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 e5a8e5db699f9955bd8337970352662ea892bb9d 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/74] 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 d1ca4d0cf3055850ba6c2d6becaa700fea9f519b 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/74] 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 1d945d7ca913eedca68175f69a382388f3b540ff 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/74] 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 b2e66f9d7d6bb837dcbf0bb4c2567f34ba385b41 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/74] 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 3b0fe258a075eefedae5f910301a613826054d2a 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/74] 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 b3c5b73507e39fcb777e3211702a651715bd1b47 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/74] 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 82d922b66e357fafcd41983bc74da09856ad168a 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/74] 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 29b3f6dae2d19fd00b19eca1cca95ab3357b69fd 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/74] 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 369752686f3d0080aa79e633e0935f8b69f71e11 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/74] 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 4be95c6ea738ed2f225eef09631bbda6ed4d0dcf 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/74] 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 d91c9b24f0e2faa8ec12743ffd4a7154a3c38203 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/74] 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 bb6756b7c95945b353d5d78209155229fad34f85 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/74] 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 631806b6004b941a0d65f52adb9f8e32a9c5ebe9 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/74] 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 629965e3383653fd292ec70ba5a107da0a97d825 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/74] 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 43f6f0a1a3b8e72b9db0aa4ad0294ab6d7ca07b1 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/74] 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 0bd9213d3bf65179b12f3ea7a51d02a80c539fb2 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/74] 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 63ed1cf4c2ae7d262af96c400b604bfeaa9a5174 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/74] 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 91f6d2f9c08b2c7a462a22034c5e55b6bb7e0c9c 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/74] 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 5fbccb53fda421bc83d60d78e1cba93b967bb4ea 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/74] 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 e1e86d197efb9d330dda18aed316fa7738988989 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/74] 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 d1c09faaf1e666e86f488fa3ecf27f76a72cbce1 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/74] 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 22f9767fb2665c9ba768047a2e9fa90bea50bfdf 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/74] 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 e237b2c3a4aca987f16c25c3abe40bd84ca8ac3a 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/74] 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 6b5a27416f0497372730258b9383135f6820afd8 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/74] 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 2010b28e03244342cbfea2257d0380c2a80cc0d4 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/74] 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 72df6af5b3c812d85bf94f83df4a0853c7376c2b 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/74] 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 720e5b946471a5a6b32da9f2d9eb530882def3c1 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/74] 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 8f23e23e30bcbfe20fbc97513f03b9dcc62f2d11 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/74] 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 462194f6a44ed0254b8f9b47e4ccd856bda04cc5 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/74] 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 1cda9e462146b956b9e79bd912a8305341de9d88 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/74] 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 a765aa5f20b513225639922bd63faaa1a7083e6d 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/74] 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 e349ca3cbfe1624b64c4d4d8cf7fdc5fcb9dac46 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/74] 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 d297666abebeb15099320688b17292dda82a11ac 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/74] 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 5e3640748e3d74342a4abaa8675febeba71f486b 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/74] 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 1325266f75485908c5e2b4d3fe1bfcd9b71e03f9 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/74] 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 207a5b123cfe71172ec205c0ae008a06bdaa70eb 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/74] 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 2f33fb5e793464c0ed296524c36cec6968bf2e8e 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/74] 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 c7ba8b776dbf4e1f230c3dd4e39e8be22e72fc45 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/74] 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 bc3662d64caad88a27c6d979c47dd5cf09b72d69 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/74] 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 da449dadf745c8586d426caabb3cce98360ed772 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/74] 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 6125e7ef60cea4780c947f011541f953357ba989 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/74] 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 f11e7e31297aee5dad384f21ae8b2d5d73133abb 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/74] 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 be0680ca0c925d7279366c7da7ccce7c7f676d08 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/74] 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 ff993284d89cc4e4bc734e2b7fb3df7a82d4f2cd 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/74] 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 8bc456d3822969875e725a2f10b7ca1f88d5e2b8 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/74] 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 48228fc300d915cbce5af622367429cc581c1355 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/74] 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 43ced3f582e43b11d274e0c957f98c9ade8fa969 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/74] 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 94699e339a685a091f5ccf5abd68d9fa4b5870ac 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/74] 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 fc978babae2fa1cfba756d5fd6559180bb705758 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/74] 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 92b661653ee06ccc0249357c348349b7fa19cd13 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/74] 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 57c50ff1c9e2fb304e343b9909cff72d7097b14a 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/74] 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 93f6bbbb48e54eee697eceeacdab6f79047e7a24 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/74] 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 f86d2924e18598329f788115aae8d38661e94692 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/74] 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 7f72ae9230476b27c21571a86af2874151d4aabe 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/74] 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 20336aea5cd884071d9084edfd15e1339031febb 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/74] 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 ca0523f2dc62d80b21d5dffdc6ae1ee8cb7b8288 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/74] 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 5ada017287b9aa7ab544ccb895571944c22e0590 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/74] 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 a9b16ce308cabd69a72ab430b1d868a1b632d13b 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/74] 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 b0a0bada754932238c4732c33ec6dd71ab882ea5 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/74] 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 e0499581b41b407cbe34757df8f92650426eac32 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/74] 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 aa8b0ae02a1247b17bb97515c4a70e89a1f55266 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/74] 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 921183038506cf43201887c84e21cc2a2b208d9c 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/74] 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 64d1ec31441bd974d2cfc0732809b938fc42716a 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/74] 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 2219a8b9b7123372455429e9afcced7df27d62f6 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/74] 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 0fb77ce2ca5a918437ad691326b0e37808245d3f 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/74] 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 8ea62ef822714125fb7b061fab45fbcfb6b6d908 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/74] 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 437aefa6dc62af8f6898cd02c127b90c5f00e9a4 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/74] 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 d870050a6b39ad8489ce1599f5ed7212c5774413 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/74] 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 = {