* 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>
324 lines
10 KiB
Lua
324 lines
10 KiB
Lua
-- This module contains code shared between at least two modules. This includes
|
|
-- actions common to multiple tree types, as well as general utility functions
|
|
-- that are specific to actions (like jumping to a file or opening a URL)
|
|
local List = require("gitlab.utils.list")
|
|
local u = require("gitlab.utils")
|
|
local reviewer = require("gitlab.reviewer")
|
|
local indicators_common = require("gitlab.indicators.common")
|
|
local state = require("gitlab.state")
|
|
local M = {}
|
|
|
|
---Build note header from note
|
|
---@param note Note|DraftNote
|
|
---@return string
|
|
M.build_note_header = function(note)
|
|
if note.note then
|
|
return "@" .. state.USER.username .. " " .. state.settings.discussion_tree.draft
|
|
end
|
|
local time = state.settings.discussion_tree.relative_date and u.time_since(note.created_at)
|
|
or u.format_to_local(note.created_at, vim.fn.strftime("%z"))
|
|
return "@" .. note.author.username .. " " .. time
|
|
end
|
|
|
|
M.switch_can_edit_bufs = function(bool, ...)
|
|
local bufnrs = { ... }
|
|
---@param v integer
|
|
for _, v in ipairs(bufnrs) do
|
|
u.switch_can_edit_buf(v, bool)
|
|
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = v })
|
|
end
|
|
end
|
|
|
|
---Takes in a chunk of text separated by new line characters and returns a lua table
|
|
---@param content string
|
|
---@return table
|
|
M.build_content = function(content)
|
|
local description_lines = u.lines_into_table(content)
|
|
table.insert(description_lines, "")
|
|
return description_lines
|
|
end
|
|
|
|
M.add_empty_titles = function()
|
|
local draft_notes = require("gitlab.actions.draft_notes")
|
|
local discussions = require("gitlab.actions.discussions")
|
|
local linked, unlinked, drafts =
|
|
List.new(u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions)),
|
|
List.new(u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.unlinked_discussions)),
|
|
List.new(u.ensure_table(state.DRAFT_NOTES))
|
|
|
|
local position_drafts = drafts:filter(function(note)
|
|
return draft_notes.has_position(note)
|
|
end)
|
|
local non_positioned_drafts = drafts:filter(function(note)
|
|
return not draft_notes.has_position(note)
|
|
end)
|
|
|
|
local fields = {
|
|
{
|
|
bufnr = discussions.linked_bufnr,
|
|
count = #linked + #position_drafts,
|
|
title = "No Discussions for this MR",
|
|
},
|
|
{
|
|
bufnr = discussions.unlinked_bufnr,
|
|
count = #unlinked + #non_positioned_drafts,
|
|
title = "No Notes (Unlinked Discussions) for this MR",
|
|
},
|
|
}
|
|
|
|
for _, v in ipairs(fields) do
|
|
if v.bufnr ~= nil then
|
|
M.switch_can_edit_bufs(true, v.bufnr)
|
|
local ns_id = vim.api.nvim_create_namespace("GitlabNamespace")
|
|
vim.cmd("highlight default TitleHighlight guifg=#787878")
|
|
|
|
-- Set empty title if applicable
|
|
if v.count == 0 then
|
|
vim.api.nvim_buf_set_lines(v.bufnr, 0, 1, false, { v.title })
|
|
local linnr = 1
|
|
vim.api.nvim_buf_set_extmark(
|
|
v.bufnr,
|
|
ns_id,
|
|
linnr - 1,
|
|
0,
|
|
{ end_row = linnr - 1, end_col = string.len(v.title), hl_group = "TitleHighlight" }
|
|
)
|
|
end
|
|
M.switch_can_edit_bufs(false, v.bufnr)
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param tree NuiTree
|
|
M.get_url = function(tree)
|
|
local current_node = tree:get_node()
|
|
local note_node = M.get_note_node(tree, current_node)
|
|
if note_node == nil then
|
|
return
|
|
end
|
|
local url = note_node.url
|
|
if url == nil then
|
|
u.notify("Could not get URL of note", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
return url
|
|
end
|
|
|
|
---@param tree NuiTree
|
|
M.open_in_browser = function(tree)
|
|
local url = M.get_url(tree)
|
|
if url ~= nil then
|
|
u.open_in_browser(url)
|
|
end
|
|
end
|
|
|
|
---@param tree NuiTree
|
|
M.copy_node_url = function(tree)
|
|
local url = M.get_url(tree)
|
|
if url ~= nil then
|
|
vim.fn.setreg("+", url)
|
|
u.notify("Copied '" .. url .. "' to clipboard", vim.log.levels.INFO)
|
|
end
|
|
end
|
|
|
|
-- For developers!
|
|
M.print_node = function(tree)
|
|
local current_node = tree:get_node()
|
|
vim.print(current_node)
|
|
end
|
|
|
|
---Check if type of node is note or note body
|
|
---@param node NuiTree.Node?
|
|
---@return boolean
|
|
M.is_node_note = function(node)
|
|
if node and (node.type == "note_body" or node.type == "note") then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
---Get root node
|
|
---@param tree NuiTree
|
|
---@param node NuiTree.Node?
|
|
---@return NuiTree.Node?
|
|
M.get_root_node = function(tree, node)
|
|
if not node then
|
|
return nil
|
|
end
|
|
if node.type == "note_body" or node.type == "note" and not node.is_root then
|
|
local parent_id = node:get_parent_id()
|
|
return M.get_root_node(tree, tree:get_node(parent_id))
|
|
elseif node.is_root then
|
|
return node
|
|
end
|
|
end
|
|
|
|
---Get note node
|
|
---@param tree NuiTree
|
|
---@param node NuiTree.Node?
|
|
---@return NuiTree.Node?
|
|
M.get_note_node = function(tree, node)
|
|
if not node then
|
|
return nil
|
|
end
|
|
|
|
if node.type == "note_body" then
|
|
local parent_id = node:get_parent_id()
|
|
if parent_id == nil then
|
|
return node
|
|
end
|
|
return M.get_note_node(tree, tree:get_node(parent_id))
|
|
elseif node.type == "note" then
|
|
return node
|
|
end
|
|
end
|
|
|
|
---Takes a node and returns the line where the note is positioned in the new SHA. If
|
|
---the line is not in the new SHA, returns nil
|
|
---@param node NuiTree.Node
|
|
---@return number|nil
|
|
local function get_new_line(node)
|
|
---@type GitlabLineRange|nil
|
|
local range = node.range
|
|
if range == nil then
|
|
return node.new_line
|
|
end
|
|
|
|
local _, new_start_line = indicators_common.parse_line_code(range.start.line_code)
|
|
return new_start_line
|
|
end
|
|
|
|
---Takes a node and returns the line where the note is positioned in the old SHA. If
|
|
---the line is not in the old SHA, returns nil
|
|
---@param node NuiTree.Node
|
|
---@return number|nil
|
|
local function get_old_line(node)
|
|
---@type GitlabLineRange|nil
|
|
local range = node.range
|
|
if range == nil then
|
|
return node.old_line
|
|
end
|
|
|
|
local old_start_line, _ = indicators_common.parse_line_code(range.start.line_code)
|
|
return old_start_line
|
|
end
|
|
|
|
---@param id string|integer
|
|
---@return integer|nil line_number
|
|
---@return boolean is_new_sha True if line number refers to NEW SHA
|
|
M.get_line_number = function(id)
|
|
---@type Discussion|DraftNote|nil
|
|
local d_or_n
|
|
d_or_n = List.new(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {}):find(function(d)
|
|
return d.id == id
|
|
end) or List.new(state.DRAFT_NOTES or {}):find(function(d)
|
|
return d.id == id
|
|
end)
|
|
|
|
if d_or_n == nil then
|
|
return nil, true
|
|
end
|
|
|
|
local first_note = indicators_common.get_first_note(d_or_n)
|
|
local is_new_sha = indicators_common.is_new_sha(d_or_n)
|
|
return ((is_new_sha and first_note.position.new_line or first_note.position.old_line) or 1), is_new_sha
|
|
end
|
|
|
|
---Return the start and end line numbers for the note range. The range is calculated from the line
|
|
---codes but the position itself is based on either the `new_line` or `old_line`.
|
|
---@param old_line integer|nil The line number in the OLD version
|
|
---@param new_line integer|nil The line number in the NEW version
|
|
---@param start_line_code string The line code for the start of the range
|
|
---@param end_line_code string The line code for the end of the range
|
|
---@return integer start_line
|
|
---@return integer end_line
|
|
---@return boolean is_new_sha True if line range refers to NEW SHA
|
|
M.get_line_numbers_for_range = function(old_line, new_line, start_line_code, end_line_code)
|
|
local old_start_line, new_start_line = indicators_common.parse_line_code(start_line_code)
|
|
local old_end_line, new_end_line = indicators_common.parse_line_code(end_line_code)
|
|
if old_line ~= nil and old_start_line ~= 0 then
|
|
local range = old_end_line - old_start_line
|
|
return (old_line - range), old_line, false
|
|
elseif new_line ~= nil then
|
|
local range = new_end_line - new_start_line
|
|
-- Force start_line to be greater than 0
|
|
local start_line = (new_line - range > 0) and (new_line - range) or 1
|
|
return start_line, new_line, true
|
|
else
|
|
u.notify("Error getting new or old line for range", vim.log.levels.ERROR)
|
|
return 1, 1, false
|
|
end
|
|
end
|
|
|
|
---@param root_node NuiTree.Node
|
|
---@return integer|nil line_number
|
|
---@return boolean is_new_sha True if line number refers to NEW SHA
|
|
M.get_line_number_from_node = function(root_node)
|
|
if root_node.range then
|
|
local line_number, _, is_new_sha = M.get_line_numbers_for_range(
|
|
root_node.old_line,
|
|
root_node.new_line,
|
|
root_node.range.start.line_code,
|
|
root_node.range["end"].line_code
|
|
)
|
|
return line_number, is_new_sha
|
|
else
|
|
return M.get_line_number(root_node.id)
|
|
end
|
|
end
|
|
|
|
-- This function (settings.keymaps.discussion_tree.jump_to_reviewer) will jump the cursor to the reviewer's location associated with the note. The implementation depends on the reviewer
|
|
M.jump_to_reviewer = function(tree)
|
|
local node = tree:get_node()
|
|
local root_node = M.get_root_node(tree, node)
|
|
if root_node == nil then
|
|
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
local line_number, is_new_sha = M.get_line_number_from_node(root_node)
|
|
if line_number == nil then
|
|
u.notify("Could not get line number", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
reviewer.jump(root_node.file_name, root_node.old_file_name, line_number, is_new_sha)
|
|
end
|
|
|
|
-- This function (settings.keymaps.discussion_tree.jump_to_file) will jump to the file changed in a new tab
|
|
M.jump_to_file = function(tree)
|
|
local node = tree:get_node()
|
|
local root_node = M.get_root_node(tree, node)
|
|
if root_node == nil then
|
|
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
if root_node.file_name == nil then
|
|
u.notify("This comment was not left on a particular location", vim.log.levels.WARN)
|
|
return
|
|
end
|
|
if vim.fn.filereadable(root_node.file_name) == 0 then
|
|
u.notify(
|
|
string.format("The file %s for which the comment was made doesn't exist in HEAD.", root_node.file_name),
|
|
vim.log.levels.WARN
|
|
)
|
|
return
|
|
end
|
|
vim.cmd.tabnew()
|
|
local line_number = get_new_line(root_node) or get_old_line(root_node)
|
|
if line_number == nil or line_number == 0 then
|
|
line_number = 1
|
|
end
|
|
local bufnr = vim.fn.bufnr(root_node.file_name)
|
|
if bufnr ~= -1 then
|
|
vim.cmd("buffer " .. bufnr)
|
|
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
|
return
|
|
end
|
|
|
|
-- If buffer is not already open, open it
|
|
vim.cmd("edit " .. root_node.file_name)
|
|
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
|
end
|
|
|
|
return M
|