Files
gitlab.nvim/lua/gitlab/actions/discussions/init.lua
Harrison (Harry) Cramer e29909cd10 Bugfixes, Etc. (#502)
* Fix: Jumping to renamed files (#484)

* fix: prevent "cursor position outside buffer" error

* fix: swap file_name and old_file_name in reviewer data

`old_file_name` is not set to the empty string for un-renamed files anymore, because then we can
remove the empty-line check in `comment_helpers.go` which was used to replace the empty string with
the current file name anyway.

* fix: add old_file_name to discussion root node data

* fix: also consider old_file_name when jumping to the reviewer

This fixes jumping to renamed files, however, may not work for comments that
were created on renamed files with the previous version of `gitlab.nvim` as
that version assigned the `file_name` and `old_file_name` incorrectly.

* refactor: don't shadow variable

* fix: check file_name or old_file_name based on which SHA comment belongs to

* Fix: Store reviewer data before creating comment popup (#476)

* Fix: Make publishing drafts more robust (#483)

* Fix: Swap file_name and old_file_name in reviewer data (#485)

* Feat: Enable toggling date format between relative and absolute (#491)

* Fix: Add opts to help popup (#492)

* Fix: Force start_line for jumping to diagnostic to be inside buffer (#494)

* fix: redefine colors after reloading colorscheme (#500)

* Fix: Use path instead of oldpath as fallback for unrenamed files (#496)

* Fix: Use file_name when old_file_name is not set (#495)

* fix(ci): fix lua tests (#501)

* Proxy Support (#499)

This is a #MINOR release.

---------

Co-authored-by: Jakub F. Bortlík <jakub.bortlik@proton.me>
Co-authored-by: Jonathan Duck <Duckbrain30@gmail.com>
2025-06-24 20:53:51 -04:00

843 lines
29 KiB
Lua

-- This module is responsible for the notes and comments discussion tree.
-- That includes things like editing existing notes in the tree,
-- replying to notes in the tree, and marking discussions as resolved/unresolved.
-- Draft notes are managed separately, under lua/gitlab/actions/draft_notes/init.lua
local Split = require("nui.split")
local Popup = require("nui.popup")
local NuiTree = require("nui.tree")
local job = require("gitlab.job")
local u = require("gitlab.utils")
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")
local signs = require("gitlab.indicators.signs")
local diagnostics = require("gitlab.indicators.diagnostics")
local winbar = require("gitlab.actions.discussions.winbar")
local help = require("gitlab.actions.help")
local emoji = require("gitlab.emoji")
local M = {
split_visible = false,
split = nil,
---@type number
linked_bufnr = nil,
---@type number
unlinked_bufnr = nil,
---@type NuiTree|nil
discussion_tree = nil,
---@type NuiTree|nil
unlinked_discussion_tree = nil,
}
---Re-fetches all discussions and re-renders the relevant view
---@param unlinked boolean
---@param all boolean|nil
M.rebuild_view = function(unlinked, all)
M.load_discussions(function()
if all then
M.rebuild_unlinked_discussion_tree()
M.rebuild_discussion_tree()
elseif unlinked then
M.rebuild_unlinked_discussion_tree()
else
M.rebuild_discussion_tree()
end
state.discussion_tree.last_updated = os.time()
M.refresh_diagnostics()
end)
end
---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil
M.load_discussions = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("discussion_data", function(data)
if not state.DISCUSSION_DATA then
state.DISCUSSION_DATA = {}
end
state.DISCUSSION_DATA.discussions = u.ensure_table(data.discussions)
state.DISCUSSION_DATA.unlinked_discussions = u.ensure_table(data.unlinked_discussions)
state.DISCUSSION_DATA.emojis = u.ensure_table(data.emojis)
if callback ~= nil then
callback()
end
end)
end
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
M.initialize_discussions = function()
state.discussion_tree.last_updated = os.time()
signs.setup_signs()
reviewer.set_callback_for_file_changed(function(args)
diagnostics.place_diagnostics(args.buf)
reviewer.update_winid_for_buffer(args.buf)
end)
reviewer.set_callback_for_reviewer_enter(function()
M.refresh_diagnostics()
end)
reviewer.set_callback_for_buf_read(function(args)
vim.api.nvim_buf_set_option(args.buf, "modifiable", false)
reviewer.set_keymaps(args.buf)
reviewer.set_reviewer_autocommands(args.buf)
end)
reviewer.set_callback_for_reviewer_leave(function()
signs.clear_signs()
diagnostics.clear_diagnostics()
end)
end
--- Take existing data and refresh the diagnostics and the signs
M.refresh_diagnostics = function()
if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics()
end
common.add_empty_titles()
end
---Opens the discussion tree, sets the keybindings. It also
---creates the tree for notes (which are not linked to specific lines of code)
---@param callback function?
---@param view_type "discussions"|"notes" Defines the view type to select (useful for overriding the default view type when jumping to discussion tree when it's closed).
M.open = function(callback, view_type)
view_type = view_type and view_type or state.settings.discussion_tree.default_view
state.DISCUSSION_DATA.discussions = u.ensure_table(state.DISCUSSION_DATA.discussions)
state.DISCUSSION_DATA.unlinked_discussions = u.ensure_table(state.DISCUSSION_DATA.unlinked_discussions)
state.DRAFT_NOTES = u.ensure_table(state.DRAFT_NOTES)
-- Make buffers, get and set buffer numbers, set filetypes
local split, linked_bufnr, unlinked_bufnr = M.create_split_and_bufs()
M.split = split
M.linked_bufnr = linked_bufnr
M.unlinked_bufnr = unlinked_bufnr
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.split.bufnr })
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.unlinked_bufnr })
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.linked_bufnr })
M.split = split
M.split_visible = true
split:mount()
-- Initialize winbar module with data from buffers
winbar.set_buffers(M.linked_bufnr, M.unlinked_bufnr)
winbar.switch_view_type(view_type)
local current_window = vim.api.nvim_get_current_win() -- Save user's current window in case they switched while content was loading
vim.api.nvim_set_current_win(M.split.winid)
common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr)
M.rebuild_discussion_tree()
M.rebuild_unlinked_discussion_tree()
-- Set default buffer
local default_buffer = winbar.bufnr_map[view_type]
vim.api.nvim_set_current_buf(default_buffer)
common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr)
vim.api.nvim_set_current_win(current_window)
if type(callback) == "function" then
callback()
end
vim.schedule(function()
M.refresh_diagnostics()
end)
end
-- Clears the discussion state and unmounts the split
M.close = function()
if M.split then
M.split:unmount()
end
M.split_visible = false
M.discussion_tree = nil
end
---Move to the discussion tree at the discussion from diagnostic on current line.
M.move_to_discussion_tree = function()
local current_line = vim.api.nvim_win_get_cursor(0)[1]
local d = vim.diagnostic.get(0, { namespace = diagnostics.diagnostics_namespace, lnum = current_line - 1 })
---Function used to jump to the discussion tree after the menu selection.
local jump_after_menu_selection = function(diagnostic)
---Function used to jump to the discussion tree after the discussion tree is opened.
local jump_after_tree_opened = function()
-- All diagnostics in `diagnotics_namespace` have diagnostic_id
local discussion_id = diagnostic.user_data.discussion_id
local discussion_node, line_number = M.discussion_tree:get_node("-" .. discussion_id)
if discussion_node == {} or discussion_node == nil then
u.notify("Discussion not found", vim.log.levels.WARN)
return
end
if not discussion_node:is_expanded() then
for _, child in ipairs(discussion_node:get_child_ids()) do
M.discussion_tree:get_node(child):expand()
end
discussion_node:expand()
end
M.discussion_tree:render()
vim.api.nvim_set_current_win(M.split.winid)
winbar.switch_view_type("discussions")
vim.api.nvim_win_set_cursor(M.split.winid, { line_number, 0 })
end
if not M.split_visible then
M.open(jump_after_tree_opened, "discussions")
else
jump_after_tree_opened()
end
end
if #d == 0 then
if state.settings.reviewer_settings.jump_with_no_diagnostics then
vim.api.nvim_win_set_cursor(M.split.winid, { M.last_row, M.last_column })
vim.api.nvim_set_current_win(M.split.winid)
else
u.notify("No diagnostics for this line.", vim.log.levels.WARN)
end
return
elseif #d > 1 then
vim.ui.select(d, {
prompt = "Choose discussion to jump to",
format_item = function(diagnostic)
return diagnostic.message
end,
}, function(diagnostic)
if not diagnostic then
return
end
jump_after_menu_selection(diagnostic)
end)
else
jump_after_menu_selection(d[1])
end
end
-- The reply popup will mount in a window when you trigger it (settings.keymaps.discussion_tree.reply) when hovering over a node in the discussion tree.
M.reply = 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 node = tree:get_node()
local discussion_node = common.get_root_node(tree, node)
if discussion_node == nil then
u.notify("Could not get discussion root", vim.log.levels.ERROR)
return
end
local discussion_id = tostring(discussion_node.id)
local comment = require("gitlab.actions.comment")
local unlinked = tree.bufnr == M.unlinked_bufnr
local layout = comment.create_comment_layout({
discussion_id = discussion_id,
unlinked = unlinked,
reply = true,
file_name = discussion_node.file_name,
})
layout:mount()
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" }, {
prompt = "Delete comment?",
}, function(choice)
if choice == "Confirm" then
local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node)
local root_node = common.get_root_node(tree, current_node)
if note_node == nil or root_node == nil then
u.notify("Could not get note or root node", vim.log.levels.ERROR)
return
end
---@type integer
if M.is_draft_note(tree) then
draft_notes.confirm_delete_draft_note(note_node.id, unlinked)
else
local note_id = note_node.is_root and root_node.root_note_id or note_node.id
local comment = require("gitlab.actions.comment")
comment.confirm_delete_comment(note_id, root_node.id, unlinked)
end
end
end)
end
-- This function (settings.keymaps.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
M.edit_comment = function(tree, unlinked)
local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node)
local root_node = common.get_root_node(tree, current_node)
if note_node == nil or root_node == nil then
u.notify("Could not get root or note node", vim.log.levels.ERROR)
return
end
local title = "Edit Comment"
title = root_node.file_name ~= nil and string.format("%s [%s]", title, root_node.file_name) or title
local edit_popup = Popup(popup.create_popup_state(title, state.settings.popup.edit))
popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
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 currentBuffer = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
-- Draft notes module handles edits for draft notes
if M.is_draft_note(tree) then
popup.set_popup_keymaps(
edit_popup,
draft_notes.confirm_edit_draft_note(note_node.id, unlinked),
nil,
popup.editable_popup_opts
)
else
local comment = require("gitlab.actions.comment")
popup.set_popup_keymaps(
edit_popup,
comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
nil,
popup.editable_popup_opts
)
end
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)
local note = tree:get_node()
if note == nil then
return
end
-- Switch to the root node to enable toggling from child nodes and note bodies
if not note.resolvable and common.is_node_note(note) then
note = common.get_root_node(tree, note)
end
if note == nil then
return
end
local body = {
discussion_id = note.id,
resolved = not note.resolved,
}
job.run_job("/mr/discussions/resolve", "PUT", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
local unlinked = tree.bufnr == M.unlinked_bufnr
M.rebuild_view(unlinked)
end)
end
---Opens a popup prompting the user to choose an emoji to attach to the current node
---@param tree any
---@param unlinked boolean
M.add_emoji_to_note = function(tree, unlinked)
local node = tree:get_node()
local note_node = common.get_note_node(tree, node)
local root_node = common.get_root_node(tree, node)
local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
local emojis = require("gitlab.emoji").emoji_list
emoji.pick_emoji(emojis, function(name)
local body = { emoji = name, note_id = note_id }
job.run_job("/mr/awardable/note/", "POST", body, function()
u.notify("Emoji added", vim.log.levels.INFO)
M.rebuild_view(unlinked)
end)
end)
end
---Opens a popup prompting the user to choose an emoji to remove from the current node
---@param tree any
---@param unlinked boolean
M.delete_emoji_from_note = function(tree, unlinked)
local node = tree:get_node()
local note_node = common.get_note_node(tree, node)
local root_node = common.get_root_node(tree, node)
local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
local note_id_str = tostring(note_id)
local e = require("gitlab.emoji")
local emojis = {}
local current_emojis = state.DISCUSSION_DATA.emojis[note_id_str]
for _, current_emoji in ipairs(current_emojis) do
if state.USER.id == current_emoji.user.id then
table.insert(emojis, e.emoji_map[current_emoji.name])
end
end
emoji.pick_emoji(emojis, function(name)
local awardable_id
for _, current_emoji in ipairs(current_emojis) do
if current_emoji.name == name and current_emoji.user.id == state.USER.id then
awardable_id = current_emoji.id
break
end
end
job.run_job(string.format("/mr/awardable/note/%d/%d", note_id, awardable_id), "DELETE", nil, function()
u.notify("Emoji removed", vim.log.levels.INFO)
M.rebuild_view(unlinked)
end)
end)
end
--
-- 🌲 Helper Functions
--
---Used to collect all nodes in a tree prior to rebuilding it, so that they
---can be re-expanded before render
---@param tree any
---@return table
M.gather_expanded_node_ids = function(tree)
-- Gather all nodes for later expansion, after rebuild
local ids = {}
for id, node in pairs(tree and tree.nodes.by_id or {}) do
if node._is_expanded then
table.insert(ids, id)
end
end
return ids
end
---Rebuilds the discussion tree, which contains all comments and draft comments
---linked to specific places in the code.
M.rebuild_discussion_tree = function()
if M.linked_bufnr == nil then
return
end
local current_node = discussions_tree.get_node_at_cursor(M.discussion_tree, M.last_node_at_cursor)
local expanded_node_ids = M.gather_expanded_node_ids(M.discussion_tree)
common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr)
vim.api.nvim_buf_set_lines(M.linked_bufnr, 0, -1, false, {})
local existing_comment_nodes = discussions_tree.add_discussions_to_table(state.DISCUSSION_DATA.discussions, false)
local draft_comment_nodes = draft_notes.add_draft_notes_to_table(false)
-- Combine inline draft notes with regular comments
local all_nodes = u.join(draft_comment_nodes, existing_comment_nodes)
local discussion_tree = NuiTree({
nodes = all_nodes,
bufnr = M.linked_bufnr,
prepare_node = tree_utils.nui_tree_prepare_node,
})
-- Re-expand already expanded nodes
for _, id in ipairs(expanded_node_ids) do
tree_utils.open_node_by_id(discussion_tree, id)
end
discussion_tree:render()
discussions_tree.restore_cursor_position(M.split.winid, discussion_tree, current_node)
M.set_tree_keymaps(discussion_tree, M.linked_bufnr, false)
M.discussion_tree = discussion_tree
common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr)
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.linked_bufnr })
state.discussion_tree.resolved_expanded = false
state.discussion_tree.unresolved_expanded = false
end
---Rebuilds the unlinked discussion tree, which contains all notes and draft notes.
M.rebuild_unlinked_discussion_tree = function()
if M.unlinked_bufnr == nil then
return
end
local current_node = discussions_tree.get_node_at_cursor(M.unlinked_discussion_tree, M.last_node_at_cursor)
local expanded_node_ids = M.gather_expanded_node_ids(M.unlinked_discussion_tree)
common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr)
vim.api.nvim_buf_set_lines(M.unlinked_bufnr, 0, -1, false, {})
local existing_note_nodes =
discussions_tree.add_discussions_to_table(state.DISCUSSION_DATA.unlinked_discussions, true)
local draft_comment_nodes = draft_notes.add_draft_notes_to_table(true)
-- Combine draft notes with regular notes
local all_nodes = u.join(draft_comment_nodes, existing_note_nodes)
local unlinked_discussion_tree = NuiTree({
nodes = all_nodes,
bufnr = M.unlinked_bufnr,
prepare_node = tree_utils.nui_tree_prepare_node,
})
-- Re-expand already expanded nodes
for _, id in ipairs(expanded_node_ids) do
tree_utils.open_node_by_id(unlinked_discussion_tree, id)
end
unlinked_discussion_tree:render()
discussions_tree.restore_cursor_position(M.split.winid, unlinked_discussion_tree, current_node)
M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_bufnr, true)
M.unlinked_discussion_tree = unlinked_discussion_tree
common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr)
state.unlinked_discussion_tree.resolved_expanded = false
state.unlinked_discussion_tree.unresolved_expanded = false
end
---Adds a discussion to the global state. Works for both notes (unlinked) and diff-linked comments,
M.add_discussion = function(arg)
local discussion = arg.data.discussion
if arg.unlinked then
if type(state.DISCUSSION_DATA.unlinked_discussions) ~= "table" then
state.DISCUSSION_DATA.unlinked_discussions = {}
end
table.insert(state.DISCUSSION_DATA.unlinked_discussions, 1, discussion)
M.rebuild_unlinked_discussion_tree()
else
if type(state.DISCUSSION_DATA.discussions) ~= "table" then
state.DISCUSSION_DATA.discussions = {}
end
table.insert(state.DISCUSSION_DATA.discussions, 1, discussion)
M.rebuild_discussion_tree()
end
end
---Creates the split for the discussion tree and returns it, with both buffer numbers
---@return NuiSplit
---@return integer
---@return integer
M.create_split_and_bufs = function()
local position = state.settings.discussion_tree.position
local size = state.settings.discussion_tree.size
local relative = state.settings.discussion_tree.relative
local split = Split({
relative = relative,
position = position,
size = size,
})
local linked_bufnr = vim.api.nvim_create_buf(true, false)
local unlinked_bufnr = vim.api.nvim_create_buf(true, false)
vim.api.nvim_create_autocmd("WinLeave", {
buffer = linked_bufnr,
callback = function()
M.last_row, M.last_column = unpack(vim.api.nvim_win_get_cursor(0))
M.last_node_at_cursor = M.discussion_tree and M.discussion_tree:get_node() or nil
end,
})
vim.api.nvim_create_autocmd("WinLeave", {
buffer = unlinked_bufnr,
callback = function()
M.last_node_at_cursor = M.unlinked_discussion_tree and M.unlinked_discussion_tree:get_node() or nil
end,
})
return split, linked_bufnr, unlinked_bufnr
end
---Check if type of current node is note or note body
---@param tree NuiTree
---@return boolean
M.is_current_node_note = function(tree)
return common.is_node_note(tree:get_node())
end
M.set_tree_keymaps = function(tree, bufnr, unlinked)
-- Require keymaps only after user settings have been merged with defaults
local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.discussion_tree.disable_all then
return
end
---Keybindings only relevant for linked (comment) view
if not unlinked then
if keymaps.discussion_tree.jump_to_file then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_file, function()
if M.is_current_node_note(tree) then
common.jump_to_file(tree)
end
end, { buffer = bufnr, desc = "Jump to file", nowait = keymaps.discussion_tree.jump_to_file_nowait })
end
if keymaps.discussion_tree.jump_to_reviewer then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
if M.is_current_node_note(tree) then
common.jump_to_reviewer(tree)
end
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
end
if keymaps.discussion_tree.toggle_tree_type then
vim.keymap.set("n", keymaps.discussion_tree.toggle_tree_type, function()
M.toggle_tree_type()
end, {
buffer = bufnr,
desc = "Change tree type between `simple` and `by_file_name`",
nowait = keymaps.discussion_tree.toggle_tree_type_nowait,
})
end
end
if keymaps.discussion_tree.refresh_data then
vim.keymap.set("n", keymaps.discussion_tree.refresh_data, function()
draft_notes.rebuild_view(unlinked, false)
end, {
buffer = bufnr,
desc = "Refresh the view with Gitlab's APIs",
nowait = keymaps.discussion_tree.refresh_data_nowait,
})
end
if keymaps.discussion_tree.edit_comment then
vim.keymap.set("n", keymaps.discussion_tree.edit_comment, function()
if M.is_current_node_note(tree) then
M.edit_comment(tree, unlinked)
end
end, { buffer = bufnr, desc = "Edit comment", nowait = keymaps.discussion_tree.edit_comment_nowait })
end
if keymaps.discussion_tree.publish_draft then
vim.keymap.set("n", keymaps.discussion_tree.publish_draft, function()
if M.is_draft_note(tree) then
draft_notes.publish_draft(tree)
end
end, { buffer = bufnr, desc = "Publish draft", nowait = keymaps.discussion_tree.publish_draft_nowait })
end
if keymaps.discussion_tree.delete_comment then
vim.keymap.set("n", keymaps.discussion_tree.delete_comment, function()
if M.is_current_node_note(tree) then
M.delete_comment(tree, unlinked)
end
end, { buffer = bufnr, desc = "Delete comment", nowait = keymaps.discussion_tree.delete_comment_nowait })
end
if keymaps.discussion_tree.toggle_draft_mode then
vim.keymap.set("n", keymaps.discussion_tree.toggle_draft_mode, function()
M.toggle_draft_mode()
end, {
buffer = bufnr,
desc = "Toggle between draft mode and live mode",
nowait = keymaps.discussion_tree.toggle_draft_mode_nowait,
})
end
if keymaps.discussion_tree.toggle_sort_method then
vim.keymap.set("n", keymaps.discussion_tree.toggle_sort_method, function()
M.toggle_sort_method()
end, {
buffer = bufnr,
desc = "Toggle sort method",
nowait = keymaps.discussion_tree.toggle_sort_method_nowait,
})
end
if keymaps.discussion_tree.toggle_date_format then
vim.keymap.set("n", keymaps.discussion_tree.toggle_date_format, function()
M.toggle_date_format()
end, {
buffer = bufnr,
desc = "Toggle date format",
nowait = keymaps.discussion_tree.toggle_date_format_nowait,
})
end
if keymaps.discussion_tree.toggle_resolved then
vim.keymap.set("n", keymaps.discussion_tree.toggle_resolved, function()
if M.is_current_node_note(tree) and not M.is_draft_note(tree) then
M.toggle_discussion_resolved(tree)
end
end, { buffer = bufnr, desc = "Toggle resolved", nowait = keymaps.discussion_tree.toggle_resolved_nowait })
end
if keymaps.discussion_tree.toggle_node then
vim.keymap.set("n", keymaps.discussion_tree.toggle_node, function()
tree_utils.toggle_node(tree)
end, { buffer = bufnr, desc = "Toggle node", nowait = keymaps.discussion_tree.toggle_node_nowait })
end
if keymaps.discussion_tree.toggle_all_discussions then
vim.keymap.set("n", keymaps.discussion_tree.toggle_all_discussions, function()
tree_utils.toggle_nodes(M.split.winid, tree, unlinked, {
toggle_resolved = true,
toggle_unresolved = true,
keep_current_open = state.settings.discussion_tree.keep_current_open,
})
end, {
buffer = bufnr,
desc = "Toggle all nodes",
nowait = keymaps.discussion_tree.toggle_all_discussions_nowait,
})
end
if keymaps.discussion_tree.toggle_resolved_discussions then
vim.keymap.set("n", keymaps.discussion_tree.toggle_resolved_discussions, function()
tree_utils.toggle_nodes(M.split.winid, tree, unlinked, {
toggle_resolved = true,
toggle_unresolved = false,
keep_current_open = state.settings.discussion_tree.keep_current_open,
})
end, {
buffer = bufnr,
desc = "Toggle resolved nodes",
nowait = keymaps.discussion_tree.toggle_resolved_discussions_nowait,
})
end
if keymaps.discussion_tree.toggle_unresolved_discussions then
vim.keymap.set("n", keymaps.discussion_tree.toggle_unresolved_discussions, function()
tree_utils.toggle_nodes(M.split.winid, tree, unlinked, {
toggle_resolved = false,
toggle_unresolved = true,
keep_current_open = state.settings.discussion_tree.keep_current_open,
})
end, {
buffer = bufnr,
desc = "Toggle unresolved nodes",
nowait = keymaps.discussion_tree.toggle_unresolved_discussions_nowait,
})
end
if keymaps.discussion_tree.reply then
vim.keymap.set("n", keymaps.discussion_tree.reply, function()
if M.is_current_node_note(tree) then
M.reply(tree)
end
end, { buffer = bufnr, desc = "Reply", nowait = keymaps.discussion_tree.reply_nowait })
end
if keymaps.discussion_tree.switch_view then
vim.keymap.set("n", keymaps.discussion_tree.switch_view, function()
winbar.switch_view_type()
end, {
buffer = bufnr,
desc = "Change view type between discussions and notes",
nowait = keymaps.discussion_tree.switch_view_nowait,
})
end
if keymaps.help then
vim.keymap.set("n", keymaps.help, function()
help.open({ discussion_tree = true })
end, { buffer = bufnr, desc = "Open help popup", nowait = keymaps.help_nowait })
end
if keymaps.discussion_tree.open_in_browser then
vim.keymap.set("n", keymaps.discussion_tree.open_in_browser, function()
common.open_in_browser(tree)
end, {
buffer = bufnr,
desc = "Open the note in your browser",
nowait = keymaps.discussion_tree.open_in_browser_nowait,
})
end
if keymaps.discussion_tree.copy_node_url then
vim.keymap.set("n", keymaps.discussion_tree.copy_node_url, function()
common.copy_node_url(tree)
end, {
buffer = bufnr,
desc = "Copy the URL of the current node to clipboard",
nowait = keymaps.discussion_tree.copy_node_url_nowait,
})
end
if keymaps.discussion_tree.print_node then
vim.keymap.set("n", keymaps.discussion_tree.print_node, function()
common.print_node(tree)
end, {
buffer = bufnr,
desc = "Print current node (for debugging)",
nowait = keymaps.discussion_tree.print_node_nowait,
})
end
if keymaps.discussion_tree.add_emoji then
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
M.add_emoji_to_note(tree, unlinked)
end, {
buffer = bufnr,
desc = "Add an emoji reaction to the note/comment",
nowait = keymaps.discussion_tree.add_emoji_nowait,
})
end
if keymaps.discussion_tree.delete_emoji then
vim.keymap.set("n", keymaps.discussion_tree.delete_emoji, function()
M.delete_emoji_from_note(tree, unlinked)
end, {
buffer = bufnr,
desc = "Remove an emoji reaction from the note/comment",
nowait = keymaps.discussion_tree.delete_emoji_nowait,
})
end
emoji.init_popup(tree, bufnr)
end
---Toggle comments tree type between "simple" and "by_file_name"
M.toggle_tree_type = function()
if state.settings.discussion_tree.tree_type == "simple" then
state.settings.discussion_tree.tree_type = "by_file_name"
else
state.settings.discussion_tree.tree_type = "simple"
end
M.rebuild_discussion_tree()
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
end
---Toggle between sorting by "original comment" (oldest at the top) or "latest reply" (newest at the
---top).
M.toggle_sort_method = function()
if state.settings.discussion_tree.sort_by == "original_comment" then
state.settings.discussion_tree.sort_by = "latest_reply"
else
state.settings.discussion_tree.sort_by = "original_comment"
end
winbar.update_winbar()
M.rebuild_view(false, true)
end
---Toggle between displaying relative time (e.g., "5 days ago") and absolute time (e.g., "04/10/2025 at 22:49")
M.toggle_date_format = function()
state.settings.discussion_tree.relative_date = not state.settings.discussion_tree.relative_date
M.rebuild_unlinked_discussion_tree()
M.rebuild_discussion_tree()
end
---Indicates whether the node under the cursor is a draft note or not
---@param tree NuiTree
---@return boolean
M.is_draft_note = function(tree)
local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node)
if note_node and note_node.is_draft then
return true
end
local root_node = common.get_root_node(tree, current_node)
return root_node ~= nil and root_node.is_draft
end
return M