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:
119
.editorconfig
Normal file
119
.editorconfig
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -12,14 +14,29 @@ import (
|
||||
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"`
|
||||
HeadCommitSHA string `json:"head_commit_sha"`
|
||||
BaseCommitSHA string `json:"base_commit_sha"`
|
||||
StartCommitSHA string `json:"start_commit_sha"`
|
||||
Type string `json:"type"`
|
||||
Comment string `json:"comment"`
|
||||
FileName string `json:"file_name"`
|
||||
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
2
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -1,35 +1,126 @@
|
||||
-- This module is responsible for creating new comments
|
||||
-- in the reviewer's buffer. The reviewer will pass back
|
||||
-- to this module the data required to make the API calls
|
||||
local Popup = require("nui.popup")
|
||||
local state = require("gitlab.state")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local M = {}
|
||||
local Popup = require("nui.popup")
|
||||
local state = require("gitlab.state")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local M = {}
|
||||
|
||||
local comment_popup = Popup(u.create_popup_state("Comment", "40%", "60%"))
|
||||
local note_popup = Popup(u.create_popup_state("Note", "40%", "60%"))
|
||||
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
|
||||
M.create_comment = function()
|
||||
-- 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)
|
||||
M.confirm_create_comment(text)
|
||||
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)
|
||||
|
||||
@@ -25,25 +25,27 @@ return {
|
||||
u.has_reviewer(args.reviewer or "delta")
|
||||
end,
|
||||
-- Global Actions 🌎
|
||||
summary = async.sequence({ info }, summary.summary),
|
||||
approve = async.sequence({ info }, approvals.approve),
|
||||
revoke = async.sequence({ info }, approvals.revoke),
|
||||
add_reviewer = async.sequence({ info, project_members }, assignees_and_reviewers.add_reviewer),
|
||||
delete_reviewer = async.sequence({ info, project_members }, assignees_and_reviewers.delete_reviewer),
|
||||
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_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),
|
||||
summary = async.sequence({ info }, summary.summary),
|
||||
approve = async.sequence({ info }, approvals.approve),
|
||||
revoke = async.sequence({ info }, approvals.revoke),
|
||||
add_reviewer = async.sequence({ info, project_members }, assignees_and_reviewers.add_reviewer),
|
||||
delete_reviewer = async.sequence({ info, project_members }, assignees_and_reviewers.delete_reviewer),
|
||||
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),
|
||||
-- Discussion Tree Actions 🌴
|
||||
toggle_discussions = async.sequence({ info }, discussions.toggle),
|
||||
edit_comment = async.sequence({ info }, discussions.edit_comment),
|
||||
delete_comment = async.sequence({ info }, discussions.delete_comment),
|
||||
toggle_resolved = async.sequence({ info }, discussions.toggle_resolved),
|
||||
reply = async.sequence({ info }, discussions.reply),
|
||||
toggle_discussions = async.sequence({ info }, discussions.toggle),
|
||||
edit_comment = async.sequence({ info }, discussions.edit_comment),
|
||||
delete_comment = async.sequence({ info }, discussions.delete_comment),
|
||||
toggle_resolved = async.sequence({ info }, discussions.toggle_resolved),
|
||||
reply = async.sequence({ info }, discussions.reply),
|
||||
-- Other functions 🤷
|
||||
state = state,
|
||||
print_settings = state.print_settings,
|
||||
open_in_browser = async.sequence({ info }, miscellaneous.open_in_browser),
|
||||
state = state,
|
||||
print_settings = state.print_settings,
|
||||
open_in_browser = async.sequence({ info }, miscellaneous.open_in_browser),
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
-- This Module contains all of the code specific to the Delta reviewer.
|
||||
local state = require("gitlab.state")
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local u = require("gitlab.utils")
|
||||
|
||||
local M = {
|
||||
bufnr = nil
|
||||
local M = {
|
||||
bufnr = nil,
|
||||
}
|
||||
|
||||
-- Public Functions
|
||||
-- These functions are exposed externally and are used
|
||||
-- when the reviewer is consumed by other code. They must follow the specification
|
||||
-- outlined in the reviewer/init.lua file
|
||||
M.open = function()
|
||||
M.open = function()
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
if current_buf == state.discussion_buf then
|
||||
vim.api.nvim_command("wincmd w")
|
||||
@@ -23,20 +23,22 @@ M.open = function()
|
||||
end
|
||||
|
||||
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.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()
|
||||
M.winnr = vim.api.nvim_get_current_win()
|
||||
end
|
||||
|
||||
M.jump = function(file_name, new_line, old_line)
|
||||
M.jump = function(file_name, new_line, old_line)
|
||||
local linnr, error = M.get_jump_location(file_name, new_line, old_line)
|
||||
if error ~= nil then
|
||||
vim.notify(error, vim.log.levels.ERROR)
|
||||
@@ -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,41 +111,53 @@ 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 🤝
|
||||
-- These functions are not exported and should be private
|
||||
-- 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)
|
||||
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
|
||||
|
||||
@@ -134,32 +171,39 @@ M.get_file_from_review_buffer = function(linenr)
|
||||
end
|
||||
end
|
||||
|
||||
M.get_change_nums = function(line)
|
||||
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
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
-- This Module contains all of the code specific to the Diffview reviewer.
|
||||
local state = require("gitlab.state")
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local async_ok, async = pcall(require, "diffview.async")
|
||||
|
||||
local M = {
|
||||
local M = {
|
||||
bufnr = nil,
|
||||
tabnr = nil
|
||||
tabnr = nil,
|
||||
}
|
||||
|
||||
-- Public Functions
|
||||
-- These functions are exposed externally and are used
|
||||
-- when the reviewer is consumed by other code. They must follow the specification
|
||||
-- outlined in the reviewer/init.lua file
|
||||
M.open = function()
|
||||
M.open = function()
|
||||
vim.api.nvim_command(string.format("DiffviewOpen %s", state.INFO.target_branch))
|
||||
M.tabnr = vim.api.nvim_get_current_tabpage()
|
||||
end
|
||||
|
||||
M.jump = function(file_name, new_line, old_line)
|
||||
M.jump = function(file_name, new_line, old_line)
|
||||
if M.tabnr == nil then
|
||||
vim.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
|
||||
return
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user