This MR fixes an issue with refreshes of the diagnostics and signs when users leave comments on unchanged lines.
292 lines
8.8 KiB
Lua
292 lines
8.8 KiB
Lua
local state = require("gitlab.state")
|
|
local u = require("gitlab.utils")
|
|
local NuiTree = require("nui.tree")
|
|
|
|
local M = {}
|
|
|
|
local attach_uuid = function(str)
|
|
return { text = str, id = u.uuid() }
|
|
end
|
|
|
|
---Create path node
|
|
---@param relative_path string
|
|
---@param full_path string
|
|
---@param child_nodes NuiTree.Node[]?
|
|
---@return NuiTree.Node
|
|
local function create_path_node(relative_path, full_path, child_nodes)
|
|
return NuiTree.Node({
|
|
text = relative_path,
|
|
path = full_path,
|
|
id = full_path,
|
|
type = "path",
|
|
icon = " ",
|
|
icon_hl = "GitlabDirectoryIcon",
|
|
text_hl = "GitlabDirectory",
|
|
}, child_nodes or {})
|
|
end
|
|
|
|
---Create file name node
|
|
---@param file_name string
|
|
---@param full_file_path string
|
|
---@param child_nodes NuiTree.Node[]?
|
|
---@return NuiTree.Node
|
|
local function create_file_name_node(file_name, full_file_path, child_nodes)
|
|
local icon, icon_hl = u.get_icon(file_name)
|
|
return NuiTree.Node({
|
|
text = file_name,
|
|
file_name = full_file_path,
|
|
id = full_file_path,
|
|
type = "file_name",
|
|
icon = icon,
|
|
icon_hl = icon_hl,
|
|
text_hl = "GitlabFileName",
|
|
}, child_nodes or {})
|
|
end
|
|
|
|
---Sort list of nodes (in place) of type "path" or "file_name"
|
|
---@param nodes NuiTree.Node[]
|
|
local function sort_nodes(nodes)
|
|
table.sort(nodes, function(node1, node2)
|
|
if node1.type == "path" and node2.type == "path" then
|
|
return node1.path < node2.path
|
|
elseif node1.type == "file_name" and node2.type == "file_name" then
|
|
return node1.file_name < node2.file_name
|
|
elseif node1.type == "path" and node2.type == "file_name" then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end)
|
|
end
|
|
|
|
---Merge path nodes which have only single path child
|
|
---@param node NuiTree.Node
|
|
local function flatten_nodes(node)
|
|
if node.type ~= "path" then
|
|
return
|
|
end
|
|
for _, child in ipairs(node.__children) do
|
|
flatten_nodes(child)
|
|
end
|
|
if #node.__children == 1 and node.__children[1].type == "path" then
|
|
local child = node.__children[1]
|
|
node.__children = child.__children
|
|
node.id = child.id
|
|
node.path = child.path
|
|
node.text = node.text .. u.path_separator .. child.text
|
|
end
|
|
sort_nodes(node.__children)
|
|
end
|
|
|
|
---Build note header from note.
|
|
---@param note Note
|
|
---@return string
|
|
local function build_note_header(note)
|
|
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
|
end
|
|
|
|
---Build note node body
|
|
---@param note Note
|
|
---@param resolve_info table?
|
|
---@return string
|
|
---@return NuiTree.Node[]
|
|
local function build_note_body(note, resolve_info)
|
|
local text_nodes = {}
|
|
for bodyLine in note.body:gmatch("[^\n]+") do
|
|
local line = attach_uuid(bodyLine)
|
|
table.insert(
|
|
text_nodes,
|
|
NuiTree.Node({
|
|
new_line = (type(note.position) == "table" and note.position.new_line),
|
|
old_line = (type(note.position) == "table" and note.position.old_line),
|
|
text = line.text,
|
|
id = line.id,
|
|
type = "note_body",
|
|
}, {})
|
|
)
|
|
end
|
|
|
|
local resolve_symbol = ""
|
|
if resolve_info ~= nil and resolve_info.resolvable then
|
|
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
|
or state.settings.discussion_tree.unresolved
|
|
end
|
|
|
|
local noteHeader = build_note_header(note) .. " " .. resolve_symbol
|
|
|
|
return noteHeader, text_nodes
|
|
end
|
|
|
|
---Build note node
|
|
---@param note Note
|
|
---@param resolve_info table?
|
|
---@return NuiTree.Node
|
|
---@return string
|
|
---@return NuiTree.Node[]
|
|
M.build_note = function(note, resolve_info)
|
|
local text, text_nodes = build_note_body(note, resolve_info)
|
|
local note_node = NuiTree.Node({
|
|
text = text,
|
|
id = note.id,
|
|
file_name = (type(note.position) == "table" and note.position.new_path),
|
|
new_line = (type(note.position) == "table" and note.position.new_line),
|
|
old_line = (type(note.position) == "table" and note.position.old_line),
|
|
type = "note",
|
|
}, text_nodes)
|
|
|
|
return note_node, text, text_nodes
|
|
end
|
|
|
|
---Create nodes for NuiTree from discussions
|
|
---@param items Discussion[]
|
|
---@param unlinked boolean? False or nil means that discussions are linked to code lines
|
|
---@return NuiTree.Node[]
|
|
M.add_discussions_to_table = function(items, unlinked)
|
|
local t = {}
|
|
for _, discussion in ipairs(items) do
|
|
local discussion_children = {}
|
|
|
|
-- These properties are filled in by the first note
|
|
---@type string?
|
|
local root_text = ""
|
|
---@type string?
|
|
local root_note_id = ""
|
|
---@type string?
|
|
local root_file_name = ""
|
|
---@type string
|
|
local root_id
|
|
local root_text_nodes = {}
|
|
local resolvable = false
|
|
local resolved = false
|
|
local undefined_type = false
|
|
local root_new_line = nil
|
|
local root_old_line = nil
|
|
|
|
for j, note in ipairs(discussion.notes) do
|
|
if j == 1 then
|
|
_, root_text, root_text_nodes = M.build_note(note, { resolved = note.resolved, resolvable = note.resolvable })
|
|
|
|
root_file_name = (type(note.position) == "table" and note.position.new_path or nil)
|
|
root_new_line = (type(note.position) == "table" and note.position.new_line or nil)
|
|
root_old_line = (type(note.position) == "table" and note.position.old_line or nil)
|
|
root_id = discussion.id
|
|
root_note_id = tostring(note.id)
|
|
resolvable = note.resolvable
|
|
resolved = note.resolved
|
|
|
|
-- This appears to be a Gitlab 🐛 where the "type" is returned as an empty string in some cases
|
|
-- We link these comments to the old file by default
|
|
if
|
|
type(note.position) == "table"
|
|
and note.position.line_range ~= nil
|
|
and note.position.line_range.start.type == ""
|
|
then
|
|
undefined_type = true
|
|
end
|
|
else -- Otherwise insert it as a child node...
|
|
local note_node = M.build_note(note)
|
|
table.insert(discussion_children, note_node)
|
|
end
|
|
end
|
|
|
|
-- Creates the first node in the discussion, and attaches children
|
|
local body = u.spread(root_text_nodes, discussion_children)
|
|
local root_node = NuiTree.Node({
|
|
text = root_text,
|
|
type = "note",
|
|
is_root = true,
|
|
id = root_id,
|
|
root_note_id = root_note_id,
|
|
file_name = root_file_name,
|
|
new_line = root_new_line,
|
|
old_line = root_old_line,
|
|
resolvable = resolvable,
|
|
resolved = resolved,
|
|
undefined_type = undefined_type,
|
|
}, body)
|
|
|
|
table.insert(t, root_node)
|
|
end
|
|
if state.settings.discussion_tree.tree_type == "simple" or unlinked == true then
|
|
return t
|
|
end
|
|
|
|
-- Create all the folder and file name nodes.
|
|
local discussion_by_file_name = {}
|
|
local top_level_path_to_node = {}
|
|
for _, node in ipairs(t) do
|
|
local path = ""
|
|
local parent_node = nil
|
|
local path_parts = u.split_path(node.file_name)
|
|
local file_name = table.remove(path_parts, #path_parts)
|
|
-- Create folders
|
|
for i, path_part in ipairs(path_parts) do
|
|
path = path ~= nil and path .. u.path_separator .. path_part or path_part
|
|
if i == 1 then
|
|
if top_level_path_to_node[path] == nil then
|
|
parent_node = create_path_node(path_part, path)
|
|
top_level_path_to_node[path] = parent_node
|
|
table.insert(discussion_by_file_name, parent_node)
|
|
end
|
|
parent_node = top_level_path_to_node[path]
|
|
elseif parent_node then
|
|
local child_node = nil
|
|
for _, child in ipairs(parent_node.__children) do
|
|
if child.path == path then
|
|
child_node = child
|
|
break
|
|
end
|
|
end
|
|
|
|
if child_node == nil then
|
|
child_node = create_path_node(path_part, path)
|
|
table.insert(parent_node.__children, child_node)
|
|
parent_node:expand()
|
|
parent_node = child_node
|
|
else
|
|
parent_node = child_node
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Create file name nodes
|
|
if parent_node == nil then
|
|
---Top level file name
|
|
if top_level_path_to_node[node.file_name] ~= nil then
|
|
table.insert(top_level_path_to_node[node.file_name].__children, node)
|
|
else
|
|
local file_node = create_file_name_node(file_name, node.file_name, { node })
|
|
file_node:expand()
|
|
top_level_path_to_node[node.file_name] = file_node
|
|
table.insert(discussion_by_file_name, file_node)
|
|
end
|
|
else
|
|
local child_node = nil
|
|
for _, child in ipairs(parent_node.__children) do
|
|
if child.file_name == node.file_name then
|
|
child_node = child
|
|
break
|
|
end
|
|
end
|
|
if child_node == nil then
|
|
child_node = create_file_name_node(file_name, node.file_name, { node })
|
|
table.insert(parent_node.__children, child_node)
|
|
parent_node:expand()
|
|
child_node:expand()
|
|
else
|
|
table.insert(child_node.__children, node)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Flatten empty folders
|
|
for _, node in ipairs(discussion_by_file_name) do
|
|
flatten_nodes(node)
|
|
end
|
|
sort_nodes(discussion_by_file_name)
|
|
|
|
return discussion_by_file_name
|
|
end
|
|
|
|
return M
|