From 3f1c5effe57527c16cb1d97191dc3cb2f03bf9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20F=2E=20Bortl=C3=ADk?= Date: Tue, 13 Feb 2024 17:39:21 +0100 Subject: [PATCH] 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. --- README.md | 8 +- doc/gitlab.nvim.txt | 50 ++++++----- lua/gitlab/actions/discussions/init.lua | 113 ++++++++++++++++++++++++ lua/gitlab/actions/help.lua | 2 +- lua/gitlab/state.lua | 10 ++- 5 files changed, 154 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 112f0f1..290da49 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ require("gitlab").setup({ 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 exit = "", perform_action = "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 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 blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc) jump_to_file = "o", -- Jump to comment location in file @@ -136,6 +136,10 @@ require("gitlab").setup({ delete_comment = "dd", -- Delete comment reply = "r", -- Reply to comment 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 position = "left", -- "top", "right", "bottom" or "left" open_in_browser = "b" -- Jump to the URL of the current note/discussion diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index be35a0f..54bcb87 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -12,7 +12,7 @@ Table of Contents *gitlab.nvim.table-of-contents* - The Summary view |gitlab.nvim.the-summary-view| - Reviewing an MR |gitlab.nvim.reviewing-an-mr| - Discussions and Notes |gitlab.nvim.discussions-and-notes| - - Labels |gitlab.nvim.labels| + - Labels |gitlab.nvim.labels| - Signs and diagnostics |gitlab.nvim.signs-and-diagnostics| - Uploading Files |gitlab.nvim.uploading-files| - 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| - Keybindings |gitlab.nvim.keybindings| - 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 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 attachment_dir = nil, -- The local directory for files (see the "summary" section) reviewer_settings = { - diffview = { - imply_local = false, -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen| - }, + diffview = { + 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 exit = "", perform_action = "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, reply = nil, squash_message = nil, - backup_register = nil, + backup_register = nil, }, discussion_tree = { -- The discussion tree that holds all comments 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 blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc) 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 reply = "r", -- Reply to comment 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 position = "left", -- "top", "right", "bottom" or "left" 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, 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 - "author", - "created_at", - "updated_at", - "merge_status", - "draft", - "conflicts", - "assignees", - "reviewers", - "branch", - "target_branch", - "pipeline", - "labels", + "author", + "created_at", + "updated_at", + "merge_status", + "draft", + "conflicts", + "assignees", + "reviewers", + "branch", + "target_branch", + "pipeline", + "labels", }, }, discussion_sign_and_diagnostic = { @@ -368,7 +372,7 @@ $XDG_CONFIG_HOME/nvim/after/ftplugin/gitlab.lua with the following contents: vim.o.breakindent = true < -LABELS *gitlab.nvim.labels* +LABELS *gitlab.nvim.labels* You can add or remove labels from the current MR. >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 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. diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index cc6b1dd..ed3a0bc 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -389,6 +389,86 @@ M.toggle_node = function(tree) tree:render() 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 -- @@ -560,6 +640,27 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked) vim.keymap.set("n", state.settings.discussion_tree.toggle_node, function() M.toggle_node(tree) 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() if M.is_current_node_note(tree) then M.reply(tree) @@ -621,6 +722,18 @@ M.redraw_resolved_status = function(tree, note, mark_resolved) tree:render() 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. ---@param data Discussion[]|UnlinkedDiscussion[] ---@param discussion_id string diff --git a/lua/gitlab/actions/help.lua b/lua/gitlab/actions/help.lua index 4ba1f71..d588ba2 100644 --- a/lua/gitlab/actions/help.lua +++ b/lua/gitlab/actions/help.lua @@ -10,7 +10,7 @@ M.open = function() local help_content_lines = {} for _, keymap in ipairs(keymaps) do 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(" ", ""), keymap.desc) table.insert(help_content_lines, new_line) end end diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 5a8086f..3c59c17 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -19,7 +19,7 @@ M.settings = { }, }, attachment_dir = "", - help = "?", + help = "g?", popup = { exit = "", perform_action = "s", @@ -39,6 +39,8 @@ M.settings = { }, discussion_tree = { auto_open = true, + switch_view = "S", + default_view = "discussions", blacklist = {}, jump_to_file = "o", jump_to_reviewer = "m", @@ -47,6 +49,10 @@ M.settings = { open_in_browser = "b", reply = "r", toggle_node = "t", + toggle_all_discussions = "T", + toggle_resolved_discussions = "R", + toggle_unresolved_discussions = "U", + keep_current_open = false, toggle_resolved = "p", relative = "editor", position = "left", @@ -54,8 +60,6 @@ M.settings = { resolved = "✓", unresolved = "-", tree_type = "simple", - switch_view = "T", - default_view = "discussions", ---@param t WinbarTable winbar = function(t) local discussions_content = t.resolvable_discussions ~= 0