Files
gitlab.nvim/lua/gitlab/actions/common.lua
Harrison (Harry) Cramer e4eabaf71d Release 1/24/26 (#519)
* 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)

* feat(ci): Cancel obsolete after a new commit is pushed in an open PR (#503)

* fix: start and clean up winbar timer properly (#513)

This is a PATCH.

* fix: put attach_file markdown on new line (#512)

This is a PATCH PR.

* docs: fix incorrect value for 'relative' option to Split (#511)

This is a PATCH.

* docs: add default keybinding maps available in the help (#506)

This is a PATCH change.

* feat: enable setting discussion tree options (#509)

* docs: add description of `refresh_data` function

* fix: only set gitlab filetype in one place

* feat: set some useful window options for the discussion tree split

This is a PATCH PR.

---------

Co-authored-by: Jakub F. Bortlík <jakub.bortlik@proton.me>
Co-authored-by: Jonathan Duck <Duckbrain30@gmail.com>
Co-authored-by: Kitsios Konstantinos <kitsios.konst@gmail.com>
Co-authored-by: Mohammad Akbari <makbari@users.noreply.github.com>
2026-01-24 10:04:47 -05:00

323 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)
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