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

@@ -10,7 +10,7 @@ It's possible that the feature you want is already implemented, or does not belo
If you are using Lazy as a plugin manager, the easiest way to work on changes is by setting a specific path for the plugin that points to your repository locally. This is what I do:
```lua
```lua
{
"harrisoncramer/gitlab.nvim",
dependencies = {
@@ -54,8 +54,8 @@ $ luacheck --globals vim busted --no-max-line-length -- .
4. Make the merge request to the `develop` branch of `.gitlab.nvim`
Please provide a description of the feature, and links to any relevant issues.
Please provide a description of the feature, and links to any relevant issues.
That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it, it will be merged into the develop branch.
That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it, it will be merged into the develop branch.
After some time, if the develop branch is found to be stable, that branch will be merged into `main` and released. When merged into `main` the pipeline will detect whether we're merging in a patch, minor, or major change, and create a new tag (e.g. 1.0.12) and release.

View File

@@ -42,19 +42,13 @@ type RequestWithPosition interface {
func buildCommentPosition(commentWithPositionData RequestWithPosition) *gitlab.PositionOptions {
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{
PositionType: &positionData.Type,
StartSHA: &positionData.StartCommitSHA,
HeadSHA: &positionData.HeadCommitSHA,
BaseSHA: &positionData.BaseCommitSHA,
NewPath: &positionData.FileName,
OldPath: &oldFileName,
OldPath: &positionData.OldFileName,
NewLine: positionData.NewLine,
OldLine: positionData.OldLine,
}

View File

@@ -20,16 +20,18 @@ type RetriggerPipelineResponse struct {
type PipelineWithJobs struct {
Jobs []*gitlab.Job `json:"jobs"`
LatestPipeline *gitlab.PipelineInfo `json:"latest_pipeline"`
Name string `json:"name"`
}
type GetPipelineAndJobsResponse struct {
SuccessResponse
Pipeline PipelineWithJobs `json:"latest_pipeline"`
Pipelines []PipelineWithJobs `json:"latest_pipeline"`
}
type PipelineManager interface {
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)
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)
}
@@ -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{})
if err != nil {
handleError(w, err, "Could not get pipeline jobs", http.StatusInternalServerError)
return
@@ -112,13 +113,51 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
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)
response := GetPipelineAndJobsResponse{
SuccessResponse: SuccessResponse{Message: "Pipeline retrieved"},
Pipeline: PipelineWithJobs{
LatestPipeline: pipeline,
Jobs: jobs,
},
Pipelines: pipelines,
}
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
}
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) {
resp, err := f.handleGitlabError()
if err != nil {

View File

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

View File

@@ -15,7 +15,6 @@ local reviewer = require("gitlab.reviewer")
local Location = require("gitlab.reviewer.location")
local M = {
current_win = nil,
start_line = nil,
end_line = 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
---via the M.settings.keymaps.popup.perform_action keybinding
---@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 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
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
return
@@ -75,30 +73,16 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
return
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 position_data = {
file_name = reviewer_data.file_name,
old_file_name = reviewer_data.old_file_name,
file_name = M.location.reviewer_data.file_name,
old_file_name = M.location.reviewer_data.old_file_name,
base_commit_sha = revision.base_commit_sha,
start_commit_sha = revision.start_commit_sha,
head_commit_sha = revision.head_commit_sha,
old_line = location_data.old_line,
new_line = location_data.new_line,
line_range = location_data.line_range,
old_line = M.location.location_data.old_line,
new_line = M.location.location_data.new_line,
line_range = M.location.location_data.line_range,
}
-- Creating a new comment (linked to specific changes)
@@ -148,10 +132,10 @@ M.confirm_edit_comment = function(discussion_id, note_id, unlinked)
end
---@class LayoutOpts
---@field ranged boolean
---@field unlinked boolean
---@field discussion_id string|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
---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 user_settings
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
elseif opts.unlinked then
title = "Note"
user_settings = popup_settings.note
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
end
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.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({
Layout.Box(M.comment_popup, { grow = 1 }),
@@ -194,22 +181,21 @@ M.create_comment_layout = function(opts)
}, internal_layout)
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
---Keybinding for focus on draft section
popup.set_popup_keymaps(M.draft_popup, function()
local text = u.get_buffer_text(M.comment_popup.bufnr)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
confirm_create_comment(text, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(current_win)
end, miscellaneous.toggle_bool, popup.non_editable_popup_opts)
---Keybinding for focus on text section
popup.set_popup_keymaps(M.comment_popup, function(text)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
confirm_create_comment(text, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(current_win)
end, miscellaneous.attach_file, popup.editable_popup_opts)
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
--- line in the current MR
M.create_comment = function()
M.location = Location.new()
if not M.can_create_comment(false) then
return
end
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
local layout = M.create_comment_layout({ unlinked = false })
layout:mount()
end
--- 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
M.create_multiline_comment = function()
M.location = Location.new()
if not M.can_create_comment(true) then
u.press_escape()
return
end
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
local layout = M.create_comment_layout({ unlinked = false })
layout:mount()
end
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
--- on the changed/updated line in the current MR
M.create_note = function()
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
local layout = M.create_comment_layout({ unlinked = true })
layout:mount()
end
---Given the current visually selected area of text, builds text to fill in the
---comment popup with a suggested change
---@return LineRange|nil
---@return integer
local build_suggestion = function()
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.end_line - M.start_line
local range_length = M.location.visual_range.end_line - M.location.visual_range.start_line
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
if string.match(line, "^```%S*$") then
@@ -270,14 +255,14 @@ local build_suggestion = function()
end
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
elseif M.end_line == current_line then
elseif M.location.visual_range.end_line == current_line then
suggestion_start = backticks .. "suggestion:-" .. range_length .. "+0"
else
--- This should never happen afaik
u.notify("Unexpected suggestion position", vim.log.levels.ERROR)
return nil, 0
return nil
end
suggestion_start = suggestion_start
local suggestion_lines = {}
@@ -285,21 +270,22 @@ local build_suggestion = function()
vim.list_extend(suggestion_lines, selected_lines)
table.insert(suggestion_lines, backticks)
return suggestion_lines, range_length
return suggestion_lines
end
--- This function will open a a popup to create a suggestion comment
--- on the changed/updated line in the current MR
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
M.create_comment_suggestion = function()
M.location = Location.new()
if not M.can_create_comment(true) then
u.press_escape()
return
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()
vim.schedule(function()
@@ -368,6 +354,11 @@ M.can_create_comment = function(must_be_visual)
return false
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
end

View File

@@ -265,7 +265,7 @@ M.get_line_number_from_node = function(root_node)
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, callback)
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
@@ -277,8 +277,7 @@ M.jump_to_reviewer = function(tree, callback)
u.notify("Could not get line number", vim.log.levels.ERROR)
return
end
reviewer.jump(root_node.file_name, line_number, is_new_sha)
callback()
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
@@ -302,7 +301,7 @@ M.jump_to_file = function(tree)
end
vim.cmd.tabnew()
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
end
local bufnr = vim.fn.bufnr(root_node.file_name)

View File

@@ -215,7 +215,7 @@ M.open_confirmation_popup = function(mr)
return
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()
local popups = {
@@ -224,6 +224,7 @@ M.open_confirmation_popup = function(mr)
delete_branch_popup,
squash_popup,
target_popup,
source_popup,
}
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.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.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.squash_bufnr, 0, -1, false, { u.bool_to_string(squash) })
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(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(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(squash_popup, M.create_mr, miscellaneous.toggle_bool, 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
local target_branch_popup = Popup(popup.create_box_popup_state("Target branch", false, settings))
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_branch_popup = Popup(popup.create_box_popup_state(delete_title, false, settings))
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(squash_popup, { size = { width = #squash_title + 4 } }))
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({
Layout.Box({
@@ -378,6 +384,7 @@ M.create_layout = function()
title_popup,
description_popup,
target_branch_popup,
source_branch_popup,
delete_branch_popup,
squash_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 discussions_tree = require("gitlab.actions.discussions.tree")
local draft_notes = require("gitlab.actions.draft_notes")
local diffview_lib = require("diffview.lib")
local signs = require("gitlab.indicators.signs")
local diagnostics = require("gitlab.indicators.diagnostics")
local winbar = require("gitlab.actions.discussions.winbar")
@@ -74,37 +73,25 @@ end
M.initialize_discussions = function()
state.discussion_tree.last_updated = os.time()
signs.setup_signs()
reviewer.set_callback_for_file_changed(function()
M.refresh_diagnostics()
M.modifiable(false)
reviewer.set_reviewer_keymaps()
reviewer.set_callback_for_file_changed(function(args)
diagnostics.place_diagnostics(args.buf)
reviewer.update_winid_for_buffer(args.buf)
end)
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)
reviewer.set_callback_for_reviewer_leave(function()
signs.clear_signs()
diagnostics.clear_diagnostics()
M.modifiable(true)
reviewer.del_reviewer_keymaps()
end)
end
--- Ensures that the both buffers in the reviewer are/not modifiable. Relevant if the user is using
--- 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
--- Take existing data and refresh the diagnostics and the signs
M.refresh_diagnostics = function()
if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics()
@@ -115,7 +102,9 @@ end
---Opens the discussion tree, sets the keybindings. It also
---creates the tree for notes (which are not linked to specific lines of code)
---@param callback function?
M.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.unlinked_discussions = u.ensure_table(state.DISCUSSION_DATA.unlinked_discussions)
state.DRAFT_NOTES = u.ensure_table(state.DRAFT_NOTES)
@@ -136,7 +125,7 @@ M.open = function(callback)
-- Initialize winbar module with data from buffers
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
vim.api.nvim_set_current_win(M.split.winid)
@@ -146,7 +135,7 @@ M.open = function(callback)
M.rebuild_unlinked_discussion_tree()
-- 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)
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()
end
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)
winbar.switch_view_type("discussions")
vim.api.nvim_win_set_cursor(M.split.winid, { line_number, 0 })
end
if not M.split_visible then
M.toggle(jump_after_tree_opened)
M.open(jump_after_tree_opened, "discussions")
else
jump_after_tree_opened()
end
@@ -247,10 +237,10 @@ M.reply = function(tree)
local comment = require("gitlab.actions.comment")
local unlinked = tree.bufnr == M.unlinked_bufnr
local layout = comment.create_comment_layout({
ranged = false,
discussion_id = discussion_id,
unlinked = unlinked,
reply = true,
file_name = discussion_node.file_name,
})
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
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 note_node = common.get_note_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)
return
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())
@@ -587,7 +579,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.jump_to_reviewer then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
if M.is_current_node_note(tree) then
common.jump_to_reviewer(tree, M.refresh_diagnostics)
common.jump_to_reviewer(tree)
end
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
end
@@ -757,6 +749,16 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
})
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
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
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 = ""
---@type string?
local root_file_name = ""
---@type string?
local root_old_file_name = ""
---@type string
local root_id
local root_text_nodes = {}
@@ -43,6 +45,7 @@ M.add_discussions_to_table = function(items, unlinked)
if j == 1 then
_, root_text, root_text_nodes = M.build_note(note, { resolved = note.resolved, resolvable = note.resolvable })
root_file_name = (type(note.position) == "table" and note.position.new_path or nil)
root_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_old_line = (type(note.position) == "table" and note.position.old_line or nil)
root_id = discussion.id
@@ -79,6 +82,7 @@ M.add_discussions_to_table = function(items, unlinked)
id = root_id,
root_note_id = root_note_id,
file_name = root_file_name,
old_file_name = root_old_file_name,
new_line = root_new_line,
old_line = root_old_line,
resolvable = resolvable,

View File

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

View File

@@ -7,41 +7,77 @@ local job = require("gitlab.job")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local M = {
pipeline_jobs = nil,
pipeline_jobs = {},
latest_pipeline = nil,
pipeline_popup = nil,
}
local function get_latest_pipeline()
local pipeline = state.PIPELINE and state.PIPELINE.latest_pipeline
if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then
u.notify("Pipeline not found", vim.log.levels.WARN)
return
local function get_latest_pipelines(count)
count = count or 1 -- Default to 1 if count is not provided
local pipelines = {}
if not state.PIPELINE then
u.notify("Pipeline state is not initialized", vim.log.levels.WARN)
return nil
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
local function get_pipeline_jobs()
M.latest_pipeline = get_latest_pipeline()
if not M.latest_pipeline then
return
end
return u.reverse(type(state.PIPELINE.jobs) == "table" and state.PIPELINE.jobs or {})
local function get_pipeline_jobs(idx)
return u.reverse(type(state.PIPELINE[idx].jobs) == "table" and state.PIPELINE[idx].jobs or {})
end
-- The function will render the Pipeline state in a popup
M.open = function()
M.pipeline_jobs = get_pipeline_jobs()
M.latest_pipeline = get_latest_pipeline()
if M.latest_pipeline == nil then
M.latest_pipelines = get_latest_pipelines()
if not M.latest_pipelines then
return
end
if not M.latest_pipelines or #M.latest_pipelines == 0 then
return
end
local width = string.len(M.latest_pipeline.web_url) + 10
local height = 6 + #M.pipeline_jobs + 3
local max_width = 0
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 =
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())
M.pipeline_popup = pipeline_popup
pipeline_popup:mount()
@@ -49,68 +85,103 @@ M.open = function()
local bufnr = vim.api.nvim_get_current_buf()
vim.opt_local.wrap = false
local lines = {}
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, "")
table.insert(lines, "Jobs:")
local all_lines = {}
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)
return v.name
end))
table.insert(lines, data.pipeline_status)
table.insert(lines, "")
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 offset = longest_title - string.len(name)
local res = string.rep(" ", offset + 5)
return res
end
local longest_title = u.get_longest_string(u.map(data.jobs, function(v)
return v.name
end))
for _, pipeline_job in ipairs(M.pipeline_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 ""
)
local function row_offset(name)
local offset = longest_title - string.len(name)
local res = string.rep(" ", offset + 5)
return res
end
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
vim.schedule(function()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
M.color_status(M.latest_pipeline.status, bufnr, lines[1], 1)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, all_lines)
for i, pipeline_job in ipairs(M.pipeline_jobs) do
M.color_status(pipeline_job.status, bufnr, lines[7 + i], 7 + i)
local line_offset = 0
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
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)
u.switch_can_edit_buf(bufnr, false)
end)
end
M.retrigger = function()
M.latest_pipeline = get_latest_pipeline()
if not M.latest_pipeline then
return
end
if M.latest_pipeline.status ~= "failed" then
u.notify("Pipeline is not in a failed state!", vim.log.levels.WARN)
local pipelines = get_latest_pipelines()
if not pipelines then
return
end
job.run_job("/pipeline/" .. M.latest_pipeline.id, "POST", nil, function()
u.notify("Pipeline re-triggered!", vim.log.levels.INFO)
end)
local failed_pipelines = {}
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
M.see_logs = function()
@@ -173,12 +244,8 @@ end
---colorize the pipeline icon.
---@param wrap_with_color boolean
---@return string
M.get_pipeline_icon = function(wrap_with_color)
M.latest_pipeline = get_latest_pipeline()
if not M.latest_pipeline then
return ""
end
local symbol = state.settings.pipeline[M.latest_pipeline.status]
M.get_pipeline_icon = function(idx, wrap_with_color)
local symbol = state.settings.pipeline[state.PIPELINE[idx].latest_pipeline.status]
if not wrap_with_color then
return symbol
end
@@ -196,12 +263,13 @@ end
---colorize the pipeline icon.
---@param wrap_with_color boolean
---@return string
M.get_pipeline_status = function(wrap_with_color)
M.latest_pipeline = get_latest_pipeline()
if not M.latest_pipeline then
return ""
end
return string.format("%s (%s)", M.get_pipeline_icon(wrap_with_color), M.latest_pipeline.status)
M.get_pipeline_status = function(idx, wrap_with_color)
return string.format(
"[%s]: Status: %s (%s)",
state.PIPELINE[idx].name,
M.get_pipeline_icon(idx, wrap_with_color),
state.PIPELINE[idx].latest_pipeline.status
)
end
M.color_status = function(status, bufnr, status_line, linnr)

View File

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

View File

@@ -120,11 +120,11 @@
---Relevant for renamed files only, the name of the file in the previous commit
---@field old_file_name string
---@field current_bufnr integer
---@field new_sha_win_id integer
---@field old_sha_win_id integer
---@field opposite_bufnr integer
---@field new_line_from_buf integer
---@field old_line_from_buf integer
---@field new_sha_focused boolean
---@field current_win_id integer
---@class LocationData
---@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_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`)
---@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
if return_results then
return #warnings + #errors == 0
return #errors == 0
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.
---@param old_line number|nil
---@param new_line number|nil
---@param is_current_sha_focused boolean
---@param new_sha_focused boolean
---@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)
if hunk_and_diff_data.hunks == nil then
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 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)
end

View File

@@ -1,6 +1,5 @@
local u = require("gitlab.utils")
local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local List = require("gitlab.utils.list")
local M = {}
@@ -11,24 +10,24 @@ local M = {}
---@field resolved boolean|nil
---@field created_at string|nil
---Return true if discussion has a placeable diagnostic, false otherwise.
---@param note NoteWithValues
---@param file string
---@return boolean
local filter_discussions_and_notes = function(note, file)
local filter_discussions_and_notes = function(note)
---Do not include unlinked notes
return note.position ~= nil
and (note.position.new_path == file or note.position.old_path == file)
---Skip resolved discussions if user wants to
and not (state.settings.discussion_signs.skip_resolved_discussion and note.resolvable and note.resolved)
---Skip discussions from old revisions
and not (
state.settings.discussion_signs.skip_old_revision_discussion
and note.created_at ~= nil
and u.from_iso_format_date_to_timestamp(note.created_at)
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
)
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[]
M.filter_placeable_discussions = function()
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 = {}
end
local file = reviewer.get_current_file_path()
if not file then
return {}
end
local filtered_discussions = List.new(discussions):filter(function(discussion)
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)
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)
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)
end
---Set diagnostics in currently new SHA.
---Set diagnostics in the given buffer.
---@param namespace number namespace for diagnostics
---@param bufnr number the bufnr for placing the diagnostics
---@param diagnostics 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 view = diffview_lib.get_current_view()
if not view then
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)
local set_diagnostics = function(namespace, bufnr, diagnostics, opts)
vim.diagnostic.set(namespace, bufnr, diagnostics, opts)
require("gitlab.indicators.signs").set_signs(diagnostics, bufnr)
end
---Set diagnostics in old SHA.
---@param namespace number namespace for diagnostics
---@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
---Refresh the diagnostics for all the reviewed files, and place diagnostics for the currently
---visible buffers.
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()
require("gitlab.indicators.signs").clear_signs()
M.clear_diagnostics()
local filtered_discussions = indicators_common.filter_placeable_discussions()
if filtered_discussions == nil then
local file_discussions = List.new(M.placeable_discussions):filter(function(discussion_or_note)
local note = discussion_or_note.notes and discussion_or_note.notes[1] or discussion_or_note
return note.position.new_path == view.cur_layout.b.file.path
or note.position.old_path == view.cur_layout.a.file.path
end)
if #file_discussions == 0 then
return
end
local new_diagnostics = M.parse_new_diagnostics(filtered_discussions)
set_diagnostics_in_new_sha(diagnostics_namespace, new_diagnostics, create_display_opts())
local new_diagnostics, old_diagnostics = List.new(file_discussions):partition(indicators_common.is_new_sha)
local old_diagnostics = M.parse_old_diagnostics(filtered_discussions)
set_diagnostics_in_old_sha(diagnostics_namespace, old_diagnostics, create_display_opts())
if bufnr == view.cur_layout.a.file.bufnr then
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)
if not ok then
@@ -134,24 +149,13 @@ M.refresh_diagnostics = function()
end
---Iterates over each discussion and returns a list of tables with sign
---data, for instance group, priority, line number etc for the new SHA
---@param discussions Discussion[]
---data, for instance group, priority, line number etc
---@param discussions List
---@return DiagnosticTable[]
M.parse_new_diagnostics = function(discussions)
local new_diagnostics = List.new(discussions):filter(indicators_common.is_new_sha)
local single_line = new_diagnostics:filter(indicators_common.is_single_line):map(create_single_line_diagnostic)
local multi_line = new_diagnostics:filter(indicators_common.is_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)
M.parse_diagnostics = function(discussions)
local single_line, multi_line = discussions:partition(indicators_common.is_single_line)
single_line = single_line:map(create_single_line_diagnostic)
multi_line = multi_line:map(create_multiline_diagnostic)
return u.combine(single_line, multi_line)
end

View File

@@ -120,7 +120,7 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts)
settings.popup.temp_registers = temp_registers
end, {
buffer = popup.bufnr,
desc = "Quit discarding changes",
desc = "Quit, discarding changes",
nowait = settings.keymaps.popup.discard_changes_nowait,
})
end
@@ -236,4 +236,16 @@ M.set_cycle_popups_keymaps = function(popups)
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

View File

@@ -16,6 +16,7 @@ local M = {
bufnr = nil,
tabnr = nil,
stored_win = nil,
buf_winids = {},
}
-- 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))
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()
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
@@ -77,9 +80,11 @@ M.open = function()
M.tabnr = nil
end
end
require("diffview.config").user_emitter:on("view_closed", function(_, ...)
M.is_open = false
on_diffview_closed(...)
require("diffview.config").user_emitter:on("view_closed", function(_, args)
if M.tabnr == args.tabpage then
M.is_open = false
on_diffview_closed(args)
end
end)
if state.settings.discussion_tree.auto_open then
@@ -100,10 +105,11 @@ M.close = function()
end
--- Jumps to the location provided in the reviewer window
---@param file_name string
---@param line_number number
---@param new_buffer boolean
M.jump = function(file_name, line_number, new_buffer)
---@param file_name string The file name after change.
---@param old_file_name string The file name before change (different from file_name for renamed/moved files).
---@param line_number number Line number from the discussion node.
---@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
u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
return
@@ -116,8 +122,8 @@ M.jump = function(file_name, line_number, new_buffer)
end
local files = view.panel:ordered_file_list()
local file = List.new(files):find(function(file)
return file.path == file_name
local file = List.new(files):find(function(f)
return new_buffer and f.path == file_name or f.oldpath == old_file_name
end)
if file == nil then
u.notify(
@@ -148,9 +154,13 @@ end
---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
---@param current_win integer The ID of the currently focused window
---@return DiffviewInfo | nil
M.get_reviewer_data = function()
M.get_reviewer_data = function(current_win)
local view = diffview_lib.get_current_view()
if view == nil then
return
end
local layout = view.cur_layout
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)
@@ -169,9 +179,9 @@ M.get_reviewer_data = function()
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 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
u.notify("Error getting modification type", vim.log.levels.ERROR)
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)
end
local current_bufnr = is_current_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 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)
local current_bufnr = new_sha_focused and layout.b.file.bufnr or layout.a.file.bufnr
local opposite_bufnr = new_sha_focused and layout.a.file.bufnr or layout.b.file.bufnr
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,
old_file_name = M.is_file_renamed() and layout.b.file.path or "",
old_line_from_buf = old_line,
new_line_from_buf = new_line,
modification_type = modification_type,
new_sha_win_id = new_sha_win_id,
current_bufnr = current_bufnr,
old_sha_win_id = old_sha_win_id,
opposite_bufnr = opposite_bufnr,
new_sha_focused = new_sha_focused,
current_win_id = current_win,
}
end
---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
M.is_current_sha_focused = function()
M.is_new_sha_focused = function(current_win)
local view = diffview_lib.get_current_view()
local layout = view.cur_layout
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 current_win = require("gitlab.actions.comment").current_win
if a_win ~= current_win and b_win ~= current_win then
current_win = M.stored_win
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
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
M.set_callback_for_file_changed = function(callback)
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" },
group = group,
callback = function(...)
M.stored_win = vim.api.nvim_get_current_win()
if M.tabnr == vim.api.nvim_get_current_tabpage() then
callback(...)
end
@@ -264,7 +273,22 @@ M.set_callback_for_file_changed = function(callback)
})
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
M.set_callback_for_reviewer_leave = function(callback)
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" },
group = group,
callback = function(...)
if M.tabnr == vim.api.nvim_get_current_tabpage() then
if vim.api.nvim_get_current_tabpage() == M.tabnr then
callback(...)
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)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.enter", {})
vim.api.nvim_create_autocmd("User", {
pattern = { "DiffviewViewOpened" },
pattern = { "DiffviewViewEnter", "DiffviewViewOpened" },
group = group,
callback = function(...)
callback(...)
if vim.api.nvim_get_current_tabpage() == M.tabnr then
callback(...)
end
end,
})
end
@@ -325,8 +354,16 @@ end
---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 keymaps table The settings keymaps table.
local set_keymaps = function(bufnr, keymaps)
M.set_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
local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.reviewer.disable_all then
return
end
-- Set mappings for creating comments
if keymaps.reviewer.create_comment ~= false then
-- Set keymap for repeated operator keybinding
@@ -399,29 +436,17 @@ local set_keymaps = function(bufnr, keymaps)
end
end
--- Sets up keymaps for both buffers in the reviewer.
M.set_reviewer_keymaps = function()
---Delete keymaps from reviewer buffers.
---@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
local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.reviewer.disable_all then
return
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
if keymaps.reviewer[func] ~= false then
for _, mode in ipairs({ "n", "o", "v" }) do
@@ -434,23 +459,33 @@ local del_keymaps = function(bufnr, keymaps)
end
end
--- Deletes keymaps from both buffers in the reviewer.
M.del_reviewer_keymaps = function()
-- 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 up autocaommands that will take care of setting and unsetting buffer-local options and keymaps
M.set_reviewer_autocommands = function(bufnr)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.win_enter." .. bufnr, {})
vim.api.nvim_create_autocmd({ "WinEnter", "BufWinEnter" }, {
group = group,
buffer = bufnr,
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()
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
del_keymaps(a, keymaps)
end
if b ~= nil and vim.api.nvim_buf_is_loaded(b) then
del_keymaps(b, keymaps)
end
--- Update the stored winid for a given reviewer buffer. This is necessary for the
--- M.set_reviewer_autocommands function to work correctly in cases like when the user closes one of
--- the original reviewer windows and Diffview automatically creates a new pair
--- of reviewer windows or the user wipes out a buffer and Diffview reloads it with a different ID.
M.update_winid_for_buffer = function(bufnr)
M.buf_winids[bufnr] = vim.fn.bufwinid(bufnr)
end
return M

View File

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

View File

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

View File

@@ -587,19 +587,20 @@ end
M.check_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode
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
end
return true
end
---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
M.get_visual_selection_boundaries = function()
M.press_escape()
local start_line = vim.api.nvim_buf_get_mark(0, "<")[1]
local end_line = vim.api.nvim_buf_get_mark(0, ">")[1]
local start_line = vim.fn.line("v")
local end_line = vim.fn.line(".")
if start_line > end_line then
start_line, end_line = end_line, start_line
end
return start_line, end_line
end