Multiline comment and suggestion (#66)

This MR adds the ability to leave multi-line comments and suggested changes to an MR. The features are only supported for `diffview` because we plan to deprecate `delta` as a reviewer soon.
This commit is contained in:
johnybx
2023-10-31 02:58:53 +01:00
committed by GitHub
parent f853c2f940
commit 3a67424fec
11 changed files with 689 additions and 161 deletions

119
.editorconfig Normal file
View File

@@ -0,0 +1,119 @@
# see https://github.com/CppCXY/EmmyLuaCodeStyle
[*.lua]
# [basic]
# optional space/tab
indent_style = space
# if indent_style is space, this is valid
indent_size = 2
# if indent_style is tab, this is valid
tab_width = 2
# none/single/double
quote_style = "double"
continuation_indent = 2
#optional keep/never/always/smart
trailing_table_separator = keep
# keep/remove/remove_table_only/remove_string_only
call_arg_parentheses = keep
detect_end_of_line = false
# this will check text end with new line
insert_final_newline = false
# [space]
space_around_table_field_list = true
space_before_attribute = true
space_before_function_open_parenthesis = false
space_before_function_call_open_parenthesis = false
space_before_closure_open_parenthesis = false
space_before_function_call_single_arg = true
space_before_open_square_bracket = false
space_inside_function_call_parentheses = false
space_inside_function_param_list_parentheses = false
space_inside_square_brackets = false
# like t[#t+1] = 1
space_around_table_append_operator = true
ignore_spaces_inside_function_call = false
space_before_inline_comment = 1
# [operator space]
space_around_math_operator = true
space_after_comma = true
space_after_comma_in_for_statement = true
space_around_concat_operator = true
# [align]
align_call_args = false
align_function_params = true
align_continuous_assign_statement = true
align_continuous_rect_table_field = true
align_if_branch = true
align_array_table = true
# [indent]
never_indent_before_if_condition = false
never_indent_comment_on_if_branch = false
# [line space]
# The following configuration supports four expressions
# keep
# fixed(n)
# min(n)
# max(n)
# for eg. min(2)
line_space_after_if_statement = keep
line_space_after_do_statement = keep
line_space_after_while_statement = keep
line_space_after_repeat_statement = keep
line_space_after_for_statement = keep
line_space_after_local_or_assign_statement = keep
line_space_after_function_statement = fixed(2)
line_space_after_expression_statement = keep
line_space_after_comment = keep
# [line break]
break_all_list_when_line_exceed = false
auto_collapse_lines = false
# [preference]
ignore_space_after_colon = true
remove_call_expression_list_finish_comma = false

View File

@@ -158,13 +158,16 @@ The upper part of the popup contains the title, which can also be edited and sen
### Reviewing Diffs ### Reviewing Diffs
The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action. The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action or for multiline comments use `create_multiline_comment` in visual mode.
```lua ```lua
require("gitlab").review() require("gitlab").review()
require("gitlab").create_comment() require("gitlab").create_comment()
require("gitlab").create_multiline_comment()
``` ```
For suggesting changes you can use `create_comment_suggestion` in visual mode which works similar to `create_multiline_comment` but prefills the comment window with gitlab [suggest changes](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html) code block with prefilled code from visual selection.
The reviewer is Delta by default, but you can configure the plugin to use Diffview instead. The reviewer is Delta by default, but you can configure the plugin to use Diffview instead.
### Discussions and Notes ### Discussions and Notes
@@ -246,6 +249,8 @@ vim.keymap.set("n", "<leader>gls", gitlab.summary)
vim.keymap.set("n", "<leader>glA", gitlab.approve) vim.keymap.set("n", "<leader>glA", gitlab.approve)
vim.keymap.set("n", "<leader>glR", gitlab.revoke) vim.keymap.set("n", "<leader>glR", gitlab.revoke)
vim.keymap.set("n", "<leader>glc", gitlab.create_comment) vim.keymap.set("n", "<leader>glc", gitlab.create_comment)
vim.keymap.set("v", "<leader>glc", gitlab.create_multiline_comment)
vim.keymap.set("v", "<leader>glC", gitlab.create_comment_suggestion)
vim.keymap.set("n", "<leader>gln", gitlab.create_note) vim.keymap.set("n", "<leader>gln", gitlab.create_note)
vim.keymap.set("n", "<leader>gld", gitlab.toggle_discussions) vim.keymap.set("n", "<leader>gld", gitlab.toggle_discussions)
vim.keymap.set("n", "<leader>glaa", gitlab.add_assignee) vim.keymap.set("n", "<leader>glaa", gitlab.add_assignee)

View File

@@ -1,8 +1,10 @@
package main package main
import ( import (
"crypto/sha1"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
@@ -14,12 +16,27 @@ const mrVersionsUrl = "%s/api/v4/projects/%s/merge_requests/%d/versions"
type PostCommentRequest struct { type PostCommentRequest struct {
Comment string `json:"comment"` Comment string `json:"comment"`
FileName string `json:"file_name"` FileName string `json:"file_name"`
NewLine int `json:"new_line"` NewLine *int `json:"new_line,omitempty"`
OldLine int `json:"old_line"` OldLine *int `json:"old_line,omitempty"`
HeadCommitSHA string `json:"head_commit_sha"` HeadCommitSHA string `json:"head_commit_sha"`
BaseCommitSHA string `json:"base_commit_sha"` BaseCommitSHA string `json:"base_commit_sha"`
StartCommitSHA string `json:"start_commit_sha"` StartCommitSHA string `json:"start_commit_sha"`
Type string `json:"type"` Type string `json:"type"`
LineRange *LineRange `json:"line_range,omitempty"`
}
// LineRange represents the range of a note.
type LineRange struct {
StartRange *LinePosition `json:"start"`
EndRange *LinePosition `json:"end"`
}
// LinePosition represents a position in a line range.
// unlike gitlab struct this does not contain LineCode with sha1 of filename
type LinePosition struct {
Type string `json:"type"`
OldLine int `json:"old_line"`
NewLine int `json:"new_line"`
} }
type DeleteCommentRequest struct { type DeleteCommentRequest struct {
@@ -115,16 +132,42 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
/* If we are leaving a comment on a line, leave position. Otherwise, /* If we are leaving a comment on a line, leave position. Otherwise,
we are leaving a note (unlinked comment) */ we are leaving a note (unlinked comment) */
if postCommentRequest.FileName != "" { if postCommentRequest.FileName != "" {
opt.Position = &gitlab.NotePosition{ opt.Position = &gitlab.PositionOptions{
PositionType: "text", PositionType: &postCommentRequest.Type,
StartSHA: postCommentRequest.StartCommitSHA, StartSHA: &postCommentRequest.StartCommitSHA,
HeadSHA: postCommentRequest.HeadCommitSHA, HeadSHA: &postCommentRequest.HeadCommitSHA,
BaseSHA: postCommentRequest.BaseCommitSHA, BaseSHA: &postCommentRequest.BaseCommitSHA,
NewPath: postCommentRequest.FileName, NewPath: &postCommentRequest.FileName,
OldPath: postCommentRequest.FileName, OldPath: &postCommentRequest.FileName,
NewLine: postCommentRequest.NewLine, NewLine: postCommentRequest.NewLine,
OldLine: postCommentRequest.OldLine, OldLine: postCommentRequest.OldLine,
} }
if postCommentRequest.LineRange != nil {
var format = "%x_%d_%d"
var start_filename_sha1 = fmt.Sprintf(
format,
sha1.Sum([]byte(postCommentRequest.FileName)),
postCommentRequest.LineRange.StartRange.OldLine,
postCommentRequest.LineRange.StartRange.NewLine,
)
var end_filename_sha1 = fmt.Sprintf(
format,
sha1.Sum([]byte(postCommentRequest.FileName)),
postCommentRequest.LineRange.EndRange.OldLine,
postCommentRequest.LineRange.EndRange.NewLine,
)
opt.Position.LineRange = &gitlab.LineRangeOptions{
Start: &gitlab.LinePositionOptions{
Type: &postCommentRequest.LineRange.StartRange.Type,
LineCode: &start_filename_sha1,
},
End: &gitlab.LinePositionOptions{
Type: &postCommentRequest.LineRange.EndRange.Type,
LineCode: &end_filename_sha1,
},
}
}
} }
discussion, _, err := c.git.Discussions.CreateMergeRequestDiscussion(c.projectId, c.mergeId, &opt) discussion, _, err := c.git.Discussions.CreateMergeRequestDiscussion(c.projectId, c.mergeId, &opt)

2
go.mod
View File

@@ -2,7 +2,7 @@ module gitlab.com/harrisoncramer/gitlab.nvim
go 1.19 go 1.19
require github.com/xanzy/go-gitlab v0.83.0 require github.com/xanzy/go-gitlab v0.93.2
require ( require (
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect

2
go.sum
View File

@@ -21,6 +21,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw= github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw=
github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw=
github.com/xanzy/go-gitlab v0.93.2 h1:kNNf3BYNYn/Zkig0B89fma12l36VLcYSGu7OnaRlRDg=
github.com/xanzy/go-gitlab v0.93.2/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=

View File

@@ -13,7 +13,8 @@ local M = {}
local comment_popup = Popup(u.create_popup_state("Comment", "40%", "60%")) local comment_popup = Popup(u.create_popup_state("Comment", "40%", "60%"))
local note_popup = Popup(u.create_popup_state("Note", "40%", "60%")) local note_popup = Popup(u.create_popup_state("Note", "40%", "60%"))
-- This function will open a comment popup in order to create a comment on the changed/updated line in the current MR -- This function will open a comment popup in order to create a comment on the changed/updated
-- line in the current MR
M.create_comment = function() M.create_comment = function()
comment_popup:mount() comment_popup:mount()
state.set_popup_keymaps(comment_popup, function(text) state.set_popup_keymaps(comment_popup, function(text)
@@ -21,15 +22,105 @@ M.create_comment = function()
end, miscellaneous.attach_file) end, miscellaneous.attach_file)
end end
M.create_note = function() ---Create multiline comment for the last selection.
note_popup:mount() M.create_multiline_comment = function()
state.set_popup_keymaps(note_popup, function(text) if not u.check_visual_mode() then
M.confirm_create_comment(text, true) return
end
local start_line, end_line = u.get_visual_selection_boundaries()
comment_popup:mount()
state.set_popup_keymaps(comment_popup, function(text)
M.confirm_create_comment(text, { start_line = start_line, end_line = end_line })
end, miscellaneous.attach_file) end, miscellaneous.attach_file)
end end
-- This function (settings.popup.perform_action) will send the comment to the Go server ---Create comment prepopulated with gitlab suggestion
M.confirm_create_comment = function(text, unlinked) ---https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
M.create_comment_suggestion = function()
if not u.check_visual_mode() then
return
end
local start_line, end_line = u.get_visual_selection_boundaries()
local current_line = vim.api.nvim_win_get_cursor(0)[1]
local range = end_line - start_line
local backticks = "```"
local selected_lines = reviewer.get_lines(start_line, end_line)
if selected_lines == nil then
-- TODO: remove when delta is supported
return
end
for line in ipairs(selected_lines) do
if string.match(line, "^```$") then
backticks = "````"
break
end
end
local suggestion_start
if start_line == current_line then
suggestion_start = backticks .. "suggestion:-0+" .. range
elseif end_line == current_line then
suggestion_start = backticks .. "suggestion:-" .. range .. "+0"
else
-- This should never happen afaik
vim.notify("Unexpected suggestion position", vim.log.levels.ERROR)
return
end
suggestion_start = suggestion_start
local suggestion_lines = {}
table.insert(suggestion_lines, suggestion_start)
vim.list_extend(suggestion_lines, selected_lines)
table.insert(suggestion_lines, backticks)
comment_popup:mount()
vim.api.nvim_buf_set_lines(comment_popup.bufnr, 0, 0, false, suggestion_lines)
state.set_popup_keymaps(comment_popup, function(text)
if range > 0 then
M.confirm_create_comment(text, { start_line = start_line, end_line = end_line })
else
M.confirm_create_comment(text, nil)
end
end, miscellaneous.attach_file)
end
M.create_note = function()
note_popup:mount()
state.set_popup_keymaps(note_popup, function(text)
M.confirm_create_comment(text, nil, true)
end, miscellaneous.attach_file)
end
---@class LineRange
---@field start_line integer
---@field end_line integer
---@class ReviewerLineInfo
---@field old_line integer
---@field new_line integer
---@field type string either "new" or "old"
---@class ReviewerRangeInfo
---@field start ReviewerLineInfo
---@field end ReviewerLineInfo
---@class ReviewerInfo
---@field file_name string
---@field old_line integer | nil
---@field new_line integer | nil
---@field range_info ReviewerRangeInfo
---This function (settings.popup.perform_action) will send the comment to the Go server
---@param text string comment text
---@param range LineRange | nil range of visuel selection or nil
---@param unlinked boolean | nil if true, the comment is not linked to a line
M.confirm_create_comment = function(text, range, unlinked)
if text == nil then
vim.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
return
end
if unlinked then if unlinked then
local body = { comment = text } local body = { comment = text }
job.run_job("/comment", "POST", body, function(data) job.run_job("/comment", "POST", body, function(data)
@@ -39,38 +130,22 @@ M.confirm_create_comment = function(text, unlinked)
return return
end end
local file_name, line_numbers, error = reviewer.get_location() local reviewer_info = reviewer.get_location(range)
if not reviewer_info then
if error then
vim.notify(error, vim.log.levels.ERROR)
return
end
if file_name == nil then
vim.notify("Reviewer did not provide file name", vim.log.levels.ERROR)
return
end
if line_numbers == nil then
vim.notify("Reviewer did not provide line numbers of change", vim.log.levels.ERROR)
return
end
if text == nil then
vim.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
return return
end end
local revision = state.MR_REVISIONS[1] local revision = state.MR_REVISIONS[1]
local body = { local body = {
comment = text, comment = text,
file_name = file_name, file_name = reviewer_info.file_name,
old_line = line_numbers.old_line, old_line = reviewer_info.old_line,
new_line = line_numbers.new_line, new_line = reviewer_info.new_line,
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,
type = "modification" type = "text",
line_range = reviewer_info.range_info,
} }
job.run_job("/comment", "POST", body, function(data) job.run_job("/comment", "POST", body, function(data)

View File

@@ -33,6 +33,8 @@ return {
add_assignee = async.sequence({ info, project_members }, assignees_and_reviewers.add_assignee), add_assignee = async.sequence({ info, project_members }, assignees_and_reviewers.add_assignee),
delete_assignee = async.sequence({ info, project_members }, assignees_and_reviewers.delete_assignee), delete_assignee = async.sequence({ info, project_members }, assignees_and_reviewers.delete_assignee),
create_comment = async.sequence({ info, revisions }, comment.create_comment), create_comment = async.sequence({ info, revisions }, comment.create_comment),
create_multiline_comment = async.sequence({ info, revisions }, comment.create_multiline_comment),
create_comment_suggestion = async.sequence({ info, revisions }, comment.create_comment_suggestion),
create_note = async.sequence({ info }, comment.create_note), create_note = async.sequence({ info }, comment.create_note),
review = async.sequence({ u.merge(info, { refresh = true }) }, function() reviewer.open() end), review = async.sequence({ u.merge(info, { refresh = true }) }, function() reviewer.open() end),
pipeline = async.sequence({ info }, pipeline.open), pipeline = async.sequence({ info }, pipeline.open),

View File

@@ -3,7 +3,7 @@ local state = require("gitlab.state")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local M = { local M = {
bufnr = nil bufnr = nil,
} }
-- Public Functions -- Public Functions
@@ -25,11 +25,13 @@ M.open = function()
local term_command_template = local term_command_template =
"GIT_PAGER='delta --hunk-header-style omit --line-numbers --paging never --file-added-label %s --file-removed-label %s --file-modified-label %s' git diff %s...HEAD" "GIT_PAGER='delta --hunk-header-style omit --line-numbers --paging never --file-added-label %s --file-removed-label %s --file-modified-label %s' git diff %s...HEAD"
local term_command = string.format(term_command_template, local term_command = string.format(
term_command_template,
state.settings.review_pane.delta.added_file, state.settings.review_pane.delta.added_file,
state.settings.review_pane.delta.removed_file, state.settings.review_pane.delta.removed_file,
state.settings.review_pane.delta.modified_file, state.settings.review_pane.delta.modified_file,
state.INFO.target_branch) state.INFO.target_branch
)
vim.fn.termopen(term_command) -- Calls delta and sends the output to the currently blank buffer vim.fn.termopen(term_command) -- Calls delta and sends the output to the currently blank buffer
M.bufnr = vim.api.nvim_get_current_buf() M.bufnr = vim.api.nvim_get_current_buf()
@@ -47,25 +49,47 @@ M.jump = function(file_name, new_line, old_line)
u.jump_to_buffer(M.bufnr, linnr) u.jump_to_buffer(M.bufnr, linnr)
end end
M.get_location = function() ---Get the location of a line within the delta buffer. If range is specified, then also the location
if M.bufnr == nil then return nil, nil, "Delta reviewer must be initialized first" end ---of the lines in range.
---@param range LineRange | nil Line range to get location for
---@return ReviewerInfo | nil nil is returned only if error was encountered
M.get_location = function(range)
if M.bufnr == nil then
vim.notify("Delta reviewer must be initialized first", vim.log.levels.ERROR)
return
end
if range then
vim.notify("Multiline comments are not yet supported for delta reviewer", vim.log.levels.ERROR)
return
end
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
if bufnr ~= M.bufnr then return nil, nil, "Line location can only be determined within reviewer window" end if bufnr ~= M.bufnr then
vim.notify("Line location can only be determined within reviewer window")
return
end
local line_num = u.get_current_line_number() local line_num = u.get_current_line_number()
local file_name = M.get_file_from_review_buffer(u.get_current_line_number()) local file_name = M.get_file_from_review_buffer(u.get_current_line_number())
local range, error = M.get_review_buffer_range(file_name) local review_range, error = M.get_review_buffer_range(file_name)
if error ~= nil then return nil, nil, error end if error ~= nil then
if range == nil then return nil, nil, "Review buffer range could not be identified" end vim.notify(error, vim.log.levels.ERROR)
return
end
if review_range == nil then
vim.notify("Review buffer range could not be identified", vim.log.levels.ERROR)
return
end
-- In case the comment is left on a line without change information, we -- In case the comment is left on a line without change information, we
-- iterate backward until we find it within the range of the changes -- iterate backward until we find it within the range of the changes
local current_line_changes = nil local current_line_changes = nil
local num = line_num local num = line_num
while range ~= nil and num >= range[1] and current_line_changes == nil do while review_range ~= nil and num >= review_range[1] and current_line_changes == nil do
local content = u.get_line_content(M.bufnr, num) local content = u.get_line_content(M.bufnr, num)
local change_nums = M.get_change_nums(content) local change_nums = M.get_change_nums(content)
current_line_changes = change_nums current_line_changes = change_nums
@@ -73,12 +97,13 @@ M.get_location = function()
end end
if current_line_changes == nil then if current_line_changes == nil then
return nil, nil, "Could not find current line change information" vim.notify("Could not find current line change information", vim.log.levels.ERROR)
return
end end
local new_line_num = line_num + 1 local new_line_num = line_num + 1
local next_line_changes = nil local next_line_changes = nil
while range ~= nil and new_line_num <= range[2] and next_line_changes == nil do while review_range ~= nil and new_line_num <= review_range[2] and next_line_changes == nil do
local content = u.get_line_content(M.bufnr, new_line_num) local content = u.get_line_content(M.bufnr, new_line_num)
local change_nums = M.get_change_nums(content) local change_nums = M.get_change_nums(content)
next_line_changes = change_nums next_line_changes = change_nums
@@ -86,20 +111,26 @@ M.get_location = function()
end end
if next_line_changes == nil then if next_line_changes == nil then
return nil, nil, "Could not find next line change information" vim.notify("Could not find next line change information", vim.log.levels.ERROR)
return
end end
local result = { file_name = file_name }
-- This is actually a modified line if these conditions are met -- This is actually a modified line if these conditions are met
if (current_line_changes.old_line and not current_line_changes.new_line and not next_line_changes.old_line and next_line_changes.new_line) then if
do current_line_changes.old_line
current_line_changes = { and not current_line_changes.new_line
old_line = current_line_changes.old, and not next_line_changes.old_line
new_line = next_line_changes.new_line and next_line_changes.new_line
} then
end result.old_line = current_line_changes.old
result.new_line = next_line_changes.new_line
else
vim.notify("Could not determine line location", vim.log.levels.ERROR)
return
end end
return file_name, current_line_changes return result
end end
-- Helper Functions 🤝 -- Helper Functions 🤝
@@ -107,20 +138,26 @@ end
-- to the delta reviewer, they are used to support the public functions -- to the delta reviewer, they are used to support the public functions
M.get_jump_location = function(file_name, new_line, old_line) M.get_jump_location = function(file_name, new_line, old_line)
local range, error = M.get_review_buffer_range(file_name) local range, error = M.get_review_buffer_range(file_name)
if error ~= nil then return nil, error end if error ~= nil then
if range == nil then return nil, "Review buffer range could not be identified" end return nil, error
end
if range == nil then
return nil, "Review buffer range could not be identified"
end
local linnr = nil local linnr = nil
local lines = M.get_review_buffer_lines(range) local lines = M.get_review_buffer_lines(range)
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
local line_data = M.get_change_nums(line.line_content) local line_data = M.get_change_nums(line.line_content)
if old_line == line_data.old_line and new_line == line_data.new_line then if line_data and old_line == line_data.old_line and new_line == line_data.new_line then
linnr = line.line_number linnr = line.line_number
break break
end end
end end
if linnr == nil then return nil, "Could not find matching line" end if linnr == nil then
return nil, "Could not find matching line"
end
return linnr, nil return linnr, nil
end end
@@ -137,29 +174,36 @@ end
M.get_change_nums = function(line) M.get_change_nums = function(line)
local data, _ = line:match("(.-)" .. "" .. "(.*)") local data, _ = line:match("(.-)" .. "" .. "(.*)")
local line_data = {} local line_data = {}
if data == nil then return nil end if data == nil then
return nil
end
if data ~= nil then if data ~= nil and data ~= "" then
local old_line = u.trim(u.get_first_chunk(data, "[^" .. "" .. "]+")) local old_line = u.trim(u.get_first_chunk(data, "[^" .. "" .. "]+"))
local new_line = u.trim(u.get_last_chunk(data, "[^" .. "" .. "]+")) local new_line = u.trim(u.get_last_chunk(data, "[^" .. "" .. "]+"))
line_data.new_line = tonumber(new_line) line_data.new_line = tonumber(new_line)
line_data.old_line = tonumber(old_line) line_data.old_line = tonumber(old_line)
end end
if line_data.new_line == nil and line_data.old_line == nil then return nil end if line_data.new_line == nil and line_data.old_line == nil then
return nil
end
return line_data return line_data
end end
M.get_review_buffer_range = function(file_name) M.get_review_buffer_range = function(file_name)
if M.bufnr == nil then return nil, "Delta reviewer must be initialized first" end if M.bufnr == nil then
return nil, "Delta reviewer must be initialized first"
end
local lines = vim.api.nvim_buf_get_lines(M.bufnr, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(M.bufnr, 0, -1, false)
local start = nil local start = nil
local stop = nil local stop = nil
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
if start ~= nil and stop ~= nil then return { start, stop } end if start ~= nil and stop ~= nil then
return { start, stop }
end
if M.starts_with_file_symbol(line) then if M.starts_with_file_symbol(line) then
-- Check if the file name matches the node name -- Check if the file name matches the node name
local delta_file_name = u.get_last_chunk(line) local delta_file_name = u.get_last_chunk(line)
@@ -173,7 +217,9 @@ M.get_review_buffer_range = function(file_name)
-- We've reached the end of the file, set "stop" in case we already found start -- We've reached the end of the file, set "stop" in case we already found start
stop = #lines stop = #lines
if start ~= nil and stop ~= nil then return { start, stop } end if start ~= nil and stop ~= nil then
return { start, stop }
end
end end
M.starts_with_file_symbol = function(line) M.starts_with_file_symbol = function(line)
@@ -200,4 +246,12 @@ M.get_review_buffer_lines = function(review_buffer_range)
return lines return lines
end end
---Return content between start_line and end_line
---@param start_line integer
---@param end_line integer
---@return string[] | nil
M.get_lines = function(start_line, end_line)
vim.notify("Getting lines in delta is not supported yet", vim.log.levels.ERROR)
return nil
end
return M return M

View File

@@ -1,10 +1,11 @@
-- This Module contains all of the code specific to the Diffview reviewer. -- This Module contains all of the code specific to the Diffview reviewer.
local u = require("gitlab.utils")
local state = require("gitlab.state") local state = require("gitlab.state")
local async_ok, async = pcall(require, "diffview.async") local async_ok, async = pcall(require, "diffview.async")
local M = { local M = {
bufnr = nil, bufnr = nil,
tabnr = nil tabnr = nil,
} }
-- Public Functions -- Public Functions
@@ -49,12 +50,25 @@ M.jump = function(file_name, new_line, old_line)
end end
end end
M.get_location = function() ---Get the location of a line within the diffview. If range is specified, then also the location
if M.tabnr == nil then return nil, nil, "Diffview reviewer must be initialized first" end ---of the lines in range.
---@param range LineRange | nil Line range to get location for
---@return ReviewerInfo | nil nil is returned only if error was encountered
M.get_location = function(range)
if M.tabnr == nil then
vim.notify("Diffview reviewer must be initialized first")
return
end
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local current_line = vim.api.nvim_win_get_cursor(0)[1]
-- check if we are in the diffview tab -- check if we are in the diffview tab
local tabnr = vim.api.nvim_get_current_tabpage() local tabnr = vim.api.nvim_get_current_tabpage()
if tabnr ~= M.tabnr then return nil, nil, "Line location can only be determined within reviewer window" end if tabnr ~= M.tabnr then
vim.notify("Line location can only be determined within reviewer window")
return
end
-- check if we are in the diffview buffer -- check if we are in the diffview buffer
local view = require("diffview.lib").get_current_view() local view = require("diffview.lib").get_current_view()
if view == nil then if view == nil then
@@ -62,18 +76,81 @@ M.get_location = function()
return return
end end
local layout = view.cur_layout local layout = view.cur_layout
local file_name = nil local result = {}
local current_line_changes = nil local type
local is_new
if layout.a.file.bufnr == bufnr then if layout.a.file.bufnr == bufnr then
file_name = layout.a.file.path result.file_name = layout.a.file.path
current_line_changes = { new_line = nil, old_line = vim.api.nvim_win_get_cursor(0)[1] } result.old_line = current_line
return file_name, current_line_changes type = "old"
is_new = false
elseif layout.b.file.bufnr == bufnr then elseif layout.b.file.bufnr == bufnr then
file_name = layout.b.file.path result.file_name = layout.b.file.path
current_line_changes = { new_line = vim.api.nvim_win_get_cursor(0)[1], old_line = nil } result.new_line = current_line
return file_name, current_line_changes type = "new"
is_new = true
else
vim.notify("Line location can only be determined within reviewer window")
return
end end
return nil, nil, "Line location can only be determined within reviewer window"
local hunks = u.parse_hunk_headers(result.file_name, state.INFO.target_branch)
if hunks == nil then
vim.notify("Could not parse hunks", vim.log.levels.ERROR)
return
end
local current_line_info
if is_new then
current_line_info = u.get_lines_from_hunks(hunks, result.new_line, is_new)
else
current_line_info = u.get_lines_from_hunks(hunks, result.old_line, is_new)
end
-- If single line comment is outside of changed lines then we need to specify both new line and old line
-- otherwise the API returns error.
-- https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff
if not current_line_info.in_hunk then
result.old_line = current_line_info.old_line
result.new_line = current_line_info.new_line
end
if range == nil then
return result
end
result.range_info = { start = {}, ["end"] = {} }
if current_line == range.start_line then
result.range_info.start.old_line = current_line_info.old_line
result.range_info.start.new_line = current_line_info.new_line
result.range_info.start.type = type
else
local start_line_info = u.get_lines_from_hunks(hunks, range.start_line, is_new)
result.range_info.start.old_line = start_line_info.old_line
result.range_info.start.new_line = start_line_info.new_line
result.range_info.start.type = type
end
if current_line == range.end_line then
result.range_info["end"].old_line = current_line_info.old_line
result.range_info["end"].new_line = current_line_info.new_line
result.range_info["end"].type = type
else
local end_line_info = u.get_lines_from_hunks(hunks, range.end_line, is_new)
result.range_info["end"].old_line = end_line_info.old_line
result.range_info["end"].new_line = end_line_info.new_line
result.range_info["end"].type = type
end
return result
end
---Return content between start_line and end_line
---@param start_line integer
---@param end_line integer
---@return string[]
M.get_lines = function(start_line, end_line)
return vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
end end
return M return M

View File

@@ -10,7 +10,7 @@ local M = {
local reviewer_map = { local reviewer_map = {
delta = delta, delta = delta,
diffview = diffview diffview = diffview,
} }
M.init = function() M.init = function()
@@ -31,9 +31,12 @@ M.init = function()
-- • {interval} The old_line of the change -- • {interval} The old_line of the change
M.get_location = reviewer.get_location M.get_location = reviewer.get_location
-- Returns the current location (based on cursor) from the reviewer window in format: -- Parameters:
-- file_name, {new_line, old_line}, error -- • {range} LineRange if function was triggered from visual selection
-- Returns the current location (based on cursor) from the reviewer window as ReviewerInfo class
M.get_lines = reviewer.get_lines
-- Returns the content of the file in the current location in the reviewer window
end end
return M return M

View File

@@ -1,7 +1,8 @@
local Job = require("plenary.job")
local M = {} local M = {}
M.get_current_line_number = function() M.get_current_line_number = function()
return vim.api.nvim_call_function('line', { '.' }) return vim.api.nvim_call_function("line", { "." })
end end
M.has_reviewer = function(reviewer) M.has_reviewer = function(reviewer)
@@ -62,13 +63,13 @@ M.format_date = function(date_string)
day = date_table.day, day = date_table.day,
hour = date_table.hour, hour = date_table.hour,
min = date_table.min, min = date_table.min,
sec = date_table.sec sec = date_table.sec,
}) })
local time_diff = current_date - date local time_diff = current_date - date
local function pluralize(num, word) local function pluralize(num, word)
return num .. string.format(" %s", word) .. (num > 1 and "s" or '') .. " ago" return num .. string.format(" %s", word) .. (num > 1 and "s" or "") .. " ago"
end end
if time_diff < 60 then if time_diff < 60 then
@@ -86,7 +87,9 @@ M.format_date = function(date_string)
end end
M.jump_to_file = function(filename, line_number) M.jump_to_file = function(filename, line_number)
if line_number == nil then line_number = 1 end if line_number == nil then
line_number = 1
end
local bufnr = vim.fn.bufnr(filename) local bufnr = vim.fn.bufnr(filename)
if bufnr ~= -1 then if bufnr ~= -1 then
M.jump_to_buffer(bufnr, line_number) M.jump_to_buffer(bufnr, line_number)
@@ -106,7 +109,7 @@ end
M.create_popup_state = function(title, width, height) M.create_popup_state = function(title, width, height)
return { return {
buf_options = { buf_options = {
filetype = 'markdown' filetype = "markdown",
}, },
relative = "editor", relative = "editor",
enter = true, enter = true,
@@ -114,7 +117,7 @@ M.create_popup_state = function(title, width, height)
border = { border = {
style = "rounded", style = "rounded",
text = { text = {
top = title top = title,
}, },
}, },
position = "50%", position = "50%",
@@ -142,7 +145,7 @@ M.join = function(tbl, separator)
-- Remove the trailing separator -- Remove the trailing separator
if separator ~= "" then if separator ~= "" then
result = result:sub(1, - #separator - 1) result = result:sub(1, -#separator - 1)
end end
return result return result
@@ -169,16 +172,16 @@ M.read_file = function(file_path)
end end
M.current_file_path = function() M.current_file_path = function()
local path = debug.getinfo(1, 'S').source:sub(2) local path = debug.getinfo(1, "S").source:sub(2)
return vim.fn.fnamemodify(path, ':p') return vim.fn.fnamemodify(path, ":p")
end end
local random = math.random local random = math.random
M.uuid = function() M.uuid = function()
local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
return string.gsub(template, '[xy]', function(c) return string.gsub(template, "[xy]", function(c)
local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) local v = (c == "x") and random(0, 0xf) or random(8, 0xb)
return string.format('%x', v) return string.format("%x", v)
end) end)
end end
@@ -192,7 +195,9 @@ end
M.table_size = function(t) M.table_size = function(t)
local count = 0 local count = 0
for _ in pairs(t) do count = count + 1 end for _ in pairs(t) do
count = count + 1
end
return count return count
end end
@@ -247,11 +252,7 @@ end
M.get_line_content = function(bufnr, start) M.get_line_content = function(bufnr, start)
local current_buffer = vim.api.nvim_get_current_buf() local current_buffer = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines( local lines = vim.api.nvim_buf_get_lines(bufnr ~= nil and bufnr or current_buffer, start - 1, start, false)
bufnr ~= nil and bufnr or current_buffer,
start - 1,
start,
false)
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
return line return line
@@ -267,8 +268,8 @@ M.get_win_from_buf = function(bufnr)
end end
M.switch_can_edit_buf = function(buf, bool) M.switch_can_edit_buf = function(buf, bool)
vim.api.nvim_buf_set_option(buf, 'modifiable', bool) vim.api.nvim_set_option_value("modifiable", bool, { buf = buf })
vim.api.nvim_buf_set_option(buf, "readonly", not bool) vim.api.nvim_set_option_value("readonly", not bool, { buf = buf })
end end
M.list_files_in_folder = function(folder_path) M.list_files_in_folder = function(folder_path)
@@ -278,12 +279,14 @@ M.list_files_in_folder = function(folder_path)
local folder_ok, folder = pcall(vim.fn.readdir, folder_path) local folder_ok, folder = pcall(vim.fn.readdir, folder_path)
if not folder_ok then return nil end if not folder_ok then
return nil
end
local files = {} local files = {}
if folder ~= nil then if folder ~= nil then
for _, file in ipairs(folder) do for _, file in ipairs(folder) do
local file_path = folder_path .. (M.is_windows() and "\\" or '/') .. file local file_path = folder_path .. (M.is_windows() and "\\" or "/") .. file
local timestamp = vim.fn.getftime(file_path) local timestamp = vim.fn.getftime(file_path)
table.insert(files, { name = file, timestamp = timestamp }) table.insert(files, { name = file, timestamp = timestamp })
end end
@@ -310,4 +313,149 @@ M.reverse = function(list)
return rev return rev
end end
---@class Hunk
---@field old_line integer
---@field old_range integer
---@field new_line integer
---@field new_range integer
---Parse git diff hunks.
---@param file_path string Path to file.
---@param base_branch string Git base branch of merge request.
---@return Hunk[] list of hunks.
M.parse_hunk_headers = function(file_path, base_branch)
local hunks = {}
local diff_job = Job:new({
command = "git",
args = { "diff", "--minimal", "--unified=0", "--no-color", base_branch, "--", file_path },
on_exit = function(j, return_code)
if return_code == 0 then
for _, line in ipairs(j:result()) do
if line:sub(1, 2) == "@@" then
-- match:
-- @@ -23 +23 @@ ...
-- @@ -23,0 +23 @@ ...
-- @@ -41,0 +42,4 @@ ...
local old_start, old_range, new_start, new_range = line:match("@@+ %-(%d+),?(%d*) %+(%d+),?(%d*) @@+")
table.insert(hunks, {
old_line = tonumber(old_start),
old_range = tonumber(old_range) or 0,
new_line = tonumber(new_start),
new_range = tonumber(new_range) or 0,
})
end
end
else
vim.notify("Failed to get git diff: " .. j:stderr(), vim.log.levels.WARN)
end
end,
})
diff_job:sync()
return hunks
end
---@class LineDiffInfo
---@field old_line integer
---@field new_line integer
---@field in_hunk boolean
---Search git diff hunks to find old and new line number corresponding to target line.
---This function does not check if target line is outside of boundaries of file.
---@param hunks Hunk[] git diff parsed hunks.
---@param target_line integer line number to search for - based on is_new paramter the search is
---either in new lines or old lines of hunks.
---@param is_new boolean whether to search for new line or old line
---@return LineDiffInfo
M.get_lines_from_hunks = function(hunks, target_line, is_new)
if #hunks == 0 then
-- If there are zero hunks, return target_line for both old and new lines
return { old_line = target_line, new_line = target_line, in_hunk = false }
end
local current_new_line = 0
local current_old_line = 0
if is_new then
for _, hunk in ipairs(hunks) do
-- target line is before current hunk
if target_line < hunk.new_line then
return {
old_line = current_old_line + (target_line - current_new_line),
new_line = target_line,
in_hunk = false,
}
-- target line is within the current hunk
elseif hunk.new_line <= target_line and target_line <= (hunk.new_line + hunk.new_range) then
-- this is interesting magic of gitlab calculation
return {
old_line = hunk.old_line + hunk.old_range + 1,
new_line = target_line,
in_hunk = true,
}
-- target line is after the current hunk
else
current_new_line = hunk.new_line + hunk.new_range
current_old_line = hunk.old_line + hunk.old_range
end
end
-- target line is after last hunk
return {
old_line = current_old_line + (target_line - current_new_line),
new_line = target_line,
in_hunk = false,
}
else
for _, hunk in ipairs(hunks) do
-- target line is before current hunk
if target_line < hunk.old_line then
return {
old_line = target_line,
new_line = current_new_line + (target_line - current_old_line),
in_hunk = false,
}
-- target line is within the current hunk
elseif hunk.old_line <= target_line and target_line <= (hunk.old_line + hunk.old_range) then
return {
old_line = target_line,
new_line = hunk.new_line,
in_hunk = true,
}
-- target line is after the current hunk
else
current_new_line = hunk.new_line + hunk.new_range
current_old_line = hunk.old_line + hunk.old_range
end
end
-- target line is after last hunk
return {
old_line = current_old_line + (target_line - current_new_line),
new_line = target_line,
in_hunk = false,
}
end
end
---Check if current mode is visual mode
---@return boolean true if current mode is visual mode
M.check_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode
if mode ~= "v" and mode ~= "V" then
vim.notify("Code suggestions are only available in visual mode", vim.log.levels.WARN)
return false
end
return true
end
---Return start line and end line of visual selection.
---Exists visual mode in order to access marks "<" , ">"
---@return integer,integer start line and end line
M.get_visual_selection_boundaries = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", false, true, true), "nx", false)
local start_line = vim.api.nvim_buf_get_mark(0, "<")[1]
local end_line = vim.api.nvim_buf_get_mark(0, ">")[1]
return start_line, end_line
end
return M return M