Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
7be16ca
feat: add first draft of suggestion preview
jakubbortlik May 30, 2025
d393e51
fix: don't attempt placing diagnostics on diffview NULL buffer
jakubbortlik May 30, 2025
afe311b
docs: mark parameter as optional
jakubbortlik May 30, 2025
bf52dd3
fix: go to note in existing tab
jakubbortlik May 30, 2025
e536957
refactor: don't use plain tabnew as it creates empty buffer
jakubbortlik May 31, 2025
9014f54
refactor: make functions local
jakubbortlik May 31, 2025
e5a8e5d
docs: add some docstrings
jakubbortlik May 31, 2025
d1ca4d0
fix: add base_sha to draft comments
jakubbortlik Jun 2, 2025
1d945d7
fix: use old path when comment is on OLD_SHA
jakubbortlik Jun 2, 2025
b2e66f9
docs: add TODO
jakubbortlik Jun 2, 2025
3b0fe25
docs: update comment
jakubbortlik Jun 2, 2025
b3c5b73
fix: improve checking whether local file should be used for suggestions
jakubbortlik Jun 2, 2025
82d922b
refactor: simplify imply_local usage
jakubbortlik Jun 3, 2025
29b3f6d
docs: update docs
jakubbortlik Jun 3, 2025
3697526
feat: enable updating suggestion comments from the preview
jakubbortlik Jun 3, 2025
4be95c6
refactor: move more keymap definitions to set_keymaps function
jakubbortlik Jun 3, 2025
d91c9b2
style: format file
jakubbortlik Jun 3, 2025
bb6756b
refactor: create autocommands in a separate function
jakubbortlik Jun 4, 2025
631806b
fix: make note buffer nomodified when discarding changes
jakubbortlik Jun 4, 2025
629965e
fix: validate buffer number before accessing it
jakubbortlik Jun 5, 2025
43f6f0a
fix: split horizontally on narrow screen
jakubbortlik Jun 5, 2025
0bd9213
fix: move virtual lines left (and up)
jakubbortlik Jun 5, 2025
63ed1cf
refactor: pass only tree to show_preview()
jakubbortlik Jun 5, 2025
91f6d2f
fix: check if suggestion preview already exists for given note
jakubbortlik Jun 6, 2025
5fbccb5
docs: update function annotations
jakubbortlik Jun 6, 2025
e1e86d1
refactor: add full text to suggestions
jakubbortlik Jun 6, 2025
d1c09fa
fix: make imply_local local
jakubbortlik Jun 6, 2025
22f9767
feat: edit suggestions for comments without suggestions
jakubbortlik Jun 7, 2025
e237b2c
refactor: determine imply_local in separate function
jakubbortlik Jun 7, 2025
6b5a274
fix: prevent error when there are multiple endquotes without a corres…
jakubbortlik Jun 7, 2025
2010b28
refactor: get original lines in seprate function
jakubbortlik Jun 7, 2025
72df6af
fix: show error when suggestion start is before first line of file
jakubbortlik Jun 7, 2025
720e5b9
fix: convert string to number when editing root node
jakubbortlik Jun 7, 2025
8f23e23
refactor rename preview suggestion to edit suggestion
jakubbortlik Jun 7, 2025
462194f
fix: unify keymap setting pattern with popups
jakubbortlik Jun 7, 2025
1cda9e4
docs: remove outdated comment
jakubbortlik Jun 7, 2025
a765aa5
feat: add keymap for pasting default suggestion
jakubbortlik Jun 7, 2025
e349ca3
fix: update suggestions on CursorMoved and CursorMovedI
jakubbortlik Jun 7, 2025
d297666
docs: update comment about winbar
jakubbortlik Jun 7, 2025
5e36407
docs: add TODOs
jakubbortlik Jun 8, 2025
1325266
feat: enable replying to comments in the suggestion preview
jakubbortlik Jun 9, 2025
207a5b1
feat: show draft mode in note header
jakubbortlik Jun 9, 2025
2f33fb5
fix: enable updating draft replies
jakubbortlik Jun 9, 2025
c7ba8b7
feat: add possibility to create suggestions with preview from the rev…
jakubbortlik Jun 10, 2025
bc3662d
docs: fix info about using feature branch
jakubbortlik Jun 11, 2025
da449da
docs: use simpler info messages
jakubbortlik Jun 13, 2025
6125e7e
fix: check is_reply first
jakubbortlik Jun 13, 2025
f11e7e3
refactor: simplify variable names
jakubbortlik Jun 13, 2025
be0680c
refactor: use ShowPreviewOpts
jakubbortlik Jun 13, 2025
ff99328
feat: add mapping for previewing suggestion with head_sha revision
jakubbortlik Jun 18, 2025
8bc456d
fix: add head_sha to root_node of draft notes
jakubbortlik Jun 20, 2025
48228fc
feat: replace extmarks by winbar
jakubbortlik Jun 20, 2025
43ced3f
feat: add winbar to suggestion window
jakubbortlik Jun 20, 2025
94699e3
feat: add winbar to orignial buffer
jakubbortlik Jun 20, 2025
fc978ba
style: apply stylua
jakubbortlik Jun 20, 2025
92b6616
fix: refresh LSP diagnostics in suggestion buffer hen settings buffer…
jakubbortlik Jul 11, 2025
57c50ff
docs: make error message more informative
jakubbortlik Jul 11, 2025
93f6bbb
docs: add note why changing modified option
jakubbortlik Jul 11, 2025
f86d292
fix: reset suggestion buffer before closing
jakubbortlik Jul 11, 2025
7f72ae9
style: apply stylua
jakubbortlik Jul 11, 2025
20336ae
fix: automatically choose head_sha if file has changed
jakubbortlik Jul 12, 2025
ca0523f
fix: remove unnecessary check
jakubbortlik Jul 12, 2025
5ada017
fix: don't reset temporary suggestion buffer before closing preview
jakubbortlik Jul 12, 2025
a9b16ce
fix: recompute folds in suggestion buffer on TextChangedI
jakubbortlik Jul 12, 2025
b0a0bad
docs: improve messages to user
jakubbortlik Jul 14, 2025
e049958
refactor: rename var
jakubbortlik Jul 14, 2025
aa8b0ae
feat: add ability to apply suggestion to local file
jakubbortlik Jul 15, 2025
9211830
docs: use better mapping description
jakubbortlik Jul 17, 2025
64d1ec3
docs: add help keymap
jakubbortlik Jul 17, 2025
2219a8b
fix: use mappings in all preview windows
jakubbortlik Jul 17, 2025
0fb77ce
feat: add attach_file keybinding
jakubbortlik Jul 17, 2025
8ea62ef
fix: don't create directories for temp files
jakubbortlik Jul 17, 2025
437aefa
docs: fix keybinding
jakubbortlik Aug 6, 2025
d870050
docs: add keybinding description
jakubbortlik Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions doc/gitlab.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,21 @@ you call this function with no values the defaults will be used:
toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
refresh_data = "<C-R>", -- Refresh the data in the view by hitting Gitlab's APIs again
print_node = "<leader>p", -- Print the current node (for debugging)
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", -- 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 = {
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
},
},
Expand Down Expand Up @@ -539,9 +549,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.
Expand Down
42 changes: 38 additions & 4 deletions lua/gitlab/actions/comment.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,25 @@ 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
---@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
end

