Bug Fixes (#470)

* fix: Restore buffer local settings outside reviewer (#446)
* fix: do not show healthcheck alert for warnings (#468)
* feat: Add MR URL to the summary details (#467)
* fix: make cycling reviewed files faster (#474)
* feat(pipeline): display trigger jobs for a pipeline in the pipelines popup  (#465)
* fix: Jumping to renamed files (#484)

---------

Co-authored-by: Jakub F. Bortlík <jakub.bortlik@proton.me>
Co-authored-by: Ashish Alex <ashish.alex10@gmail.com>
This commit is contained in:
Harrison (Harry) Cramer
2025-03-01 13:28:02 -05:00
committed by GitHub
parent 3b396a5e6b
commit 9f898aa1a8
23 changed files with 524 additions and 359 deletions

View File

@@ -42,19 +42,13 @@ type RequestWithPosition interface {
func buildCommentPosition(commentWithPositionData RequestWithPosition) *gitlab.PositionOptions { func buildCommentPosition(commentWithPositionData RequestWithPosition) *gitlab.PositionOptions {
positionData := commentWithPositionData.GetPositionData() positionData := commentWithPositionData.GetPositionData()
// If the file has been renamed, then this is a relevant part of the payload
oldFileName := positionData.OldFileName
if oldFileName == "" {
oldFileName = positionData.FileName
}
opt := &gitlab.PositionOptions{ opt := &gitlab.PositionOptions{
PositionType: &positionData.Type, PositionType: &positionData.Type,
StartSHA: &positionData.StartCommitSHA, StartSHA: &positionData.StartCommitSHA,
HeadSHA: &positionData.HeadCommitSHA, HeadSHA: &positionData.HeadCommitSHA,
BaseSHA: &positionData.BaseCommitSHA, BaseSHA: &positionData.BaseCommitSHA,
NewPath: &positionData.FileName, NewPath: &positionData.FileName,
OldPath: &oldFileName, OldPath: &positionData.OldFileName,
NewLine: positionData.NewLine, NewLine: positionData.NewLine,
OldLine: positionData.OldLine, OldLine: positionData.OldLine,
} }

View File

@@ -20,16 +20,18 @@ type RetriggerPipelineResponse struct {
type PipelineWithJobs struct { type PipelineWithJobs struct {
Jobs []*gitlab.Job `json:"jobs"` Jobs []*gitlab.Job `json:"jobs"`
LatestPipeline *gitlab.PipelineInfo `json:"latest_pipeline"` LatestPipeline *gitlab.PipelineInfo `json:"latest_pipeline"`
Name string `json:"name"`
} }
type GetPipelineAndJobsResponse struct { type GetPipelineAndJobsResponse struct {
SuccessResponse SuccessResponse
Pipeline PipelineWithJobs `json:"latest_pipeline"` Pipelines []PipelineWithJobs `json:"latest_pipeline"`
} }
type PipelineManager interface { type PipelineManager interface {
ListProjectPipelines(pid interface{}, opt *gitlab.ListProjectPipelinesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.PipelineInfo, *gitlab.Response, error) ListProjectPipelines(pid interface{}, opt *gitlab.ListProjectPipelinesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.PipelineInfo, *gitlab.Response, error)
ListPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) ListPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error)
ListPipelineBridges(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Bridge, *gitlab.Response, error)
RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error)
} }
@@ -101,7 +103,6 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
} }
jobs, res, err := a.client.ListPipelineJobs(a.projectInfo.ProjectId, pipeline.ID, &gitlab.ListJobsOptions{}) jobs, res, err := a.client.ListPipelineJobs(a.projectInfo.ProjectId, pipeline.ID, &gitlab.ListJobsOptions{})
if err != nil { if err != nil {
handleError(w, err, "Could not get pipeline jobs", http.StatusInternalServerError) handleError(w, err, "Could not get pipeline jobs", http.StatusInternalServerError)
return return
@@ -112,13 +113,51 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
return return
} }
pipelines := []PipelineWithJobs{}
pipelines = append(pipelines, PipelineWithJobs{
Jobs: jobs,
LatestPipeline: pipeline,
Name: "root",
})
bridges, res, err := a.client.ListPipelineBridges(a.projectInfo.ProjectId, pipeline.ID, &gitlab.ListJobsOptions{})
if err != nil {
handleError(w, err, "Could not get pipeline trigger jobs", http.StatusInternalServerError)
return
}
if res.StatusCode >= 300 {
handleError(w, GenericError{r.URL.Path}, "Could not get pipeline trigger jobs", res.StatusCode)
return
}
for _, bridge := range bridges {
if bridge.DownstreamPipeline == nil {
continue
}
pipelineIdInBridge := bridge.DownstreamPipeline.ID
bridgePipelineJobs, res, err := a.client.ListPipelineJobs(bridge.DownstreamPipeline.ProjectID, pipelineIdInBridge, &gitlab.ListJobsOptions{})
if err != nil {
handleError(w, err, "Could not get jobs for a pipeline from a trigger job", http.StatusInternalServerError)
return
}
if res.StatusCode >= 300 {
handleError(w, GenericError{r.URL.Path}, "Could not get jobs for a pipeline from a trigger job", res.StatusCode)
return
}
pipelines = append(pipelines, PipelineWithJobs{
Jobs: bridgePipelineJobs,
LatestPipeline: bridge.DownstreamPipeline,
Name: bridge.Name,
})
}
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
response := GetPipelineAndJobsResponse{ response := GetPipelineAndJobsResponse{
SuccessResponse: SuccessResponse{Message: "Pipeline retrieved"}, SuccessResponse: SuccessResponse{Message: "Pipeline retrieved"},
Pipeline: PipelineWithJobs{ Pipelines: pipelines,
LatestPipeline: pipeline,
Jobs: jobs,
},
} }
err = json.NewEncoder(w).Encode(response) err = json.NewEncoder(w).Encode(response)

View File

@@ -27,6 +27,14 @@ func (f fakePipelineManager) ListPipelineJobs(pid interface{}, pipelineID int, o
return []*gitlab.Job{}, resp, err return []*gitlab.Job{}, resp, err
} }
func (f fakePipelineManager) ListPipelineBridges(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Bridge, *gitlab.Response, error) {
resp, err := f.handleGitlabError()
if err != nil {
return nil, nil, err
}
return []*gitlab.Bridge{}, resp, err
}
func (f fakePipelineManager) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) { func (f fakePipelineManager) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) {
resp, err := f.handleGitlabError() resp, err := f.handleGitlabError()
if err != nil { if err != nil {

View File

@@ -302,6 +302,7 @@ you call this function with no values the defaults will be used:
"delete_branch", "delete_branch",
"squash", "squash",
"labels", "labels",
"web_url",
}, },
}, },
discussion_signs = { discussion_signs = {
@@ -315,6 +316,7 @@ you call this function with no values the defaults will be used:
comment = "→|", comment = "→|",
range = " |", range = " |",
}, },
skip_old_revision_discussion = false, -- Don't show diagnostics for discussions that were created for earlier MR revisions
}, },
pipeline = { pipeline = {
created = "", created = "",

View File

@@ -15,7 +15,6 @@ local reviewer = require("gitlab.reviewer")
local Location = require("gitlab.reviewer.location") local Location = require("gitlab.reviewer.location")
local M = { local M = {
current_win = nil,
start_line = nil, start_line = nil,
end_line = nil, end_line = nil,
draft_popup = nil, draft_popup = nil,
@@ -25,10 +24,9 @@ local M = {
---Fires the API that sends the comment data to the Go server, called when you "confirm" creation ---Fires the API that sends the comment data to the Go server, called when you "confirm" creation
---via the M.settings.keymaps.popup.perform_action keybinding ---via the M.settings.keymaps.popup.perform_action keybinding
---@param text string comment text ---@param text string comment text
---@param visual_range LineRange | nil range of visual selection or nil
---@param unlinked boolean if true, the comment is not linked to a line ---@param unlinked boolean if true, the comment is not linked to a line
---@param discussion_id string | nil The ID of the discussion to which the reply is responding, nil if not a reply ---@param discussion_id string | nil The ID of the discussion to which the reply is responding, nil if not a reply
local confirm_create_comment = function(text, visual_range, unlinked, discussion_id) local confirm_create_comment = function(text, unlinked, discussion_id)
if text == nil then if text == nil then
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR) u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
return return
@@ -75,30 +73,16 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
return return
end end
local reviewer_data = reviewer.get_reviewer_data()
if reviewer_data == nil then
u.notify("Error getting reviewer data", vim.log.levels.ERROR)
return
end
local location = Location.new(reviewer_data, visual_range)
location:build_location_data()
local location_data = location.location_data
if location_data == nil then
u.notify("Error getting location information", vim.log.levels.ERROR)
return
end
local revision = state.MR_REVISIONS[1] local revision = state.MR_REVISIONS[1]
local position_data = { local position_data = {
file_name = reviewer_data.file_name, file_name = M.location.reviewer_data.file_name,
old_file_name = reviewer_data.old_file_name, old_file_name = M.location.reviewer_data.old_file_name,
base_commit_sha = revision.base_commit_sha, base_commit_sha = revision.base_commit_sha,
start_commit_sha = revision.start_commit_sha, start_commit_sha = revision.start_commit_sha,
head_commit_sha = revision.head_commit_sha, head_commit_sha = revision.head_commit_sha,
old_line = location_data.old_line, old_line = M.location.location_data.old_line,
new_line = location_data.new_line, new_line = M.location.location_data.new_line,
line_range = location_data.line_range, line_range = M.location.location_data.line_range,
} }
-- Creating a new comment (linked to specific changes) -- Creating a new comment (linked to specific changes)
@@ -148,10 +132,10 @@ M.confirm_edit_comment = function(discussion_id, note_id, unlinked)
end end
---@class LayoutOpts ---@class LayoutOpts
---@field ranged boolean
---@field unlinked boolean ---@field unlinked boolean
---@field discussion_id string|nil ---@field discussion_id string|nil
---@field reply boolean|nil ---@field reply boolean|nil
---@field file_name string|nil
---This function sets up the layout and popups needed to create a comment, note and ---This function sets up the layout and popups needed to create a comment, note and
---multi-line comment. It also sets up the basic keybindings for switching between ---multi-line comment. It also sets up the basic keybindings for switching between
@@ -163,21 +147,24 @@ M.create_comment_layout = function(opts)
local title local title
local user_settings local user_settings
if opts.discussion_id ~= nil then if opts.discussion_id ~= nil then
title = "Reply" title = "Reply" .. (opts.file_name and string.format(" [%s]", opts.file_name) or "")
user_settings = popup_settings.reply user_settings = popup_settings.reply
elseif opts.unlinked then elseif opts.unlinked then
title = "Note" title = "Note"
user_settings = popup_settings.note user_settings = popup_settings.note
else else
title = "Comment" -- TODO: investigate why `old_file_name` is in fact the new name for renamed files!
local file_name = M.location.reviewer_data.old_file_name ~= "" and M.location.reviewer_data.old_file_name
or M.location.reviewer_data.file_name
title =
popup.create_title("Comment", file_name, M.location.visual_range.start_line, M.location.visual_range.end_line)
user_settings = popup_settings.comment user_settings = popup_settings.comment
end end
local settings = u.merge(popup_settings, user_settings or {}) local settings = u.merge(popup_settings, user_settings or {})
M.current_win = vim.api.nvim_get_current_win() local current_win = vim.api.nvim_get_current_win()
M.comment_popup = Popup(popup.create_popup_state(title, settings)) M.comment_popup = Popup(popup.create_popup_state(title, settings))
M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings)) M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings))
M.start_line, M.end_line = u.get_visual_selection_boundaries()
local internal_layout = Layout.Box({ local internal_layout = Layout.Box({
Layout.Box(M.comment_popup, { grow = 1 }), Layout.Box(M.comment_popup, { grow = 1 }),
@@ -194,22 +181,21 @@ M.create_comment_layout = function(opts)
}, internal_layout) }, internal_layout)
popup.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup }) popup.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
popup.set_up_autocommands(M.comment_popup, layout, M.current_win) popup.set_up_autocommands(M.comment_popup, layout, current_win)
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
local unlinked = opts.unlinked or false local unlinked = opts.unlinked or false
---Keybinding for focus on draft section ---Keybinding for focus on draft section
popup.set_popup_keymaps(M.draft_popup, function() popup.set_popup_keymaps(M.draft_popup, function()
local text = u.get_buffer_text(M.comment_popup.bufnr) local text = u.get_buffer_text(M.comment_popup.bufnr)
confirm_create_comment(text, range, unlinked, opts.discussion_id) confirm_create_comment(text, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win) vim.api.nvim_set_current_win(current_win)
end, miscellaneous.toggle_bool, popup.non_editable_popup_opts) end, miscellaneous.toggle_bool, popup.non_editable_popup_opts)
---Keybinding for focus on text section ---Keybinding for focus on text section
popup.set_popup_keymaps(M.comment_popup, function(text) popup.set_popup_keymaps(M.comment_popup, function(text)
confirm_create_comment(text, range, unlinked, opts.discussion_id) confirm_create_comment(text, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win) vim.api.nvim_set_current_win(current_win)
end, miscellaneous.attach_file, popup.editable_popup_opts) end, miscellaneous.attach_file, popup.editable_popup_opts)
vim.schedule(function() vim.schedule(function()
@@ -223,44 +209,43 @@ end
--- This function will open a comment popup in order to create a comment on the changed/updated --- This function will open a comment popup in order to create a comment on the changed/updated
--- line in the current MR --- line in the current MR
M.create_comment = function() M.create_comment = function()
M.location = Location.new()
if not M.can_create_comment(false) then if not M.can_create_comment(false) then
return return
end end
local layout = M.create_comment_layout({ ranged = false, unlinked = false }) local layout = M.create_comment_layout({ unlinked = false })
layout:mount() layout:mount()
end end
--- This function will open a multi-line comment popup in order to create a multi-line comment --- This function will open a multi-line comment popup in order to create a multi-line comment
--- on the changed/updated line in the current MR --- on the changed/updated line in the current MR
M.create_multiline_comment = function() M.create_multiline_comment = function()
M.location = Location.new()
if not M.can_create_comment(true) then if not M.can_create_comment(true) then
u.press_escape() u.press_escape()
return return
end end
local layout = M.create_comment_layout({ ranged = true, unlinked = false }) local layout = M.create_comment_layout({ unlinked = false })
layout:mount() layout:mount()
end end
--- This function will open a a popup to create a "note" (e.g. unlinked comment) --- This function will open a a popup to create a "note" (e.g. unlinked comment)
--- on the changed/updated line in the current MR --- on the changed/updated line in the current MR
M.create_note = function() M.create_note = function()
local layout = M.create_comment_layout({ ranged = false, unlinked = true }) local layout = M.create_comment_layout({ unlinked = true })
layout:mount() layout:mount()
end end
---Given the current visually selected area of text, builds text to fill in the ---Given the current visually selected area of text, builds text to fill in the
---comment popup with a suggested change ---comment popup with a suggested change
---@return LineRange|nil ---@return LineRange|nil
---@return integer
local build_suggestion = function() local build_suggestion = function()
local current_line = vim.api.nvim_win_get_cursor(0)[1] local current_line = vim.api.nvim_win_get_cursor(0)[1]
M.start_line, M.end_line = u.get_visual_selection_boundaries() local range_length = M.location.visual_range.end_line - M.location.visual_range.start_line
local range_length = M.end_line - M.start_line
local backticks = "```" local backticks = "```"
local selected_lines = u.get_lines(M.start_line, M.end_line) local selected_lines = u.get_lines(M.location.visual_range.start_line, M.location.visual_range.end_line)
for _, line in ipairs(selected_lines) do for _, line in ipairs(selected_lines) do
if string.match(line, "^```%S*$") then if string.match(line, "^```%S*$") then
@@ -270,14 +255,14 @@ local build_suggestion = function()
end end
local suggestion_start local suggestion_start
if M.start_line == current_line then if M.location.visual_range.start_line == current_line then
suggestion_start = backticks .. "suggestion:-0+" .. range_length suggestion_start = backticks .. "suggestion:-0+" .. range_length
elseif M.end_line == current_line then elseif M.location.visual_range.end_line == current_line then
suggestion_start = backticks .. "suggestion:-" .. range_length .. "+0" suggestion_start = backticks .. "suggestion:-" .. range_length .. "+0"
else else
--- This should never happen afaik --- This should never happen afaik
u.notify("Unexpected suggestion position", vim.log.levels.ERROR) u.notify("Unexpected suggestion position", vim.log.levels.ERROR)
return nil, 0 return nil
end end
suggestion_start = suggestion_start suggestion_start = suggestion_start
local suggestion_lines = {} local suggestion_lines = {}
@@ -285,21 +270,22 @@ local build_suggestion = function()
vim.list_extend(suggestion_lines, selected_lines) vim.list_extend(suggestion_lines, selected_lines)
table.insert(suggestion_lines, backticks) table.insert(suggestion_lines, backticks)
return suggestion_lines, range_length return suggestion_lines
end end
--- This function will open a a popup to create a suggestion comment --- This function will open a a popup to create a suggestion comment
--- on the changed/updated line in the current MR --- on the changed/updated line in the current MR
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html --- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
M.create_comment_suggestion = function() M.create_comment_suggestion = function()
M.location = Location.new()
if not M.can_create_comment(true) then if not M.can_create_comment(true) then
u.press_escape() u.press_escape()
return return
end end
local suggestion_lines, range_length = build_suggestion() local suggestion_lines = build_suggestion()
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false }) local layout = M.create_comment_layout({ unlinked = false })
layout:mount() layout:mount()
vim.schedule(function() vim.schedule(function()
@@ -368,6 +354,11 @@ M.can_create_comment = function(must_be_visual)
return false return false
end end
if M.location == nil or M.location.location_data == nil then
u.notify("Error getting location information", vim.log.levels.ERROR)
return false
end
return true return true
end end

View File

@@ -265,7 +265,7 @@ M.get_line_number_from_node = function(root_node)
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 -- 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, callback) M.jump_to_reviewer = function(tree)
local node = tree:get_node() local node = tree:get_node()
local root_node = M.get_root_node(tree, node) local root_node = M.get_root_node(tree, node)
if root_node == nil then if root_node == nil then
@@ -277,8 +277,7 @@ M.jump_to_reviewer = function(tree, callback)
u.notify("Could not get line number", vim.log.levels.ERROR) u.notify("Could not get line number", vim.log.levels.ERROR)
return return
end end
reviewer.jump(root_node.file_name, line_number, is_new_sha) reviewer.jump(root_node.file_name, root_node.old_file_name, line_number, is_new_sha)
callback()
end end
-- This function (settings.keymaps.discussion_tree.jump_to_file) will jump to the file changed in a new tab -- This function (settings.keymaps.discussion_tree.jump_to_file) will jump to the file changed in a new tab
@@ -302,7 +301,7 @@ M.jump_to_file = function(tree)
end end
vim.cmd.tabnew() vim.cmd.tabnew()
local line_number = get_new_line(root_node) or get_old_line(root_node) local line_number = get_new_line(root_node) or get_old_line(root_node)
if line_number == nil then if line_number == nil or line_number == 0 then
line_number = 1 line_number = 1
end end
local bufnr = vim.fn.bufnr(root_node.file_name) local bufnr = vim.fn.bufnr(root_node.file_name)

View File

@@ -215,7 +215,7 @@ M.open_confirmation_popup = function(mr)
return return
end end
local layout, title_popup, description_popup, target_popup, delete_branch_popup, squash_popup, forked_project_id_popup = local layout, title_popup, description_popup, target_popup, source_popup, delete_branch_popup, squash_popup, forked_project_id_popup =
M.create_layout() M.create_layout()
local popups = { local popups = {
@@ -224,6 +224,7 @@ M.open_confirmation_popup = function(mr)
delete_branch_popup, delete_branch_popup,
squash_popup, squash_popup,
target_popup, target_popup,
source_popup,
} }
if state.settings.create_mr.fork.enabled then if state.settings.create_mr.fork.enabled then
@@ -261,6 +262,7 @@ M.open_confirmation_popup = function(mr)
vim.api.nvim_buf_set_lines(M.description_bufnr, 0, -1, false, description_lines) vim.api.nvim_buf_set_lines(M.description_bufnr, 0, -1, false, description_lines)
vim.api.nvim_buf_set_lines(M.title_bufnr, 0, -1, false, { mr.title }) vim.api.nvim_buf_set_lines(M.title_bufnr, 0, -1, false, { mr.title })
vim.api.nvim_buf_set_lines(M.target_bufnr, 0, -1, false, { mr.target }) vim.api.nvim_buf_set_lines(M.target_bufnr, 0, -1, false, { mr.target })
vim.api.nvim_buf_set_lines(M.source_bufnr, 0, -1, false, { require("gitlab.git").get_current_branch() })
vim.api.nvim_buf_set_lines(M.delete_branch_bufnr, 0, -1, false, { u.bool_to_string(delete_branch) }) vim.api.nvim_buf_set_lines(M.delete_branch_bufnr, 0, -1, false, { u.bool_to_string(delete_branch) })
vim.api.nvim_buf_set_lines(M.squash_bufnr, 0, -1, false, { u.bool_to_string(squash) }) vim.api.nvim_buf_set_lines(M.squash_bufnr, 0, -1, false, { u.bool_to_string(squash) })
if state.settings.create_mr.fork.enabled then if state.settings.create_mr.fork.enabled then
@@ -281,6 +283,7 @@ M.open_confirmation_popup = function(mr)
popup.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts) popup.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts)
popup.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts) popup.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts)
popup.set_popup_keymaps(target_popup, M.create_mr, M.select_new_target, popup_opts) popup.set_popup_keymaps(target_popup, M.create_mr, M.select_new_target, popup_opts)
popup.set_popup_keymaps(source_popup, M.create_mr, nil, popup_opts)
popup.set_popup_keymaps(delete_branch_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts) popup.set_popup_keymaps(delete_branch_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
popup.set_popup_keymaps(squash_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts) popup.set_popup_keymaps(squash_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
popup.set_popup_keymaps(forked_project_id_popup, M.create_mr, nil, popup_opts) popup.set_popup_keymaps(forked_project_id_popup, M.create_mr, nil, popup_opts)
@@ -336,6 +339,8 @@ M.create_layout = function()
M.description_bufnr = description_popup.bufnr M.description_bufnr = description_popup.bufnr
local target_branch_popup = Popup(popup.create_box_popup_state("Target branch", false, settings)) local target_branch_popup = Popup(popup.create_box_popup_state("Target branch", false, settings))
M.target_bufnr = target_branch_popup.bufnr M.target_bufnr = target_branch_popup.bufnr
local source_branch_popup = Popup(popup.create_box_popup_state("Source branch", false, settings))
M.source_bufnr = source_branch_popup.bufnr
local delete_title = vim.o.columns > 110 and "Delete source branch" or "Delete source" local delete_title = vim.o.columns > 110 and "Delete source branch" or "Delete source"
local delete_branch_popup = Popup(popup.create_box_popup_state(delete_title, false, settings)) local delete_branch_popup = Popup(popup.create_box_popup_state(delete_title, false, settings))
M.delete_branch_bufnr = delete_branch_popup.bufnr M.delete_branch_bufnr = delete_branch_popup.bufnr
@@ -352,6 +357,7 @@ M.create_layout = function()
table.insert(boxes, Layout.Box(delete_branch_popup, { size = { width = #delete_title + 4 } })) table.insert(boxes, Layout.Box(delete_branch_popup, { size = { width = #delete_title + 4 } }))
table.insert(boxes, Layout.Box(squash_popup, { size = { width = #squash_title + 4 } })) table.insert(boxes, Layout.Box(squash_popup, { size = { width = #squash_title + 4 } }))
table.insert(boxes, Layout.Box(target_branch_popup, { grow = 1 })) table.insert(boxes, Layout.Box(target_branch_popup, { grow = 1 }))
table.insert(boxes, Layout.Box(source_branch_popup, { grow = 1 }))
local internal_layout = Layout.Box({ local internal_layout = Layout.Box({
Layout.Box({ Layout.Box({
@@ -378,6 +384,7 @@ M.create_layout = function()
title_popup, title_popup,
description_popup, description_popup,
target_branch_popup, target_branch_popup,
source_branch_popup,
delete_branch_popup, delete_branch_popup,
squash_popup, squash_popup,
forked_project_id_popup forked_project_id_popup

View File

@@ -15,7 +15,6 @@ local List = require("gitlab.utils.list")
local tree_utils = require("gitlab.actions.discussions.tree") local tree_utils = require("gitlab.actions.discussions.tree")
local discussions_tree = require("gitlab.actions.discussions.tree") local discussions_tree = require("gitlab.actions.discussions.tree")
local draft_notes = require("gitlab.actions.draft_notes") local draft_notes = require("gitlab.actions.draft_notes")
local diffview_lib = require("diffview.lib")
local signs = require("gitlab.indicators.signs") local signs = require("gitlab.indicators.signs")
local diagnostics = require("gitlab.indicators.diagnostics") local diagnostics = require("gitlab.indicators.diagnostics")
local winbar = require("gitlab.actions.discussions.winbar") local winbar = require("gitlab.actions.discussions.winbar")
@@ -74,37 +73,25 @@ end
M.initialize_discussions = function() M.initialize_discussions = function()
state.discussion_tree.last_updated = os.time() state.discussion_tree.last_updated = os.time()
signs.setup_signs() signs.setup_signs()
reviewer.set_callback_for_file_changed(function() reviewer.set_callback_for_file_changed(function(args)
M.refresh_diagnostics() diagnostics.place_diagnostics(args.buf)
M.modifiable(false) reviewer.update_winid_for_buffer(args.buf)
reviewer.set_reviewer_keymaps()
end) end)
reviewer.set_callback_for_reviewer_enter(function() reviewer.set_callback_for_reviewer_enter(function()
M.modifiable(false) 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) end)
reviewer.set_callback_for_reviewer_leave(function() reviewer.set_callback_for_reviewer_leave(function()
signs.clear_signs() signs.clear_signs()
diagnostics.clear_diagnostics() diagnostics.clear_diagnostics()
M.modifiable(true)
reviewer.del_reviewer_keymaps()
end) end)
end end
--- Ensures that the both buffers in the reviewer are/not modifiable. Relevant if the user is using --- Take existing data and refresh the diagnostics and the signs
--- the --imply-local setting
M.modifiable = function(bool)
local view = diffview_lib.get_current_view()
local a = view.cur_layout.a.file.bufnr
local b = view.cur_layout.b.file.bufnr
if a ~= nil and vim.api.nvim_buf_is_loaded(a) then
vim.api.nvim_buf_set_option(a, "modifiable", bool)
end
if b ~= nil and vim.api.nvim_buf_is_loaded(b) then
vim.api.nvim_buf_set_option(b, "modifiable", bool)
end
end
--- Take existing data and refresh the diagnostics, the winbar, and the signs
M.refresh_diagnostics = function() M.refresh_diagnostics = function()
if state.settings.discussion_signs.enabled then if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics() diagnostics.refresh_diagnostics()
@@ -115,7 +102,9 @@ end
---Opens the discussion tree, sets the keybindings. It also ---Opens the discussion tree, sets the keybindings. It also
---creates the tree for notes (which are not linked to specific lines of code) ---creates the tree for notes (which are not linked to specific lines of code)
---@param callback function? ---@param callback function?
M.open = function(callback) ---@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.discussions = u.ensure_table(state.DISCUSSION_DATA.discussions)
state.DISCUSSION_DATA.unlinked_discussions = u.ensure_table(state.DISCUSSION_DATA.unlinked_discussions) state.DISCUSSION_DATA.unlinked_discussions = u.ensure_table(state.DISCUSSION_DATA.unlinked_discussions)
state.DRAFT_NOTES = u.ensure_table(state.DRAFT_NOTES) state.DRAFT_NOTES = u.ensure_table(state.DRAFT_NOTES)
@@ -136,7 +125,7 @@ M.open = function(callback)
-- Initialize winbar module with data from buffers -- Initialize winbar module with data from buffers
winbar.set_buffers(M.linked_bufnr, M.unlinked_bufnr) winbar.set_buffers(M.linked_bufnr, M.unlinked_bufnr)
winbar.switch_view_type(state.settings.discussion_tree.default_view) 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 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) vim.api.nvim_set_current_win(M.split.winid)
@@ -146,7 +135,7 @@ M.open = function(callback)
M.rebuild_unlinked_discussion_tree() M.rebuild_unlinked_discussion_tree()
-- Set default buffer -- Set default buffer
local default_buffer = winbar.bufnr_map[state.settings.discussion_tree.default_view] local default_buffer = winbar.bufnr_map[view_type]
vim.api.nvim_set_current_buf(default_buffer) vim.api.nvim_set_current_buf(default_buffer)
common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr) common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr)
@@ -192,12 +181,13 @@ M.move_to_discussion_tree = function()
discussion_node:expand() discussion_node:expand()
end end
M.discussion_tree:render() M.discussion_tree:render()
vim.api.nvim_win_set_cursor(M.split.winid, { line_number, 0 })
vim.api.nvim_set_current_win(M.split.winid) 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 end
if not M.split_visible then if not M.split_visible then
M.toggle(jump_after_tree_opened) M.open(jump_after_tree_opened, "discussions")
else else
jump_after_tree_opened() jump_after_tree_opened()
end end
@@ -247,10 +237,10 @@ M.reply = function(tree)
local comment = require("gitlab.actions.comment") local comment = require("gitlab.actions.comment")
local unlinked = tree.bufnr == M.unlinked_bufnr local unlinked = tree.bufnr == M.unlinked_bufnr
local layout = comment.create_comment_layout({ local layout = comment.create_comment_layout({
ranged = false,
discussion_id = discussion_id, discussion_id = discussion_id,
unlinked = unlinked, unlinked = unlinked,
reply = true, reply = true,
file_name = discussion_node.file_name,
}) })
layout:mount() layout:mount()
@@ -284,7 +274,6 @@ end
-- This function (settings.keymaps.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree -- 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) M.edit_comment = function(tree, unlinked)
local edit_popup = Popup(popup.create_popup_state("Edit Comment", state.settings.popup.edit))
local current_node = tree:get_node() local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node) local note_node = common.get_note_node(tree, current_node)
local root_node = common.get_root_node(tree, current_node) local root_node = common.get_root_node(tree, current_node)
@@ -292,6 +281,9 @@ M.edit_comment = function(tree, unlinked)
u.notify("Could not get root or note node", vim.log.levels.ERROR) u.notify("Could not get root or note node", vim.log.levels.ERROR)
return return
end 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()) popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
@@ -587,7 +579,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.jump_to_reviewer then if keymaps.discussion_tree.jump_to_reviewer then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function() vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
if M.is_current_node_note(tree) then if M.is_current_node_note(tree) then
common.jump_to_reviewer(tree, M.refresh_diagnostics) common.jump_to_reviewer(tree)
end end
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait }) end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
end end
@@ -757,6 +749,16 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
}) })
end 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 if keymaps.discussion_tree.add_emoji then
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function() vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
M.add_emoji_to_note(tree, unlinked) M.add_emoji_to_note(tree, unlinked)

View File

@@ -28,6 +28,8 @@ M.add_discussions_to_table = function(items, unlinked)
local root_note_id = "" local root_note_id = ""
---@type string? ---@type string?
local root_file_name = "" local root_file_name = ""
---@type string?
local root_old_file_name = ""
---@type string ---@type string
local root_id local root_id
local root_text_nodes = {} local root_text_nodes = {}
@@ -43,6 +45,7 @@ M.add_discussions_to_table = function(items, unlinked)
if j == 1 then if j == 1 then
_, root_text, root_text_nodes = M.build_note(note, { resolved = note.resolved, resolvable = note.resolvable }) _, 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_file_name = (type(note.position) == "table" and note.position.new_path or nil)
root_old_file_name = (type(note.position) == "table" and note.position.old_path or nil)
root_new_line = (type(note.position) == "table" and note.position.new_line 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_old_line = (type(note.position) == "table" and note.position.old_line or nil)
root_id = discussion.id root_id = discussion.id
@@ -79,6 +82,7 @@ M.add_discussions_to_table = function(items, unlinked)
id = root_id, id = root_id,
root_note_id = root_note_id, root_note_id = root_note_id,
file_name = root_file_name, file_name = root_file_name,
old_file_name = root_old_file_name,
new_line = root_new_line, new_line = root_new_line,
old_line = root_old_line, old_line = root_old_line,
resolvable = resolvable, resolvable = resolvable,

View File

@@ -254,9 +254,8 @@ M.get_mode = function()
end end
end end
---Sets the current view type (if provided an argument) ---Toggles the current view type (or sets it to `override`) and then updates the view.
---and then updates the view ---@param override "discussions"|"notes" Defines the view type to select.
---@param override any
M.switch_view_type = function(override) M.switch_view_type = function(override)
if override then if override then
M.current_view_type = override M.current_view_type = override

View File

@@ -7,41 +7,77 @@ local job = require("gitlab.job")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local popup = require("gitlab.popup") local popup = require("gitlab.popup")
local M = { local M = {
pipeline_jobs = nil, pipeline_jobs = {},
latest_pipeline = nil, latest_pipeline = nil,
pipeline_popup = nil, pipeline_popup = nil,
} }
local function get_latest_pipeline() local function get_latest_pipelines(count)
local pipeline = state.PIPELINE and state.PIPELINE.latest_pipeline count = count or 1 -- Default to 1 if count is not provided
if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then local pipelines = {}
u.notify("Pipeline not found", vim.log.levels.WARN)
return if not state.PIPELINE then
u.notify("Pipeline state is not initialized", vim.log.levels.WARN)
return nil
end end
return pipeline
for i = 1, math.max(count, #state.PIPELINE) do
local pipeline = state.PIPELINE[i].latest_pipeline
if type(pipeline) == "table" and u.table_size(pipeline) > 0 then
table.insert(pipelines, pipeline)
end
end
if #pipelines == 0 then
u.notify("No valid pipelines found", vim.log.levels.WARN)
return nil
end
return pipelines
end end
local function get_pipeline_jobs() local function get_pipeline_jobs(idx)
M.latest_pipeline = get_latest_pipeline() return u.reverse(type(state.PIPELINE[idx].jobs) == "table" and state.PIPELINE[idx].jobs or {})
if not M.latest_pipeline then
return
end
return u.reverse(type(state.PIPELINE.jobs) == "table" and state.PIPELINE.jobs or {})
end end
-- The function will render the Pipeline state in a popup -- The function will render the Pipeline state in a popup
M.open = function() M.open = function()
M.pipeline_jobs = get_pipeline_jobs() M.latest_pipelines = get_latest_pipelines()
M.latest_pipeline = get_latest_pipeline() if not M.latest_pipelines then
if M.latest_pipeline == nil then return
end
if not M.latest_pipelines or #M.latest_pipelines == 0 then
return return
end end
local width = string.len(M.latest_pipeline.web_url) + 10 local max_width = 0
local height = 6 + #M.pipeline_jobs + 3 local total_height = 0
local pipelines_data = {}
for idx, pipeline in ipairs(M.latest_pipelines) do
local width = string.len(pipeline.web_url) + 10
max_width = math.max(max_width, width)
local pipeline_jobs = get_pipeline_jobs(idx)
for _, j in ipairs(pipeline_jobs) do
table.insert(M.pipeline_jobs, j)
end
local pipeline_status = M.get_pipeline_status(idx, false)
local height = 6 + #pipeline_jobs + 3
total_height = total_height + height
table.insert(pipelines_data, {
pipeline = pipeline,
pipeline_status = pipeline_status,
jobs = pipeline_jobs,
width = width,
height = 6 + #pipeline_jobs + 3,
lines = {},
})
end
local pipeline_popup = local pipeline_popup =
Popup(popup.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60)) Popup(popup.create_popup_state("Loading Pipelines...", state.settings.popup.pipeline, max_width, total_height, 60))
popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win()) popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win())
M.pipeline_popup = pipeline_popup M.pipeline_popup = pipeline_popup
pipeline_popup:mount() pipeline_popup:mount()
@@ -49,68 +85,103 @@ M.open = function()
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
vim.opt_local.wrap = false vim.opt_local.wrap = false
local lines = {}
u.switch_can_edit_buf(bufnr, true) u.switch_can_edit_buf(bufnr, true)
table.insert(lines, "Status: " .. M.get_pipeline_status(false))
table.insert(lines, "")
table.insert(lines, string.format("Last Run: %s", u.time_since(M.latest_pipeline.created_at)))
table.insert(lines, string.format("Url: %s", M.latest_pipeline.web_url))
table.insert(lines, string.format("Triggered By: %s", M.latest_pipeline.source))
table.insert(lines, "") local all_lines = {}
table.insert(lines, "Jobs:") for i, data in ipairs(pipelines_data) do
local pipeline = data.pipeline
local lines = data.lines
local longest_title = u.get_longest_string(u.map(M.pipeline_jobs, function(v) table.insert(lines, data.pipeline_status)
return v.name table.insert(lines, "")
end)) table.insert(lines, string.format("Last Run: %s", u.time_since(pipeline.created_at)))
table.insert(lines, string.format("Url: %s", pipeline.web_url))
table.insert(lines, string.format("Triggered By: %s", pipeline.source))
table.insert(lines, "")
table.insert(lines, "Jobs:")
local function row_offset(name) local longest_title = u.get_longest_string(u.map(data.jobs, function(v)
local offset = longest_title - string.len(name) return v.name
local res = string.rep(" ", offset + 5) end))
return res
end
for _, pipeline_job in ipairs(M.pipeline_jobs) do local function row_offset(name)
local offset = row_offset(pipeline_job.name) local offset = longest_title - string.len(name)
local row = string.format( local res = string.rep(" ", offset + 5)
"%s%s %s (%s)", return res
pipeline_job.name, end
offset,
state.settings.pipeline[pipeline_job.status] or "*",
pipeline_job.status or ""
)
table.insert(lines, row) for _, pipeline_job in ipairs(data.jobs) do
local offset = row_offset(pipeline_job.name)
local row = string.format(
"%s%s %s (%s)",
pipeline_job.name,
offset,
state.settings.pipeline[pipeline_job.status] or "*",
pipeline_job.status or ""
)
table.insert(lines, row)
end
-- Add separator between pipelines
if i < #pipelines_data then
table.insert(lines, "")
table.insert(lines, string.rep("-", max_width))
table.insert(lines, "")
end
for _, line in ipairs(lines) do
table.insert(all_lines, line)
end
end end
vim.schedule(function() vim.schedule(function()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, all_lines)
M.color_status(M.latest_pipeline.status, bufnr, lines[1], 1)
for i, pipeline_job in ipairs(M.pipeline_jobs) do local line_offset = 0
M.color_status(pipeline_job.status, bufnr, lines[7 + i], 7 + i) for _, data in ipairs(pipelines_data) do
local pipeline = data.pipeline
local lines = data.lines
M.color_status(pipeline.status, bufnr, all_lines[line_offset + 1], line_offset + 1)
for j, pipeline_job in ipairs(data.jobs) do
M.color_status(pipeline_job.status, bufnr, all_lines[line_offset + 7 + j], line_offset + 7 + j)
end
line_offset = line_offset + #lines
end end
pipeline_popup.border:set_text("top", "Pipeline Status", "center") pipeline_popup.border:set_text("top", "Pipelines Status", "center")
popup.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs) popup.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
u.switch_can_edit_buf(bufnr, false) u.switch_can_edit_buf(bufnr, false)
end) end)
end end
M.retrigger = function() M.retrigger = function()
M.latest_pipeline = get_latest_pipeline() local pipelines = get_latest_pipelines()
if not M.latest_pipeline then if not pipelines then
return
end
if M.latest_pipeline.status ~= "failed" then
u.notify("Pipeline is not in a failed state!", vim.log.levels.WARN)
return return
end end
job.run_job("/pipeline/" .. M.latest_pipeline.id, "POST", nil, function() local failed_pipelines = {}
u.notify("Pipeline re-triggered!", vim.log.levels.INFO)
end) for idx, pipeline in ipairs(pipelines) do
local pipeline_jobs = get_pipeline_jobs(idx)
for _, pjob in ipairs(pipeline_jobs) do
if pjob.status == "failed" then
if pipeline.status ~= "failed" then
u.notify("Pipeline is not in a failed state!", vim.log.levels.WARN)
return
end
if not failed_pipelines[pipeline.id] then
job.run_job("/pipeline/trigger/" .. pipeline.id, "POST", nil, function()
u.notify("Pipeline " .. pipeline.id .. " re-triggered!", vim.log.levels.INFO)
end)
failed_pipelines[pipeline.id] = true
end
end
end
end
end end
M.see_logs = function() M.see_logs = function()
@@ -173,12 +244,8 @@ end
---colorize the pipeline icon. ---colorize the pipeline icon.
---@param wrap_with_color boolean ---@param wrap_with_color boolean
---@return string ---@return string
M.get_pipeline_icon = function(wrap_with_color) M.get_pipeline_icon = function(idx, wrap_with_color)
M.latest_pipeline = get_latest_pipeline() local symbol = state.settings.pipeline[state.PIPELINE[idx].latest_pipeline.status]
if not M.latest_pipeline then
return ""
end
local symbol = state.settings.pipeline[M.latest_pipeline.status]
if not wrap_with_color then if not wrap_with_color then
return symbol return symbol
end end
@@ -196,12 +263,13 @@ end
---colorize the pipeline icon. ---colorize the pipeline icon.
---@param wrap_with_color boolean ---@param wrap_with_color boolean
---@return string ---@return string
M.get_pipeline_status = function(wrap_with_color) M.get_pipeline_status = function(idx, wrap_with_color)
M.latest_pipeline = get_latest_pipeline() return string.format(
if not M.latest_pipeline then "[%s]: Status: %s (%s)",
return "" state.PIPELINE[idx].name,
end M.get_pipeline_icon(idx, wrap_with_color),
return string.format("%s (%s)", M.get_pipeline_icon(wrap_with_color), M.latest_pipeline.status) state.PIPELINE[idx].latest_pipeline.status
)
end end
M.color_status = function(status, bufnr, status_line, linnr) M.color_status = function(status, bufnr, status_line, linnr)

View File

@@ -139,6 +139,7 @@ M.build_info_lines = function()
return pipeline.status return pipeline.status
end, end,
}, },
web_url = { title = "MR URL", content = info.web_url },
} }
local longest_used = "" local longest_used = ""

