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:
committed by
GitHub
parent
3b396a5e6b
commit
9f898aa1a8
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -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:
|
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",
|
"harrisoncramer/gitlab.nvim",
|
||||||
dependencies = {
|
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`
|
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.
|
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.
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = "",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ M.settings = {
|
|||||||
"delete_branch",
|
"delete_branch",
|
||||||
"squash",
|
"squash",
|
||||||
"labels",
|
"labels",
|
||||||
|
"web_url",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
discussion_signs = {
|
discussion_signs = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user