local is_draft = M.draft_popup and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr))
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
Expand Down Expand Up @@ -188,13 +195,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)

Expand Down Expand Up @@ -295,6 +302,33 @@ 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

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 = {
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,
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
---@param must_be_visual boolean True if current mode must be visual
---@return boolean
Expand Down
60 changes: 55 additions & 5 deletions lua/gitlab/actions/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,32 @@ 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
---@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
Expand Down Expand Up @@ -254,17 +275,19 @@ end
---@param root_node NuiTree.Node
---@return integer|nil line_number
---@return boolean is_new_sha True if line number refers to NEW SHA
---@return integer|nil end_line
M.get_line_number_from_node = function(root_node)
if root_node.range then
local line_number, _, is_new_sha = M.get_line_numbers_for_range(
local line_number, end_line, is_new_sha = M.get_line_numbers_for_range(
root_node.old_line,
root_node.new_line,
root_node.range.start.line_code,
root_node.range["end"].line_code
)
return line_number, is_new_sha
return line_number, is_new_sha, end_line
else
return M.get_line_number(root_node.id)
local start_line, is_new_sha = M.get_line_number(root_node.id)
return start_line, is_new_sha, start_line
end
end

Expand Down Expand Up @@ -304,7 +327,7 @@ M.jump_to_file = function(tree)
return
end
vim.cmd.tabnew()
local line_number = get_new_line(root_node) or get_old_line(root_node)
local line_number = M.get_new_line(root_node) or get_old_line(root_node)
if line_number == nil or line_number == 0 then
line_number = 1
end
Expand All @@ -320,4 +343,31 @@ M.jump_to_file = function(tree)
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
end

---Determine whether commented line has changed since making the comment.
---@param tree NuiTree The current discussion tree instance.
---@param note_node NuiTree.Node The main node of the note containing the note author etc.
---@return boolean line_changed True if any of the notes in the thread is a system note starting with "changed this line".
M.commented_line_has_changed = function(tree, note_node)
local line_changed = List.new(note_node:get_child_ids()):includes(function(child_id)
local child_node = tree:get_node(child_id)
if child_node == nil then
return false
end

-- Inspect note bodies or recourse to child notes.
if child_node.type == "note_body" then
local line = tree:get_node(child_id).text
if string.match(line, "^changed this line") and note_node.system then
return true
end
elseif child_node.type == "note" and M.commented_line_has_changed(tree, child_node) then
return true
end

return false
end)

return line_changed
end

return M
115 changes: 105 additions & 10 deletions lua/gitlab/actions/discussions/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -240,12 +239,84 @@ 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,
})

layout:mount()
end

---Open a new tab with a suggestion preview.
---@param tree NuiTree The current discussion tree instance.
---@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
u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN)
return
end

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)

-- 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)
if start_line == nil or end_line == nil then
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
revision = common.commented_line_has_changed(tree, root_node) and root_node.head_sha or "HEAD"
else
revision = root_node.base_sha
end

---@type ShowPreviewOpts
local opts = {
old_file_name = 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 = revision,
note_header = note_node.text,
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,
}
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
M.delete_comment = function(tree, unlinked)
vim.ui.select({ "Confirm", "Cancel" }, {
Expand Down Expand Up @@ -289,15 +360,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)
Expand Down Expand Up @@ -593,6 +656,34 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
nowait = keymaps.discussion_tree.toggle_tree_type_nowait,
})
end

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.suggestion_preview(tree, "edit")
end
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
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
Expand Down Expand Up @@ -805,6 +896,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
Expand Down
Loading
Loading