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
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
require("gitlab").review()
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.
### 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>glR", gitlab.revoke)
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>gld", gitlab.toggle_discussions)
vim.keymap.set("n", "<leader>glaa", gitlab.add_assignee)

View File

@@ -1,8 +1,10 @@
package main
import (
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -14,12 +16,27 @@ const mrVersionsUrl = "%s/api/v4/projects/%s/merge_requests/%d/versions"
type PostCommentRequest struct {
Comment string `json:"comment"`
FileName string `json:"file_name"`
NewLine int `json:"new_line"`
OldLine int `json:"old_line"`
NewLine *int `json:"new_line,omitempty"`
OldLine *int `json:"old_line,omitempty"`
HeadCommitSHA string `json:"head_commit_sha"`
BaseCommitSHA string `json:"base_commit_sha"`
StartCommitSHA string `json:"start_commit_sha"`
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 {
@@ -115,16 +132,42 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
/* If we are leaving a comment on a line, leave position. Otherwise,
we are leaving a note (unlinked comment) */
if postCommentRequest.FileName != "" {
opt.Position = &gitlab.NotePosition{
PositionType: "text",
StartSHA: postCommentRequest.StartCommitSHA,
HeadSHA: postCommentRequest.HeadCommitSHA,
BaseSHA: postCommentRequest.BaseCommitSHA,
NewPath: postCommentRequest.FileName,
OldPath: postCommentRequest.FileName,
opt.Position = &gitlab.PositionOptions{
PositionType: &postCommentRequest.Type,
StartSHA: &postCommentRequest.StartCommitSHA,
HeadSHA: &postCommentRequest.HeadCommitSHA,
BaseSHA: &postCommentRequest.BaseCommitSHA,
NewPath: &postCommentRequest.FileName,
OldPath: &postCommentRequest.FileName,
NewLine: postCommentRequest.NewLine,
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)

2
go.mod
View File

@@ -2,7 +2,7 @@ module gitlab.com/harrisoncramer/gitlab.nvim
go 1.19
require github.com/xanzy/go-gitlab v0.83.0
require github.com/xanzy/go-gitlab v0.93.2
require (
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/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.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/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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 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()
comment_popup:mount()
state.set_popup_keymaps(comment_popup, function(text)
@@ -21,15 +22,105 @@ M.create_comment = function()
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, true)
---Create multiline comment for the last selection.
M.create_multiline_comment = function()
if not u.check_visual_mode() then
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
-- This function (settings.popup.perform_action) will send the comment to the Go server
M.confirm_create_comment = function(text, unlinked)
---Create comment prepopulated with gitlab suggestion
---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
local body = { comment = text }
job.run_job("/comment", "POST", body, function(data)
@@ -39,38 +130,22 @@ M.confirm_create_comment = function(text, unlinked)
return
end
local file_name, line_numbers, error = reviewer.get_location()
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)
local reviewer_info = reviewer.get_location(range)
if not reviewer_info then
return
end
local revision = state.MR_REVISIONS[1]
local body = {
comment = text,
file_name = file_name,
old_line = line_numbers.old_line,
new_line = line_numbers.new_line,
file_name = reviewer_info.file_name,
old_line = reviewer_info.old_line,
new_line = reviewer_info.new_line,
base_commit_sha = revision.base_commit_sha,
start_commit_sha = revision.start_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)

View File

@@ -33,6 +33,8 @@ return {
add_assignee = async.sequence({ info, project_members }, assignees_and_reviewers.add_assignee),
delete_assignee = async.sequence({ info, project_members }, assignees_and_reviewers.delete_assignee),
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),
review = async.sequence({ u.merge(info, { refresh = true }) }, function() reviewer.open() end),
pipeline = async.sequence({ info }, pipeline.open),

View File

@@ -3,7 +3,7 @@ local state = require("gitlab.state")
local u = require("gitlab.utils")
local M = {
bufnr = nil
bufnr = nil,
}
-- Public Functions
@@ -25,11 +25,13 @@ M.open = function()
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"
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.removed_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
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)
end
M.get_location = function()
if M.bufnr == nil then return nil, nil, "Delta reviewer must be initialized first" end
---Get the location of a line within the delta buffer. If range is specified, then also the location
---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()
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 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 range == nil then return nil, nil, "Review buffer range could not be identified" end
if error ~= nil then
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
-- iterate backward until we find it within the range of the changes
local current_line_changes = nil
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 change_nums = M.get_change_nums(content)
current_line_changes = change_nums
@@ -73,12 +97,13 @@ M.get_location = function()
end
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
local new_line_num = line_num + 1
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 change_nums = M.get_change_nums(content)
next_line_changes = change_nums
@@ -86,20 +111,26 @@ M.get_location = function()
end
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
local result = { file_name = file_name }
-- 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
do
current_line_changes = {
old_line = current_line_changes.old,
new_line = next_line_changes.new_line
}
end
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
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
return file_name, current_line_changes
return result
end
-- Helper Functions 🤝
@@ -107,20 +138,26 @@ end
-- to the delta reviewer, they are used to support the public functions
M.get_jump_location = function(file_name, new_line, old_line)
local range, error = M.get_review_buffer_range(file_name)
if error ~= nil then return nil, error end
if range == nil then return nil, "Review buffer range could not be identified" end
if error ~= nil then
return nil, error
end
if range == nil then
return nil, "Review buffer range could not be identified"
end
local linnr = nil
local lines = M.get_review_buffer_lines(range)
for _, line in ipairs(lines) do
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
break
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
end
@@ -137,29 +174,36 @@ end
M.get_change_nums = function(line)
local data, _ = line:match("(.-)" .. "" .. "(.*)")
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 new_line = u.trim(u.get_last_chunk(data, "[^" .. "" .. "]+"))
line_data.new_line = tonumber(new_line)
line_data.old_line = tonumber(old_line)
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
end
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 start = nil
local stop = nil
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
-- Check if the file name matches the node name
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
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
M.starts_with_file_symbol = function(line)
@@ -200,4 +246,12 @@ M.get_review_buffer_lines = function(review_buffer_range)
return lines
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

View File

@@ -1,10 +1,11 @@
-- This Module contains all of the code specific to the Diffview reviewer.
local u = require("gitlab.utils")
local state = require("gitlab.state")
local async_ok, async = pcall(require, "diffview.async")
local M = {
bufnr = nil,
tabnr = nil
tabnr = nil,
}
-- Public Functions
@@ -49,12 +50,25 @@ M.jump = function(file_name, new_line, old_line)
end
end
M.get_location = function()
if M.tabnr == nil then return nil, nil, "Diffview reviewer must be initialized first" end
---Get the location of a line within the diffview. If range is specified, then also the location
---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 current_line = vim.api.nvim_win_get_cursor(0)[1]
-- check if we are in the diffview tab
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
local view = require("diffview.lib").get_current_view()
if view == nil then
@@ -62,18 +76,81 @@ M.get_location = function()
return
end
local layout = view.cur_layout
local file_name = nil
local current_line_changes = nil
local result = {}
local type
local is_new
if layout.a.file.bufnr == bufnr then
file_name = layout.a.file.path
current_line_changes = { new_line = nil, old_line = vim.api.nvim_win_get_cursor(0)[1] }
return file_name, current_line_changes
result.file_name = layout.a.file.path
result.old_line = current_line
type = "old"
is_new = false
elseif layout.b.file.bufnr == bufnr then
file_name = layout.b.file.path
current_line_changes = { new_line = vim.api.nvim_win_get_cursor(0)[1], old_line = nil }
return file_name, current_line_changes
result.file_name = layout.b.file.path
result.new_line = current_line
type = "new"
is_new = true
else
vim.notify("Line location can only be determined within reviewer window")
return
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
return M

View File

@@ -10,7 +10,7 @@ local M = {
local reviewer_map = {
delta = delta,
diffview = diffview
diffview = diffview,
}
M.init = function()
@@ -31,9 +31,12 @@ M.init = function()
-- • {interval} The old_line of the change
M.get_location = reviewer.get_location
-- Returns the current location (based on cursor) from the reviewer window in format:
-- file_name, {new_line, old_line}, error
-- Parameters:
-- • {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
return M

View File

@@ -1,7 +1,8 @@
local Job = require("plenary.job")
local M = {}
M.get_current_line_number = function()
return vim.api.nvim_call_function('line', { '.' })
return vim.api.nvim_call_function("line", { "." })
end
M.has_reviewer = function(reviewer)
@@ -62,13 +63,13 @@ M.format_date = function(date_string)
day = date_table.day,
hour = date_table.hour,
min = date_table.min,
sec = date_table.sec
sec = date_table.sec,
})
local time_diff = current_date - date
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
if time_diff < 60 then
@@ -86,7 +87,9 @@ M.format_date = function(date_string)
end
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)
if bufnr ~= -1 then
M.jump_to_buffer(bufnr, line_number)
@@ -106,7 +109,7 @@ end
M.create_popup_state = function(title, width, height)
return {
buf_options = {
filetype = 'markdown'
filetype = "markdown",
},
relative = "editor",
enter = true,
@@ -114,7 +117,7 @@ M.create_popup_state = function(title, width, height)
border = {
style = "rounded",
text = {
top = title
top = title,
},
},
position = "50%",
@@ -142,7 +145,7 @@ M.join = function(tbl, separator)
-- Remove the trailing separator
if separator ~= "" then
result = result:sub(1, - #separator - 1)
result = result:sub(1, -#separator - 1)
end
return result
@@ -169,16 +172,16 @@ M.read_file = function(file_path)
end
M.current_file_path = function()
local path = debug.getinfo(1, 'S').source:sub(2)
return vim.fn.fnamemodify(path, ':p')
local path = debug.getinfo(1, "S").source:sub(2)
return vim.fn.fnamemodify(path, ":p")
end
local random = math.random
M.uuid = function()
local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
return string.gsub(template, '[xy]', function(c)
local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
return string.format('%x', v)
local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
return string.gsub(template, "[xy]", function(c)
local v = (c == "x") and random(0, 0xf) or random(8, 0xb)
return string.format("%x", v)
end)
end
@@ -192,7 +195,9 @@ end
M.table_size = function(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
for _ in pairs(t) do
count = count + 1
end
return count
end
@@ -247,11 +252,7 @@ end
M.get_line_content = function(bufnr, start)
local current_buffer = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(
bufnr ~= nil and bufnr or current_buffer,
start - 1,
start,
false)
local lines = vim.api.nvim_buf_get_lines(bufnr ~= nil and bufnr or current_buffer, start - 1, start, false)
for _, line in ipairs(lines) do
return line
@@ -267,8 +268,8 @@ M.get_win_from_buf = function(bufnr)
end
M.switch_can_edit_buf = function(buf, bool)
vim.api.nvim_buf_set_option(buf, 'modifiable', bool)
vim.api.nvim_buf_set_option(buf, "readonly", not bool)
vim.api.nvim_set_option_value("modifiable", bool, { buf = buf })
vim.api.nvim_set_option_value("readonly", not bool, { buf = buf })
end
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)
if not folder_ok then return nil end
if not folder_ok then
return nil
end
local files = {}
if folder ~= nil then
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)
table.insert(files, { name = file, timestamp = timestamp })
end
@@ -310,4 +313,149 @@ M.reverse = function(list)
return rev
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