Feat collapse and expand nodes (#176)

This MR adds the ability to expand and collapse nodes in the discussion tree in bulk. You can now toggle expansion of all nodes, toggle expansion of only resolved discussions, and toggle expansion of only unresolved discussions.

The MR also adjusts keybindings in the discussion tree to support forward and backward searching, as well as the keybinding for the help popup.

Thank you for the contribution @jakubbortlik!

This is a #MINOR bump since it's changing keybindings, although core workflows are unchanged.
This commit is contained in:
Jakub F. Bortlík
2024-02-13 17:39:21 +01:00
committed by GitHub
parent 6046669391
commit 3f1c5effe5
5 changed files with 154 additions and 29 deletions

View File

@@ -108,7 +108,7 @@ require("gitlab").setup({
imply_local = false, -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen| imply_local = false, -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen|
}, },
}, },
help = "?", -- Opens a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc) help = "g?", -- Opens a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc)
popup = { -- The popup for comment creation, editing, and replying popup = { -- The popup for comment creation, editing, and replying
exit = "<Esc>", exit = "<Esc>",
perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc) perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc)
@@ -127,7 +127,7 @@ require("gitlab").setup({
}, },
discussion_tree = { -- The discussion tree that holds all comments discussion_tree = { -- The discussion tree that holds all comments
auto_open = true, -- Automatically open when the reviewer is opened auto_open = true, -- Automatically open when the reviewer is opened
switch_view = "T", -- Toggles between the notes and discussions views switch_view = "S", -- Toggles between the notes and discussions views
default_view = "discussions" -- Show "discussions" or "notes" by default default_view = "discussions" -- Show "discussions" or "notes" by default
blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc) blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc)
jump_to_file = "o", -- Jump to comment location in file jump_to_file = "o", -- Jump to comment location in file
@@ -136,6 +136,10 @@ require("gitlab").setup({
delete_comment = "dd", -- Delete comment delete_comment = "dd", -- Delete comment
reply = "r", -- Reply to comment reply = "r", -- Reply to comment
toggle_node = "t", -- Opens or closes the discussion toggle_node = "t", -- Opens or closes the discussion
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
toggle_resolved_discussions = "R", -- Open or close all resolved discussions
toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
toggle_resolved = "p" -- Toggles the resolved status of the whole discussion toggle_resolved = "p" -- Toggles the resolved status of the whole discussion
position = "left", -- "top", "right", "bottom" or "left" position = "left", -- "top", "right", "bottom" or "left"
open_in_browser = "b" -- Jump to the URL of the current note/discussion open_in_browser = "b" -- Jump to the URL of the current note/discussion

View File

@@ -12,7 +12,7 @@ Table of Contents *gitlab.nvim.table-of-contents*
- The Summary view |gitlab.nvim.the-summary-view| - The Summary view |gitlab.nvim.the-summary-view|
- Reviewing an MR |gitlab.nvim.reviewing-an-mr| - Reviewing an MR |gitlab.nvim.reviewing-an-mr|
- Discussions and Notes |gitlab.nvim.discussions-and-notes| - Discussions and Notes |gitlab.nvim.discussions-and-notes|
- Labels |gitlab.nvim.labels| - Labels |gitlab.nvim.labels|
- Signs and diagnostics |gitlab.nvim.signs-and-diagnostics| - Signs and diagnostics |gitlab.nvim.signs-and-diagnostics|
- Uploading Files |gitlab.nvim.uploading-files| - Uploading Files |gitlab.nvim.uploading-files|
- MR Approvals |gitlab.nvim.mr-approvals| - MR Approvals |gitlab.nvim.mr-approvals|
@@ -23,9 +23,9 @@ Table of Contents *gitlab.nvim.table-of-contents*
- Restarting or Shutting Down |gitlab.nvim.restarting-or-shutting-down| - Restarting or Shutting Down |gitlab.nvim.restarting-or-shutting-down|
- Keybindings |gitlab.nvim.keybindings| - Keybindings |gitlab.nvim.keybindings|
- Troubleshooting |gitlab.nvim.troubleshooting| - Troubleshooting |gitlab.nvim.troubleshooting|
- Api |gitlab.nvim.api| - Api |gitlab.nvim.api|
OVERVIEW *gitlab.nvim.overview* OVERVIEW *gitlab.nvim.overview*
This Neovim plugin is designed to make it easy to review Gitlab MRs from within This Neovim plugin is designed to make it easy to review Gitlab MRs from within
the editor. This means you can do things like: the editor. This means you can do things like:
@@ -137,11 +137,11 @@ you call this function with no values the defaults will be used:
debug = { go_request = false, go_response = false }, -- Which values to log debug = { go_request = false, go_response = false }, -- Which values to log
attachment_dir = nil, -- The local directory for files (see the "summary" section) attachment_dir = nil, -- The local directory for files (see the "summary" section)
reviewer_settings = { reviewer_settings = {
diffview = { diffview = {
imply_local = false, -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen| imply_local = false, -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen|
}, },
}, },
help = "?", -- Opens a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc) help = "g?", -- Opens a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc)
popup = { -- The popup for comment creation, editing, and replying popup = { -- The popup for comment creation, editing, and replying
exit = "<Esc>", exit = "<Esc>",
perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc) perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc)
@@ -156,11 +156,11 @@ you call this function with no values the defaults will be used:
pipeline = nil, pipeline = nil,
reply = nil, reply = nil,
squash_message = nil, squash_message = nil,
backup_register = nil, backup_register = nil,
}, },
discussion_tree = { -- The discussion tree that holds all comments discussion_tree = { -- The discussion tree that holds all comments
auto_open = true, -- Automatically open when the reviewer is opened auto_open = true, -- Automatically open when the reviewer is opened
switch_view = "T", -- Toggles between the notes and discussions views switch_view = "S", -- Toggles between the notes and discussions views
default_view = "discussions" -- Show "discussions" or "notes" by default default_view = "discussions" -- Show "discussions" or "notes" by default
blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc) blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc)
jump_to_file = "o", -- Jump to comment location in file jump_to_file = "o", -- Jump to comment location in file
@@ -169,6 +169,10 @@ you call this function with no values the defaults will be used:
delete_comment = "dd", -- Delete comment delete_comment = "dd", -- Delete comment
reply = "r", -- Reply to comment reply = "r", -- Reply to comment
toggle_node = "t", -- Opens or closes the discussion toggle_node = "t", -- Opens or closes the discussion
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
toggle_resolved_discussions = "R", -- Open or close all resolved discussions
toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
toggle_resolved = "p" -- Toggles the resolved status of the whole discussion toggle_resolved = "p" -- Toggles the resolved status of the whole discussion
position = "left", -- "top", "right", "bottom" or "left" position = "left", -- "top", "right", "bottom" or "left"
open_in_browser = "b" -- Jump to the URL of the current note/discussion open_in_browser = "b" -- Jump to the URL of the current note/discussion
@@ -184,18 +188,18 @@ you call this function with no values the defaults will be used:
enabled = true, enabled = true,
horizontal = false, -- Display metadata to the left of the summary rather than underneath horizontal = false, -- Display metadata to the left of the summary rather than underneath
fields = { -- The fields listed here will be displayed, in whatever order you choose fields = { -- The fields listed here will be displayed, in whatever order you choose
"author", "author",
"created_at", "created_at",
"updated_at", "updated_at",
"merge_status", "merge_status",
"draft", "draft",
"conflicts", "conflicts",
"assignees", "assignees",
"reviewers", "reviewers",
"branch", "branch",
"target_branch", "target_branch",
"pipeline", "pipeline",
"labels", "labels",
}, },
}, },
discussion_sign_and_diagnostic = { discussion_sign_and_diagnostic = {
@@ -368,7 +372,7 @@ $XDG_CONFIG_HOME/nvim/after/ftplugin/gitlab.lua with the following contents:
vim.o.breakindent = true vim.o.breakindent = true
< <
LABELS *gitlab.nvim.labels* LABELS *gitlab.nvim.labels*
You can add or remove labels from the current MR. You can add or remove labels from the current MR.
>lua >lua
@@ -418,7 +422,7 @@ diagnostics for discussions on outdated diff revisions.
When interacting with multiline comments, the cursor must be on the "main" line When interacting with multiline comments, the cursor must be on the "main" line
of diagnostic, where the `discussion_sign.text` is shown, otherwise of diagnostic, where the `discussion_sign.text` is shown, otherwise
`vim.diagnostic.show` and `jump_to_discussion_tree_from_diagnostic` will not `vim.diagnostic.show` and `move_to_discussion_tree_from_diagnostic` will not
work. work.

View File

@@ -389,6 +389,86 @@ M.toggle_node = function(tree)
tree:render() tree:render()
end end
---@class ToggleNodesOptions
---@field toggle_resolved boolean Whether to toggle resolved discussions.
---@field toggle_unresolved boolean Whether to toggle unresolved discussions.
---@field keep_current_open boolean Whether to keep the current discussion open even if it should otherwise be closed.
---This function (settings.discussion_tree.toggle_nodes) expands/collapses all nodes and their children according to the opts.
---@param tree NuiTree
---@param opts ToggleNodesOptions
M.toggle_nodes = function(tree, opts)
local current_node = tree:get_node()
if current_node == nil then
return
end
local root_node = M.get_root_node(tree, current_node)
for _, node in ipairs(tree:get_nodes()) do
if opts.toggle_resolved then
if state.resolved_expanded then
M.collapse_recursively(tree, node, root_node, opts.keep_current_open, true)
else
M.expand_recursively(tree, node, true)
end
end
if opts.toggle_unresolved then
if state.unresolved_expanded then
M.collapse_recursively(tree, node, root_node, opts.keep_current_open, false)
else
M.expand_recursively(tree, node, false)
end
end
end
if opts.toggle_resolved then
state.resolved_expanded = not state.resolved_expanded
end
if opts.toggle_unresolved then
state.unresolved_expanded = not state.unresolved_expanded
end
tree:render()
M.restore_cursor_position(tree, current_node, root_node)
end
---This function (settings.discussion_tree.collapse_recursively) collapses a node and its children.
---@param tree NuiTree
---@param node NuiTree.Node
---@param current_root_node NuiTree.Node The root node of the current node.
---@param keep_current_open boolean If true, the current node stays open, even if it should otherwise be collapsed.
---@param is_resolved boolean If true, collapse resolved discussions. If false, collapse unresolved discussions.
M.collapse_recursively = function(tree, node, current_root_node, keep_current_open, is_resolved)
if node == nil then
return
end
local root_node = M.get_root_node(tree, node)
if M.is_node_note(node) and root_node.resolved == is_resolved then
if keep_current_open and root_node == current_root_node then
return
end
node:collapse()
end
local children = node:get_child_ids()
for _, child in ipairs(children) do
M.collapse_recursively(tree, tree:get_node(child), current_root_node, keep_current_open, is_resolved)
end
end
---This function (settings.discussion_tree.expand_recursively) expands a node and its children.
---@param tree NuiTree
---@param node NuiTree.Node
---@param is_resolved boolean If true, expand resolved discussions. If false, expand unresolved discussions.
M.expand_recursively = function(tree, node, is_resolved)
if node == nil then
return
end
if M.is_node_note(node) and M.get_root_node(tree, node).resolved == is_resolved then
node:expand()
end
local children = node:get_child_ids()
for _, child in ipairs(children) do
M.expand_recursively(tree, tree:get_node(child), is_resolved)
end
end
-- --
-- 🌲 Helper Functions -- 🌲 Helper Functions
-- --
@@ -560,6 +640,27 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
vim.keymap.set("n", state.settings.discussion_tree.toggle_node, function() vim.keymap.set("n", state.settings.discussion_tree.toggle_node, function()
M.toggle_node(tree) M.toggle_node(tree)
end, { buffer = bufnr, desc = "Toggle node" }) end, { buffer = bufnr, desc = "Toggle node" })
vim.keymap.set("n", state.settings.discussion_tree.toggle_all_discussions, function()
M.toggle_nodes(tree, {
toggle_resolved = true,
toggle_unresolved = true,
keep_current_open = state.settings.discussion_tree.keep_current_open,
})
end, { buffer = bufnr, desc = "Toggle all nodes" })
vim.keymap.set("n", state.settings.discussion_tree.toggle_resolved_discussions, function()
M.toggle_nodes(tree, {
toggle_resolved = true,
toggle_unresolved = false,
keep_current_open = state.settings.discussion_tree.keep_current_open,
})
end, { buffer = bufnr, desc = "Toggle resolved nodes" })
vim.keymap.set("n", state.settings.discussion_tree.toggle_unresolved_discussions, function()
M.toggle_nodes(tree, {
toggle_resolved = false,
toggle_unresolved = true,
keep_current_open = state.settings.discussion_tree.keep_current_open,
})
end, { buffer = bufnr, desc = "Toggle unresolved nodes" })
vim.keymap.set("n", state.settings.discussion_tree.reply, function() vim.keymap.set("n", state.settings.discussion_tree.reply, function()
if M.is_current_node_note(tree) then if M.is_current_node_note(tree) then
M.reply(tree) M.reply(tree)
@@ -621,6 +722,18 @@ M.redraw_resolved_status = function(tree, note, mark_resolved)
tree:render() tree:render()
end end
---Restore cursor position to the original node if possible
M.restore_cursor_position = function(tree, original_node, root_node)
local _, line_number = tree:get_node("-" .. tostring(original_node.id))
-- If current_node is has been collapsed, get line number of root node instead
if line_number == nil and root_node then
_, line_number = tree:get_node("-" .. tostring(root_node.id))
end
if line_number ~= nil then
vim.api.nvim_win_set_cursor(M.split.winid, { line_number, 0 })
end
end
---Replace text in discussion after note update. ---Replace text in discussion after note update.
---@param data Discussion[]|UnlinkedDiscussion[] ---@param data Discussion[]|UnlinkedDiscussion[]
---@param discussion_id string ---@param discussion_id string

View File

@@ -10,7 +10,7 @@ M.open = function()
local help_content_lines = {} local help_content_lines = {}
for _, keymap in ipairs(keymaps) do for _, keymap in ipairs(keymaps) do
if keymap.desc ~= nil then if keymap.desc ~= nil then
local new_line = string.format("%s: %s", keymap.lhs, keymap.desc) local new_line = string.format("%s: %s", keymap.lhs:gsub(" ", "<space>"), keymap.desc)
table.insert(help_content_lines, new_line) table.insert(help_content_lines, new_line)
end end
end end

View File

@@ -19,7 +19,7 @@ M.settings = {
}, },
}, },
attachment_dir = "", attachment_dir = "",
help = "?", help = "g?",
popup = { popup = {
exit = "<Esc>", exit = "<Esc>",
perform_action = "<leader>s", perform_action = "<leader>s",
@@ -39,6 +39,8 @@ M.settings = {
}, },
discussion_tree = { discussion_tree = {
auto_open = true, auto_open = true,
switch_view = "S",
default_view = "discussions",
blacklist = {}, blacklist = {},
jump_to_file = "o", jump_to_file = "o",
jump_to_reviewer = "m", jump_to_reviewer = "m",
@@ -47,6 +49,10 @@ M.settings = {
open_in_browser = "b", open_in_browser = "b",
reply = "r", reply = "r",
toggle_node = "t", toggle_node = "t",
toggle_all_discussions = "T",
toggle_resolved_discussions = "R",
toggle_unresolved_discussions = "U",
keep_current_open = false,
toggle_resolved = "p", toggle_resolved = "p",
relative = "editor", relative = "editor",
position = "left", position = "left",
@@ -54,8 +60,6 @@ M.settings = {
resolved = "", resolved = "",
unresolved = "-", unresolved = "-",
tree_type = "simple", tree_type = "simple",
switch_view = "T",
default_view = "discussions",
---@param t WinbarTable ---@param t WinbarTable
winbar = function(t) winbar = function(t)
local discussions_content = t.resolvable_discussions ~= 0 local discussions_content = t.resolvable_discussions ~= 0