Winbar Support + Notes and Discussions; Help Popup + Auto-Open (#133)

- Adds support for toggling between discussions and notes views
- Deprecates the split view shared with both discussions and notes at the same time
- Adds winbar to discussion split, with metadata about resolved and unresolved discussions
- Adds help popups with information about keybindings for all views
- Modifies highlights in discussion tree and default symbol for unresolved discussions

This is a MINOR version bump as the default behavior of the discussion tree is changed slightly. Existing configurations should still function.
This commit is contained in:
Harrison (Harry) Cramer
2023-12-13 17:46:34 -05:00
committed by GitHub
parent d5038d63ca
commit d5510f9d9a
13 changed files with 588 additions and 428 deletions

View File

@@ -64,3 +64,10 @@
---@class DiscussionData
---@field discussions Discussion[]
---@field unlinked_discussions UnlinkedDiscussion[]
---@class WinbarTable
---@field name string
---@field resolvable_discussions number
---@field resolved_discussions number
---@field resolvable_notes number
---@field resolved_notes number

View File

@@ -5,417 +5,146 @@ local Split = require("nui.split")
local Popup = require("nui.popup")
local NuiTree = require("nui.tree")
local NuiLine = require("nui.line")
local Layout = require("nui.layout")
local job = require("gitlab.job")
local u = require("gitlab.utils")
local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local miscellaneous = require("gitlab.actions.miscellaneous")
local discussions_tree = require("gitlab.actions.discussions.tree")
local discussion_sign_name = "gitlab_discussion"
local discussion_helper_sign_start = "gitlab_discussion_helper_start"
local discussion_helper_sign_mid = "gitlab_discussion_helper_mid"
local discussion_helper_sign_end = "gitlab_discussion_helper_end"
local diagnostics_namespace = vim.api.nvim_create_namespace(discussion_sign_name)
local signs = require("gitlab.actions.discussions.signs")
local winbar = require("gitlab.actions.discussions.winbar")
local help = require("gitlab.actions.help")
local M = {
layout_visible = false,
layout = nil,
layout_buf = nil,
split_visible = false,
split = nil,
---@type number
split_bufnr = nil,
---@type Discussion[]
discussions = {},
---@type UnlinkedDiscussion[]
unlinked_discussions = {},
linked_section = nil,
unlinked_section = nil,
---@type number
linked_bufnr = nil,
---@type number
unlinked_bufnr = nil,
---@type number
focused_bufnr = nil,
discussion_tree = nil,
}
---Load the discussion data, storage them in M.discussions and M.unlinked_discussions and call
---Makes API call to get the discussion data, store it in M.discussions and M.unlinked_discussions and call
---callback with data
---@param callback (fun(data: DiscussionData): nil)?
M.load_discussions = function(callback)
job.run_job("/discussions/list", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
M.discussions = data.discussions
M.unlinked_discussions = data.unlinked_discussions
M.discussions = data.discussions ~= vim.NIL and data.discussions or {}
M.unlinked_discussions = data.unlinked_discussions ~= vim.NIL and data.unlinked_discussions or {}
if type(callback) == "function" then
callback(data)
end
end)
end
---Parse line code and return old and new line numbers
---@param line_code string gitlab line code -> 588440f66559714280628a4f9799f0c4eb880a4a_10_10
---@return number?
---@return number?
local function _parse_line_code(line_code)
local line_code_regex = "%w+_(%d+)_(%d+)"
local old_line, new_line = line_code:match(line_code_regex)
return tonumber(old_line), tonumber(new_line)
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
M.initialize_discussions = function()
signs.setup_signs()
-- Setup callback to refresh discussion data, discussion signs and diagnostics whenever the reviewed file changes.
reviewer.set_callback_for_file_changed(M.refresh_discussion_data)
-- Setup callback to clear signs and diagnostics whenever reviewer is left.
reviewer.set_callback_for_reviewer_leave(signs.clear_signs_and_discussions)
end
---Filter all discussions which are relevant for currently visible signs and diagnostscs.
---@return Discussion[]?
M.filter_discussions_for_signs_and_diagnostics = function()
if type(M.discussions) ~= "table" then
return
end
local file = reviewer.get_current_file()
if not file then
return
end
local discussions = {}
for _, discussion in ipairs(M.discussions) do
local first_note = discussion.notes[1]
if
type(first_note.position) == "table"
and (first_note.position.new_path == file or first_note.position.old_path == file)
then
if
--Skip resolved discussions
not (
state.settings.discussion_sign_and_diagnostic.skip_resolved_discussion
and first_note.resolvable
and first_note.resolved
)
--Skip discussions from old revisions
and not (
state.settings.discussion_sign_and_diagnostic.skip_old_revision_discussion
and u.from_iso_format_date_to_timestamp(first_note.created_at)
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
)
then
table.insert(discussions, discussion)
end
end
end
return discussions
end
---Refresh the discussion signs for currently loaded file in reviewer For convinience we use same
---string for sign name and sign group ( currently there is only one sign needed)
M.refresh_signs = function()
local diagnostics = M.filter_discussions_for_signs_and_diagnostics()
if diagnostics == nil then
vim.diagnostic.reset(diagnostics_namespace)
return
end
local new_signs = {}
local old_signs = {}
for _, discussion in ipairs(diagnostics) do
local first_note = discussion.notes[1]
local base_sign = {
name = discussion_sign_name,
group = discussion_sign_name,
priority = state.settings.discussion_sign.priority,
}
local base_helper_sign = {
name = discussion_sign_name,
group = discussion_sign_name,
priority = state.settings.discussion_sign.priority - 1,
}
if first_note.position.line_range ~= nil then
local start_old_line, start_new_line = _parse_line_code(first_note.position.line_range.start.line_code)
local end_old_line, end_new_line = _parse_line_code(first_note.position.line_range["end"].line_code)
local discussion_line, start_line, end_line
if first_note.position.line_range.start.type == "new" then
table.insert(
new_signs,
vim.tbl_deep_extend("force", {
id = first_note.id,
lnum = first_note.position.new_line,
}, base_sign)
)
discussion_line = first_note.position.new_line
start_line = start_new_line
end_line = end_new_line
elseif first_note.position.line_range.start.type == "old" then
table.insert(
old_signs,
vim.tbl_deep_extend("force", {
id = first_note.id,
lnum = first_note.position.old_line,
}, base_sign)
)
discussion_line = first_note.position.old_line
start_line = start_old_line
end_line = end_old_line
end
-- Helper signs does not have specific ids currently.
if state.settings.discussion_sign.helper_signs.enabled then
local helper_signs = {}
if start_line > end_line then
start_line, end_line = end_line, start_line
end
for i = start_line, end_line do
if i ~= discussion_line then
local sign_name
if i == start_line then
sign_name = discussion_helper_sign_start
elseif i == end_line then
sign_name = discussion_helper_sign_end
else
sign_name = discussion_helper_sign_mid
end
table.insert(
helper_signs,
vim.tbl_deep_extend("keep", {
name = sign_name,
lnum = i,
}, base_helper_sign)
)
end
end
if first_note.position.line_range.start.type == "new" then
vim.list_extend(new_signs, helper_signs)
elseif first_note.position.line_range.start.type == "old" then
vim.list_extend(old_signs, helper_signs)
end
end
else
local sign = vim.tbl_deep_extend("force", {
id = first_note.id,
}, base_sign)
if first_note.position.new_line ~= nil then
table.insert(new_signs, vim.tbl_deep_extend("force", { lnum = first_note.position.new_line }, sign))
end
if first_note.position.old_line ~= nil then
table.insert(old_signs, vim.tbl_deep_extend("force", { lnum = first_note.position.old_line }, sign))
end
end
end
vim.fn.sign_unplace(discussion_sign_name)
reviewer.place_sign(old_signs, "old")
reviewer.place_sign(new_signs, "new")
end
---Build note header from note.
---@param note Note
---@return string
M.build_note_header = function(note)
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
end
---Refresh the diagnostics for the currently reviewed file
M.refresh_diagnostics = function()
-- Keep in mind that diagnostic line numbers use 0-based indexing while line numbers use
-- 1-based indexing
local diagnostics = M.filter_discussions_for_signs_and_diagnostics()
if diagnostics == nil then
vim.diagnostic.reset(diagnostics_namespace)
return
end
local new_diagnostics = {}
local old_diagnostics = {}
for _, discussion in ipairs(diagnostics) do
local first_note = discussion.notes[1]
local message = ""
for _, note in ipairs(discussion.notes) do
message = message .. M.build_note_header(note) .. "\n" .. note.body .. "\n"
end
local diagnostic = {
message = message,
col = 0,
severity = state.settings.discussion_diagnostic.severity,
user_data = { discussion_id = discussion.id, header = M.build_note_header(discussion.notes[1]) },
source = "gitlab",
code = state.settings.discussion_diagnostic.code,
}
if first_note.position.line_range ~= nil then
-- Diagnostics for line range discussions are tricky - you need to set lnum to
-- line number equal to note.position.new_line or note.position.old_line because that is
-- only line where you can trigger the diagnostic show. This also need to be in sinc
-- with the sign placement.
local start_old_line, start_new_line = _parse_line_code(first_note.position.line_range.start.line_code)
local end_old_line, end_new_line = _parse_line_code(first_note.position.line_range["end"].line_code)
if first_note.position.line_range.start.type == "new" then
local new_diagnostic
if first_note.position.new_line == start_new_line then
new_diagnostic = {
lnum = start_new_line - 1,
end_lnum = end_new_line - 1,
}
else
new_diagnostic = {
lnum = end_new_line - 1,
end_lnum = start_new_line - 1,
}
end
new_diagnostic = vim.tbl_deep_extend("force", new_diagnostic, diagnostic)
table.insert(new_diagnostics, new_diagnostic)
elseif first_note.position.line_range.start.type == "old" then
local old_diagnostic
if first_note.position.old_line == start_old_line then
old_diagnostic = {
lnum = start_old_line - 1,
end_lnum = end_old_line - 1,
}
else
old_diagnostic = {
lnum = end_old_line - 1,
end_lnum = start_old_line - 1,
}
end
old_diagnostic = vim.tbl_deep_extend("force", old_diagnostic, diagnostic)
table.insert(old_diagnostics, old_diagnostic)
end
else
-- Diagnostics for single line discussions.
if first_note.position.new_line ~= nil then
local new_diagnostic = {
lnum = first_note.position.new_line - 1,
}
new_diagnostic = vim.tbl_deep_extend("force", new_diagnostic, diagnostic)
table.insert(new_diagnostics, new_diagnostic)
end
if first_note.position.old_line ~= nil then
local old_diagnostic = {
lnum = first_note.position.old_line - 1,
}
old_diagnostic = vim.tbl_deep_extend("force", old_diagnostic, diagnostic)
table.insert(old_diagnostics, old_diagnostic)
end
end
end
vim.diagnostic.reset(diagnostics_namespace)
reviewer.set_diagnostics(
diagnostics_namespace,
new_diagnostics,
"new",
state.settings.discussion_diagnostic.display_opts
)
reviewer.set_diagnostics(
diagnostics_namespace,
old_diagnostics,
"old",
state.settings.discussion_diagnostic.display_opts
)
end
---Refresh discussion data, discussion signs and diagnostics
---Refresh discussion data, signs, diagnostics, and winbar with new data from API
M.refresh_discussion_data = function()
M.load_discussions(function()
if state.settings.discussion_sign.enabled then
M.refresh_signs()
signs.refresh_signs(M.discussions)
end
if state.settings.discussion_diagnostic.enabled then
M.refresh_diagnostics()
signs.refresh_diagnostics(M.discussions)
end
if M.split_visible then
local linked_is_focused = M.linked_bufnr == M.focused_bufnr
winbar.update_winbar(M.discussions, M.unlinked_discussions, linked_is_focused and "Discussions" or "Notes")
end
end)
end
---Define signs for discussions if not already defined
M.setup_signs = function()
local discussion_sign = state.settings.discussion_sign
local signs = {
[discussion_sign_name] = discussion_sign.text,
[discussion_helper_sign_start] = discussion_sign.helper_signs.start,
[discussion_helper_sign_mid] = discussion_sign.helper_signs.mid,
[discussion_helper_sign_end] = discussion_sign.helper_signs["end"],
}
for sign_name, sign_text in pairs(signs) do
if #vim.fn.sign_getdefined(sign_name) == 0 then
vim.fn.sign_define(sign_name, {
text = sign_text,
linehl = discussion_sign.linehl,
texthl = discussion_sign.texthl,
culhl = discussion_sign.culhl,
numhl = discussion_sign.numhl,
})
end
end
end
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
M.initialize_discussions = function()
M.setup_signs()
M.setup_refresh_discussion_data_callback()
M.setup_leave_reviewer_callback()
end
---Setup callback to refresh discussion data, discussion signs and diagnostics whenever the
---reviewed file changes.
M.setup_refresh_discussion_data_callback = function()
reviewer.set_callback_for_file_changed(M.refresh_discussion_data)
end
---Clear all signs and diagnostics
M.clear_signs_and_discussions = function()
vim.fn.sign_unplace(discussion_sign_name)
vim.diagnostic.reset(diagnostics_namespace)
end
---Setup callback to clear signs and diagnostics whenever reviewer is left.
M.setup_leave_reviewer_callback = function()
reviewer.set_callback_for_reviewer_leave(M.clear_signs_and_discussions)
end
M.refresh_discussion_tree = function()
if M.layout_visible == false then
return
end
if type(M.discussions) == "table" then
M.rebuild_discussion_tree()
end
if type(M.unlinked_discussions) == "table" then
M.rebuild_unlinked_discussion_tree()
end
M.switch_can_edit_bufs(true)
M.add_empty_titles({
{ M.linked_section.bufnr, M.discussions, "No Discussions for this MR" },
{ M.unlinked_section.bufnr, M.unlinked_discussions, "No Notes (Unlinked Discussions) for this MR" },
})
M.switch_can_edit_bufs(false)
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?
M.toggle = function(callback)
if M.layout_visible then
M.layout:unmount()
M.layout_visible = false
M.discussion_tree = nil
M.linked_section = nil
M.unlinked_section = nil
if M.split_visible then
M.close()
return
end
local linked_section, unlinked_section, layout = M.create_layout()
M.linked_section = linked_section
M.unlinked_section = unlinked_section
local split, linked_bufnr, unlinked_bufnr = M.create_split_and_bufs()
M.linked_bufnr = linked_bufnr
M.unlinked_bufnr = unlinked_bufnr
M.split = split
M.split_visible = true
M.split_bufnr = split.bufnr
split:mount()
M.switch_can_edit_bufs(true)
vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, { "Loading data..." })
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 })
local default_discussions = state.settings.discussion_tree.default_view == "discussions"
winbar.update_winbar({}, {}, default_discussions and "Discussions" or "Notes")
M.load_discussions(function()
if type(M.discussions) ~= "table" and type(M.unlinked_discussions) ~= "table" then
vim.notify("No discussions or notes for this MR", vim.log.levels.WARN)
vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, { "" })
return
end
layout:mount()
layout:show()
M.rebuild_discussion_tree()
M.rebuild_unlinked_discussion_tree()
M.add_empty_titles({
{ M.linked_bufnr, M.discussions, "No Discussions for this MR" },
{ M.unlinked_bufnr, M.unlinked_discussions, "No Notes (Unlinked Discussions) for this MR" },
})
M.layout = layout
M.layout_visible = true
M.layout_buf = layout.bufnr
state.discussion_buf = layout.bufnr
M.refresh_discussion_tree()
local default_buffer = default_discussions and M.linked_bufnr or M.unlinked_bufnr
vim.api.nvim_set_current_buf(default_buffer)
M.focused_bufnr = default_buffer
M.switch_can_edit_bufs(false)
winbar.update_winbar(M.discussions, M.unlinked_discussions, default_discussions and "Discussions" or "Notes")
if type(callback) == "function" then
callback()
end
end)
end
local switch_view_type = function()
local change_to_unlinked = M.linked_bufnr == M.focused_bufnr
local new_bufnr = change_to_unlinked and M.unlinked_bufnr or M.linked_bufnr
vim.api.nvim_set_current_buf(new_bufnr)
winbar.update_winbar(M.discussions, M.unlinked_discussions, change_to_unlinked and "Notes" or "Discussions")
M.focused_bufnr = new_bufnr
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 diagnostics = vim.diagnostic.get(0, { namespace = diagnostics_namespace, lnum = current_line - 1 })
local diagnostics = vim.diagnostic.get(0, { namespace = signs.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)
@@ -435,11 +164,11 @@ M.move_to_discussion_tree = function()
discussion_node:expand()
end
M.discussion_tree:render()
vim.api.nvim_win_set_cursor(M.linked_section.winid, { line_number, 0 })
vim.api.nvim_set_current_win(M.linked_section.winid)
vim.api.nvim_win_set_cursor(M.split.winid, { line_number, 0 })
vim.api.nvim_set_current_win(M.split.winid)
end
if not M.layout_visible then
if not M.split_visible then
M.toggle(jump_after_tree_opened)
else
jump_after_tree_opened()
@@ -523,13 +252,14 @@ M.send_deletion = function(tree, unlinked)
M.discussions = u.remove_first_value(M.discussions)
M.rebuild_discussion_tree()
end
M.switch_can_edit_bufs(true)
M.add_empty_titles({
{ M.linked_section.bufnr, M.discussions, "No Discussions for this MR" },
{ M.unlinked_section.bufnr, M.unlinked_discussions, "No Notes (Unlinked Discussions) for this MR" },
{ M.linked_bufnr, M.discussions, "No Discussions for this MR" },
{ M.unlinked_bufnr, M.unlinked_discussions, "No Notes (Unlinked Discussions) for this MR" },
})
M.switch_can_edit_bufs(false)
end
M.refresh_discussion_data()
end)
end
@@ -573,6 +303,7 @@ M.send_edits = function(discussion_id, note_id, unlinked)
}
job.run_job("/comment", "PATCH", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
M.rebuild_discussion_tree()
if unlinked then
M.replace_text(M.unlinked_discussions, discussion_id, note_id, text)
M.rebuild_unlinked_discussion_tree()
@@ -599,6 +330,7 @@ M.toggle_discussion_resolved = function(tree)
job.run_job("/discussions/resolve", "PUT", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
M.redraw_resolved_status(tree, note, not note.resolved)
M.refresh_discussion_data()
end)
end
@@ -693,36 +425,37 @@ end
M.rebuild_discussion_tree = function()
M.switch_can_edit_bufs(true)
vim.api.nvim_buf_set_lines(M.linked_section.bufnr, 0, -1, false, {})
vim.api.nvim_buf_set_lines(M.linked_bufnr, 0, -1, false, {})
local discussion_tree_nodes = discussions_tree.add_discussions_to_table(M.discussions, false)
local discussion_tree =
NuiTree({ nodes = discussion_tree_nodes, bufnr = M.linked_section.bufnr, prepare_node = nui_tree_prepare_node })
NuiTree({ nodes = discussion_tree_nodes, bufnr = M.linked_bufnr, prepare_node = nui_tree_prepare_node })
discussion_tree:render()
M.set_tree_keymaps(discussion_tree, M.linked_section.bufnr, false)
M.set_tree_keymaps(discussion_tree, M.linked_bufnr, false)
M.discussion_tree = discussion_tree
M.switch_can_edit_bufs(false)
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.linked_section.bufnr })
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.linked_bufnr })
end
M.rebuild_unlinked_discussion_tree = function()
M.switch_can_edit_bufs(true)
vim.api.nvim_buf_set_lines(M.unlinked_section.bufnr, 0, -1, false, {})
vim.api.nvim_buf_set_lines(M.unlinked_bufnr, 0, -1, false, {})
local unlinked_discussion_tree_nodes = discussions_tree.add_discussions_to_table(M.unlinked_discussions, true)
local unlinked_discussion_tree = NuiTree({
nodes = unlinked_discussion_tree_nodes,
bufnr = M.unlinked_section.bufnr,
bufnr = M.unlinked_bufnr,
prepare_node = nui_tree_prepare_node,
})
unlinked_discussion_tree:render()
M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_section.bufnr, true)
M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_bufnr, true)
M.unlinked_discussion_tree = unlinked_discussion_tree
M.switch_can_edit_bufs(false)
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.unlinked_section.bufnr })
end
M.switch_can_edit_bufs = function(bool)
u.switch_can_edit_buf(M.unlinked_section.bufnr, bool)
u.switch_can_edit_buf(M.linked_section.bufnr, bool)
u.switch_can_edit_buf(M.unlinked_bufnr, bool)
u.switch_can_edit_buf(M.linked_bufnr, bool)
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.unlinked_bufnr })
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = M.linked_bufnr })
end
M.add_discussion = function(arg)
@@ -732,44 +465,35 @@ M.add_discussion = function(arg)
M.unlinked_discussions = {}
end
table.insert(M.unlinked_discussions, 1, discussion)
if M.unlinked_section ~= nil then
M.rebuild_unlinked_discussion_tree()
end
M.rebuild_unlinked_discussion_tree()
return
end
if type(M.discussions) ~= "table" then
M.discussions = {}
end
table.insert(M.discussions, 1, discussion)
if M.linked_section ~= nil then
M.rebuild_discussion_tree()
end
M.rebuild_discussion_tree()
end
M.create_layout = function()
local linked_section = Split({ enter = true })
local unlinked_section = Split({})
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 layout = Layout(
{
position = position,
size = size,
relative = relative,
},
Layout.Box({
Layout.Box(linked_section, { size = "50%" }),
Layout.Box(unlinked_section, { size = "50%" }),
}, { dir = (position == "left" and "col" or "row") })
)
local split = Split({
relative = relative,
position = position,
size = size,
})
return linked_section, unlinked_section, layout
local linked_bufnr = vim.api.nvim_create_buf(true, false)
local unlinked_bufnr = vim.api.nvim_create_buf(true, false)
return split, linked_bufnr, unlinked_bufnr
end
M.add_empty_titles = function(args)
M.switch_can_edit_bufs(true)
local ns_id = vim.api.nvim_create_namespace("GitlabNamespace")
vim.cmd("highlight default TitleHighlight guifg=#787878")
for _, section in ipairs(args) do
@@ -830,7 +554,12 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
M.reply(tree)
end
end, { buffer = bufnr, desc = "Reply" })
vim.keymap.set("n", state.settings.discussion_tree.switch_view, function()
switch_view_type()
end, { buffer = bufnr, desc = "Switch view type" })
vim.keymap.set("n", state.settings.help, function()
help.open()
end, { buffer = bufnr, desc = "Open help popup" })
if not unlinked then
vim.keymap.set("n", state.settings.discussion_tree.jump_to_file, function()
if M.is_current_node_note(tree) then

View File

@@ -0,0 +1,293 @@
local state = require("gitlab.state")
local u = require("gitlab.utils")
local reviewer = require("gitlab.reviewer")
local discussion_sign_name = "gitlab_discussion"
local discussion_helper_sign_start = "gitlab_discussion_helper_start"
local discussion_helper_sign_mid = "gitlab_discussion_helper_mid"
local discussion_helper_sign_end = "gitlab_discussion_helper_end"
local diagnostics_namespace = vim.api.nvim_create_namespace(discussion_sign_name)
local M = {}
M.diagnostics_namespace = diagnostics_namespace
---Parse line code and return old and new line numbers
---@param line_code string gitlab line code -> 588440f66559714280628a4f9799f0c4eb880a4a_10_10
---@return number?
---@return number?
local function _parse_line_code(line_code)
local line_code_regex = "%w+_(%d+)_(%d+)"
local old_line, new_line = line_code:match(line_code_regex)
return tonumber(old_line), tonumber(new_line)
end
---Filter all discussions which are relevant for currently visible signs and diagnostscs.
---@return Discussion[]?
local filter_discussions_for_signs_and_diagnostics = function(all_discussions)
if type(all_discussions) ~= "table" then
return
end
local file = reviewer.get_current_file()
if not file then
return
end
local discussions = {}
for _, discussion in ipairs(all_discussions) do
local first_note = discussion.notes[1]
if
type(first_note.position) == "table"
and (first_note.position.new_path == file or first_note.position.old_path == file)
then
if
--Skip resolved discussions
not (
state.settings.discussion_sign_and_diagnostic.skip_resolved_discussion
and first_note.resolvable
and first_note.resolved
)
--Skip discussions from old revisions
and not (
state.settings.discussion_sign_and_diagnostic.skip_old_revision_discussion
and u.from_iso_format_date_to_timestamp(first_note.created_at)
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
)
then
table.insert(discussions, discussion)
end
end
end
return discussions
end
---Build note header from note.
---@param note Note
---@return string
local build_note_header = function(note)
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
end
---Define signs for discussions if not already defined
M.setup_signs = function()
local discussion_sign = state.settings.discussion_sign
local signs = {
[discussion_sign_name] = discussion_sign.text,
[discussion_helper_sign_start] = discussion_sign.helper_signs.start,
[discussion_helper_sign_mid] = discussion_sign.helper_signs.mid,
[discussion_helper_sign_end] = discussion_sign.helper_signs["end"],
}
for sign_name, sign_text in pairs(signs) do
if #vim.fn.sign_getdefined(sign_name) == 0 then
vim.fn.sign_define(sign_name, {
text = sign_text,
linehl = discussion_sign.linehl,
texthl = discussion_sign.texthl,
culhl = discussion_sign.culhl,
numhl = discussion_sign.numhl,
})
end
end
end
---Refresh the discussion signs for currently loaded file in reviewer For convinience we use same
---string for sign name and sign group ( currently there is only one sign needed)
M.refresh_signs = function(discussions)
local diagnostics = filter_discussions_for_signs_and_diagnostics(discussions)
if diagnostics == nil then
vim.diagnostic.reset(diagnostics_namespace)
return
end
local new_signs = {}
local old_signs = {}
for _, discussion in ipairs(diagnostics) do
local first_note = discussion.notes[1]
local base_sign = {
name = discussion_sign_name,
group = discussion_sign_name,
priority = state.settings.discussion_sign.priority,
}
local base_helper_sign = {
name = discussion_sign_name,
group = discussion_sign_name,
priority = state.settings.discussion_sign.priority - 1,
}
if first_note.position.line_range ~= nil then
local start_old_line, start_new_line = _parse_line_code(first_note.position.line_range.start.line_code)
local end_old_line, end_new_line = _parse_line_code(first_note.position.line_range["end"].line_code)
local discussion_line, start_line, end_line
if first_note.position.line_range.start.type == "new" then
table.insert(
new_signs,
vim.tbl_deep_extend("force", {
id = first_note.id,
lnum = first_note.position.new_line,
}, base_sign)
)
discussion_line = first_note.position.new_line
start_line = start_new_line
end_line = end_new_line
elseif first_note.position.line_range.start.type == "old" then
table.insert(
old_signs,
vim.tbl_deep_extend("force", {
id = first_note.id,
lnum = first_note.position.old_line,
}, base_sign)
)
discussion_line = first_note.position.old_line
start_line = start_old_line
end_line = end_old_line
end
-- Helper signs does not have specific ids currently.
if state.settings.discussion_sign.helper_signs.enabled then
local helper_signs = {}
if start_line > end_line then
start_line, end_line = end_line, start_line
end
for i = start_line, end_line do
if i ~= discussion_line then
local sign_name
if i == start_line then
sign_name = discussion_helper_sign_start
elseif i == end_line then
sign_name = discussion_helper_sign_end
else
sign_name = discussion_helper_sign_mid
end
table.insert(
helper_signs,
vim.tbl_deep_extend("keep", {
name = sign_name,
lnum = i,
}, base_helper_sign)
)
end
end
if first_note.position.line_range.start.type == "new" then
vim.list_extend(new_signs, helper_signs)
elseif first_note.position.line_range.start.type == "old" then
vim.list_extend(old_signs, helper_signs)
end
end
else
local sign = vim.tbl_deep_extend("force", {
id = first_note.id,
}, base_sign)
if first_note.position.new_line ~= nil then
table.insert(new_signs, vim.tbl_deep_extend("force", { lnum = first_note.position.new_line }, sign))
end
if first_note.position.old_line ~= nil then
table.insert(old_signs, vim.tbl_deep_extend("force", { lnum = first_note.position.old_line }, sign))
end
end
end
vim.fn.sign_unplace(discussion_sign_name)
reviewer.place_sign(old_signs, "old")
reviewer.place_sign(new_signs, "new")
end
---Refresh the diagnostics for the currently reviewed file
M.refresh_diagnostics = function(discussions)
-- Keep in mind that diagnostic line numbers use 0-based indexing while line numbers use
-- 1-based indexing
local diagnostics = filter_discussions_for_signs_and_diagnostics(discussions)
if diagnostics == nil then
vim.diagnostic.reset(diagnostics_namespace)
return
end
local new_diagnostics = {}
local old_diagnostics = {}
for _, discussion in ipairs(diagnostics) do
local first_note = discussion.notes[1]
local message = ""
for _, note in ipairs(discussion.notes) do
message = message .. build_note_header(note) .. "\n" .. note.body .. "\n"
end
local diagnostic = {
message = message,
col = 0,
severity = state.settings.discussion_diagnostic.severity,
user_data = { discussion_id = discussion.id, header = build_note_header(discussion.notes[1]) },
source = "gitlab",
code = state.settings.discussion_diagnostic.code,
}
if first_note.position.line_range ~= nil then
-- Diagnostics for line range discussions are tricky - you need to set lnum to
-- line number equal to note.position.new_line or note.position.old_line because that is
-- only line where you can trigger the diagnostic show. This also need to be in sinc
-- with the sign placement.
local start_old_line, start_new_line = _parse_line_code(first_note.position.line_range.start.line_code)
local end_old_line, end_new_line = _parse_line_code(first_note.position.line_range["end"].line_code)
if first_note.position.line_range.start.type == "new" then
local new_diagnostic
if first_note.position.new_line == start_new_line then
new_diagnostic = {
lnum = start_new_line - 1,
end_lnum = end_new_line - 1,
}
else
new_diagnostic = {
lnum = end_new_line - 1,
end_lnum = start_new_line - 1,
}
end
new_diagnostic = vim.tbl_deep_extend("force", new_diagnostic, diagnostic)
table.insert(new_diagnostics, new_diagnostic)
elseif first_note.position.line_range.start.type == "old" then
local old_diagnostic
if first_note.position.old_line == start_old_line then
old_diagnostic = {
lnum = start_old_line - 1,
end_lnum = end_old_line - 1,
}
else
old_diagnostic = {
lnum = end_old_line - 1,
end_lnum = start_old_line - 1,
}
end
old_diagnostic = vim.tbl_deep_extend("force", old_diagnostic, diagnostic)
table.insert(old_diagnostics, old_diagnostic)
end
else
-- Diagnostics for single line discussions.
if first_note.position.new_line ~= nil then
local new_diagnostic = {
lnum = first_note.position.new_line - 1,
}
new_diagnostic = vim.tbl_deep_extend("force", new_diagnostic, diagnostic)
table.insert(new_diagnostics, new_diagnostic)
end
if first_note.position.old_line ~= nil then
local old_diagnostic = {
lnum = first_note.position.old_line - 1,
}
old_diagnostic = vim.tbl_deep_extend("force", old_diagnostic, diagnostic)
table.insert(old_diagnostics, old_diagnostic)
end
end
end
vim.diagnostic.reset(diagnostics_namespace)
reviewer.set_diagnostics(
diagnostics_namespace,
new_diagnostics,
"new",
state.settings.discussion_diagnostic.display_opts
)
reviewer.set_diagnostics(
diagnostics_namespace,
old_diagnostics,
"old",
state.settings.discussion_diagnostic.display_opts
)
end
---Clear all signs and diagnostics
M.clear_signs_and_discussions = function()
vim.fn.sign_unplace(discussion_sign_name)
vim.diagnostic.reset(diagnostics_namespace)
end
return M

View File

@@ -0,0 +1,58 @@
local M = {}
local state = require("gitlab.state")
---@param nodes Discussion[]|UnlinkedDiscussion[]|nil
local get_data = function(nodes)
if nodes == nil then
return 0, 0
end
local total_resolvable = 0
local total_resolved = 0
if nodes == vim.NIL then
return ""
end
for _, d in ipairs(nodes) do
local first_child = d.notes[1]
if first_child ~= nil then
if first_child.resolvable then
total_resolvable = total_resolvable + 1
end
if first_child.resolved then
total_resolved = total_resolved + 1
end
end
end
return total_resolvable, total_resolved
end
---@param discussions Discussion[]|nil
---@param unlinked_discussions UnlinkedDiscussion[]|nil
---@param file_name string
local function content(discussions, unlinked_discussions, file_name)
local resolvable_discussions, resolved_discussions = get_data(discussions)
local resolvable_notes, resolved_notes = get_data(unlinked_discussions)
local t = {
name = file_name,
resolvable_discussions = resolvable_discussions,
resolved_discussions = resolved_discussions,
resolvable_notes = resolvable_notes,
resolved_notes = resolved_notes,
}
return state.settings.discussion_tree.winbar(t)
end
---This function sends the edited comment to the Go server
---@param discussions Discussion[]
---@param unlinked_discussions UnlinkedDiscussion[]
---@param base_title string
M.update_winbar = function(discussions, unlinked_discussions, base_title)
local d = require("gitlab.actions.discussions")
local winId = d.split.winid
vim.wo[winId].winbar = content(discussions, unlinked_discussions, base_title)
end
return M