View File

@@ -120,11 +120,11 @@
---Relevant for renamed files only, the name of the file in the previous commit ---Relevant for renamed files only, the name of the file in the previous commit
---@field old_file_name string ---@field old_file_name string
---@field current_bufnr integer ---@field current_bufnr integer
---@field new_sha_win_id integer
---@field old_sha_win_id integer
---@field opposite_bufnr integer ---@field opposite_bufnr integer
---@field new_line_from_buf integer ---@field new_line_from_buf integer
---@field old_line_from_buf integer ---@field old_line_from_buf integer
---@field new_sha_focused boolean
---@field current_win_id integer
---@class LocationData ---@class LocationData
---@field old_line integer | nil ---@field old_line integer | nil
@@ -346,3 +346,15 @@
---@field perform_action? string -- Once in normal mode, does action (like saving comment or applying description edit, etc) ---@field perform_action? string -- Once in normal mode, does action (like saving comment or applying description edit, etc)
---@field perform_linewise_action? string -- Once in normal mode, does the linewise action (see logs for this job, etc) ---@field perform_linewise_action? string -- Once in normal mode, does the linewise action (see logs for this job, etc)
---@field discard_changes? string -- Quit the popup discarding changes, the popup content is not? saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`) ---@field discard_changes? string -- Quit the popup discarding changes, the popup content is not? saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
---@class List The base class for all list objects
---@field new function -- Creates a new List from a table
---@field map function -- Mutates a given list
---@field filter function -- Filters a given list
---@field partition function -- Partitions a given list into two lists
---@field reduce function -- Applies a function to reduce the list to a single value
---@field sort function -- Sorts the list in place based on a comparator function
---@field find function -- Returns the first element that satisfies the callback
---@field slice function -- Returns a portion of the list between start and end indices
---@field includes function -- Returns true if any of the elements can satisfy the callback
---@field values function -- Returns an iterator over the list's values

View File

@@ -139,7 +139,7 @@ M.check = function(return_results)
end end
if return_results then if return_results then
return #warnings + #errors == 0 return #errors == 0
end end
end end

View File

@@ -230,9 +230,9 @@ end
---This is in order to build the payload for Gitlab correctly by setting the old line and new line. ---This is in order to build the payload for Gitlab correctly by setting the old line and new line.
---@param old_line number|nil ---@param old_line number|nil
---@param new_line number|nil ---@param new_line number|nil
---@param is_current_sha_focused boolean ---@param new_sha_focused boolean
---@return string|nil ---@return string|nil
function M.get_modification_type(old_line, new_line, is_current_sha_focused) function M.get_modification_type(old_line, new_line, new_sha_focused)
local hunk_and_diff_data = parse_hunks_and_diff(state.INFO.diff_refs.base_sha) local hunk_and_diff_data = parse_hunks_and_diff(state.INFO.diff_refs.base_sha)
if hunk_and_diff_data.hunks == nil then if hunk_and_diff_data.hunks == nil then
return return
@@ -240,7 +240,7 @@ function M.get_modification_type(old_line, new_line, is_current_sha_focused)
local hunks = hunk_and_diff_data.hunks local hunks = hunk_and_diff_data.hunks
local all_diff_output = hunk_and_diff_data.all_diff_output local all_diff_output = hunk_and_diff_data.all_diff_output
return is_current_sha_focused and get_modification_type_from_new_sha(new_line, hunks, all_diff_output) return new_sha_focused and get_modification_type_from_new_sha(new_line, hunks, all_diff_output)
or get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output) or get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output)
end end

View File

@@ -1,6 +1,5 @@
local u = require("gitlab.utils") local u = require("gitlab.utils")
local state = require("gitlab.state") local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local List = require("gitlab.utils.list") local List = require("gitlab.utils.list")
local M = {} local M = {}
@@ -11,24 +10,24 @@ local M = {}
---@field resolved boolean|nil ---@field resolved boolean|nil
---@field created_at string|nil ---@field created_at string|nil
---Return true if discussion has a placeable diagnostic, false otherwise.
---@param note NoteWithValues ---@param note NoteWithValues
---@param file string
---@return boolean ---@return boolean
local filter_discussions_and_notes = function(note, file) local filter_discussions_and_notes = function(note)
---Do not include unlinked notes ---Do not include unlinked notes
return note.position ~= nil return note.position ~= nil
and (note.position.new_path == file or note.position.old_path == file)
---Skip resolved discussions if user wants to ---Skip resolved discussions if user wants to
and not (state.settings.discussion_signs.skip_resolved_discussion and note.resolvable and note.resolved) and not (state.settings.discussion_signs.skip_resolved_discussion and note.resolvable and note.resolved)
---Skip discussions from old revisions ---Skip discussions from old revisions
and not ( and not (
state.settings.discussion_signs.skip_old_revision_discussion state.settings.discussion_signs.skip_old_revision_discussion
and note.created_at ~= nil
and u.from_iso_format_date_to_timestamp(note.created_at) and u.from_iso_format_date_to_timestamp(note.created_at)
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at) <= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
) )
end end
---Filter all discussions which are relevant for currently visible signs and diagnostics. ---Filter all discussions and drafts which have placeable signs and diagnostics.
---@return Discussion|DraftNote[] ---@return Discussion|DraftNote[]
M.filter_placeable_discussions = function() M.filter_placeable_discussions = function()
local discussions = u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {}) local discussions = u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {})
@@ -41,18 +40,13 @@ M.filter_placeable_discussions = function()
draft_notes = {} draft_notes = {}
end end
local file = reviewer.get_current_file_path()
if not file then
return {}
end
local filtered_discussions = List.new(discussions):filter(function(discussion) local filtered_discussions = List.new(discussions):filter(function(discussion)
local first_note = discussion.notes[1] local first_note = discussion.notes[1]
return type(first_note.position) == "table" and filter_discussions_and_notes(first_note, file) return type(first_note.position) == "table" and filter_discussions_and_notes(first_note)
end) end)
local filtered_draft_notes = List.new(draft_notes):filter(function(note) local filtered_draft_notes = List.new(draft_notes):filter(function(note)
return filter_discussions_and_notes(note, file) return filter_discussions_and_notes(note)
end) end)
return u.join(filtered_discussions, filtered_draft_notes) return u.join(filtered_discussions, filtered_draft_notes)

View File

@@ -85,47 +85,62 @@ local create_multiline_diagnostic = function(d_or_n)
}, d_or_n) }, d_or_n)
end end
---Set diagnostics in currently new SHA. ---Set diagnostics in the given buffer.
---@param namespace number namespace for diagnostics ---@param namespace number namespace for diagnostics
---@param bufnr number the bufnr for placing the diagnostics
---@param diagnostics table see :h vim.diagnostic.set ---@param diagnostics table see :h vim.diagnostic.set
---@param opts table? see :h vim.diagnostic.set ---@param opts table? see :h vim.diagnostic.set
local set_diagnostics_in_new_sha = function(namespace, diagnostics, opts) local set_diagnostics = function(namespace, bufnr, diagnostics, opts)
local view = diffview_lib.get_current_view() vim.diagnostic.set(namespace, bufnr, diagnostics, opts)
if not view then require("gitlab.indicators.signs").set_signs(diagnostics, bufnr)
return
end
vim.diagnostic.set(namespace, view.cur_layout.b.file.bufnr, diagnostics, opts)
require("gitlab.indicators.signs").set_signs(diagnostics, view.cur_layout.b.file.bufnr)
end end
---Set diagnostics in old SHA. ---Refresh the diagnostics for all the reviewed files, and place diagnostics for the currently
---@param namespace number namespace for diagnostics ---visible buffers.
---@param diagnostics table see :h vim.diagnostic.set
---@param opts table? see :h vim.diagnostic.set
local set_diagnostics_in_old_sha = function(namespace, diagnostics, opts)
local view = diffview_lib.get_current_view()
if not view then
return
end
vim.diagnostic.set(namespace, view.cur_layout.a.file.bufnr, diagnostics, opts)
require("gitlab.indicators.signs").set_signs(diagnostics, view.cur_layout.a.file.bufnr)
end
---Refresh the diagnostics for the currently reviewed file
M.refresh_diagnostics = function() M.refresh_diagnostics = function()
require("gitlab.indicators.signs").clear_signs()
M.clear_diagnostics()
M.placeable_discussions = indicators_common.filter_placeable_discussions()
local view = diffview_lib.get_current_view()
if view == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
return
end
M.place_diagnostics(view.cur_layout.a.file.bufnr)
M.place_diagnostics(view.cur_layout.b.file.bufnr)
end
---Filter and place the diagnostics for the given buffer.
---@param bufnr number The number of the buffer for placing diagnostics.
M.place_diagnostics = function(bufnr)
if not state.settings.discussion_signs.enabled then
return
end
local view = diffview_lib.get_current_view()
if view == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
return
end
local ok, err = pcall(function() local ok, err = pcall(function()
require("gitlab.indicators.signs").clear_signs() local file_discussions = List.new(M.placeable_discussions):filter(function(discussion_or_note)
M.clear_diagnostics() local note = discussion_or_note.notes and discussion_or_note.notes[1] or discussion_or_note
local filtered_discussions = indicators_common.filter_placeable_discussions() return note.position.new_path == view.cur_layout.b.file.path
if filtered_discussions == nil then or note.position.old_path == view.cur_layout.a.file.path
end)
if #file_discussions == 0 then
return return
end end
local new_diagnostics = M.parse_new_diagnostics(filtered_discussions) local new_diagnostics, old_diagnostics = List.new(file_discussions):partition(indicators_common.is_new_sha)
set_diagnostics_in_new_sha(diagnostics_namespace, new_diagnostics, create_display_opts())
local old_diagnostics = M.parse_old_diagnostics(filtered_discussions) if bufnr == view.cur_layout.a.file.bufnr then
set_diagnostics_in_old_sha(diagnostics_namespace, old_diagnostics, create_display_opts()) set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(old_diagnostics), create_display_opts())
elseif bufnr == view.cur_layout.b.file.bufnr then
set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(new_diagnostics), create_display_opts())
end
end) end)
if not ok then if not ok then
@@ -134,24 +149,13 @@ M.refresh_diagnostics = function()
end end
---Iterates over each discussion and returns a list of tables with sign ---Iterates over each discussion and returns a list of tables with sign
---data, for instance group, priority, line number etc for the new SHA ---data, for instance group, priority, line number etc
---@param discussions Discussion[] ---@param discussions List
---@return DiagnosticTable[] ---@return DiagnosticTable[]
M.parse_new_diagnostics = function(discussions) M.parse_diagnostics = function(discussions)
local new_diagnostics = List.new(discussions):filter(indicators_common.is_new_sha) local single_line, multi_line = discussions:partition(indicators_common.is_single_line)
local single_line = new_diagnostics:filter(indicators_common.is_single_line):map(create_single_line_diagnostic) single_line = single_line:map(create_single_line_diagnostic)
local multi_line = new_diagnostics:filter(indicators_common.is_multi_line):map(create_multiline_diagnostic) multi_line = multi_line:map(create_multiline_diagnostic)
return u.combine(single_line, multi_line)
end
---Iterates over each discussion and returns a list of tables with sign
---data, for instance group, priority, line number etc for the old SHA
---@param discussions Discussion[]
---@return DiagnosticTable[]
M.parse_old_diagnostics = function(discussions)
local old_diagnostics = List.new(discussions):filter(indicators_common.is_old_sha)
local single_line = old_diagnostics:filter(indicators_common.is_single_line):map(create_single_line_diagnostic)
local multi_line = old_diagnostics:filter(indicators_common.is_multi_line):map(create_multiline_diagnostic)
return u.combine(single_line, multi_line) return u.combine(single_line, multi_line)
end end

View File

@@ -120,7 +120,7 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts)
settings.popup.temp_registers = temp_registers settings.popup.temp_registers = temp_registers
end, { end, {
buffer = popup.bufnr, buffer = popup.bufnr,
desc = "Quit discarding changes", desc = "Quit, discarding changes",
nowait = settings.keymaps.popup.discard_changes_nowait, nowait = settings.keymaps.popup.discard_changes_nowait,
}) })
end end
@@ -236,4 +236,16 @@ M.set_cycle_popups_keymaps = function(popups)
end end
end end
---Create the title for the comment popup.
---@param title string The main title, e.g., "Comment".
---@param file_name string Name of file for which comment is created.
---@param start_line integer Start of the line range.
---@param end_line integer End of the line range.
---@return string title The full title of the popup.
M.create_title = function(title, file_name, start_line, end_line)
local range = start_line < end_line and string.format("-%s", end_line) or ""
local position = string.format("%s%s", start_line, range)
return string.format("%s [%s:%s]", title, file_name, position)
end
return M return M

View File

@@ -16,6 +16,7 @@ local M = {
bufnr = nil, bufnr = nil,
tabnr = nil, tabnr = nil,
stored_win = nil, stored_win = nil,
buf_winids = {},
} }
-- Checks for legacy installations, only Diffview is supported. -- Checks for legacy installations, only Diffview is supported.
@@ -62,6 +63,8 @@ M.open = function()
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha)) vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
M.is_open = true M.is_open = true
local cur_view = diffview_lib.get_current_view()
M.diffview_layout = cur_view.cur_layout
M.tabnr = vim.api.nvim_get_current_tabpage() M.tabnr = vim.api.nvim_get_current_tabpage()
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
@@ -77,9 +80,11 @@ M.open = function()
M.tabnr = nil M.tabnr = nil
end end
end end
require("diffview.config").user_emitter:on("view_closed", function(_, ...) require("diffview.config").user_emitter:on("view_closed", function(_, args)
M.is_open = false if M.tabnr == args.tabpage then
on_diffview_closed(...) M.is_open = false
on_diffview_closed(args)
end
end) end)
if state.settings.discussion_tree.auto_open then if state.settings.discussion_tree.auto_open then
@@ -100,10 +105,11 @@ M.close = function()
end end
--- Jumps to the location provided in the reviewer window --- Jumps to the location provided in the reviewer window
---@param file_name string ---@param file_name string The file name after change.
---@param line_number number ---@param old_file_name string The file name before change (different from file_name for renamed/moved files).
---@param new_buffer boolean ---@param line_number number Line number from the discussion node.
M.jump = function(file_name, line_number, new_buffer) ---@param new_buffer boolean If true, jump to the NEW SHA.
M.jump = function(file_name, old_file_name, line_number, new_buffer)
if M.tabnr == nil then if M.tabnr == nil then
u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR) u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
return return
@@ -116,8 +122,8 @@ M.jump = function(file_name, line_number, new_buffer)
end end
local files = view.panel:ordered_file_list() local files = view.panel:ordered_file_list()
local file = List.new(files):find(function(file) local file = List.new(files):find(function(f)
return file.path == file_name return new_buffer and f.path == file_name or f.oldpath == old_file_name
end) end)
if file == nil then if file == nil then
u.notify( u.notify(
@@ -148,9 +154,13 @@ end
---Get the data from diffview, such as line information and file name. May be used by ---Get the data from diffview, such as line information and file name. May be used by
---other modules such as the comment module to create line codes or set diagnostics ---other modules such as the comment module to create line codes or set diagnostics
---@param current_win integer The ID of the currently focused window
---@return DiffviewInfo | nil ---@return DiffviewInfo | nil
M.get_reviewer_data = function() M.get_reviewer_data = function(current_win)
local view = diffview_lib.get_current_view() local view = diffview_lib.get_current_view()
if view == nil then
return
end
local layout = view.cur_layout local layout = view.cur_layout
local old_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr) local old_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
local new_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr) local new_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
@@ -169,9 +179,9 @@ M.get_reviewer_data = function()
local new_line = vim.api.nvim_win_get_cursor(new_win)[1] local new_line = vim.api.nvim_win_get_cursor(new_win)[1]
local old_line = vim.api.nvim_win_get_cursor(old_win)[1] local old_line = vim.api.nvim_win_get_cursor(old_win)[1]
local is_current_sha_focused = M.is_current_sha_focused() local new_sha_focused = M.is_new_sha_focused(current_win)
local modification_type = hunks.get_modification_type(old_line, new_line, is_current_sha_focused) local modification_type = hunks.get_modification_type(old_line, new_line, new_sha_focused)
if modification_type == nil then if modification_type == nil then
u.notify("Error getting modification type", vim.log.levels.ERROR) u.notify("Error getting modification type", vim.log.levels.ERROR)
return return
@@ -181,32 +191,32 @@ M.get_reviewer_data = function()
u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN) u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN)
end end
local current_bufnr = is_current_sha_focused and layout.b.file.bufnr or layout.a.file.bufnr local current_bufnr = new_sha_focused and layout.b.file.bufnr or layout.a.file.bufnr
local opposite_bufnr = is_current_sha_focused and layout.a.file.bufnr or layout.b.file.bufnr local opposite_bufnr = new_sha_focused and layout.a.file.bufnr or layout.b.file.bufnr
local old_sha_win_id = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
local new_sha_win_id = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
return { return {
-- TODO: swap 'a' and 'b' to fix lua/gitlab/actions/comment.lua:158, and hopefully also
-- lua/gitlab/indicators/diagnostics.lua:129.
file_name = layout.a.file.path, file_name = layout.a.file.path,
old_file_name = M.is_file_renamed() and layout.b.file.path or "", old_file_name = M.is_file_renamed() and layout.b.file.path or "",
old_line_from_buf = old_line, old_line_from_buf = old_line,
new_line_from_buf = new_line, new_line_from_buf = new_line,
modification_type = modification_type, modification_type = modification_type,
new_sha_win_id = new_sha_win_id,
current_bufnr = current_bufnr, current_bufnr = current_bufnr,
old_sha_win_id = old_sha_win_id,
opposite_bufnr = opposite_bufnr, opposite_bufnr = opposite_bufnr,
new_sha_focused = new_sha_focused,
current_win_id = current_win,
} }
end end
---Return whether user is focused on the new version of the file ---Return whether user is focused on the new version of the file
---@param current_win integer The ID of the currently focused window
---@return boolean ---@return boolean
M.is_current_sha_focused = function() M.is_new_sha_focused = function(current_win)
local view = diffview_lib.get_current_view() local view = diffview_lib.get_current_view()
local layout = view.cur_layout local layout = view.cur_layout
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr) local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr) local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
local current_win = require("gitlab.actions.comment").current_win
if a_win ~= current_win and b_win ~= current_win then if a_win ~= current_win and b_win ~= current_win then
current_win = M.stored_win current_win = M.stored_win
M.stored_win = nil M.stored_win = nil
@@ -248,7 +258,7 @@ M.does_file_have_changes = function()
return file_data.stats.additions > 0 or file_data.stats.deletions > 0 return file_data.stats.additions > 0 or file_data.stats.deletions > 0
end end
---Diffview exposes events which can be used to setup autocommands. ---Run callback every time the buffer in one of the two reviewer windows changes.
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd ---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
M.set_callback_for_file_changed = function(callback) M.set_callback_for_file_changed = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.file_changed", {}) local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.file_changed", {})
@@ -256,7 +266,6 @@ M.set_callback_for_file_changed = function(callback)
pattern = { "DiffviewDiffBufWinEnter" }, pattern = { "DiffviewDiffBufWinEnter" },
group = group, group = group,
callback = function(...) callback = function(...)
M.stored_win = vim.api.nvim_get_current_win()
if M.tabnr == vim.api.nvim_get_current_tabpage() then if M.tabnr == vim.api.nvim_get_current_tabpage() then
callback(...) callback(...)
end end
@@ -264,7 +273,22 @@ M.set_callback_for_file_changed = function(callback)
}) })
end end
---Diffview exposes events which can be used to setup autocommands. ---Run callback the first time a new diff buffer is created and loaded into a window.
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
M.set_callback_for_buf_read = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.buf_read", {})
vim.api.nvim_create_autocmd("User", {
pattern = { "DiffviewDiffBufRead" },
group = group,
callback = function(...)
if vim.api.nvim_get_current_tabpage() == M.tabnr then
callback(...)
end
end,
})
end
---Run callback when the reviewer is closed or the user switches to another tab.
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd ---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
M.set_callback_for_reviewer_leave = function(callback) M.set_callback_for_reviewer_leave = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.leave", {}) local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.leave", {})
@@ -272,20 +296,25 @@ M.set_callback_for_reviewer_leave = function(callback)
pattern = { "DiffviewViewLeave", "DiffviewViewClosed" }, pattern = { "DiffviewViewLeave", "DiffviewViewClosed" },
group = group, group = group,
callback = function(...) callback = function(...)
if M.tabnr == vim.api.nvim_get_current_tabpage() then if vim.api.nvim_get_current_tabpage() == M.tabnr then
callback(...) callback(...)
end end
end, end,
}) })
end end
---Run callback when the reviewer is opened for the first time or the view is entered from another
---tab page.
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
M.set_callback_for_reviewer_enter = function(callback) M.set_callback_for_reviewer_enter = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.enter", {}) local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.enter", {})
vim.api.nvim_create_autocmd("User", { vim.api.nvim_create_autocmd("User", {
pattern = { "DiffviewViewOpened" }, pattern = { "DiffviewViewEnter", "DiffviewViewOpened" },
group = group, group = group,
callback = function(...) callback = function(...)
callback(...) if vim.api.nvim_get_current_tabpage() == M.tabnr then
callback(...)
end
end, end,
}) })
end end
@@ -325,8 +354,16 @@ end
---Set keymaps for creating comments, suggestions and for jumping to discussion tree. ---Set keymaps for creating comments, suggestions and for jumping to discussion tree.
---@param bufnr integer Number of the buffer for which the keybindings will be created. ---@param bufnr integer Number of the buffer for which the keybindings will be created.
---@param keymaps table The settings keymaps table. M.set_keymaps = function(bufnr)
local set_keymaps = function(bufnr, keymaps) if bufnr == nil or not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
-- Require keymaps only after user settings have been merged with defaults
local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.reviewer.disable_all then
return
end
-- Set mappings for creating comments -- Set mappings for creating comments
if keymaps.reviewer.create_comment ~= false then if keymaps.reviewer.create_comment ~= false then
-- Set keymap for repeated operator keybinding -- Set keymap for repeated operator keybinding
@@ -399,29 +436,17 @@ local set_keymaps = function(bufnr, keymaps)
end end
end end
--- Sets up keymaps for both buffers in the reviewer. ---Delete keymaps from reviewer buffers.
M.set_reviewer_keymaps = function() ---@param bufnr integer Number of the buffer from which the keybindings will be removed.
local del_keymaps = function(bufnr)
if bufnr == nil or not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
-- Require keymaps only after user settings have been merged with defaults -- Require keymaps only after user settings have been merged with defaults
local keymaps = require("gitlab.state").settings.keymaps local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.reviewer.disable_all then if keymaps.disable_all or keymaps.reviewer.disable_all then
return return
end end
local view = diffview_lib.get_current_view()
local a = view.cur_layout.a.file.bufnr
local b = view.cur_layout.b.file.bufnr
if a ~= nil and vim.api.nvim_buf_is_loaded(a) then
set_keymaps(a, keymaps)
end
if b ~= nil and vim.api.nvim_buf_is_loaded(b) then
set_keymaps(b, keymaps)
end
end
---Delete keymaps from reviewer buffers.
---@param bufnr integer Number of the buffer from which the keybindings will be removed.
---@param keymaps table The settings keymaps table.
local del_keymaps = function(bufnr, keymaps)
for _, func in ipairs({ "create_comment", "create_suggestion" }) do for _, func in ipairs({ "create_comment", "create_suggestion" }) do
if keymaps.reviewer[func] ~= false then if keymaps.reviewer[func] ~= false then
for _, mode in ipairs({ "n", "o", "v" }) do for _, mode in ipairs({ "n", "o", "v" }) do
@@ -434,23 +459,33 @@ local del_keymaps = function(bufnr, keymaps)
end end
end end
--- Deletes keymaps from both buffers in the reviewer. --- Set up autocaommands that will take care of setting and unsetting buffer-local options and keymaps
M.del_reviewer_keymaps = function() M.set_reviewer_autocommands = function(bufnr)
-- Require keymaps only after user settings have been merged with defaults local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.win_enter." .. bufnr, {})
local keymaps = require("gitlab.state").settings.keymaps vim.api.nvim_create_autocmd({ "WinEnter", "BufWinEnter" }, {
if keymaps.disable_all or keymaps.reviewer.disable_all then group = group,
return buffer = bufnr,
end callback = function()
if vim.api.nvim_get_current_win() == M.buf_winids[bufnr] then
M.stored_win = vim.api.nvim_get_current_win()
vim.api.nvim_buf_set_option(bufnr, "modifiable", false)
M.set_keymaps(bufnr)
else
if M.diffview_layout.b.id == M.buf_winids[bufnr] then
vim.api.nvim_buf_set_option(bufnr, "modifiable", true)
end
del_keymaps(bufnr)
end
end,
})
end
local view = diffview_lib.get_current_view() --- Update the stored winid for a given reviewer buffer. This is necessary for the
local a = view.cur_layout.a.file.bufnr --- M.set_reviewer_autocommands function to work correctly in cases like when the user closes one of
local b = view.cur_layout.b.file.bufnr --- the original reviewer windows and Diffview automatically creates a new pair
if a ~= nil and vim.api.nvim_buf_is_loaded(a) then --- of reviewer windows or the user wipes out a buffer and Diffview reloads it with a different ID.
del_keymaps(a, keymaps) M.update_winid_for_buffer = function(bufnr)
end M.buf_winids[bufnr] = vim.fn.bufwinid(bufnr)
if b ~= nil and vim.api.nvim_buf_is_loaded(b) then
del_keymaps(b, keymaps)
end
end end
return M return M

View File

@@ -7,6 +7,7 @@ local state = require("gitlab.state")
---@field reviewer_data DiffviewInfo ---@field reviewer_data DiffviewInfo
---@field run function ---@field run function
---@field build_location_data function ---@field build_location_data function
---@field visual_range table
---@class ReviewerLineInfo ---@class ReviewerLineInfo
---@field old_line integer|nil ---@field old_line integer|nil
@@ -19,16 +20,21 @@ local state = require("gitlab.state")
local Location = {} local Location = {}
Location.__index = Location Location.__index = Location
---@param reviewer_data DiffviewInfo ---The new() function returns nil when the location cannot be created due to missing
---@param visual_range LineRange | nil ---reviewer data.
---@return Location ---@return Location | nil
function Location.new(reviewer_data, visual_range) function Location.new()
local current_win = vim.api.nvim_get_current_win()
local reviewer_data = require("gitlab.reviewer").get_reviewer_data(current_win)
if reviewer_data == nil then
return nil
end
local location = {} local location = {}
local instance = setmetatable(location, Location) local instance = setmetatable(location, Location)
instance.reviewer_data = reviewer_data instance.reviewer_data = reviewer_data
instance.visual_range = visual_range
instance.base_sha = state.INFO.diff_refs.base_sha instance.base_sha = state.INFO.diff_refs.base_sha
instance.head_sha = state.INFO.diff_refs.head_sha instance.head_sha = state.INFO.diff_refs.head_sha
instance:build_location_data()
return instance return instance
end end
@@ -37,11 +43,13 @@ end
function Location:build_location_data() function Location:build_location_data()
---@type DiffviewInfo ---@type DiffviewInfo
local reviewer_data = self.reviewer_data local reviewer_data = self.reviewer_data
---@type LineRange | nil
local visual_range = self.visual_range local start_line, end_line = u.get_visual_selection_boundaries()
---@type LineRange
self.visual_range = { start_line = start_line, end_line = end_line }
---@type LocationData ---@type LocationData
local location_data = { self.location_data = {
old_line = nil, old_line = nil,
new_line = nil, new_line = nil,
line_range = nil, line_range = nil,
@@ -51,30 +59,29 @@ function Location:build_location_data()
-- Comment on deleted line: Include only old_line in payload. -- Comment on deleted line: Include only old_line in payload.
-- The line was not found in any hunks, send both lines. -- The line was not found in any hunks, send both lines.
if reviewer_data.modification_type == "added" then if reviewer_data.modification_type == "added" then
location_data.old_line = nil self.location_data.old_line = nil
location_data.new_line = reviewer_data.new_line_from_buf self.location_data.new_line = reviewer_data.new_line_from_buf
elseif reviewer_data.modification_type == "deleted" then elseif reviewer_data.modification_type == "deleted" then
location_data.old_line = reviewer_data.old_line_from_buf self.location_data.old_line = reviewer_data.old_line_from_buf
location_data.new_line = nil self.location_data.new_line = nil
elseif elseif
reviewer_data.modification_type == "unmodified" or reviewer_data.modification_type == "bad_file_unmodified" reviewer_data.modification_type == "unmodified" or reviewer_data.modification_type == "bad_file_unmodified"
then then
location_data.old_line = reviewer_data.old_line_from_buf self.location_data.old_line = reviewer_data.old_line_from_buf
location_data.new_line = reviewer_data.new_line_from_buf self.location_data.new_line = reviewer_data.new_line_from_buf
end end
self.location_data = location_data if end_line > start_line then
if visual_range == nil then
return
else
self.location_data.line_range = { self.location_data.line_range = {
start = {}, start = {},
["end"] = {}, ["end"] = {},
} }
else
return
end end
self:set_start_range(visual_range) self:set_start_range()
self:set_end_range(visual_range) self:set_end_range()
-- Ranged comments should always use the end of the range. -- Ranged comments should always use the end of the range.
-- Otherwise they will not highlight the full comment in Gitlab. -- Otherwise they will not highlight the full comment in Gitlab.
@@ -90,9 +97,7 @@ end
---@param line number ---@param line number
---@return number|nil ---@return number|nil
function Location:get_line_number_from_new_sha(line) function Location:get_line_number_from_new_sha(line)
local reviewer = require("gitlab.reviewer") if self.reviewer_data.new_sha_focused then
local is_current_sha_focused = reviewer.is_current_sha_focused()
if is_current_sha_focused then
return line return line
end end
-- Otherwise we want to get the matching line in the opposite buffer -- Otherwise we want to get the matching line in the opposite buffer
@@ -111,9 +116,7 @@ end
---@param line number ---@param line number
---@return number|nil ---@return number|nil
function Location:get_line_number_from_old_sha(line) function Location:get_line_number_from_old_sha(line)
local reviewer = require("gitlab.reviewer") if not self.reviewer_data.new_sha_focused then
local is_current_sha_focused = reviewer.is_current_sha_focused()
if not is_current_sha_focused then
return line return line
end end
@@ -131,32 +134,24 @@ end
-- the reviewer is focused in. -- the reviewer is focused in.
---@return number|nil ---@return number|nil
function Location:get_current_line() function Location:get_current_line()
local reviewer = require("gitlab.reviewer") if self.reviewer_data.current_win_id == nil then
local win_id = reviewer.is_current_sha_focused() and self.reviewer_data.new_sha_win_id
or self.reviewer_data.old_sha_win_id
if win_id == nil then
return return
end end
local current_line = vim.api.nvim_win_get_cursor(win_id)[1] local current_line = vim.api.nvim_win_get_cursor(self.reviewer_data.current_win_id)[1]
return current_line return current_line
end end
-- Given a new_line and old_line from the start of a ranged comment, returns the start -- Given a modification type, a visual selection range, and the hunk data, sets the start range
-- range information for the Gitlab payload -- information to the location_data for the Gitlab payload
---@param visual_range LineRange function Location:set_start_range()
---@return ReviewerLineInfo|nil
function Location:set_start_range(visual_range)
local current_file = require("gitlab.reviewer").get_current_file_path() local current_file = require("gitlab.reviewer").get_current_file_path()
if current_file == nil then if current_file == nil then
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR) u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
return return
end end
local reviewer = require("gitlab.reviewer") if self.reviewer_data.current_win_id == nil then
local is_current_sha_focused = reviewer.is_current_sha_focused()
local win_id = is_current_sha_focused and self.reviewer_data.new_sha_win_id or self.reviewer_data.old_sha_win_id
if win_id == nil then
u.notify("Error getting window number of SHA for start range", vim.log.levels.ERROR) u.notify("Error getting window number of SHA for start range", vim.log.levels.ERROR)
return return
end end
@@ -167,8 +162,8 @@ function Location:set_start_range(visual_range)
return return
end end
local new_line = self:get_line_number_from_new_sha(visual_range.start_line) local new_line = self:get_line_number_from_new_sha(self.visual_range.start_line)
local old_line = self:get_line_number_from_old_sha(visual_range.start_line) local old_line = self:get_line_number_from_old_sha(self.visual_range.start_line)
if if
(new_line == nil and self.reviewer_data.modification_type ~= "deleted") (new_line == nil and self.reviewer_data.modification_type ~= "deleted")
or (old_line == nil and self.reviewer_data.modification_type ~= "added") or (old_line == nil and self.reviewer_data.modification_type ~= "added")
@@ -177,7 +172,7 @@ function Location:set_start_range(visual_range)
return return
end end
local modification_type = hunks.get_modification_type(old_line, new_line, is_current_sha_focused) local modification_type = hunks.get_modification_type(old_line, new_line, self.reviewer_data.new_sha_focused)
if modification_type == nil then if modification_type == nil then
u.notify("Error getting modification type for start of range", vim.log.levels.ERROR) u.notify("Error getting modification type for start of range", vim.log.levels.ERROR)
return return
@@ -190,10 +185,9 @@ function Location:set_start_range(visual_range)
} }
end end
-- Given a modification type, a range, and the hunk data, returns the end range information -- Given a modification type, a visual selection range, and the hunk data, sets the end range
-- for the Gitlab payload -- information to the location_data for the Gitlab payload
---@param visual_range LineRange function Location:set_end_range()
function Location:set_end_range(visual_range)
local current_file = require("gitlab.reviewer").get_current_file_path() local current_file = require("gitlab.reviewer").get_current_file_path()
if current_file == nil then if current_file == nil then
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR) u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
@@ -206,8 +200,8 @@ function Location:set_end_range(visual_range)
return return
end end
local new_line = self:get_line_number_from_new_sha(visual_range.end_line) local new_line = self:get_line_number_from_new_sha(self.visual_range.end_line)
local old_line = self:get_line_number_from_old_sha(visual_range.end_line) local old_line = self:get_line_number_from_old_sha(self.visual_range.end_line)
if if
(new_line == nil and self.reviewer_data.modification_type ~= "deleted") (new_line == nil and self.reviewer_data.modification_type ~= "deleted")
@@ -217,9 +211,7 @@ function Location:set_end_range(visual_range)
return return
end end
local reviewer = require("gitlab.reviewer") local modification_type = hunks.get_modification_type(old_line, new_line, self.reviewer_data.new_sha_focused)
local is_current_sha_focused = reviewer.is_current_sha_focused()
local modification_type = hunks.get_modification_type(old_line, new_line, is_current_sha_focused)
if modification_type == nil then if modification_type == nil then
u.notify("Error getting modification type for end of range", vim.log.levels.ERROR) u.notify("Error getting modification type for end of range", vim.log.levels.ERROR)
return return

View File

@@ -208,6 +208,7 @@ M.settings = {
"delete_branch", "delete_branch",
"squash", "squash",
"labels", "labels",
"web_url",
}, },
}, },
discussion_signs = { discussion_signs = {

View File

@@ -587,19 +587,20 @@ end
M.check_visual_mode = function() M.check_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode local mode = vim.api.nvim_get_mode().mode
if mode ~= "v" and mode ~= "V" then if mode ~= "v" and mode ~= "V" then
M.notify("Code suggestions and multiline comments are only available in visual mode", vim.log.levels.WARN) M.notify("Code suggestions and multiline comments are only available in visual mode", vim.log.levels.ERROR)
return false return false
end end
return true return true
end end
---Return start line and end line of visual selection. ---Return start line and end line of visual selection.
---Exists visual mode in order to access marks "<" , ">"
---@return integer start,integer end Start line and end line ---@return integer start,integer end Start line and end line
M.get_visual_selection_boundaries = function() M.get_visual_selection_boundaries = function()
M.press_escape() local start_line = vim.fn.line("v")
local start_line = vim.api.nvim_buf_get_mark(0, "<")[1] local end_line = vim.fn.line(".")
local end_line = vim.api.nvim_buf_get_mark(0, ">")[1] if start_line > end_line then
start_line, end_line = end_line, start_line
end
return start_line, end_line return start_line, end_line
end end