From e7e6f1f56553b7b1232622873abb604a1adcf9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Sat, 1 Mar 2025 19:35:01 +0100 Subject: [PATCH 01/80] Fix: Make publishing drafts more robust (#483) --- lua/gitlab/job.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lua/gitlab/job.lua b/lua/gitlab/job.lua index 128591be..33308210 100644 --- a/lua/gitlab/job.lua +++ b/lua/gitlab/job.lua @@ -4,7 +4,7 @@ local Job = require("plenary.job") local u = require("gitlab.utils") local M = {} -M.run_job = function(endpoint, method, body, callback) +M.run_job = function(endpoint, method, body, callback, on_error_callback) local state = require("gitlab.state") local args = { "-s", "-X", (method or "POST"), string.format("localhost:%s", state.settings.port) .. endpoint } @@ -16,7 +16,8 @@ M.run_job = function(endpoint, method, body, callback) -- This handler will handle all responses from the Go server. Anything with a successful -- status will call the callback (if it is supplied for the job). Otherwise, it will print out the - -- success message or error message and details from the Go server. + -- success message or error message and details from the Go server and run the on_error_callback + -- (if supplied for the job). local stderr = {} Job:new({ command = "curl", @@ -53,6 +54,9 @@ M.run_job = function(endpoint, method, body, callback) -- Handle error case local message = string.format("%s: %s", data.message, data.details) u.notify(message, vim.log.levels.ERROR) + if on_error_callback then + on_error_callback(data) + end end end, 0) end, From 0f74fc7e12908e6c4f53c7af29d6c67e81d8ab5d 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 02/80] 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 2db9f633..c76a8dc2 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 30140c7c..2e1570d0 100644 --- a/lua/gitlab/actions/common.lua +++ b/lua/gitlab/actions/common.lua @@ -173,6 +173,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 @@ -253,17 +274,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 dc0526e3..8b3df92c 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") @@ -251,6 +250,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" }, { @@ -294,15 +302,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) @@ -597,6 +597,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 d67e0c06..3b42456d 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 e146397751fc16d75bfa9ef1b1b21e254feb3c54 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 03/80] 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 a1ccc33ebae8a306e548338c7a5d93e560ad5670 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 04/80] 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 719c48f7..c88b33a2 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 94f7508c6b6d865d9489e0bbe0274900a7e050d6 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 05/80] 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 509e1150c5620b787045d81b3e45d9e7a1f4ed9d 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 06/80] 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 911a360e5ef734b305a70eed5755615ae2b866e0 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 07/80] 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 da4640c1eba9a603b636aa4767368dd9b4d92b3e 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 08/80] 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 ad6f1f8f4fb338be86de6c36b2de0ca67a55460e 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 09/80] 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 4b998854831b476d863e9954254f6c549a7f441e 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 10/80] 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 4353c31dfd8457cde8fa8e1ca2caa317e99cc653 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 11/80] 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 f3579eb2e544da3d442ff2faa9cd00e800f7c477 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 12/80] 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 4a24a3f353ebb9b8325407d050c054cfe65a0c08 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 13/80] 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 f5b0851425f29ead42fbeaeb435deb4b89bdc3fa 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 14/80] 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 78f60d999f604aa0455f251425539cf66b764324 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 15/80] 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 939ca8555a6eb3fa9ca65f7d8f5c785590a2da6a 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 16/80] 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 9a4d1640e3e7953970492d001555427fd5f76549 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 17/80] 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 b1f0f879bc10d2bcd56c7ce6c7a8f134a3cdbd14 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 18/80] 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 0fa4333ab82b89d8fb9296590a9a94be1263b28b 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 19/80] 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 d604537a76ccb0b1c1b5a53df99b067545fc62ec 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 20/80] 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 9daa3e48423ca79c2d51abbadf42c988f09af16d 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 21/80] 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 07f55c858ff9b183bfd4d6ec212abcaad5d797e4 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 22/80] 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 8a17a931548101081adfad44274e6a8aba9aab48 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 23/80] 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 5b84434b1a88281d70bfc79b96a6cb09347fd357 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 24/80] 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 8b3df92c..e4b8bb17 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -253,10 +253,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 687383c358c7f5382f5bcb5eb04372227aaf81e5 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 25/80] 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 f94307d17b71103a1f12cd8f7f6c65c512d06679 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 26/80] 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 0e15c4e269cd369cd5e260195d905cd25ec6e490 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 27/80] 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 a219aebd6f0cebbecc0d5c62a4fab73e3369b6d3 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 28/80] 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 872cc1f2f2ff5aaa77018ff62cc08360dfe0b2bf 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 29/80] 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 6a1d05a0a6d0734c3508953e5d202d2e935e6715 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 30/80] 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 5321a4bade62ef265f024d7f0dd4f85e2ef61392 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 31/80] 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 d9b9960cf4b64ae32e1280816d7fdb883e2710a8 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 32/80] 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 623586fecdc7729d2f08c2b5d4a8af496f7f30ad 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 33/80] 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 cde6ab9d343ecf8dadd0fdfc6717b5a991d1bde8 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 34/80] 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 f48953395c4d438fa6a66707641c5e0d4ea57b9a 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 35/80] 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 c76a8dc2..00c32301 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 e4b8bb17..2f13f001 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -251,9 +251,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 @@ -595,12 +595,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 3b42456d..a11f0b92 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 1bdb2c3b14bd338c3e93c0927d3fadff04c9be33 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 36/80] 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 00c32301..e98480c4 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 a11f0b92..9c51cbe3 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 dd7efa301a2f021c03c7e451b3d643323149bf41 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 37/80] 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 753115311811bf3b8fc90e924a48157fa93344cf 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 38/80] 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 e98480c4..2fe69388 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 9c51cbe3..3db1a6de 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 9f1bb57e9b055d80db09d20c77c2666f83225ad5 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 39/80] 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 f65c6634fa41b14bae550448ea2b97aa1c9d3fca 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 40/80] 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 86cc49323f07098faea932bc65e9db12ceaf2454 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 41/80] 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 2f13f001..e0393ecc 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -244,6 +244,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 9c6c21bf140ec60c6432247f5f52019a47a6a091 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 42/80] 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 2fe69388..71cff46c 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 e0393ecc..c2193ef3 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -251,7 +251,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) @@ -604,6 +614,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 3db1a6de..2fbc24f9 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 69975933cc6f85714b156f8bbdf7bfd79d592f2c 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 43/80] 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 c2193ef3..b502d32c 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -834,6 +834,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 fd46a3f150b57ff4157fe91fc28f857cdc11f8c0 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 44/80] 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 b69e43603107d94fcd69e94e4be4222b391c2fcd 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 45/80] 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 71cff46c..d0f6eaa9 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 8c52c73b..a56f1885 100644 --- a/lua/gitlab/init.lua +++ b/lua/gitlab/init.lua @@ -75,6 +75,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 76d24f32..451b0817 100644 --- a/lua/gitlab/reviewer/init.lua +++ b/lua/gitlab/reviewer/init.lua @@ -431,6 +431,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 2fbc24f9..18f581ed 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 93c199cc394402f7279aad1e22ee57e55b4c1922 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 46/80] 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 9015c84dd3a6aad13747ab83a066aaba5e95624e 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 47/80] 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 73d0a8600fa92bb96eb116563a443c02bca8b112 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 48/80] 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 d73a4c4a281d1903b02b7e77eb166f2fb1f21d89 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 49/80] 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 28cf95228a893a8865bdd599653aa0604dec76e0 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 50/80] 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 b502d32c..3f566996 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -251,20 +251,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 @@ -609,7 +648,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 @@ -617,7 +656,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 2ae1eab9889cf3ec02dd273408f5eca3146758eb 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 51/80] 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 d0f6eaa9..a708ba6b 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 3f566996..19fa9b94 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -254,7 +254,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) @@ -277,6 +278,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) @@ -296,7 +298,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, @@ -653,6 +655,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 18f581ed..0e5dd5a7 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 f7625618bcfd7da1471942dcebec9d2175c7b983 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 52/80] 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 19fa9b94..4667d046 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -271,12 +271,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 e4b653ad3aede9d238b5b83ee7288f3cca00d571 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 53/80] 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 58b6288950b1aefe846190c113e723b49df6d991 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 54/80] 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 a80134ca5e001f3abe9cbeb8191a8d0142572764 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 55/80] 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 957c3a7a269096922cc473676d0b7be5041c6f37 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 56/80] 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 4667d046..9ad52c55 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -654,17 +654,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 @@ -879,7 +891,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 54c55d4e02e4dd32f5dcef5e0561c561e0befe77 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 57/80] 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 dfa40f340737c6964758a2c13d00b01c68ad0561 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 58/80] 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 6a2d2950d436dee42ad763cd07e29d1c444e9b6f 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 59/80] 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 17bd0f006cc27c6f18a6ce3618dcaf26e1614d82 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 60/80] 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 bae89397d6f47487d5214038cd671553b7bc6d89 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 61/80] 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 b9e77d6259057d3eaa9a3f4045f64346797059b8 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 62/80] 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 a708ba6b..f1b28a5b 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 2e1570d0..6c44e211 100644 --- a/lua/gitlab/actions/common.lua +++ b/lua/gitlab/actions/common.lua @@ -342,4 +342,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 9ad52c55..bcc6d24c 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -254,8 +254,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) @@ -266,35 +265,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, @@ -649,18 +657,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 0e5dd5a7..18f581ed 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 37cd4dfba67949ec8f2e770c003c15d77a6b9128 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 63/80] 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 089876973c3f6fe2b93f0a451f5c8971a4f22699 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 64/80] 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 61a5c8941746822b75b663d46f2934dba3969805 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 65/80] 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 fd00017a599e966bb7ca4b9fd502dfc263bb9aed 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 66/80] 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 bcc6d24c..5fac27ea 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -658,20 +658,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 f1b56ce8917ccfadf1791fe0eae89877cb6ff1a8 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 67/80] 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 5fac27ea..1e30dce2 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -284,18 +284,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 b5e29c7615e103af442c58e18a9ce81d4d2e8898 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 68/80] 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 f1b28a5b..ebe2b683 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 6c44e211..44386639 100644 --- a/lua/gitlab/actions/common.lua +++ b/lua/gitlab/actions/common.lua @@ -198,7 +198,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 @@ -326,7 +326,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 1e30dce2..48d5b56a 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -253,7 +253,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 @@ -279,10 +279,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 @@ -655,6 +669,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 18f581ed..988b8820 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 443bd42b3a76adf7008820bebc43e0847f11269c 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 69/80] 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 f1588f7896cda943d95cbe14b852b3de480c0515 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 70/80] 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 edc4e85e1ff2a23fc468bd05c69ef7ce5b7974f3 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 71/80] 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 927cd036568513e7959882f25724113f25fc9f5b 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 72/80] 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 ebe2b683..072eb4be 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -554,9 +554,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 988b8820..61f3ab82 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 bb08580f13316371228f5c1b44fa143b3bd81a3d 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 73/80] 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 3ac71fe7e4055bd9a492da8649ef2d70c4bd9523 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 74/80] 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 072eb4be..bf0dd2b1 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 cc2ea6e7fa616d1577bf7c910183e0f8e2b68249 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 75/80] 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 bf0dd2b1..34e1de0f 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -230,6 +230,7 @@ you call this function with no values the defaults will be used: suggestion_preview = { apply_changes = "ZZ", -- Close suggestion preview tab, and post suggestion comment to Gitlab (and discard changes to local file) or "apply" changes to local file discard_changes = "ZQ", -- Close suggestion preview tab and discard changes to local file + attach_file = "ZA", -- Attach a file from the `settings.attachment_dir` paste_default_suggestion = "glS", -- Paste the default suggestion below the cursor (overrides default "glS" (start review) keybinding for the "Note" buffer) }, reviewer = { From 8c5a33d954c03c767a815e04efdfa51531bf225d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 23 Sep 2025 06:50:20 +0200 Subject: [PATCH 76/80] fix: don't update suggestion buffer if the text doesn't change This prevents the folding in the suggestion buffer to get off when the suggestion doesn't actually modify the original and the user types some text outside of the tripple-quoted suggestion segment. --- lua/gitlab/actions/suggestions.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index baa64537..263bdce1 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -527,7 +527,12 @@ local create_autocommands = function( local suggestion = List.new(suggestions):find(function(sug) return current_line <= sug.note_end_linenr end) - if not suggestion or suggestion == last_suggestion then + local old_buffer_text = u.get_buffer_text(suggestion_buf) + if + not suggestion + or suggestion == last_suggestion + or old_buffer_text == table.concat(suggestion.full_text, "\n") + then return end set_buffer_lines(suggestion_buf, suggestion.full_text, imply_local) From 3fffa43ce333569022deb3e0d3aa50c01cc52ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 23 Sep 2025 07:21:25 +0200 Subject: [PATCH 77/80] refactor: simplify checking if suggestion has changed --- lua/gitlab/actions/suggestions.lua | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index 263bdce1..b5b864ed 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -516,7 +516,7 @@ local create_autocommands = function( imply_local, opts ) - local last_line, last_suggestion = suggestions[1].note_start_linenr, suggestions[1] + local last_line = suggestions[1].note_start_linenr ---Update the suggestion buffer if the selected suggestion changes in the Comment buffer. local update_suggestion_buffer = function() @@ -527,16 +527,11 @@ local create_autocommands = function( local suggestion = List.new(suggestions):find(function(sug) return current_line <= sug.note_end_linenr end) - local old_buffer_text = u.get_buffer_text(suggestion_buf) - if - not suggestion - or suggestion == last_suggestion - or old_buffer_text == table.concat(suggestion.full_text, "\n") - then + if not suggestion or u.get_buffer_text(suggestion_buf) == table.concat(suggestion.full_text, "\n") then return end set_buffer_lines(suggestion_buf, suggestion.full_text, imply_local) - last_line, last_suggestion = current_line, suggestion + last_line = current_line refresh_signs(suggestion, note_buf) end From 11d1c58a028be50b259bfc41e00764bae129ba63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Mon, 6 Oct 2025 17:24:53 +0200 Subject: [PATCH 78/80] feat: apply suggestion with new commit and resolve thread --- doc/gitlab.nvim.txt | 3 ++- lua/gitlab/actions/discussions/init.lua | 14 ++++++++-- lua/gitlab/actions/suggestions.lua | 36 ++++++++++++++++++++++--- lua/gitlab/git.lua | 32 ++++++++++++++++++++++ lua/gitlab/state.lua | 1 + 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 34e1de0f..1f15188c 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -228,9 +228,10 @@ you call this function with no values the defaults will be used: apply_suggestion = "sa", -- Apply the suggestion to the local file with a preview in a new tab }, suggestion_preview = { - apply_changes = "ZZ", -- Close suggestion preview tab, and post suggestion comment to Gitlab (and discard changes to local file) or "apply" changes to local file + apply_changes = "ZZ", -- Close suggestion preview tab, and post comment to Gitlab (discarding changes to local file). In "apply mode", accept suggestion, commit changes, then push to remote and resolve thread discard_changes = "ZQ", -- Close suggestion preview tab and discard changes to local file attach_file = "ZA", -- Attach a file from the `settings.attachment_dir` + apply_changes_locally = "Zz", -- Only in "apply mode", write suggestion buffer to local file paste_default_suggestion = "glS", -- Paste the default suggestion below the cursor (overrides default "glS" (start review) keybinding for the "Note" buffer) }, reviewer = { diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 48d5b56a..d253e354 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -318,6 +318,7 @@ M.suggestion_preview = function(tree, action) note_lines = action ~= "reply" and common.get_note_lines(tree) or nil, root_node_id = root_node.id, note_node_id = note_node_id, + tree = tree, } require("gitlab.actions.suggestions").show_preview(opts) end @@ -390,7 +391,9 @@ M.edit_comment = function(tree, unlinked) end -- This function (settings.keymaps.discussion_tree.toggle_discussion_resolved) will toggle the resolved status of the current discussion and send the change to the Go server -M.toggle_discussion_resolved = function(tree) +---@param tree NuiTree +---@param override boolean|nil If not nil, set resolved to `override` value instead of toggling. +M.toggle_discussion_resolved = function(tree, override) local note = tree:get_node() if note == nil then return @@ -404,9 +407,16 @@ M.toggle_discussion_resolved = function(tree) return end + local resolved + if override ~= nil then + resolved = override + else + resolved = not note.resolved + end + local body = { discussion_id = note.id, - resolved = not note.resolved, + resolved = resolved, } job.run_job("/mr/discussions/resolve", "PUT", body, function(data) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index b5b864ed..fc3f0913 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -123,7 +123,7 @@ local set_keymaps = function( }) end - -- Post updated suggestion note buffer to the server. + -- Post suggestion note to the server. if keymaps.suggestion_preview.apply_changes then vim.keymap.set("n", keymaps.suggestion_preview.apply_changes, function() vim.api.nvim_buf_call(note_buf, function() @@ -141,10 +141,18 @@ local set_keymaps = function( require("gitlab.actions.comment").confirm_create_comment(buf_text, false) elseif opts.comment_type == "apply" then if imply_local then - -- Override original with current buffer contents original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) + if + not git.add({ filename = vim.fn.bufname(suggestion_buf) }) + or not git.commit({ commit_message = "Apply 1 suggestion to 1 file" }) + or not git.push() + then + return + end + require("gitlab.actions.discussions").toggle_discussion_resolved(opts.tree, true) else u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) + return end else -- This should not really happen. @@ -155,11 +163,32 @@ local set_keymaps = function( vim.cmd.tabclose() end, { buffer = bufnr, - desc = opts.comment_type == "apply" and "Write changes to local file" or "Post suggestion comment to Gitlab", + desc = opts.comment_type == "apply" and "Apply suggestion and resolve thread" + or "Post suggestion comment to Gitlab", nowait = keymaps.suggestion_preview.apply_changes_nowait, }) end + if opts.comment_type == "apply" and keymaps.suggestion_preview.apply_changes_locally then + vim.keymap.set("n", keymaps.suggestion_preview.apply_changes_locally, function() + vim.api.nvim_buf_call(note_buf, function() + vim.api.nvim_cmd({ cmd = "write", mods = { silent = true } }, {}) + end) + if imply_local then + original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) + else + u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) + return + end + reset_suggestion_buf(imply_local, suggestion_buf, original_lines, original_suggestion_winbar, suggestion_winid) + vim.cmd.tabclose() + end, { + buffer = bufnr, + desc = "Write changes to local file", + nowait = keymaps.suggestion_preview.apply_changes_locally_nowait, + }) + end + if keymaps.help then vim.keymap.set("n", keymaps.help, function() local help = require("gitlab.actions.help") @@ -586,6 +615,7 @@ end ---@field note_lines string[]|nil ---@field root_node_id string ---@field note_node_id integer +---@field tree NuiTree ---Get suggestions from the current note and preview them in a new tab. ---@param opts ShowPreviewOpts The options passed to the M.show_preview function. diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index 42d58277..73536180 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -260,4 +260,36 @@ M.file_differs_in_revisions = function(opts) return result ~= "" end +M.add = function(opts) + local _, add_err = run_system({ "git", "add", opts.filename }) + if add_err ~= nil then + require("gitlab.utils").notify("Adding changes failed: " .. add_err, vim.log.levels.ERROR) + return false + end + return true +end + +M.commit = function(opts) + local _, commit_err = run_system({ "git", "commit", "-m", opts.commit_message, "-q" }) + if commit_err ~= nil then + require("gitlab.utils").notify("Committing changes failed: " .. commit_err, vim.log.levels.ERROR) + return false + end + return true +end + +M.push = function() + local remote_branch = M.get_remote_branch() + if remote_branch == nil then + return false + end + local remote, branch = string.match(remote_branch, "([^/]+)/(.*)") + local _, push_err = run_system({ "git", "push", remote, branch }) + if push_err ~= nil then + require("gitlab.utils").notify("Pushing remote-tracking branch failed: " .. push_err, vim.log.levels.ERROR) + return false + end + return true +end + return M diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 61f3ab82..438bf4f6 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -133,6 +133,7 @@ M.settings = { apply_changes = "ZZ", discard_changes = "ZQ", attach_file = "ZA", + apply_changes_locally = "Zz", paste_default_suggestion = "glS", }, reviewer = { From 6f2415903f3dce119b6f3275b96fffbad900d06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 7 Oct 2025 00:07:50 +0200 Subject: [PATCH 79/80] fix: add check that there are no staged changes --- lua/gitlab/actions/suggestions.lua | 25 ++++++++++++++----------- lua/gitlab/git.lua | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lua/gitlab/actions/suggestions.lua b/lua/gitlab/actions/suggestions.lua index fc3f0913..0cbba441 100644 --- a/lua/gitlab/actions/suggestions.lua +++ b/lua/gitlab/actions/suggestions.lua @@ -140,20 +140,23 @@ local set_keymaps = function( elseif opts.comment_type == "new" then require("gitlab.actions.comment").confirm_create_comment(buf_text, false) elseif opts.comment_type == "apply" then - if imply_local then - original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) - if - not git.add({ filename = vim.fn.bufname(suggestion_buf) }) - or not git.commit({ commit_message = "Apply 1 suggestion to 1 file" }) - or not git.push() - then - return - end - require("gitlab.actions.discussions").toggle_discussion_resolved(opts.tree, true) - else + if not imply_local then u.notify("Cannot apply temp-file preview to local file.", vim.log.levels.ERROR) return end + if git.has_staged_changes() then + u.notify("Cannot commit suggestion when there are staged changes", vim.log.levels.ERROR) + return + end + if + not git.add({ filename = vim.fn.bufname(suggestion_buf) }) + or not git.commit({ commit_message = "Apply 1 suggestion to 1 file" }) + or not git.push() + then + return + end + require("gitlab.actions.discussions").toggle_discussion_resolved(opts.tree, true) + original_lines = vim.api.nvim_buf_get_lines(suggestion_buf, 0, -1, false) else -- This should not really happen. u.notify(string.format("Cannot perform unsupported action `%s`", opts.comment_type), vim.log.levels.ERROR) diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua index 73536180..c5c8676e 100644 --- a/lua/gitlab/git.lua +++ b/lua/gitlab/git.lua @@ -260,6 +260,12 @@ M.file_differs_in_revisions = function(opts) return result ~= "" end +---@class AddOpts +---@field filename string The file to stage + +---Returns true if staging succeeds, false otherwise +---@param opts AddOpts +---@return boolean M.add = function(opts) local _, add_err = run_system({ "git", "add", opts.filename }) if add_err ~= nil then @@ -269,6 +275,12 @@ M.add = function(opts) return true end +---@class CommitOpts +---@field commit_message string The commit message to include in the commit + +---Returns true if the commit succeeds, false otherwise +---@param opts CommitOpts +---@return boolean M.commit = function(opts) local _, commit_err = run_system({ "git", "commit", "-m", opts.commit_message, "-q" }) if commit_err ~= nil then @@ -278,6 +290,15 @@ M.commit = function(opts) return true end +---Returns true if there are staged changes +---@return boolean +M.has_staged_changes = function() + local result = run_system({ "git", "diff", "--staged" }) + return result ~= "" +end + +---Returns true if the push succeeds, false otherwise +---@return boolean M.push = function() local remote_branch = M.get_remote_branch() if remote_branch == nil then From d50ac711e1b695e40a6b709fd0632db4a2dbd8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Fri, 24 Oct 2025 17:00:54 +0200 Subject: [PATCH 80/80] docs: improve suggestion keymaps descriptions --- doc/gitlab.nvim.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index 1f15188c..8bb7c3c9 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -229,9 +229,9 @@ you call this function with no values the defaults will be used: }, suggestion_preview = { apply_changes = "ZZ", -- Close suggestion preview tab, and post comment to Gitlab (discarding changes to local file). In "apply mode", accept suggestion, commit changes, then push to remote and resolve thread - discard_changes = "ZQ", -- Close suggestion preview tab and discard changes to local file + discard_changes = "ZQ", -- Close suggestion preview tab and discard changes in local file attach_file = "ZA", -- Attach a file from the `settings.attachment_dir` - apply_changes_locally = "Zz", -- Only in "apply mode", write suggestion buffer to local file + apply_changes_locally = "Zz", -- Only in "apply mode", close suggestion preview tab and write suggestion buffer to local file (no changes posted to Gitlab) paste_default_suggestion = "glS", -- Paste the default suggestion below the cursor (overrides default "glS" (start review) keybinding for the "Note" buffer) }, reviewer = {