2.0.0 (#196)
This MR is a #MAJOR breaking change to the plugin. While the plugin will continue to work for users with their existing settings, they will be informed of outdated configuration (diagnostics and signs have been simplified) the next time they open the reviewer. Fix: Trim trailing slash from custom URLs Update: .github/CONTRIBUTING.md, .github/ISSUE_TEMPLATE/bug_report.md Feat: Improve discussion tree toggling (#192) Fix: Toggle modified notes (#188) Fix: Toggle discussion nodes correctly Feat: Show Help keymap in discussion tree winbar Fix: Enable toggling nodes from the note body Fix: Enable toggling resolved status from child nodes Fix: Only try to show emoji popup on note nodes Feat: Add keymap for toggling tree type Fix: Disable tree type toggling in Notes Fix Multi Line Issues (Large Refactor) (#197) Fix: Multi-line discussions. The calculation of a range for a multiline comment has been consolidated and moved into the location.lua file. This does not attempt to fix diagnostics. Refactor: It refactors the discussions code to split hunk parsing and management into a separate module Fix: Don't allow comments on modified buffers #194 by preventing comments on the reviewer when using --imply-local and when the working tree is dirty entirely. Refactor: It introduces a new List class for data aggregation, filtering, etc. Fix: It removes redundant API calls and refreshes from the discussion pane Fix: Location provider (#198) Fix: add nil check for Diffview performance issue (#199) Fix: Switch Tabs During Comment Creation (#200) Fix: Check if file is modified (#201) Fix: Off-By-One Issue in Old SHA (#202) Fix: Rebuild Diagnostics + Signs (#203) Fix: Off-By-One Issue in New SHA (#205) Fix: Reviewer Jumps to wrong location (#206) BREAKING CHANGE: Changes configuration of diagnostics and signs in the setup call.
This commit is contained in:
committed by
GitHub
parent
f6a5238d4b
commit
b5b475ce8b
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -45,8 +45,10 @@ $ stylua .
|
||||
$ luacheck --globals vim busted --no-max-line-length -- .
|
||||
```
|
||||
|
||||
4. Make the merge request to the `main` branch of `.gitlab.nvim`
|
||||
4. Make the merge request to the `develop` branch of `.gitlab.nvim`
|
||||
|
||||
Please provide a description of the feature, and links to any relevant issues.
|
||||
|
||||
That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it and it's been merged into main, the pipeline will detect whether we're merging in a patch, minor, or major change, and create a new tag (e.g. 1.0.12) and release.
|
||||
That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it, it will be merged into the develop branch.
|
||||
|
||||
After some time, if the develop branch is found to be stable, that branch will be merged into `main` and released. When merged into `main` the pipeline will detect whether we're merging in a patch, minor, or major change, and create a new tag (e.g. 1.0.12) and release.
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -3,15 +3,17 @@ name: Bug report
|
||||
about: Create a report to help improve gitlab.nvim!
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Prerequsities
|
||||
|
||||
- [ ] The "Troubleshooting" section of the README did not help
|
||||
- [ ] I've installed the required dependencies
|
||||
- [ ] I'm on the latest version of the plugin
|
||||
- [ ] I've installed the required dependencies
|
||||
- [ ] I've run `:h gitlab.nvim.troubleshooting` and followed the steps there
|
||||
|
||||
## Setup Configuration and Environment
|
||||
|
||||
Please post here the options you're passing to configure `gitlab.nvim` and specify any environment variables you're relying on.
|
||||
|
||||
## Bug Description
|
||||
|
||||
@@ -26,5 +28,3 @@ A clear and concise description of what the bug is.
|
||||
## Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Other Details
|
||||
|
||||
1
.github/workflows/go.yaml
vendored
1
.github/workflows/go.yaml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
jobs:
|
||||
go_lint:
|
||||
name: Lint Go 💅
|
||||
|
||||
1
.github/workflows/lua.yaml
vendored
1
.github/workflows/lua.yaml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
jobs:
|
||||
lua_lint:
|
||||
name: Lint Lua 💅
|
||||
|
||||
40
README.md
40
README.md
@@ -150,6 +150,7 @@ require("gitlab").setup({
|
||||
resolved = '✓', -- Symbol to show next to resolved discussions
|
||||
unresolved = '-', -- Symbol to show next to unresolved discussions
|
||||
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
|
||||
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
|
||||
winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
|
||||
-- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
|
||||
},
|
||||
@@ -169,36 +170,17 @@ require("gitlab").setup({
|
||||
"pipeline",
|
||||
},
|
||||
},
|
||||
discussion_sign_and_diagnostic = {
|
||||
skip_resolved_discussion = false,
|
||||
skip_old_revision_discussion = true,
|
||||
},
|
||||
discussion_sign = {
|
||||
-- See :h sign_define for details about sign configuration.
|
||||
enabled = true,
|
||||
text = "💬",
|
||||
linehl = nil,
|
||||
texthl = nil,
|
||||
culhl = nil,
|
||||
numhl = nil,
|
||||
priority = 20, -- Priority of sign, the lower the number the higher the priority
|
||||
helper_signs = {
|
||||
-- For multiline comments the helper signs are used to indicate the whole context
|
||||
-- Priority of helper signs is lower than the main sign (-1).
|
||||
enabled = true,
|
||||
start = "↑",
|
||||
mid = "|",
|
||||
["end"] = "↓",
|
||||
discussion_signs = {
|
||||
enabled = true, -- Show diagnostics for gitlab comments in the reviewer
|
||||
skip_resolved_discussion = false, -- Show diagnostics for resolved discussions
|
||||
severity = vim.diagnostic.severity.INFO, -- ERROR, WARN, INFO, or HINT
|
||||
virtual_text = false, -- Whether to show the comment text inline as floating virtual text
|
||||
priority = 100, -- Higher will override LSP warnings, etc
|
||||
icons = {
|
||||
comment = "→|",
|
||||
range = " |",
|
||||
},
|
||||
},
|
||||
discussion_diagnostic = {
|
||||
-- If you want to customize diagnostics for discussions you can make special config
|
||||
-- for namespace `gitlab_discussion`. See :h vim.diagnostic.config
|
||||
enabled = true,
|
||||
severity = vim.diagnostic.severity.INFO,
|
||||
code = nil, -- see :h diagnostic-structure
|
||||
display_opts = {}, -- see opts in vim.diagnostic.set
|
||||
},
|
||||
pipeline = {
|
||||
created = "",
|
||||
pending = "",
|
||||
@@ -230,7 +212,7 @@ require("gitlab").setup({
|
||||
directory = "Directory",
|
||||
directory_icon = "DiffviewFolderSign",
|
||||
file_name = "Normal",
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -182,6 +182,7 @@ you call this function with no values the defaults will be used:
|
||||
resolved = '✓', -- Symbol to show next to resolved discussions
|
||||
unresolved = '-', -- Symbol to show next to unresolved discussions
|
||||
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
|
||||
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
|
||||
winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
|
||||
-- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
|
||||
},
|
||||
@@ -203,35 +204,16 @@ you call this function with no values the defaults will be used:
|
||||
"labels",
|
||||
},
|
||||
},
|
||||
discussion_sign_and_diagnostic = {
|
||||
skip_resolved_discussion = false,
|
||||
skip_old_revision_discussion = true,
|
||||
},
|
||||
discussion_sign = {
|
||||
-- See :h sign_define for details about sign configuration.
|
||||
enabled = true,
|
||||
text = "💬",
|
||||
linehl = nil,
|
||||
texthl = nil,
|
||||
culhl = nil,
|
||||
numhl = nil,
|
||||
priority = 20, -- Priority of sign, the lower the number the higher the priority
|
||||
helper_signs = {
|
||||
-- For multiline comments the helper signs are used to indicate the whole context
|
||||
-- Priority of helper signs is lower than the main sign (-1).
|
||||
enabled = true,
|
||||
start = "↑",
|
||||
mid = "|",
|
||||
["end"] = "↓",
|
||||
},
|
||||
},
|
||||
discussion_diagnostic = {
|
||||
-- If you want to customize diagnostics for discussions you can make special config
|
||||
-- for namespace `gitlab_discussion`. See :h vim.diagnostic.config
|
||||
enabled = true,
|
||||
severity = vim.diagnostic.severity.INFO,
|
||||
code = nil, -- see :h diagnostic-structure
|
||||
display_opts = {}, -- see opts in vim.diagnostic.set
|
||||
discussion_signs = {
|
||||
enabled = true,
|
||||
skip_resolved_discussion = false,
|
||||
skip_old_revision_discussion = false,
|
||||
severity = vim.diagnostic.severity.INFO,
|
||||
virtual_text = false,
|
||||
icons = {
|
||||
comment = "→|",
|
||||
range = " |",
|
||||
},
|
||||
},
|
||||
pipeline = {
|
||||
created = "",
|
||||
@@ -385,46 +367,17 @@ These labels will be visible in the summary panel, as long as you provide the
|
||||
|
||||
SIGNS AND DIAGNOSTICS *gitlab.nvim.signs-and-diagnostics*
|
||||
|
||||
By default when reviewing files you will see signs and diagnostics (if enabled
|
||||
in configuration). When cursor is on diagnostic line you can view discussion
|
||||
thread by using `vim.diagnostic.show`. You can also jump to discussion tree
|
||||
where you can reply, edit or delete discussion.
|
||||
By default when reviewing files you will see diagnostics in the reviewer
|
||||
for comments that have been added to a review. When the cursor is on
|
||||
diagnostic line you can view discussion thread by using `vim.diagnostic.show`.
|
||||
|
||||
You can also jump to discussion tree for the given comment:
|
||||
>lua
|
||||
require("gitlab").move_to_discussion_tree_from_diagnostic()
|
||||
<
|
||||
|
||||
The `discussion_sign` configuration controls the display of signs for
|
||||
discussions in the reviewer pane. This allows users to jump to comments in the
|
||||
current buffer in the reviewer pane directly. Keep in mind that the highlights
|
||||
provided here can be overridden by other highlights (for example from
|
||||
`diffview.nvim`).
|
||||
|
||||
These diagnostics are configurable in the same way that diagnostics are
|
||||
typically configurable in Neovim. For instance, the `severity` key sets the
|
||||
diagnostic severity level and should be set to one of
|
||||
`vim.diagnostic.severity.ERROR`, `vim.diagnostic.severity.WARN`,
|
||||
`vim.diagnostic.severity.INFO`, or `vim.diagnostic.severity.HINT`. The
|
||||
`display_opts` option configures the diagnostic display options (this is
|
||||
directly used as opts in vim.diagnostic.set). Here you can configure values
|
||||
like:
|
||||
|
||||
- `virtual_text` - Show virtual text for diagnostics.
|
||||
- `underline` - Underline text for diagnostics.
|
||||
|
||||
Diagnostics for discussions use the `gitlab_discussion` namespace. See
|
||||
|vim.diagnostic.config| and |diagnostic-structure| for more details. Signs and
|
||||
diagnostics have common settings in `discussion_sign_and_diagnostic`. This
|
||||
allows customizing if discussions that are resolved or no longer relevant
|
||||
should still display visual indicators in the editor. The
|
||||
`skip_resolved_discussion` Boolean will control visibility of resolved
|
||||
discussions, and `skip_old_revision_discussion` whether to show signs and
|
||||
diagnostics for discussions on outdated diff revisions.
|
||||
|
||||
When interacting with multiline comments, the cursor must be on the "main" line
|
||||
of diagnostic, where the `discussion_sign.text` is shown, otherwise
|
||||
`vim.diagnostic.show` and `move_to_discussion_tree_from_diagnostic` will not
|
||||
work.
|
||||
You may skip resolved discussions by toggling `discussion_signs.skip_resolved_discussion`
|
||||
in your setup function to true. By default, discussions from this plugin
|
||||
are shown at the INFO severity level (see :h vim.diagnostic.severity).
|
||||
|
||||
EMOJIS *gitlab.nvim.emojis*
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-- and assignees in Gitlab, those who must review an MR.
|
||||
local u = require("gitlab.utils")
|
||||
local job = require("gitlab.job")
|
||||
local List = require("gitlab.utils.list")
|
||||
local state = require("gitlab.state")
|
||||
local M = {}
|
||||
|
||||
@@ -67,13 +68,11 @@ end
|
||||
|
||||
M.filter_eligible = function(current, to_remove)
|
||||
local ids = u.extract(to_remove, "id")
|
||||
local res = {}
|
||||
for _, member in ipairs(current) do
|
||||
return List.new(current):filter(function(member)
|
||||
if not u.contains(ids, member.id) then
|
||||
table.insert(res, member)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return res
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -5,9 +5,11 @@ local Popup = require("nui.popup")
|
||||
local state = require("gitlab.state")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local git = require("gitlab.git")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local Location = require("gitlab.reviewer.location")
|
||||
local M = {}
|
||||
|
||||
-- Popup creation is wrapped in a function so that it is performed *after* user
|
||||
@@ -20,6 +22,15 @@ end
|
||||
-- This function will open a comment popup in order to create a comment on the changed/updated
|
||||
-- line in the current MR
|
||||
M.create_comment = function()
|
||||
local has_clean_tree = git.has_clean_tree()
|
||||
local is_modified = vim.api.nvim_buf_get_option(0, "modified")
|
||||
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
||||
u.notify(
|
||||
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
local comment_popup = create_comment_popup()
|
||||
comment_popup:mount()
|
||||
state.set_popup_keymaps(comment_popup, function(text)
|
||||
@@ -95,30 +106,11 @@ M.create_note = function()
|
||||
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|nil
|
||||
|
||||
---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 visual_range LineRange | nil range of visual 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)
|
||||
M.confirm_create_comment = function(text, visual_range, unlinked)
|
||||
if text == nil then
|
||||
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
|
||||
return
|
||||
@@ -129,33 +121,42 @@ M.confirm_create_comment = function(text, range, unlinked)
|
||||
job.run_job("/mr/comment", "POST", body, function(data)
|
||||
u.notify("Note created!", vim.log.levels.INFO)
|
||||
discussions.add_discussion({ data = data, unlinked = true })
|
||||
discussions.refresh_discussion_data()
|
||||
discussions.refresh()
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
local reviewer_info = reviewer.get_location(range)
|
||||
if not reviewer_info then
|
||||
local reviewer_data = reviewer.get_reviewer_data()
|
||||
if reviewer_data == nil then
|
||||
u.notify("Error getting reviewer data", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local location = Location.new(reviewer_data, visual_range)
|
||||
location:build_location_data()
|
||||
local location_data = location.location_data
|
||||
if location_data == nil then
|
||||
u.notify("Error getting location information", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local revision = state.MR_REVISIONS[1]
|
||||
local body = {
|
||||
type = "text",
|
||||
comment = text,
|
||||
file_name = reviewer_info.file_name,
|
||||
old_line = reviewer_info.old_line,
|
||||
new_line = reviewer_info.new_line,
|
||||
file_name = reviewer_data.file_name,
|
||||
base_commit_sha = revision.base_commit_sha,
|
||||
start_commit_sha = revision.start_commit_sha,
|
||||
head_commit_sha = revision.head_commit_sha,
|
||||
type = "text",
|
||||
line_range = reviewer_info.range_info,
|
||||
old_line = location_data.old_line,
|
||||
new_line = location_data.new_line,
|
||||
line_range = location_data.line_range,
|
||||
}
|
||||
|
||||
job.run_job("/mr/comment", "POST", body, function(data)
|
||||
u.notify("Comment created!", vim.log.levels.INFO)
|
||||
discussions.add_discussion({ data = data, unlinked = false })
|
||||
discussions.refresh_discussion_data()
|
||||
discussions.refresh()
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ local Input = require("nui.input")
|
||||
local Popup = require("nui.popup")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local git = require("gitlab.git")
|
||||
local state = require("gitlab.state")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
|
||||
@@ -124,7 +125,7 @@ M.pick_target = function(args)
|
||||
end
|
||||
|
||||
local function make_template_path(t)
|
||||
local base_dir = vim.fn.trim(vim.fn.system({ "git", "rev-parse", "--show-toplevel" }))
|
||||
local base_dir = git.base_dir()
|
||||
return base_dir
|
||||
.. state.settings.file_separator
|
||||
.. ".gitlab"
|
||||
|
||||
@@ -101,3 +101,22 @@
|
||||
---@field user_data table
|
||||
---@field source string
|
||||
---@field code string?
|
||||
|
||||
---@class LineRange
|
||||
---@field start_line integer
|
||||
---@field end_line integer
|
||||
|
||||
---@class DiffviewInfo
|
||||
---@field modification_type string
|
||||
---@field file_name string
|
||||
---@field current_bufnr integer
|
||||
---@field new_sha_win_id integer
|
||||
---@field old_sha_win_id integer
|
||||
---@field opposite_bufnr integer
|
||||
---@field new_line_from_buf integer
|
||||
---@field old_line_from_buf integer
|
||||
|
||||
---@class LocationData
|
||||
---@field old_line integer | nil
|
||||
---@field new_line integer | nil
|
||||
---@field line_range ReviewerRangeInfo|nil
|
||||
|
||||
@@ -9,9 +9,13 @@ local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local List = require("gitlab.utils.list")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
local discussions_tree = require("gitlab.actions.discussions.tree")
|
||||
local signs = require("gitlab.actions.discussions.signs")
|
||||
local diffview_lib = require("diffview.lib")
|
||||
local common = require("gitlab.indicators.common")
|
||||
local signs = require("gitlab.indicators.signs")
|
||||
local diagnostics = require("gitlab.indicators.diagnostics")
|
||||
local winbar = require("gitlab.actions.discussions.winbar")
|
||||
local help = require("gitlab.actions.help")
|
||||
local emoji = require("gitlab.emoji")
|
||||
@@ -53,28 +57,68 @@ end
|
||||
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
|
||||
M.initialize_discussions = function()
|
||||
signs.setup_signs()
|
||||
-- Setup callback to refresh discussion data, discussion signs and diagnostics whenever the reviewed file changes.
|
||||
reviewer.set_callback_for_file_changed(M.refresh_discussion_data)
|
||||
-- Setup callback to clear signs and diagnostics whenever reviewer is left.
|
||||
reviewer.set_callback_for_reviewer_leave(signs.clear_signs_and_diagnostics)
|
||||
reviewer.set_callback_for_file_changed(function()
|
||||
M.refresh_view()
|
||||
M.modifiable(false)
|
||||
end)
|
||||
reviewer.set_callback_for_reviewer_enter(function()
|
||||
M.modifiable(false)
|
||||
end)
|
||||
reviewer.set_callback_for_reviewer_leave(function()
|
||||
signs.clear_signs()
|
||||
diagnostics.clear_diagnostics()
|
||||
M.modifiable(true)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Ensures that the both buffers in the reviewer are/not modifiable. Relevant if the user is using
|
||||
--- the --imply-local setting
|
||||
M.modifiable = function(bool)
|
||||
local view = diffview_lib.get_current_view()
|
||||
local a = view.cur_layout.a.file.bufnr
|
||||
local b = view.cur_layout.b.file.bufnr
|
||||
if a ~= nil and vim.api.nvim_buf_is_loaded(a) then
|
||||
vim.api.nvim_buf_set_option(a, "modifiable", bool)
|
||||
end
|
||||
if b ~= nil and vim.api.nvim_buf_is_loaded(b) then
|
||||
vim.api.nvim_buf_set_option(b, "modifiable", bool)
|
||||
end
|
||||
end
|
||||
|
||||
---Refresh discussion data, signs, diagnostics, and winbar with new data from API
|
||||
M.refresh_discussion_data = function()
|
||||
--- and rebuild the entire view
|
||||
M.refresh = function()
|
||||
M.load_discussions(function()
|
||||
if state.settings.discussion_sign.enabled then
|
||||
signs.refresh_signs(M.discussions)
|
||||
end
|
||||
if state.settings.discussion_diagnostic.enabled then
|
||||
signs.refresh_diagnostics(M.discussions)
|
||||
end
|
||||
if M.split_visible then
|
||||
local linked_is_focused = M.linked_bufnr == M.focused_bufnr
|
||||
winbar.update_winbar(M.discussions, M.unlinked_discussions, linked_is_focused and "Discussions" or "Notes")
|
||||
end
|
||||
M.refresh_view()
|
||||
end)
|
||||
end
|
||||
|
||||
--- Take existing data and refresh the diagnostics, the winbar, and the signs
|
||||
M.refresh_view = function()
|
||||
if state.settings.discussion_signs.enabled then
|
||||
diagnostics.refresh_diagnostics(M.discussions)
|
||||
end
|
||||
if M.split_visible then
|
||||
local linked_is_focused = M.linked_bufnr == M.focused_bufnr
|
||||
winbar.update_winbar(M.discussions, M.unlinked_discussions, linked_is_focused and "Discussions" or "Notes")
|
||||
end
|
||||
end
|
||||
|
||||
---Toggle Discussions tree type between "simple" and "by_file_name"
|
||||
---@param unlinked boolean True if selected view type is Notes (unlinked discussions)
|
||||
M.toggle_tree_type = function(unlinked)
|
||||
if unlinked then
|
||||
u.notify("Toggling tree type is only possible in Discussions", vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
if state.settings.discussion_tree.tree_type == "simple" then
|
||||
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||
else
|
||||
state.settings.discussion_tree.tree_type = "simple"
|
||||
end
|
||||
M.rebuild_discussion_tree()
|
||||
end
|
||||
|
||||
---Opens the discussion tree, sets the keybindings. It also
|
||||
---creates the tree for notes (which are not linked to specific lines of code)
|
||||
---@param callback function?
|
||||
@@ -124,7 +168,7 @@ M.toggle = function(callback)
|
||||
M.focused_bufnr = default_buffer
|
||||
|
||||
M.switch_can_edit_bufs(false)
|
||||
winbar.update_winbar(M.discussions, M.unlinked_discussions, default_discussions and "Discussions" or "Notes")
|
||||
M.refresh_view()
|
||||
|
||||
vim.api.nvim_set_current_win(current_window)
|
||||
if type(callback) == "function" then
|
||||
@@ -133,6 +177,7 @@ M.toggle = function(callback)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Change between views in the discussion panel, either notes or discussions
|
||||
local switch_view_type = function()
|
||||
local change_to_unlinked = M.linked_bufnr == M.focused_bufnr
|
||||
local new_bufnr = change_to_unlinked and M.unlinked_bufnr or M.linked_bufnr
|
||||
@@ -153,7 +198,7 @@ end
|
||||
---Move to the discussion tree at the discussion from diagnostic on current line.
|
||||
M.move_to_discussion_tree = function()
|
||||
local current_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local diagnostics = vim.diagnostic.get(0, { namespace = signs.diagnostics_namespace, lnum = current_line - 1 })
|
||||
local d = vim.diagnostic.get(0, { namespace = diagnostics.diagnostics_namespace, lnum = current_line - 1 })
|
||||
|
||||
---Function used to jump to the discussion tree after the menu selection.
|
||||
local jump_after_menu_selection = function(diagnostic)
|
||||
@@ -184,11 +229,11 @@ M.move_to_discussion_tree = function()
|
||||
end
|
||||
end
|
||||
|
||||
if #diagnostics == 0 then
|
||||
if #d == 0 then
|
||||
u.notify("No diagnostics for this line", vim.log.levels.WARN)
|
||||
return
|
||||
elseif #diagnostics > 1 then
|
||||
vim.ui.select(diagnostics, {
|
||||
elseif #d > 1 then
|
||||
vim.ui.select(d, {
|
||||
prompt = "Choose discussion to jump to",
|
||||
format_item = function(diagnostic)
|
||||
return diagnostic.message
|
||||
@@ -200,7 +245,7 @@ M.move_to_discussion_tree = function()
|
||||
jump_after_menu_selection(diagnostic)
|
||||
end)
|
||||
else
|
||||
jump_after_menu_selection(diagnostics[1])
|
||||
jump_after_menu_selection(d[1])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -239,36 +284,23 @@ end
|
||||
|
||||
-- This function will actually send the deletion to Gitlab
|
||||
-- when you make a selection, and re-render the tree
|
||||
M.send_deletion = function(tree, unlinked)
|
||||
M.send_deletion = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
|
||||
local note_node = M.get_note_node(tree, current_node)
|
||||
local root_node = M.get_root_node(tree, current_node)
|
||||
local note_id = note_node.is_root and root_node.root_note_id or note_node.id
|
||||
|
||||
local body = { discussion_id = root_node.id, note_id = tonumber(note_id) }
|
||||
|
||||
job.run_job("/mr/comment", "DELETE", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
if not note_node.is_root then
|
||||
tree:remove_node("-" .. note_id) -- Note is not a discussion root, safe to remove
|
||||
tree:render()
|
||||
if note_node.is_root then
|
||||
-- Replace root node w/ current node's contents...
|
||||
tree:remove_node("-" .. root_node.id)
|
||||
else
|
||||
if unlinked then
|
||||
M.unlinked_discussions = u.remove_first_value(M.unlinked_discussions)
|
||||
M.rebuild_unlinked_discussion_tree()
|
||||
else
|
||||
M.discussions = u.remove_first_value(M.discussions)
|
||||
M.rebuild_discussion_tree()
|
||||
end
|
||||
M.add_empty_titles({
|
||||
{ M.linked_bufnr, M.discussions, "No Discussions for this MR" },
|
||||
{ M.unlinked_bufnr, M.unlinked_discussions, "No Notes (Unlinked Discussions) for this MR" },
|
||||
})
|
||||
M.switch_can_edit_bufs(false)
|
||||
tree:remove_node("-" .. note_id)
|
||||
end
|
||||
|
||||
M.refresh_discussion_data()
|
||||
tree:render()
|
||||
M.refresh()
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -278,18 +310,22 @@ M.edit_comment = function(tree, unlinked)
|
||||
local current_node = tree:get_node()
|
||||
local note_node = M.get_note_node(tree, current_node)
|
||||
local root_node = M.get_root_node(tree, current_node)
|
||||
if note_node == nil or root_node == nil then
|
||||
u.notify("Could not get root or note node", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
edit_popup:mount()
|
||||
|
||||
local lines = {} -- Gather all lines from immediate children that aren't note nodes
|
||||
local children_ids = note_node:get_child_ids()
|
||||
for _, child_id in ipairs(children_ids) do
|
||||
-- Gather all lines from immediate children that aren't note nodes
|
||||
local lines = List.new(note_node:get_child_ids()):reduce(function(agg, child_id)
|
||||
local child_node = tree:get_node(child_id)
|
||||
if not child_node:has_children() then
|
||||
local line = tree:get_node(child_id).text
|
||||
table.insert(lines, line)
|
||||
table.insert(agg, line)
|
||||
end
|
||||
end
|
||||
return agg
|
||||
end, {})
|
||||
|
||||
local currentBuffer = vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
|
||||
@@ -327,7 +363,15 @@ end
|
||||
-- This function (settings.discussion_tree.toggle_discussion_resolved) will toggle the resolved status of the current discussion and send the change to the Go server
|
||||
M.toggle_discussion_resolved = function(tree)
|
||||
local note = tree:get_node()
|
||||
if not note or not note.resolvable then
|
||||
if note == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Switch to the root node to enable toggling from child nodes and note bodies
|
||||
if not note.resolvable and M.is_node_note(note) then
|
||||
note = M.get_root_node(tree, note)
|
||||
end
|
||||
if note == nil then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -339,29 +383,86 @@ M.toggle_discussion_resolved = function(tree)
|
||||
job.run_job("/mr/discussions/resolve", "PUT", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
M.redraw_resolved_status(tree, note, not note.resolved)
|
||||
M.refresh_discussion_data()
|
||||
M.refresh()
|
||||
end)
|
||||
end
|
||||
|
||||
---Takes a node and returns the line where the note is positioned in the new SHA. If
|
||||
---the line is not in the new SHA, returns nil
|
||||
---@param node any
|
||||
---@return number|nil
|
||||
local function get_new_line(node)
|
||||
if node.new_line == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
---@type GitlabLineRange|nil
|
||||
local range = node.range
|
||||
if range == nil then
|
||||
if node.new_line == nil then
|
||||
return nil
|
||||
end
|
||||
return node.new_line
|
||||
end
|
||||
|
||||
local start_new_line, _ = common.parse_line_code(range.start.line_code)
|
||||
return start_new_line
|
||||
end
|
||||
|
||||
---Takes a node and returns the line where the note is positioned in the old SHA. If
|
||||
---the line is not in the old SHA, returns nil
|
||||
---@param node any
|
||||
---@return number|nil
|
||||
local function get_old_line(node)
|
||||
if node.old_line == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
---@type GitlabLineRange|nil
|
||||
local range = node.range
|
||||
if range == nil then
|
||||
return node.old_line
|
||||
end
|
||||
|
||||
local _, start_old_line = common.parse_line_code(range.start.line_code)
|
||||
return start_old_line
|
||||
end
|
||||
|
||||
-- This function (settings.discussion_tree.jump_to_reviewer) will jump the cursor to the reviewer's location associated with the note. The implementation depends on the reviewer
|
||||
M.jump_to_reviewer = function(tree)
|
||||
local file_name, new_line, old_line, is_undefined_type, error = M.get_note_location(tree)
|
||||
if error ~= nil then
|
||||
u.notify(error, vim.log.levels.ERROR)
|
||||
local node = tree:get_node()
|
||||
local root_node = M.get_root_node(tree, node)
|
||||
if root_node == nil then
|
||||
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
reviewer.jump(file_name, new_line, old_line, { is_undefined_type = is_undefined_type })
|
||||
reviewer.jump(root_node.file_name, get_new_line(root_node), get_old_line(root_node))
|
||||
M.refresh_view()
|
||||
end
|
||||
|
||||
-- This function (settings.discussion_tree.jump_to_file) will jump to the file changed in a new tab
|
||||
M.jump_to_file = function(tree)
|
||||
local file_name, new_line, old_line, _, error = M.get_note_location(tree)
|
||||
if error ~= nil then
|
||||
u.notify(error, vim.log.levels.ERROR)
|
||||
local node = tree:get_node()
|
||||
local root_node = M.get_root_node(tree, node)
|
||||
if root_node == nil then
|
||||
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.cmd.tabnew()
|
||||
u.jump_to_file(file_name, (new_line or old_line))
|
||||
local line_number = get_new_line(root_node) or get_old_line(root_node)
|
||||
if line_number == nil then
|
||||
line_number = 1
|
||||
end
|
||||
local bufnr = vim.fn.bufnr(root_node.filename)
|
||||
if bufnr ~= -1 then
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||
return
|
||||
end
|
||||
|
||||
-- If buffer is not already open, open it
|
||||
vim.cmd("edit " .. root_node.filename)
|
||||
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||
end
|
||||
|
||||
-- This function (settings.discussion_tree.toggle_node) expands/collapses the current node and its children
|
||||
@@ -370,6 +471,15 @@ M.toggle_node = function(tree)
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Switch to the "note" node from "note_body" nodes to enable toggling discussions inside comments
|
||||
if node.type == "note_body" then
|
||||
node = tree:get_node(node:get_parent_id())
|
||||
end
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local children = node:get_child_ids()
|
||||
if node == nil then
|
||||
return
|
||||
@@ -663,6 +773,9 @@ M.is_current_node_note = function(tree)
|
||||
end
|
||||
|
||||
M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||
vim.keymap.set("n", state.settings.discussion_tree.toggle_tree_type, function()
|
||||
M.toggle_tree_type(unlinked)
|
||||
end, { buffer = bufnr, desc = "Toggle tree type between `simple` and `by_file_name`" })
|
||||
vim.keymap.set("n", state.settings.discussion_tree.edit_comment, function()
|
||||
if M.is_current_node_note(tree) then
|
||||
M.edit_comment(tree, unlinked)
|
||||
@@ -843,25 +956,6 @@ M.add_reply_to_tree = function(tree, note, discussion_id)
|
||||
tree:render()
|
||||
end
|
||||
|
||||
---Get note location
|
||||
---@param tree NuiTree
|
||||
---@return string, string, string, boolean, string?
|
||||
M.get_note_location = function(tree)
|
||||
local node = tree:get_node()
|
||||
if node == nil then
|
||||
return "", "", "", false, "Could not get node"
|
||||
end
|
||||
local discussion_node = M.get_root_node(tree, node)
|
||||
if discussion_node == nil then
|
||||
return "", "", "", false, "Could not get discussion node"
|
||||
end
|
||||
return discussion_node.file_name,
|
||||
discussion_node.new_line,
|
||||
discussion_node.old_line,
|
||||
discussion_node.undefined_type or false,
|
||||
nil
|
||||
end
|
||||
|
||||
---@param tree NuiTree
|
||||
M.open_in_browser = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
local state = require("gitlab.state")
|
||||
local u = require("gitlab.utils")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local discussion_sign_name = "gitlab_discussion"
|
||||
local discussion_helper_sign_start = "gitlab_discussion_helper_start"
|
||||
local discussion_helper_sign_mid = "gitlab_discussion_helper_mid"
|
||||
local discussion_helper_sign_end = "gitlab_discussion_helper_end"
|
||||
local diagnostics_namespace = vim.api.nvim_create_namespace(discussion_sign_name)
|
||||
|
||||
local M = {}
|
||||
M.diagnostics_namespace = diagnostics_namespace
|
||||
|
||||
---Clear all signs and diagnostics
|
||||
M.clear_signs_and_diagnostics = function()
|
||||
vim.fn.sign_unplace(discussion_sign_name)
|
||||
vim.diagnostic.reset(diagnostics_namespace)
|
||||
end
|
||||
|
||||
---Refresh the discussion signs for currently loaded file in reviewer For convinience we use same
|
||||
---string for sign name and sign group ( currently there is only one sign needed)
|
||||
---@param discussions Discussion[]
|
||||
M.refresh_signs = function(discussions)
|
||||
local filtered_discussions = M.filter_discussions_for_signs_and_diagnostics(discussions)
|
||||
if filtered_discussions == nil then
|
||||
vim.diagnostic.reset(diagnostics_namespace)
|
||||
return
|
||||
end
|
||||
|
||||
local new_signs, old_signs, error = M.parse_signs_from_discussions(filtered_discussions)
|
||||
if error ~= nil then
|
||||
vim.notify(error, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.fn.sign_unplace(discussion_sign_name)
|
||||
reviewer.place_sign(old_signs, "old")
|
||||
reviewer.place_sign(new_signs, "new")
|
||||
end
|
||||
|
||||
---Refresh the diagnostics for the currently reviewed file
|
||||
---@param discussions Discussion[]
|
||||
M.refresh_diagnostics = function(discussions)
|
||||
-- Keep in mind that diagnostic line numbers use 0-based indexing while line numbers use
|
||||
-- 1-based indexing
|
||||
local filtered_discussions = M.filter_discussions_for_signs_and_diagnostics(discussions)
|
||||
if filtered_discussions == nil then
|
||||
vim.diagnostic.reset(diagnostics_namespace)
|
||||
return
|
||||
end
|
||||
|
||||
local new_diagnostics, old_diagnostics = M.parse_diagnostics_from_discussions(filtered_discussions)
|
||||
|
||||
vim.diagnostic.reset(diagnostics_namespace)
|
||||
reviewer.set_diagnostics(
|
||||
diagnostics_namespace,
|
||||
new_diagnostics,
|
||||
"new",
|
||||
state.settings.discussion_diagnostic.display_opts
|
||||
)
|
||||
reviewer.set_diagnostics(
|
||||
diagnostics_namespace,
|
||||
old_diagnostics,
|
||||
"old",
|
||||
state.settings.discussion_diagnostic.display_opts
|
||||
)
|
||||
end
|
||||
|
||||
---Filter all discussions which are relevant for currently visible signs and diagnostscs.
|
||||
---@return Discussion[]?
|
||||
M.filter_discussions_for_signs_and_diagnostics = function(all_discussions)
|
||||
if type(all_discussions) ~= "table" then
|
||||
return
|
||||
end
|
||||
local file = reviewer.get_current_file()
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
local discussions = {}
|
||||
for _, discussion in ipairs(all_discussions) do
|
||||
local first_note = discussion.notes[1]
|
||||
if
|
||||
type(first_note.position) == "table"
|
||||
and (first_note.position.new_path == file or first_note.position.old_path == file)
|
||||
then
|
||||
if
|
||||
--Skip resolved discussions
|
||||
not (
|
||||
state.settings.discussion_sign_and_diagnostic.skip_resolved_discussion
|
||||
and first_note.resolvable
|
||||
and first_note.resolved
|
||||
)
|
||||
--Skip discussions from old revisions
|
||||
and not (
|
||||
state.settings.discussion_sign_and_diagnostic.skip_old_revision_discussion
|
||||
and u.from_iso_format_date_to_timestamp(first_note.created_at)
|
||||
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
|
||||
)
|
||||
then
|
||||
table.insert(discussions, discussion)
|
||||
end
|
||||
end
|
||||
end
|
||||
return discussions
|
||||
end
|
||||
|
||||
---Define signs for discussions if not already defined
|
||||
M.setup_signs = function()
|
||||
local discussion_sign = state.settings.discussion_sign
|
||||
local signs = {
|
||||
[discussion_sign_name] = discussion_sign.text,
|
||||
[discussion_helper_sign_start] = discussion_sign.helper_signs.start,
|
||||
[discussion_helper_sign_mid] = discussion_sign.helper_signs.mid,
|
||||
[discussion_helper_sign_end] = discussion_sign.helper_signs["end"],
|
||||
}
|
||||
for sign_name, sign_text in pairs(signs) do
|
||||
if #vim.fn.sign_getdefined(sign_name) == 0 then
|
||||
vim.fn.sign_define(sign_name, {
|
||||
text = sign_text,
|
||||
linehl = discussion_sign.linehl,
|
||||
texthl = discussion_sign.texthl,
|
||||
culhl = discussion_sign.culhl,
|
||||
numhl = discussion_sign.numhl,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Iterates over each discussion and returns a list of tables with sign
|
||||
---data, for instance group, priority, line number etc.
|
||||
---@param discussions Discussion[]
|
||||
---@return DiagnosticTable[], DiagnosticTable[], string?
|
||||
M.parse_diagnostics_from_discussions = function(discussions)
|
||||
local new_diagnostics = {}
|
||||
local old_diagnostics = {}
|
||||
for _, discussion in ipairs(discussions) do
|
||||
local first_note = discussion.notes[1]
|
||||
local message = ""
|
||||
for _, note in ipairs(discussion.notes) do
|
||||
message = message .. M.build_note_header(note) .. "\n" .. note.body .. "\n"
|
||||
end
|
||||
|
||||
local diagnostic = {
|
||||
message = message,
|
||||
col = 0,
|
||||
severity = state.settings.discussion_diagnostic.severity,
|
||||
user_data = { discussion_id = discussion.id, header = M.build_note_header(discussion.notes[1]) },
|
||||
source = "gitlab",
|
||||
code = state.settings.discussion_diagnostic.code,
|
||||
}
|
||||
|
||||
-- Diagnostics for line range discussions are tricky - you need to set lnum to be the
|
||||
-- line number equal to note.position.new_line or note.position.old_line because that is the
|
||||
-- only line where you can trigger the diagnostic to show. This also needs to be in sync
|
||||
-- with the sign placement.
|
||||
local line_range = first_note.position.line_range
|
||||
if line_range ~= nil then
|
||||
local start_old_line, start_new_line = M.parse_line_code(line_range.start.line_code)
|
||||
local end_old_line, end_new_line = M.parse_line_code(line_range["end"].line_code)
|
||||
|
||||
local start_type = line_range.start.type
|
||||
if start_type == "new" then
|
||||
local new_diagnostic
|
||||
if first_note.position.new_line == start_new_line then
|
||||
new_diagnostic = {
|
||||
lnum = start_new_line - 1,
|
||||
end_lnum = end_new_line - 1,
|
||||
}
|
||||
else
|
||||
new_diagnostic = {
|
||||
lnum = end_new_line - 1,
|
||||
end_lnum = start_new_line - 1,
|
||||
}
|
||||
end
|
||||
new_diagnostic = vim.tbl_deep_extend("force", new_diagnostic, diagnostic)
|
||||
table.insert(new_diagnostics, new_diagnostic)
|
||||
elseif start_type == "old" or start_type == "expanded" or start_type == "" then
|
||||
local old_diagnostic
|
||||
if first_note.position.old_line == start_old_line then
|
||||
old_diagnostic = {
|
||||
lnum = start_old_line - 1,
|
||||
end_lnum = end_old_line - 1,
|
||||
}
|
||||
else
|
||||
old_diagnostic = {
|
||||
lnum = end_old_line - 1,
|
||||
end_lnum = start_old_line - 1,
|
||||
}
|
||||
end
|
||||
old_diagnostic = vim.tbl_deep_extend("force", old_diagnostic, diagnostic)
|
||||
table.insert(old_diagnostics, old_diagnostic)
|
||||
else -- Comments on expanded, non-changed lines
|
||||
return {}, {}, string.format("Unsupported line range type found for discussion %s", discussion.id)
|
||||
end
|
||||
else -- Diagnostics for single line discussions.
|
||||
if first_note.position.new_line ~= nil and first_note.position.old_line == nil then
|
||||
local new_diagnostic = {
|
||||
lnum = first_note.position.new_line - 1,
|
||||
}
|
||||
new_diagnostic = vim.tbl_deep_extend("force", new_diagnostic, diagnostic)
|
||||
table.insert(new_diagnostics, new_diagnostic)
|
||||
end
|
||||
if first_note.position.old_line ~= nil then
|
||||
local old_diagnostic = {
|
||||
lnum = first_note.position.old_line - 1,
|
||||
}
|
||||
old_diagnostic = vim.tbl_deep_extend("force", old_diagnostic, diagnostic)
|
||||
table.insert(old_diagnostics, old_diagnostic)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return new_diagnostics, old_diagnostics
|
||||
end
|
||||
|
||||
local base_sign = {
|
||||
name = discussion_sign_name,
|
||||
group = discussion_sign_name,
|
||||
priority = state.settings.discussion_sign.priority,
|
||||
buffer = nil,
|
||||
}
|
||||
local base_helper_sign = {
|
||||
name = discussion_sign_name,
|
||||
group = discussion_sign_name,
|
||||
priority = state.settings.discussion_sign.priority - 1,
|
||||
buffer = nil,
|
||||
}
|
||||
|
||||
---Iterates over each discussion and returns a list of tables with sign
|
||||
---data, for instance group, priority, line number etc.
|
||||
---@param discussions Discussion[]
|
||||
---@return SignTable[], SignTable[], string?
|
||||
M.parse_signs_from_discussions = function(discussions)
|
||||
local new_signs = {}
|
||||
local old_signs = {}
|
||||
for _, discussion in ipairs(discussions) do
|
||||
local first_note = discussion.notes[1]
|
||||
local line_range = first_note.position.line_range
|
||||
|
||||
-- We have a line range which means we either have a multi-line comment or a comment
|
||||
-- on a line in an "expanded" part of a file
|
||||
if line_range ~= nil then
|
||||
local start_old_line, start_new_line = M.parse_line_code(line_range.start.line_code)
|
||||
local end_old_line, end_new_line = M.parse_line_code(line_range["end"].line_code)
|
||||
local discussion_line, start_line, end_line
|
||||
|
||||
local start_type = line_range.start.type
|
||||
if start_type == "new" then
|
||||
table.insert(
|
||||
new_signs,
|
||||
vim.tbl_deep_extend("force", {
|
||||
id = first_note.id,
|
||||
lnum = first_note.position.new_line,
|
||||
}, base_sign)
|
||||
)
|
||||
discussion_line = first_note.position.new_line
|
||||
start_line = start_new_line
|
||||
end_line = end_new_line
|
||||
elseif start_type == "old" or start_type == "expanded" or start_type == "" then
|
||||
table.insert(
|
||||
old_signs,
|
||||
vim.tbl_deep_extend("force", {
|
||||
id = first_note.id,
|
||||
lnum = first_note.position.old_line,
|
||||
}, base_sign)
|
||||
)
|
||||
discussion_line = first_note.position.old_line
|
||||
start_line = start_old_line
|
||||
end_line = end_old_line
|
||||
else
|
||||
return {}, {}, string.format("Unsupported line range type found for discussion %s", discussion.id)
|
||||
end
|
||||
|
||||
-- Helper signs does not have specific ids currently.
|
||||
if state.settings.discussion_sign.helper_signs.enabled then
|
||||
local helper_signs = {}
|
||||
if start_line > end_line then
|
||||
start_line, end_line = end_line, start_line
|
||||
end
|
||||
for i = start_line, end_line do
|
||||
if i ~= discussion_line then
|
||||
local sign_name
|
||||
if i == start_line then
|
||||
sign_name = discussion_helper_sign_start
|
||||
elseif i == end_line then
|
||||
sign_name = discussion_helper_sign_end
|
||||
else
|
||||
sign_name = discussion_helper_sign_mid
|
||||
end
|
||||
table.insert(
|
||||
helper_signs,
|
||||
vim.tbl_deep_extend("keep", {
|
||||
name = sign_name,
|
||||
lnum = i,
|
||||
}, base_helper_sign)
|
||||
)
|
||||
end
|
||||
end
|
||||
if start_type == "new" then
|
||||
vim.list_extend(new_signs, helper_signs)
|
||||
elseif start_type == "old" or start_type == "expanded" or start_type == "" then
|
||||
vim.list_extend(old_signs, helper_signs)
|
||||
end
|
||||
end
|
||||
else -- The note is a normal comment, not a range comment
|
||||
local sign = vim.tbl_deep_extend("force", {
|
||||
id = first_note.id,
|
||||
}, base_sign)
|
||||
if first_note.position.new_line ~= nil and first_note.position.old_line == nil then
|
||||
table.insert(new_signs, vim.tbl_deep_extend("force", { lnum = first_note.position.new_line }, sign))
|
||||
end
|
||||
if first_note.position.old_line ~= nil then
|
||||
table.insert(old_signs, vim.tbl_deep_extend("force", { lnum = first_note.position.old_line }, sign))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return new_signs, old_signs, nil
|
||||
end
|
||||
|
||||
---Parse line code and return old and new line numbers
|
||||
---@param line_code string gitlab line code -> 588440f66559714280628a4f9799f0c4eb880a4a_10_10
|
||||
---@return number?
|
||||
M.parse_line_code = function(line_code)
|
||||
local line_code_regex = "%w+_(%d+)_(%d+)"
|
||||
local old_line, new_line = line_code:match(line_code_regex)
|
||||
return tonumber(old_line), tonumber(new_line)
|
||||
end
|
||||
|
||||
---Build note header from note.
|
||||
---@param note Note
|
||||
---@return string
|
||||
M.build_note_header = function(note)
|
||||
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -81,7 +81,7 @@ end
|
||||
---Build note header from note.
|
||||
---@param note Note
|
||||
---@return string
|
||||
local function build_note_header(note)
|
||||
M.build_note_header = function(note)
|
||||
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||
end
|
||||
|
||||
@@ -112,7 +112,7 @@ local function build_note_body(note, resolve_info)
|
||||
or state.settings.discussion_tree.unresolved
|
||||
end
|
||||
|
||||
local noteHeader = build_note_header(note) .. " " .. resolve_symbol
|
||||
local noteHeader = M.build_note_header(note) .. " " .. resolve_symbol
|
||||
|
||||
return noteHeader, text_nodes
|
||||
end
|
||||
@@ -158,8 +158,9 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
local root_id
|
||||
local root_text_nodes = {}
|
||||
local resolvable = false
|
||||
---@type GitlabLineRange|nil
|
||||
local range = nil
|
||||
local resolved = false
|
||||
local undefined_type = false
|
||||
local root_new_line = nil
|
||||
local root_old_line = nil
|
||||
local root_url
|
||||
@@ -175,16 +176,7 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
resolvable = note.resolvable
|
||||
resolved = note.resolved
|
||||
root_url = state.INFO.web_url .. "#note_" .. note.id
|
||||
|
||||
-- This appears to be a Gitlab 🐛 where the "type" is returned as an empty string in some cases
|
||||
-- We link these comments to the old file by default
|
||||
if
|
||||
type(note.position) == "table"
|
||||
and note.position.line_range ~= nil
|
||||
and note.position.line_range.start.type == ""
|
||||
then
|
||||
undefined_type = true
|
||||
end
|
||||
range = (type(note.position) == "table" and note.position.line_range or nil)
|
||||
else -- Otherwise insert it as a child node...
|
||||
local note_node = M.build_note(note)
|
||||
table.insert(discussion_children, note_node)
|
||||
@@ -194,6 +186,7 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
-- Creates the first node in the discussion, and attaches children
|
||||
local body = u.spread(root_text_nodes, discussion_children)
|
||||
local root_node = NuiTree.Node({
|
||||
range = range,
|
||||
text = root_text,
|
||||
type = "note",
|
||||
is_root = true,
|
||||
@@ -204,7 +197,6 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
old_line = root_old_line,
|
||||
resolvable = resolvable,
|
||||
resolved = resolved,
|
||||
undefined_type = undefined_type,
|
||||
url = root_url,
|
||||
}, body)
|
||||
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
local M = {}
|
||||
local state = require("gitlab.state")
|
||||
local List = require("gitlab.utils.list")
|
||||
|
||||
---@param nodes Discussion[]|UnlinkedDiscussion[]|nil
|
||||
---@return number, number
|
||||
local get_data = function(nodes)
|
||||
if nodes == nil then
|
||||
return 0, 0
|
||||
end
|
||||
local total_resolvable = 0
|
||||
local total_resolved = 0
|
||||
if nodes == vim.NIL then
|
||||
return ""
|
||||
if nodes == nil or nodes == vim.NIL then
|
||||
return total_resolvable, total_resolved
|
||||
end
|
||||
|
||||
for _, d in ipairs(nodes) do
|
||||
total_resolvable = List.new(nodes):reduce(function(agg, d)
|
||||
local first_child = d.notes[1]
|
||||
if first_child ~= nil then
|
||||
if first_child.resolvable then
|
||||
total_resolvable = total_resolvable + 1
|
||||
end
|
||||
if first_child.resolved then
|
||||
total_resolved = total_resolved + 1
|
||||
end
|
||||
if first_child and first_child.resolvable then
|
||||
agg = agg + 1
|
||||
end
|
||||
end
|
||||
return agg
|
||||
end, 0)
|
||||
|
||||
total_resolved = List.new(nodes):reduce(function(agg, d)
|
||||
local first_child = d.notes[1]
|
||||
if first_child and first_child.resolved then
|
||||
agg = agg + 1
|
||||
end
|
||||
return agg
|
||||
end, 0)
|
||||
|
||||
return total_resolvable, total_resolved
|
||||
end
|
||||
|
||||
@@ -2,18 +2,19 @@ local M = {}
|
||||
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local List = require("gitlab.utils.list")
|
||||
local Popup = require("nui.popup")
|
||||
|
||||
M.open = function()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local keymaps = vim.api.nvim_buf_get_keymap(bufnr, "n")
|
||||
local help_content_lines = {}
|
||||
for _, keymap in ipairs(keymaps) do
|
||||
local help_content_lines = List.new(keymaps):reduce(function(agg, keymap)
|
||||
if keymap.desc ~= nil then
|
||||
local new_line = string.format("%s: %s", keymap.lhs:gsub(" ", "<space>"), keymap.desc)
|
||||
table.insert(help_content_lines, new_line)
|
||||
table.insert(agg, new_line)
|
||||
end
|
||||
end
|
||||
return agg
|
||||
end, {})
|
||||
local longest_line = u.get_longest_string(help_content_lines)
|
||||
local help_popup =
|
||||
Popup(u.create_popup_state("Help", state.settings.popup.help, longest_line + 3, #help_content_lines + 3, 60))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
local u = require("gitlab.utils")
|
||||
local job = require("gitlab.job")
|
||||
local state = require("gitlab.state")
|
||||
local List = require("gitlab.utils.list")
|
||||
local M = {}
|
||||
|
||||
M.add_label = function()
|
||||
@@ -14,11 +15,9 @@ M.delete_label = function()
|
||||
end
|
||||
|
||||
local refresh_label_state = function(labels)
|
||||
local new_labels = ""
|
||||
for _, label in ipairs(labels) do
|
||||
new_labels = new_labels .. "," .. label
|
||||
end
|
||||
state.INFO.labels = new_labels
|
||||
state.INFO.labels = List.new(labels):reduce(function(agg, label)
|
||||
return agg .. "," .. label
|
||||
end, "")
|
||||
end
|
||||
|
||||
local get_current_labels = function()
|
||||
@@ -31,11 +30,9 @@ local get_current_labels = function()
|
||||
end
|
||||
|
||||
local get_all_labels = function()
|
||||
local labels = {}
|
||||
for _, label in ipairs(state.LABELS) do -- How can we use the colors??
|
||||
table.insert(labels, label.Name)
|
||||
end
|
||||
return labels
|
||||
return List.new(state.LABELS):map(function(label)
|
||||
return label.Name
|
||||
end)
|
||||
end
|
||||
|
||||
M.add_popup = function(type)
|
||||
|
||||
@@ -5,6 +5,7 @@ local Layout = require("nui.layout")
|
||||
local Popup = require("nui.popup")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local List = require("gitlab.utils.list")
|
||||
local state = require("gitlab.state")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
local pipeline = require("gitlab.actions.pipeline")
|
||||
@@ -159,8 +160,7 @@ M.build_info_lines = function()
|
||||
return string.rep(" ", offset + 3)
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for _, v in ipairs(state.settings.info.fields) do
|
||||
return List.new(state.settings.info.fields):map(function(v)
|
||||
if v == "merge_status" then
|
||||
v = "detailed_merge_status"
|
||||
end
|
||||
@@ -174,10 +174,8 @@ M.build_info_lines = function()
|
||||
else
|
||||
line = line .. row.content
|
||||
end
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
return lines
|
||||
return line
|
||||
end)
|
||||
end
|
||||
|
||||
-- This function will PUT the new description to the Go server
|
||||
|
||||
@@ -70,7 +70,7 @@ M.init_popup = function(tree, bufnr)
|
||||
vim.api.nvim_create_autocmd({ "CursorHold" }, {
|
||||
callback = function()
|
||||
local node = tree:get_node()
|
||||
if node == nil then
|
||||
if node == nil or not require("gitlab.actions.discussions").is_node_note(node) then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
11
lua/gitlab/git.lua
Normal file
11
lua/gitlab/git.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
local M = {}
|
||||
|
||||
M.has_clean_tree = function()
|
||||
return vim.fn.trim(vim.fn.system({ "git", "status", "--short", "--untracked-files=no" })) == ""
|
||||
end
|
||||
|
||||
M.base_dir = function()
|
||||
return vim.fn.trim(vim.fn.system({ "git", "rev-parse", "--show-toplevel" }))
|
||||
end
|
||||
|
||||
return M
|
||||
280
lua/gitlab/hunks/init.lua
Normal file
280
lua/gitlab/hunks/init.lua
Normal file
@@ -0,0 +1,280 @@
|
||||
local List = require("gitlab.utils.list")
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local M = {}
|
||||
|
||||
---@class Hunk
|
||||
---@field old_line integer
|
||||
---@field old_range integer
|
||||
---@field new_line integer
|
||||
---@field new_range integer
|
||||
|
||||
---@class HunksAndDiff
|
||||
---@field hunks Hunk[] list of hunks
|
||||
---@field all_diff_output table The data from the git diff command
|
||||
|
||||
---Turn hunk line into Lua table
|
||||
---@param line table
|
||||
---@return Hunk|nil
|
||||
M.parse_possible_hunk_headers = function(line)
|
||||
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*) @@+")
|
||||
|
||||
return {
|
||||
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
|
||||
---@param linnr number
|
||||
---@param hunk Hunk
|
||||
---@param all_diff_output table
|
||||
---@return boolean
|
||||
local line_was_removed = function(linnr, hunk, all_diff_output)
|
||||
for matching_line_index, line in ipairs(all_diff_output) do
|
||||
local found_hunk = M.parse_possible_hunk_headers(line)
|
||||
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
|
||||
-- We found a matching hunk, now we need to iterate over the lines from the raw diff output
|
||||
-- at that hunk until we reach the line we are looking for. When the indexes match we check
|
||||
-- to see if that line is deleted or not.
|
||||
for hunk_line_index = found_hunk.old_line, hunk.old_line + hunk.old_range, 1 do
|
||||
local line_content = all_diff_output[matching_line_index + 1]
|
||||
if hunk_line_index == linnr then
|
||||
if string.match(line_content, "^%-") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param linnr number
|
||||
---@param hunk Hunk
|
||||
---@param all_diff_output table
|
||||
---@return boolean
|
||||
local line_was_added = function(linnr, hunk, all_diff_output)
|
||||
for matching_line_index, line in ipairs(all_diff_output) do
|
||||
local found_hunk = M.parse_possible_hunk_headers(line)
|
||||
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
|
||||
-- For added lines, we only want to iterate over the part of the diff that has has new lines,
|
||||
-- so we skip over the old range. We then keep track of the increment to the original new line index,
|
||||
-- and iterate until we reach the end of the total range of this hunk. If we arrive at the matching
|
||||
-- index for the line number, we check to see if the line was added.
|
||||
local i = 0
|
||||
local old_range = (found_hunk.old_range == 0 and found_hunk.old_line ~= 0) and 1 or found_hunk.old_range
|
||||
for hunk_line_index = matching_line_index + old_range, matching_line_index + old_range + found_hunk.new_range, 1 do
|
||||
local line_content = all_diff_output[hunk_line_index]
|
||||
if (found_hunk.new_line + i) == linnr then
|
||||
if string.match(line_content, "^%+") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Parse git diff hunks.
|
||||
---@param file_path string Path to file.
|
||||
---@param base_branch string Git base branch of merge request.
|
||||
---@return HunksAndDiff
|
||||
local parse_hunks_and_diff = function(file_path, base_branch)
|
||||
local hunks = {}
|
||||
local all_diff_output = {}
|
||||
|
||||
local Job = require("plenary.job")
|
||||
|
||||
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
|
||||
all_diff_output = j:result()
|
||||
for _, line in ipairs(all_diff_output) do
|
||||
local hunk = M.parse_possible_hunk_headers(line)
|
||||
if hunk ~= nil then
|
||||
table.insert(hunks, hunk)
|
||||
end
|
||||
end
|
||||
else
|
||||
M.notify("Failed to get git diff: " .. j:stderr(), vim.log.levels.WARN)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
diff_job:sync()
|
||||
|
||||
return { hunks = hunks, all_diff_output = all_diff_output }
|
||||
end
|
||||
|
||||
-- Parses the lines from a diff and returns the
|
||||
-- index of the next hunk, when provided an initial index
|
||||
---@param lines table
|
||||
---@param i integer
|
||||
---@return integer|nil
|
||||
local next_hunk_index = function(lines, i)
|
||||
for j, line in ipairs(lines) do
|
||||
local hunk = M.parse_possible_hunk_headers(line)
|
||||
if hunk ~= nil and j > i then
|
||||
return j
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Processes the number of changes until the target is reached. This returns
|
||||
--- a negative or positive number indicating the number of lines in the hunk
|
||||
--that have been added or removed prior to the target line
|
||||
---comment
|
||||
---@param line_number number
|
||||
---@param hunk Hunk
|
||||
---@param lines table
|
||||
---@return integer
|
||||
local net_changed_in_hunk_before_line = function(line_number, hunk, lines)
|
||||
local net_lines = 0
|
||||
local current_line_old = hunk.old_line
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
if line:sub(1, 1) == "-" then
|
||||
if current_line_old < line_number then
|
||||
net_lines = net_lines - 1
|
||||
end
|
||||
current_line_old = current_line_old + 1
|
||||
elseif line:sub(1, 1) == "+" then
|
||||
if current_line_old < line_number then
|
||||
net_lines = net_lines + 1
|
||||
end
|
||||
else
|
||||
current_line_old = current_line_old + 1
|
||||
end
|
||||
end
|
||||
|
||||
return net_lines
|
||||
end
|
||||
|
||||
---Counts the total number of changes in a set of lines, can be positive if added lines or negative if removed lines
|
||||
---@param lines table
|
||||
---@return integer
|
||||
local count_changes = function(lines)
|
||||
local total = 0
|
||||
for _, line in ipairs(lines) do
|
||||
if line:match("^%+") then
|
||||
total = total + 1
|
||||
else
|
||||
total = total - 1
|
||||
end
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
---@param new_line number|nil
|
||||
---@param hunks Hunk[]
|
||||
---@param all_diff_output table
|
||||
---@return string|nil
|
||||
local function get_modification_type_from_new_sha(new_line, hunks, all_diff_output)
|
||||
if new_line == nil then
|
||||
return nil
|
||||
end
|
||||
return List.new(hunks):find(function(hunk)
|
||||
local new_line_end = hunk.new_line + hunk.new_range
|
||||
local in_new_range = new_line >= hunk.new_line and new_line <= new_line_end
|
||||
local is_range_zero = hunk.new_range == 0 and hunk.old_range == 0
|
||||
return in_new_range and (is_range_zero or line_was_added(new_line, hunk, all_diff_output))
|
||||
end) and "added" or "bad_file_unmodified"
|
||||
end
|
||||
|
||||
---@param old_line number|nil
|
||||
---@param new_line number|nil
|
||||
---@param hunks Hunk[]
|
||||
---@param all_diff_output table
|
||||
---@return string|nil
|
||||
local function get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output)
|
||||
if old_line == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
return List.new(hunks):find(function(hunk)
|
||||
local old_line_end = hunk.old_line + hunk.old_range
|
||||
local new_line_end = hunk.new_line + hunk.new_range
|
||||
local in_old_range = old_line >= hunk.old_line and old_line <= old_line_end
|
||||
local in_new_range = old_line >= hunk.new_line and new_line <= new_line_end
|
||||
return (in_old_range or in_new_range) and line_was_removed(old_line, hunk, all_diff_output)
|
||||
end) and "deleted" or "unmodified"
|
||||
end
|
||||
|
||||
---Returns whether the comment is on a deleted line, added line, or unmodified line.
|
||||
---This is in order to build the payload for Gitlab correctly by setting the old line and new line.
|
||||
---@param old_line number|nil
|
||||
---@param new_line number|nil
|
||||
---@param current_file string
|
||||
---@param is_current_sha_focused boolean
|
||||
---@return string|nil
|
||||
function M.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
||||
local hunk_and_diff_data = parse_hunks_and_diff(current_file, state.INFO.target_branch)
|
||||
if hunk_and_diff_data.hunks == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local hunks = hunk_and_diff_data.hunks
|
||||
local all_diff_output = hunk_and_diff_data.all_diff_output
|
||||
return is_current_sha_focused and get_modification_type_from_new_sha(new_line, hunks, all_diff_output)
|
||||
or get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output)
|
||||
end
|
||||
|
||||
---Returns the matching line number of a line in the new/old version of the file compared to the current SHA.
|
||||
---@param old_sha string
|
||||
---@param new_sha string
|
||||
---@param file_path string
|
||||
---@param line_number number
|
||||
---@return number|nil
|
||||
M.calculate_matching_line_new = function(old_sha, new_sha, file_path, line_number)
|
||||
local net_change = 0
|
||||
local diff_cmd = string.format("git diff --minimal --unified=0 --no-color %s %s -- %s", old_sha, new_sha, file_path)
|
||||
local handle = io.popen(diff_cmd)
|
||||
if handle == nil then
|
||||
u.notify(string.format("Error running git diff command for %s", file_path), vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
|
||||
local all_lines = List.new({})
|
||||
for line in handle:lines() do
|
||||
table.insert(all_lines, line)
|
||||
end
|
||||
|
||||
for i, line in ipairs(all_lines) do
|
||||
local hunk = M.parse_possible_hunk_headers(line)
|
||||
if hunk ~= nil then
|
||||
if line_number <= hunk.old_line then
|
||||
-- We have reached a hunk which starts after our target, return the changed total lines
|
||||
return line_number + net_change
|
||||
end
|
||||
|
||||
local n = next_hunk_index(all_lines, i) or #all_lines
|
||||
local diff_lines = all_lines:slice(i + 1, n - 1)
|
||||
|
||||
-- If the line is IN the hunk, process the hunk and return the change until that line
|
||||
if line_number >= hunk.old_line and line_number < hunk.old_line + hunk.old_range then
|
||||
net_change = line_number + net_change + net_changed_in_hunk_before_line(line_number, hunk, diff_lines)
|
||||
return net_change
|
||||
end
|
||||
|
||||
-- If it's not it's after this hunk, just add all the changes and keep iterating
|
||||
net_change = net_change + count_changes(diff_lines)
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: Possibly handle lines that are out of range in the new files
|
||||
return line_number
|
||||
end
|
||||
|
||||
return M
|
||||
73
lua/gitlab/indicators/common.lua
Normal file
73
lua/gitlab/indicators/common.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local List = require("gitlab.utils.list")
|
||||
|
||||
local M = {}
|
||||
|
||||
---Filter all discussions which are relevant for currently visible signs and diagnostics.
|
||||
---@return Discussion[]
|
||||
M.filter_placeable_discussions = function(all_discussions)
|
||||
if type(all_discussions) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
local file = reviewer.get_current_file()
|
||||
if not file then
|
||||
return {}
|
||||
end
|
||||
return List.new(all_discussions):filter(function(discussion)
|
||||
local first_note = discussion.notes[1]
|
||||
return type(first_note.position) == "table"
|
||||
--Do not include unlinked notes
|
||||
and (first_note.position.new_path == file or first_note.position.old_path == file)
|
||||
--Skip resolved discussions if user wants to
|
||||
and not (state.settings.discussion_signs.skip_resolved_discussion and first_note.resolvable and first_note.resolved)
|
||||
--Skip discussions from old revisions
|
||||
and not (
|
||||
state.settings.discussion_signs.skip_old_revision_discussion
|
||||
and u.from_iso_format_date_to_timestamp(first_note.created_at)
|
||||
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
M.parse_line_code = function(line_code)
|
||||
local line_code_regex = "%w+_(%d+)_(%d+)"
|
||||
local old_line, new_line = line_code:match(line_code_regex)
|
||||
return tonumber(old_line), tonumber(new_line)
|
||||
end
|
||||
|
||||
---@param discussion Discussion
|
||||
---@return boolean
|
||||
M.is_old_sha = function(discussion)
|
||||
local first_note = discussion.notes[1]
|
||||
return first_note.position.old_line ~= nil
|
||||
end
|
||||
|
||||
---@param discussion Discussion
|
||||
---@return boolean
|
||||
M.is_new_sha = function(discussion)
|
||||
return not M.is_old_sha(discussion)
|
||||
end
|
||||
|
||||
---@param discussion Discussion
|
||||
---@return boolean
|
||||
M.is_single_line = function(discussion)
|
||||
local first_note = discussion.notes[1]
|
||||
local line_range = first_note.position.line_range
|
||||
return line_range == nil
|
||||
end
|
||||
|
||||
---@param discussion Discussion
|
||||
---@return boolean
|
||||
M.is_multi_line = function(discussion)
|
||||
return not M.is_single_line(discussion)
|
||||
end
|
||||
|
||||
---@param discussion Discussion
|
||||
---@return Note
|
||||
M.get_first_note = function(discussion)
|
||||
return discussion.notes[1]
|
||||
end
|
||||
|
||||
return M
|
||||
152
lua/gitlab/indicators/diagnostics.lua
Normal file
152
lua/gitlab/indicators/diagnostics.lua
Normal file
@@ -0,0 +1,152 @@
|
||||
local u = require("gitlab.utils")
|
||||
local diffview_lib = require("diffview.lib")
|
||||
local discussion_tree = require("gitlab.actions.discussions.tree")
|
||||
local common = require("gitlab.indicators.common")
|
||||
local List = require("gitlab.utils.list")
|
||||
local state = require("gitlab.state")
|
||||
local discussion_sign_name = "gitlab_discussion"
|
||||
|
||||
local M = {}
|
||||
local diagnostics_namespace = vim.api.nvim_create_namespace(discussion_sign_name)
|
||||
M.diagnostics_namespace = diagnostics_namespace
|
||||
M.discussion_sign_name = discussion_sign_name
|
||||
M.clear_diagnostics = function()
|
||||
vim.diagnostic.reset(diagnostics_namespace)
|
||||
end
|
||||
|
||||
-- Display options for the diagnostic
|
||||
local display_opts = {
|
||||
virtual_text = state.settings.discussion_signs.virtual_text,
|
||||
severity_sort = true,
|
||||
underline = false,
|
||||
}
|
||||
|
||||
---Takes some range information and data about a discussion
|
||||
---and creates a diagnostic to be placed in the reviewer
|
||||
---@param range_info table
|
||||
---@param discussion Discussion
|
||||
---@return Diagnostic
|
||||
local function create_diagnostic(range_info, discussion)
|
||||
local message = ""
|
||||
for _, note in ipairs(discussion.notes) do
|
||||
message = message .. discussion_tree.build_note_header(note) .. "\n" .. note.body .. "\n"
|
||||
end
|
||||
|
||||
local diagnostic = {
|
||||
message = message,
|
||||
col = 0,
|
||||
severity = state.settings.discussion_signs.severity,
|
||||
user_data = { discussion_id = discussion.id, header = discussion_tree.build_note_header(discussion.notes[1]) },
|
||||
source = "gitlab",
|
||||
code = "gitlab.nvim",
|
||||
}
|
||||
return vim.tbl_deep_extend("force", diagnostic, range_info)
|
||||
end
|
||||
|
||||
---Creates a single line diagnostic
|
||||
---@param discussion Discussion
|
||||
---@return Diagnostic
|
||||
local create_single_line_diagnostic = function(discussion)
|
||||
local first_note = discussion.notes[1]
|
||||
return create_diagnostic({
|
||||
lnum = first_note.position.new_line - 1,
|
||||
}, discussion)
|
||||
end
|
||||
|
||||
---Creates a mutli-line line diagnostic
|
||||
---@param discussion Discussion
|
||||
---@return Diagnostic
|
||||
local create_multiline_diagnostic = function(discussion)
|
||||
local first_note = discussion.notes[1]
|
||||
local line_range = first_note.position.line_range
|
||||
if line_range == nil then
|
||||
error("Parsing multi-line comment but note does not contain line range")
|
||||
end
|
||||
|
||||
local start_old_line, start_new_line = common.parse_line_code(line_range.start.line_code)
|
||||
|
||||
if common.is_new_sha(discussion) then
|
||||
return create_diagnostic({
|
||||
lnum = start_new_line - 1,
|
||||
end_lnum = first_note.position.new_line - 1,
|
||||
}, discussion)
|
||||
else
|
||||
return create_diagnostic({
|
||||
lnum = start_old_line - 1,
|
||||
end_lnum = first_note.position.old_line - 1,
|
||||
}, discussion)
|
||||
end
|
||||
end
|
||||
|
||||
---Set diagnostics in currently new SHA.
|
||||
---@param namespace number namespace for diagnostics
|
||||
---@param diagnostics table see :h vim.diagnostic.set
|
||||
---@param opts table? see :h vim.diagnostic.set
|
||||
local set_diagnostics_in_new_sha = function(namespace, diagnostics, opts)
|
||||
local view = diffview_lib.get_current_view()
|
||||
if not view then
|
||||
return
|
||||
end
|
||||
vim.diagnostic.set(namespace, view.cur_layout.b.file.bufnr, diagnostics, opts)
|
||||
require("gitlab.indicators.signs").set_signs(diagnostics, view.cur_layout.b.file.bufnr)
|
||||
end
|
||||
|
||||
---Set diagnostics in old SHA.
|
||||
---@param namespace number namespace for diagnostics
|
||||
---@param diagnostics table see :h vim.diagnostic.set
|
||||
---@param opts table? see :h vim.diagnostic.set
|
||||
local set_diagnostics_in_old_sha = function(namespace, diagnostics, opts)
|
||||
local view = diffview_lib.get_current_view()
|
||||
if not view then
|
||||
return
|
||||
end
|
||||
vim.diagnostic.set(namespace, view.cur_layout.a.file.bufnr, diagnostics, opts)
|
||||
require("gitlab.indicators.signs").set_signs(diagnostics, view.cur_layout.a.file.bufnr)
|
||||
end
|
||||
|
||||
---Refresh the diagnostics for the currently reviewed file
|
||||
---@param discussions Discussion[]
|
||||
M.refresh_diagnostics = function(discussions)
|
||||
local ok, err = pcall(function()
|
||||
require("gitlab.indicators.signs").clear_signs()
|
||||
M.clear_diagnostics()
|
||||
local filtered_discussions = common.filter_placeable_discussions(discussions)
|
||||
if filtered_discussions == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local new_diagnostics = M.parse_new_diagnostics(filtered_discussions)
|
||||
set_diagnostics_in_new_sha(diagnostics_namespace, new_diagnostics, display_opts)
|
||||
|
||||
local old_diagnostics = M.parse_old_diagnostics(filtered_discussions)
|
||||
set_diagnostics_in_old_sha(diagnostics_namespace, old_diagnostics, display_opts)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
u.notify(string.format("Error setting diagnostics: %s", err), vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
---Iterates over each discussion and returns a list of tables with sign
|
||||
---data, for instance group, priority, line number etc for the new SHA
|
||||
---@param discussions Discussion[]
|
||||
---@return DiagnosticTable[]
|
||||
M.parse_new_diagnostics = function(discussions)
|
||||
local new_diagnostics = List.new(discussions):filter(common.is_new_sha)
|
||||
local single_line = new_diagnostics:filter(common.is_single_line):map(create_single_line_diagnostic)
|
||||
local multi_line = new_diagnostics:filter(common.is_multi_line):map(create_multiline_diagnostic)
|
||||
return u.combine(single_line, multi_line)
|
||||
end
|
||||
|
||||
---Iterates over each discussion and returns a list of tables with sign
|
||||
---data, for instance group, priority, line number etc for the old SHA
|
||||
---@param discussions Discussion[]
|
||||
---@return DiagnosticTable[]
|
||||
M.parse_old_diagnostics = function(discussions)
|
||||
local old_diagnostics = List.new(discussions):filter(common.is_old_sha)
|
||||
local single_line = old_diagnostics:filter(common.is_single_line):map(create_single_line_diagnostic)
|
||||
local multi_line = old_diagnostics:filter(common.is_multi_line):map(create_multiline_diagnostic)
|
||||
return u.combine(single_line, multi_line)
|
||||
end
|
||||
|
||||
return M
|
||||
96
lua/gitlab/indicators/signs.lua
Normal file
96
lua/gitlab/indicators/signs.lua
Normal file
@@ -0,0 +1,96 @@
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local List = require("gitlab.utils.list")
|
||||
local discussion_sign_name = require("gitlab.indicators.diagnostics").discussion_sign_name
|
||||
local namespace = require("gitlab.indicators.diagnostics").diagnostics_namespace
|
||||
|
||||
local M = {}
|
||||
M.clear_signs = function()
|
||||
vim.fn.sign_unplace(discussion_sign_name)
|
||||
end
|
||||
|
||||
local gitlab_comment = "GitlabComment"
|
||||
local gitlab_range = "GitlabRange"
|
||||
|
||||
local severity_map = {
|
||||
"Error",
|
||||
"Warn",
|
||||
"Info",
|
||||
"Hint",
|
||||
}
|
||||
|
||||
---Refresh the discussion signs for currently loaded file in reviewer For convinience we use same
|
||||
---string for sign name and sign group ( currently there is only one sign needed)
|
||||
---@param diagnostics Diagnostic[]
|
||||
---@param bufnr number
|
||||
M.set_signs = function(diagnostics, bufnr)
|
||||
if not state.settings.discussion_sign.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
-- Filter diagnostics from the 'gitlab' source and apply custom signs
|
||||
for _, diagnostic in ipairs(diagnostics) do
|
||||
---@type SignTable[]
|
||||
local existing_signs =
|
||||
vim.fn.sign_getplaced(vim.api.nvim_get_current_buf(), { group = "gitlab_discussion" })[1].signs
|
||||
|
||||
local sign_id = string.format("%s__%d", namespace, diagnostic.lnum)
|
||||
if diagnostic.end_lnum then
|
||||
local linenr = diagnostic.lnum + 1
|
||||
while linenr <= diagnostic.end_lnum do
|
||||
linenr = linenr + 1
|
||||
local conflicting_comment_sign = List.new(existing_signs):find(function(sign)
|
||||
return u.ends_with(sign.name, gitlab_comment) and sign.lnum == linenr
|
||||
end)
|
||||
if conflicting_comment_sign == nil then
|
||||
vim.fn.sign_place(
|
||||
sign_id,
|
||||
discussion_sign_name,
|
||||
"DiagnosticSign" .. M.severity .. gitlab_range,
|
||||
bufnr,
|
||||
{ lnum = linenr, priority = state.settings.discussion_signs.priority }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
vim.fn.sign_place(
|
||||
sign_id,
|
||||
discussion_sign_name,
|
||||
"DiagnosticSign" .. M.severity .. gitlab_comment,
|
||||
bufnr,
|
||||
{ lnum = diagnostic.lnum + 1, priority = state.settings.discussion_signs.priority }
|
||||
)
|
||||
|
||||
-- TODO: Detect whether diagnostic is ranged and set helper signs
|
||||
end
|
||||
end
|
||||
|
||||
---Define signs for discussions
|
||||
M.setup_signs = function()
|
||||
local discussion_sign_settings = state.settings.discussion_signs
|
||||
local comment_icon = discussion_sign_settings.icons.comment
|
||||
local range_icon = discussion_sign_settings.icons.range
|
||||
M.severity = severity_map[state.settings.discussion_signs.severity]
|
||||
local signs = { "Error", "Warn", "Hint", "Info" }
|
||||
for _, type in ipairs(signs) do
|
||||
-- Define comment highlight group
|
||||
local hl = "DiagnosticSign" .. type
|
||||
local comment_hl = hl .. gitlab_comment
|
||||
vim.fn.sign_define(comment_hl, {
|
||||
text = comment_icon,
|
||||
texthl = comment_hl,
|
||||
})
|
||||
vim.cmd(string.format("highlight link %s %s", comment_hl, hl))
|
||||
|
||||
-- Define range highlight group
|
||||
local range_hl = hl .. gitlab_range
|
||||
vim.fn.sign_define(range_hl, {
|
||||
text = range_icon,
|
||||
texthl = range_hl,
|
||||
})
|
||||
vim.cmd(string.format("highlight link %s %s", range_hl, hl))
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,3 +1,4 @@
|
||||
require("gitlab.utils.list")
|
||||
local u = require("gitlab.utils")
|
||||
local async = require("gitlab.async")
|
||||
local server = require("gitlab.server")
|
||||
|
||||
@@ -1,432 +0,0 @@
|
||||
-- This Module contains all of the reviewer code for diffview
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local async_ok, async = pcall(require, "diffview.async")
|
||||
local diffview_lib = require("diffview.lib")
|
||||
|
||||
local M = {
|
||||
bufnr = nil,
|
||||
tabnr = nil,
|
||||
}
|
||||
|
||||
local all_git_manged_files_unmodified = function()
|
||||
-- check local managed files are unmodified, matching the state in the MR
|
||||
-- TODO: ensure correct CWD?
|
||||
return vim.fn.trim(vim.fn.system({ "git", "status", "--short", "--untracked-files=no" })) == ""
|
||||
end
|
||||
|
||||
M.open = function()
|
||||
local diff_refs = state.INFO.diff_refs
|
||||
if diff_refs == nil then
|
||||
u.notify("Gitlab did not provide diff refs required to review this MR", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if diff_refs.base_sha == "" or diff_refs.head_sha == "" then
|
||||
u.notify("Merge request contains no changes", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local diffview_open_command = "DiffviewOpen"
|
||||
local diffview_feature_imply_local = {
|
||||
user_requested = state.settings.reviewer_settings.diffview.imply_local,
|
||||
usable = all_git_manged_files_unmodified(),
|
||||
}
|
||||
if diffview_feature_imply_local.user_requested and diffview_feature_imply_local.usable then
|
||||
diffview_open_command = diffview_open_command .. " --imply-local"
|
||||
end
|
||||
|
||||
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
|
||||
M.tabnr = vim.api.nvim_get_current_tabpage()
|
||||
|
||||
if diffview_feature_imply_local.user_requested and not diffview_feature_imply_local.usable then
|
||||
u.notify(
|
||||
"There are uncommited changes in the working tree, cannot use 'imply_local' setting for gitlab reviews. Stash or commit all changes to use.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
|
||||
if state.INFO.has_conflicts then
|
||||
u.notify("This merge request has conflicts!", vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
-- Register Diffview hook for close event to set tab page # to nil
|
||||
local on_diffview_closed = function(view)
|
||||
if view.tabpage == M.tabnr then
|
||||
M.tabnr = nil
|
||||
end
|
||||
end
|
||||
require("diffview.config").user_emitter:on("view_closed", function(_, ...)
|
||||
on_diffview_closed(...)
|
||||
end)
|
||||
|
||||
if state.settings.discussion_tree.auto_open then
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
discussions.close()
|
||||
discussions.toggle()
|
||||
end
|
||||
end
|
||||
|
||||
M.close = function()
|
||||
vim.cmd("DiffviewClose")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
discussions.close()
|
||||
end
|
||||
|
||||
M.jump = function(file_name, new_line, old_line, opts)
|
||||
if M.tabnr == nil then
|
||||
u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.api.nvim_set_current_tabpage(M.tabnr)
|
||||
vim.cmd("DiffviewFocusFiles")
|
||||
local view = diffview_lib.get_current_view()
|
||||
if view == nil then
|
||||
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local files = view.panel:ordered_file_list()
|
||||
local layout = view.cur_layout
|
||||
for _, file in ipairs(files) do
|
||||
if file.path == file_name then
|
||||
if not async_ok then
|
||||
u.notify("Could not load Diffview async", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
async.await(view:set_file(file))
|
||||
-- TODO: Ranged comments on unchanged lines will have both a
|
||||
-- new line and a old line.
|
||||
--
|
||||
-- The same is true when the user leaves a single-line comment
|
||||
-- on an unchanged line in the "b" buffer.
|
||||
--
|
||||
-- We need to distinguish them somehow from
|
||||
-- range comments (which also have this) so that we can know
|
||||
-- which buffer to jump to. Right now, we jump to the wrong
|
||||
-- buffer for ranged comments on unchanged lines.
|
||||
if new_line ~= nil and not opts.is_undefined_type then
|
||||
layout.b:focus()
|
||||
vim.api.nvim_win_set_cursor(0, { tonumber(new_line), 0 })
|
||||
elseif old_line ~= nil then
|
||||
layout.a:focus()
|
||||
vim.api.nvim_win_set_cursor(0, { tonumber(old_line), 0 })
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
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
|
||||
u.notify("Diffview reviewer must be initialized first", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- If there's a range, use the start of the visual selection, not the current line
|
||||
local current_line = range and range.start_line or 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
|
||||
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if we are in the diffview buffer
|
||||
local view = diffview_lib.get_current_view()
|
||||
if view == nil then
|
||||
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local layout = view.cur_layout
|
||||
|
||||
---@type ReviewerInfo
|
||||
local reviewer_info = {
|
||||
file_name = layout.a.file.path,
|
||||
new_line = nil,
|
||||
old_line = nil,
|
||||
range_info = nil,
|
||||
}
|
||||
|
||||
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
||||
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
||||
local current_win = vim.fn.win_getid()
|
||||
local is_current_sha = current_win == b_win
|
||||
|
||||
if a_win == nil or b_win == nil then
|
||||
u.notify("Error retrieving window IDs for current files", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local current_file = M.get_current_file()
|
||||
if current_file == nil then
|
||||
u.notify("Error retrieving current file from Diffview", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local a_linenr = vim.api.nvim_win_get_cursor(a_win)[1]
|
||||
local b_linenr = vim.api.nvim_win_get_cursor(b_win)[1]
|
||||
|
||||
local data = u.parse_hunk_headers(current_file, state.INFO.target_branch)
|
||||
|
||||
if data.hunks == nil then
|
||||
u.notify("Could not parse hunks", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Will be different depending on focused window.
|
||||
local modification_type =
|
||||
M.get_modification_type(a_linenr, b_linenr, is_current_sha, data.hunks, data.all_diff_output)
|
||||
|
||||
if modification_type == "bad_file_unmodified" then
|
||||
u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
-- Comment on new line: Include only new_line in payload.
|
||||
if modification_type == "added" then
|
||||
reviewer_info.old_line = nil
|
||||
reviewer_info.new_line = b_linenr
|
||||
-- Comment on deleted line: Include only new_line in payload.
|
||||
elseif modification_type == "deleted" then
|
||||
reviewer_info.old_line = a_linenr
|
||||
reviewer_info.new_line = nil
|
||||
-- The line was not found in any hunks, only send the old line number
|
||||
elseif modification_type == "unmodified" or modification_type == "bad_file_unmodified" then
|
||||
reviewer_info.old_line = a_linenr
|
||||
reviewer_info.new_line = b_linenr
|
||||
end
|
||||
|
||||
if range == nil then
|
||||
return reviewer_info
|
||||
end
|
||||
|
||||
-- If leaving a multi-line comment, we want to also add range_info to the payload.
|
||||
local is_new = reviewer_info.new_line ~= nil
|
||||
local current_line_info = is_new and u.get_lines_from_hunks(data.hunks, reviewer_info.new_line, is_new)
|
||||
or u.get_lines_from_hunks(data.hunks, reviewer_info.old_line, is_new)
|
||||
local type = is_new and "new" or "old"
|
||||
|
||||
---@type ReviewerRangeInfo
|
||||
local range_info = { start = {}, ["end"] = {} }
|
||||
|
||||
if current_line == range.start_line then
|
||||
range_info.start.old_line = current_line_info.old_line
|
||||
range_info.start.new_line = current_line_info.new_line
|
||||
range_info.start.type = type
|
||||
else
|
||||
local start_line_info = u.get_lines_from_hunks(data.hunks, range.start_line, is_new)
|
||||
range_info.start.old_line = start_line_info.old_line
|
||||
range_info.start.new_line = start_line_info.new_line
|
||||
range_info.start.type = type
|
||||
end
|
||||
if current_line == range.end_line then
|
||||
range_info["end"].old_line = current_line_info.old_line
|
||||
range_info["end"].new_line = current_line_info.new_line
|
||||
range_info["end"].type = type
|
||||
else
|
||||
local end_line_info = u.get_lines_from_hunks(data.hunks, range.end_line, is_new)
|
||||
range_info["end"].old_line = end_line_info.old_line
|
||||
range_info["end"].new_line = end_line_info.new_line
|
||||
range_info["end"].type = type
|
||||
end
|
||||
|
||||
reviewer_info.range_info = range_info
|
||||
return reviewer_info
|
||||
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
|
||||
|
||||
---Checks whether the lines in the two buffers are the same
|
||||
---@return boolean
|
||||
M.lines_are_same = function(layout, a_cursor, b_cursor)
|
||||
local line_a = u.get_line_content(layout.a.file.bufnr, a_cursor)
|
||||
local line_b = u.get_line_content(layout.b.file.bufnr, b_cursor)
|
||||
return line_a == line_b
|
||||
end
|
||||
|
||||
---Get currently shown file
|
||||
M.get_current_file = function()
|
||||
local view = diffview_lib.get_current_view()
|
||||
if not view then
|
||||
return
|
||||
end
|
||||
return view.panel.cur_file.path
|
||||
end
|
||||
|
||||
---Place a sign in currently reviewed file. Use new line for identifing lines after changes, old
|
||||
---line for identifing lines before changes and both if line was not changed.
|
||||
---@param signs SignTable[] table of signs. See :h sign_placelist
|
||||
---@param type string "new" if diagnostic should be in file after changes else "old"
|
||||
M.place_sign = function(signs, type)
|
||||
local view = diffview_lib.get_current_view()
|
||||
if not view then
|
||||
return
|
||||
end
|
||||
if type == "new" then
|
||||
for _, sign in ipairs(signs) do
|
||||
sign.buffer = view.cur_layout.b.file.bufnr
|
||||
end
|
||||
elseif type == "old" then
|
||||
for _, sign in ipairs(signs) do
|
||||
sign.buffer = view.cur_layout.a.file.bufnr
|
||||
end
|
||||
end
|
||||
vim.fn.sign_placelist(signs)
|
||||
end
|
||||
|
||||
---Set diagnostics in currently reviewed file.
|
||||
---@param namespace integer namespace for diagnostics
|
||||
---@param diagnostics table see :h vim.diagnostic.set
|
||||
---@param type string "new" if diagnostic should be in file after changes else "old"
|
||||
---@param opts table? see :h vim.diagnostic.set
|
||||
M.set_diagnostics = function(namespace, diagnostics, type, opts)
|
||||
local view = diffview_lib.get_current_view()
|
||||
if not view then
|
||||
return
|
||||
end
|
||||
if type == "new" and view.cur_layout.b.file.bufnr then
|
||||
vim.diagnostic.set(namespace, view.cur_layout.b.file.bufnr, diagnostics, opts)
|
||||
elseif type == "old" and view.cur_layout.a.file.bufnr then
|
||||
vim.diagnostic.set(namespace, view.cur_layout.a.file.bufnr, diagnostics, opts)
|
||||
end
|
||||
end
|
||||
|
||||
---Diffview exposes events which can be used to setup autocommands.
|
||||
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
|
||||
M.set_callback_for_file_changed = function(callback)
|
||||
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.file_changed", {})
|
||||
vim.api.nvim_create_autocmd("User", {
|
||||
pattern = { "DiffviewDiffBufWinEnter", "DiffviewViewEnter" },
|
||||
group = group,
|
||||
callback = function(...)
|
||||
if M.tabnr == vim.api.nvim_get_current_tabpage() then
|
||||
callback(...)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Diffview exposes events which can be used to setup autocommands.
|
||||
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
|
||||
M.set_callback_for_reviewer_leave = function(callback)
|
||||
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.leave", {})
|
||||
vim.api.nvim_create_autocmd("User", {
|
||||
pattern = { "DiffviewViewLeave", "DiffviewViewClosed" },
|
||||
group = group,
|
||||
callback = function(...)
|
||||
if M.tabnr == vim.api.nvim_get_current_tabpage() then
|
||||
callback(...)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Returns whether the comment is on a deleted line, added line, or unmodified line.
|
||||
---This is in order to build the payload for Gitlab correctly by setting the old line and new line.
|
||||
---@param a_linenr number
|
||||
---@param b_linenr number
|
||||
---@param is_current_sha boolean
|
||||
---@param hunks Hunk[] A list of hunks
|
||||
---@param all_diff_output table The raw diff output
|
||||
function M.get_modification_type(a_linenr, b_linenr, is_current_sha, hunks, all_diff_output)
|
||||
for _, hunk in ipairs(hunks) do
|
||||
local old_line_end = hunk.old_line + hunk.old_range
|
||||
local new_line_end = hunk.new_line + hunk.new_range
|
||||
|
||||
if is_current_sha then
|
||||
-- If it is a single line change and neither hunk has a range, then it's added
|
||||
if b_linenr >= hunk.new_line and b_linenr <= new_line_end then
|
||||
if hunk.new_range == 0 and hunk.old_range == 0 then
|
||||
return "added"
|
||||
end
|
||||
-- If leaving a comment on the new window, we may be commenting on an added line
|
||||
-- or on an unmodified line. To tell, we have to check whether the line itself is
|
||||
-- prefixed with "+" and only return "added" if it is.
|
||||
if M.line_was_added(b_linenr, hunk, all_diff_output) then
|
||||
return "added"
|
||||
end
|
||||
end
|
||||
else
|
||||
-- It's a deletion if it's in the range of the hunks and the new
|
||||
-- range is zero, since that is only a deletion hunk, or if we find
|
||||
-- a match in another hunk with a range, and the corresponding line is prefixed
|
||||
-- with a "-" only. If it is, then it's a deletion.
|
||||
if a_linenr >= hunk.old_line and a_linenr <= old_line_end and hunk.old_range == 0 then
|
||||
return "deleted"
|
||||
end
|
||||
if
|
||||
(a_linenr >= hunk.old_line and a_linenr <= old_line_end)
|
||||
or (a_linenr >= hunk.new_line and b_linenr <= new_line_end)
|
||||
then
|
||||
if M.line_was_removed(a_linenr, hunk, all_diff_output) then
|
||||
return "deleted"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If we can't find the line, this means the user is either trying to leave
|
||||
-- a comment on an unchanged line in the new or old file SHA. This is only
|
||||
-- allowed in the old file
|
||||
return is_current_sha and "bad_file_unmodified" or "unmodified"
|
||||
end
|
||||
|
||||
---@param linnr number
|
||||
---@param hunk Hunk
|
||||
---@param all_diff_output table
|
||||
M.line_was_removed = function(linnr, hunk, all_diff_output)
|
||||
for matching_line_index, line in ipairs(all_diff_output) do
|
||||
local found_hunk = u.parse_possible_hunk_headers(line)
|
||||
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
|
||||
-- We found a matching hunk, now we need to iterate over the lines from the raw diff output
|
||||
-- at that hunk until we reach the line we are looking for. When the indexes match we check
|
||||
-- to see if that line is deleted or not.
|
||||
for hunk_line_index = found_hunk.old_line, hunk.old_line + hunk.old_range - 1, 1 do
|
||||
local line_content = all_diff_output[matching_line_index + 1]
|
||||
if hunk_line_index == linnr then
|
||||
if string.match(line_content, "^%-") then
|
||||
return "deleted"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param linnr number
|
||||
---@param hunk Hunk
|
||||
---@param all_diff_output table
|
||||
M.line_was_added = function(linnr, hunk, all_diff_output)
|
||||
for matching_line_index, line in ipairs(all_diff_output) do
|
||||
local found_hunk = u.parse_possible_hunk_headers(line)
|
||||
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
|
||||
-- For added lines, we only want to iterate over the part of the diff that has has new lines,
|
||||
-- so we skip over the old range. We then keep track of the increment to the original new line index,
|
||||
-- and iterate until we reach the end of the total range of this hunk. If we arrive at the matching
|
||||
-- index for the line number, we check to see if the line was added.
|
||||
local i = 0
|
||||
local old_range = (found_hunk.old_range == 0 and found_hunk.old_line ~= 0) and 1 or found_hunk.old_range
|
||||
for hunk_line_index = matching_line_index + old_range + 1, matching_line_index + old_range + found_hunk.new_range, 1 do
|
||||
local line_content = all_diff_output[hunk_line_index]
|
||||
if (found_hunk.new_line + i) == linnr then
|
||||
if string.match(line_content, "^%+") then
|
||||
return "added"
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return M
|
||||
@@ -1,74 +1,265 @@
|
||||
-- This Module contains all of the reviewer code. This is the code
|
||||
-- that parses or interacts with diffview directly, such as opening
|
||||
-- and closing, getting metadata about the current view, and registering
|
||||
-- callbacks for open/close actions.
|
||||
|
||||
local List = require("gitlab.utils.list")
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local diffview = require("gitlab.reviewer.diffview")
|
||||
local git = require("gitlab.git")
|
||||
local hunks = require("gitlab.hunks")
|
||||
local async = require("diffview.async")
|
||||
local diffview_lib = require("diffview.lib")
|
||||
|
||||
local M = {
|
||||
reviewer = nil,
|
||||
}
|
||||
|
||||
local reviewer_map = {
|
||||
diffview = diffview,
|
||||
bufnr = nil,
|
||||
tabnr = nil,
|
||||
stored_win = nil,
|
||||
}
|
||||
|
||||
-- Checks for legacy installations, only Diffview is supported.
|
||||
M.init = function()
|
||||
local reviewer = reviewer_map[state.settings.reviewer]
|
||||
if reviewer == nil then
|
||||
if state.settings.reviewer ~= "diffview" then
|
||||
vim.notify(
|
||||
string.format("gitlab.nvim could not find reviewer %s, only diffview is supported", state.settings.reviewer),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- Opens the reviewer window.
|
||||
M.open = function()
|
||||
local diff_refs = state.INFO.diff_refs
|
||||
if diff_refs == nil then
|
||||
u.notify("Gitlab did not provide diff refs required to review this MR", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
M.open = reviewer.open
|
||||
-- Opens the reviewer window
|
||||
if diff_refs.base_sha == "" or diff_refs.head_sha == "" then
|
||||
u.notify("Merge request contains no changes", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
M.close = reviewer.close
|
||||
-- Closes the reviewer and cleans up
|
||||
local diffview_open_command = "DiffviewOpen"
|
||||
local has_clean_tree = git.has_clean_tree()
|
||||
if state.settings.reviewer_settings.diffview.imply_local and has_clean_tree then
|
||||
diffview_open_command = diffview_open_command .. " --imply-local"
|
||||
end
|
||||
|
||||
M.jump = reviewer.jump
|
||||
-- Jumps to the location provided in the reviewer window
|
||||
-- Parameters:
|
||||
-- • {file_name} The name of the file to jump to
|
||||
-- • {new_line} The new_line of the change
|
||||
-- • {interval} The old_line of the change
|
||||
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
|
||||
M.tabnr = vim.api.nvim_get_current_tabpage()
|
||||
|
||||
M.get_location = reviewer.get_location
|
||||
-- Parameters:
|
||||
-- • {range} LineRange if function was triggered from visual selection
|
||||
-- Returns the current location (based on cursor) from the reviewer window as ReviewerInfo class
|
||||
if state.settings.reviewer_settings.diffview.imply_local and not has_clean_tree then
|
||||
u.notify(
|
||||
"There are uncommited changes in the working tree, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
|
||||
M.get_lines = reviewer.get_lines
|
||||
-- Returns the content of the file in the current location in the reviewer window
|
||||
if state.INFO.has_conflicts then
|
||||
u.notify("This merge request has conflicts!", vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
M.get_current_file = reviewer.get_current_file
|
||||
-- Get currently loaded file
|
||||
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
|
||||
u.notify(
|
||||
"Diagnostics are now configured settings.discussion_signs, see :h gitlab.signs_and_diagnostics",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
|
||||
M.place_sign = reviewer.place_sign
|
||||
-- Places a sign on the line for currently reviewed file.
|
||||
-- Parameters:
|
||||
-- • {id} The sign id
|
||||
-- • {sign} The sign to place
|
||||
-- • {group} The sign group to place on
|
||||
-- • {new_line} The line to place the sign on
|
||||
-- • {old_line} The buffer number to place the sign on
|
||||
-- Register Diffview hook for close event to set tab page # to nil
|
||||
local on_diffview_closed = function(view)
|
||||
if view.tabpage == M.tabnr then
|
||||
M.tabnr = nil
|
||||
end
|
||||
end
|
||||
require("diffview.config").user_emitter:on("view_closed", function(_, ...)
|
||||
on_diffview_closed(...)
|
||||
end)
|
||||
|
||||
M.set_callback_for_file_changed = reviewer.set_callback_for_file_changed
|
||||
-- Call callback whenever the file changes
|
||||
-- Parameters:
|
||||
-- • {callback} The callback to call
|
||||
if state.settings.discussion_tree.auto_open then
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
discussions.close()
|
||||
discussions.toggle()
|
||||
end
|
||||
end
|
||||
|
||||
M.set_callback_for_reviewer_leave = reviewer.set_callback_for_reviewer_leave
|
||||
-- Call callback whenever the reviewer is left
|
||||
-- Parameters:
|
||||
-- • {callback} The callback to call
|
||||
-- Closes the reviewer and cleans up
|
||||
M.close = function()
|
||||
vim.cmd("DiffviewClose")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
discussions.close()
|
||||
end
|
||||
|
||||
M.set_diagnostics = reviewer.set_diagnostics
|
||||
-- Set diagnostics for currently reviewed file
|
||||
-- Parameters:
|
||||
-- • {namespace} The namespace for diagnostics
|
||||
-- • {diagnostics} The diagnostics to set
|
||||
-- • {type} "new" if diagnostic should be in file after changes else "old"
|
||||
-- • {opts} see opts in :h vim.diagnostic.set
|
||||
-- Jumps to the location provided in the reviewer window
|
||||
---@param file_name string
|
||||
---@param new_line number|nil
|
||||
---@param old_line number|nil
|
||||
M.jump = function(file_name, new_line, old_line)
|
||||
if M.tabnr == nil then
|
||||
u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.api.nvim_set_current_tabpage(M.tabnr)
|
||||
vim.cmd("DiffviewFocusFiles")
|
||||
local view = diffview_lib.get_current_view()
|
||||
if view == nil then
|
||||
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local files = view.panel:ordered_file_list()
|
||||
local file = List.new(files):find(function(file)
|
||||
return file.path == file_name
|
||||
end)
|
||||
async.await(view:set_file(file))
|
||||
|
||||
local layout = view.cur_layout
|
||||
if old_line == nil then
|
||||
layout.b:focus()
|
||||
vim.api.nvim_win_set_cursor(0, { new_line, 0 })
|
||||
else
|
||||
layout.a:focus()
|
||||
vim.api.nvim_win_set_cursor(0, { old_line, 0 })
|
||||
end
|
||||
end
|
||||
|
||||
---Get the data from diffview, such as line information and file name. May be used by
|
||||
---other modules such as the comment module to create line codes or set diagnostics
|
||||
---@return DiffviewInfo | nil
|
||||
M.get_reviewer_data = function()
|
||||
if M.tabnr == nil then
|
||||
u.notify("Diffview reviewer must be initialized first", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if we are in the diffview tab
|
||||
local tabnr = vim.api.nvim_get_current_tabpage()
|
||||
if tabnr ~= M.tabnr then
|
||||
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if we are in the diffview buffer
|
||||
local view = diffview_lib.get_current_view()
|
||||
if view == nil then
|
||||
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local layout = view.cur_layout
|
||||
local old_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
||||
local new_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
||||
|
||||
if old_win == nil or new_win == nil then
|
||||
u.notify("Error getting window IDs for current files", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local current_file = M.get_current_file()
|
||||
if current_file == nil then
|
||||
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local new_line = vim.api.nvim_win_get_cursor(new_win)[1]
|
||||
local old_line = vim.api.nvim_win_get_cursor(old_win)[1]
|
||||
|
||||
local is_current_sha_focused = M.is_current_sha_focused()
|
||||
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
||||
if modification_type == nil then
|
||||
u.notify("Error getting modification type", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if modification_type == "bad_file_unmodified" then
|
||||
u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
local current_bufnr = is_current_sha_focused and layout.b.file.bufnr or layout.a.file.bufnr
|
||||
local opposite_bufnr = is_current_sha_focused and layout.a.file.bufnr or layout.b.file.bufnr
|
||||
local old_sha_win_id = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
||||
local new_sha_win_id = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
||||
|
||||
return {
|
||||
file_name = layout.a.file.path,
|
||||
old_line_from_buf = old_line,
|
||||
new_line_from_buf = new_line,
|
||||
modification_type = modification_type,
|
||||
new_sha_win_id = new_sha_win_id,
|
||||
current_bufnr = current_bufnr,
|
||||
old_sha_win_id = old_sha_win_id,
|
||||
opposite_bufnr = opposite_bufnr,
|
||||
}
|
||||
end
|
||||
|
||||
---Return whether user is focused on the new version of the file
|
||||
---@return boolean
|
||||
M.is_current_sha_focused = function()
|
||||
local view = diffview_lib.get_current_view()
|
||||
local layout = view.cur_layout
|
||||
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
||||
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
||||
local current_win = vim.fn.win_getid()
|
||||
|
||||
-- Handle cases where user navigates tabs in the middle of making a comment
|
||||
if a_win ~= current_win and b_win ~= current_win then
|
||||
current_win = M.stored_win
|
||||
M.stored_win = nil
|
||||
end
|
||||
return current_win == b_win
|
||||
end
|
||||
|
||||
---Get currently shown file
|
||||
---@return string|nil
|
||||
M.get_current_file = function()
|
||||
local view = diffview_lib.get_current_view()
|
||||
if not view then
|
||||
return
|
||||
end
|
||||
return view.panel.cur_file.path
|
||||
end
|
||||
|
||||
---Diffview exposes events which can be used to setup autocommands.
|
||||
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
|
||||
M.set_callback_for_file_changed = function(callback)
|
||||
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.file_changed", {})
|
||||
vim.api.nvim_create_autocmd("User", {
|
||||
pattern = { "DiffviewDiffBufWinEnter" },
|
||||
group = group,
|
||||
callback = function(...)
|
||||
M.stored_win = vim.api.nvim_get_current_win()
|
||||
if M.tabnr == vim.api.nvim_get_current_tabpage() then
|
||||
callback(...)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Diffview exposes events which can be used to setup autocommands.
|
||||
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
|
||||
M.set_callback_for_reviewer_leave = function(callback)
|
||||
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.leave", {})
|
||||
vim.api.nvim_create_autocmd("User", {
|
||||
pattern = { "DiffviewViewLeave", "DiffviewViewClosed" },
|
||||
group = group,
|
||||
callback = function(...)
|
||||
if M.tabnr == vim.api.nvim_get_current_tabpage() then
|
||||
callback(...)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
M.set_callback_for_reviewer_enter = function(callback)
|
||||
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.enter", {})
|
||||
vim.api.nvim_create_autocmd("User", {
|
||||
pattern = { "DiffviewViewOpened" },
|
||||
group = group,
|
||||
callback = function(...)
|
||||
callback(...)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
223
lua/gitlab/reviewer/location.lua
Executable file
223
lua/gitlab/reviewer/location.lua
Executable file
@@ -0,0 +1,223 @@
|
||||
local u = require("gitlab.utils")
|
||||
local hunks = require("gitlab.hunks")
|
||||
local state = require("gitlab.state")
|
||||
|
||||
---@class Location
|
||||
---@field location_data LocationData
|
||||
---@field reviewer_data DiffviewInfo
|
||||
---@field run function
|
||||
---@field build_location_data function
|
||||
|
||||
---@class ReviewerLineInfo
|
||||
---@field old_line integer|nil
|
||||
---@field new_line integer|nil
|
||||
---@field type "new"|"old"
|
||||
|
||||
---@class ReviewerRangeInfo
|
||||
---@field start ReviewerLineInfo
|
||||
---@field end ReviewerLineInfo
|
||||
|
||||
local Location = {}
|
||||
Location.__index = Location
|
||||
---@param reviewer_data DiffviewInfo
|
||||
---@param visual_range LineRange | nil
|
||||
---@return Location
|
||||
function Location.new(reviewer_data, visual_range)
|
||||
local location = {}
|
||||
local instance = setmetatable(location, Location)
|
||||
instance.reviewer_data = reviewer_data
|
||||
instance.visual_range = visual_range
|
||||
instance.base_sha = state.INFO.diff_refs.base_sha
|
||||
instance.head_sha = state.INFO.diff_refs.head_sha
|
||||
return instance
|
||||
end
|
||||
|
||||
---Takes in information about the current changes, such as the file name, modification type of the diff, and the line numbers
|
||||
---and builds the appropriate payload when creating a comment.
|
||||
function Location:build_location_data()
|
||||
---@type DiffviewInfo
|
||||
local reviewer_data = self.reviewer_data
|
||||
---@type LineRange | nil
|
||||
local visual_range = self.visual_range
|
||||
|
||||
---@type LocationData
|
||||
local location_data = {
|
||||
old_line = nil,
|
||||
new_line = nil,
|
||||
line_range = nil,
|
||||
}
|
||||
|
||||
-- Comment on new line: Include only new_line in payload.
|
||||
-- Comment on deleted line: Include only old_line in payload.
|
||||
-- The line was not found in any hunks, send both lines.
|
||||
if reviewer_data.modification_type == "added" then
|
||||
location_data.old_line = nil
|
||||
location_data.new_line = reviewer_data.new_line_from_buf
|
||||
elseif reviewer_data.modification_type == "deleted" then
|
||||
location_data.old_line = reviewer_data.old_line_from_buf
|
||||
location_data.new_line = nil
|
||||
elseif
|
||||
reviewer_data.modification_type == "unmodified" or reviewer_data.modification_type == "bad_file_unmodified"
|
||||
then
|
||||
location_data.old_line = reviewer_data.old_line_from_buf
|
||||
location_data.new_line = reviewer_data.new_line_from_buf
|
||||
end
|
||||
|
||||
self.location_data = location_data
|
||||
if visual_range == nil then
|
||||
return
|
||||
else
|
||||
self.location_data.line_range = {
|
||||
start = {},
|
||||
["end"] = {},
|
||||
}
|
||||
end
|
||||
|
||||
self:set_start_range(visual_range)
|
||||
self:set_end_range(visual_range)
|
||||
|
||||
-- Ranged comments should always use the end of the range.
|
||||
-- Otherwise they will not highlight the full comment in Gitlab.
|
||||
self.location_data.old_line = self.location_data.line_range["end"].old_line
|
||||
self.location_data.new_line = self.location_data.line_range["end"].new_line
|
||||
end
|
||||
|
||||
-- Helper methods 🤝
|
||||
|
||||
-- Returns the matching line from the new SHA.
|
||||
-- For instance, line 12 in the new SHA may be scroll-linked
|
||||
-- to line 10 in the old SHA.
|
||||
---@param line number
|
||||
---@return number|nil
|
||||
function Location:get_line_number_from_new_sha(line)
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local is_current_sha_focused = reviewer.is_current_sha_focused()
|
||||
if is_current_sha_focused then
|
||||
return line
|
||||
end
|
||||
-- Otherwise we want to get the matching line in the opposite buffer
|
||||
return hunks.calculate_matching_line_new(self.base_sha, self.head_sha, self.reviewer_data.file_name, line)
|
||||
end
|
||||
|
||||
-- Returns the matching line from the old SHA.
|
||||
-- For instance, line 12 in the new SHA may be scroll-linked
|
||||
-- to line 10 in the old SHA.
|
||||
---@param line number
|
||||
---@return number|nil
|
||||
function Location:get_line_number_from_old_sha(line)
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local is_current_sha_focused = reviewer.is_current_sha_focused()
|
||||
if not is_current_sha_focused then
|
||||
return line
|
||||
end
|
||||
|
||||
-- Otherwise we want to get the matching line in the opposite buffer
|
||||
return hunks.calculate_matching_line_new(self.head_sha, self.base_sha, self.reviewer_data.file_name, line)
|
||||
end
|
||||
|
||||
-- Returns the current line number from whatever SHA (new or old)
|
||||
-- the reviewer is focused in.
|
||||
---@return number|nil
|
||||
function Location:get_current_line()
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local win_id = reviewer.is_current_sha_focused() and self.reviewer_data.new_sha_win_id
|
||||
or self.reviewer_data.old_sha_win_id
|
||||
if win_id == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local current_line = vim.api.nvim_win_get_cursor(win_id)[1]
|
||||
return current_line
|
||||
end
|
||||
|
||||
-- Given a new_line and old_line from the start of a ranged comment, returns the start
|
||||
-- range information for the Gitlab payload
|
||||
---@param visual_range LineRange
|
||||
---@return ReviewerLineInfo|nil
|
||||
function Location:set_start_range(visual_range)
|
||||
local current_file = require("gitlab.reviewer").get_current_file()
|
||||
if current_file == nil then
|
||||
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local is_current_sha_focused = reviewer.is_current_sha_focused()
|
||||
local win_id = is_current_sha_focused and self.reviewer_data.new_sha_win_id or self.reviewer_data.old_sha_win_id
|
||||
if win_id == nil then
|
||||
u.notify("Error getting window number of SHA for start range", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local current_line = self:get_current_line()
|
||||
if current_line == nil then
|
||||
u.notify("Error getting current line for start range", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local new_line = self:get_line_number_from_new_sha(visual_range.start_line)
|
||||
local old_line = self:get_line_number_from_old_sha(visual_range.start_line)
|
||||
if
|
||||
(new_line == nil and self.reviewer_data.modification_type ~= "deleted")
|
||||
or (old_line == nil and self.reviewer_data.modification_type ~= "added")
|
||||
then
|
||||
u.notify("Error getting new or old line for start range", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
||||
if modification_type == nil then
|
||||
u.notify("Error getting modification type for start of range", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
self.location_data.line_range.start = {
|
||||
new_line = modification_type ~= "deleted" and new_line or nil,
|
||||
old_line = modification_type ~= "added" and old_line or nil,
|
||||
type = modification_type == "added" and "new" or "old",
|
||||
}
|
||||
end
|
||||
|
||||
-- Given a modification type, a range, and the hunk data, returns the end range information
|
||||
-- for the Gitlab payload
|
||||
---@param visual_range LineRange
|
||||
function Location:set_end_range(visual_range)
|
||||
local current_file = require("gitlab.reviewer").get_current_file()
|
||||
if current_file == nil then
|
||||
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local current_line = self:get_current_line()
|
||||
if current_line == nil then
|
||||
u.notify("Error getting current line for end range", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local new_line = self:get_line_number_from_new_sha(visual_range.end_line)
|
||||
local old_line = self:get_line_number_from_old_sha(visual_range.end_line)
|
||||
|
||||
if
|
||||
(new_line == nil and self.reviewer_data.modification_type ~= "deleted")
|
||||
or (old_line == nil and self.reviewer_data.modification_type ~= "added")
|
||||
then
|
||||
u.notify("Error getting new or old line for end range", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local is_current_sha_focused = reviewer.is_current_sha_focused()
|
||||
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
||||
if modification_type == nil then
|
||||
u.notify("Error getting modification type for end of range", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
self.location_data.line_range["end"] = {
|
||||
new_line = modification_type ~= "deleted" and new_line or nil,
|
||||
old_line = modification_type ~= "added" and old_line or nil,
|
||||
type = modification_type == "added" and "new" or "old",
|
||||
}
|
||||
end
|
||||
|
||||
return Location
|
||||
@@ -1,6 +1,7 @@
|
||||
-- This module contains the logic responsible for building and starting
|
||||
-- the Golang server. The Go server is responsible for making API calls
|
||||
-- to Gitlab and returning the data
|
||||
local List = require("gitlab.utils.list")
|
||||
local state = require("gitlab.state")
|
||||
local u = require("gitlab.utils")
|
||||
local job = require("gitlab.job")
|
||||
@@ -49,12 +50,12 @@ M.start = function(callback)
|
||||
end
|
||||
end,
|
||||
on_stderr = function(_, errors)
|
||||
local err_msg = ""
|
||||
for _, err in ipairs(errors) do
|
||||
local err_msg = List.new(errors):reduce(function(agg, err)
|
||||
if err ~= "" and err ~= nil then
|
||||
err_msg = err_msg .. err .. "\n"
|
||||
agg = agg .. err .. "\n"
|
||||
end
|
||||
end
|
||||
return agg
|
||||
end, "")
|
||||
|
||||
if err_msg ~= "" then
|
||||
u.notify(err_msg, vim.log.levels.ERROR)
|
||||
|
||||
@@ -64,6 +64,7 @@ M.settings = {
|
||||
resolved = "✓",
|
||||
unresolved = "-",
|
||||
tree_type = "simple",
|
||||
toggle_tree_type = "i",
|
||||
---@param t WinbarTable
|
||||
winbar = function(t)
|
||||
local discussions_content = t.resolvable_discussions ~= 0
|
||||
@@ -113,35 +114,17 @@ M.settings = {
|
||||
"labels",
|
||||
},
|
||||
},
|
||||
discussion_sign_and_diagnostic = {
|
||||
discussion_signs = {
|
||||
enabled = true,
|
||||
skip_resolved_discussion = false,
|
||||
skip_old_revision_discussion = false,
|
||||
},
|
||||
discussion_sign = {
|
||||
-- See :h sign_define for details about sign configuration.
|
||||
enabled = true,
|
||||
text = "💬",
|
||||
linehl = nil,
|
||||
texthl = nil,
|
||||
culhl = nil,
|
||||
numhl = nil,
|
||||
priority = 20,
|
||||
helper_signs = {
|
||||
-- For multiline comments the helper signs are used to indicate the whole context
|
||||
-- Priority of helper signs is lower than the main sign (-1).
|
||||
enabled = true,
|
||||
start = "↑",
|
||||
mid = "|",
|
||||
["end"] = "↓",
|
||||
},
|
||||
},
|
||||
discussion_diagnostic = {
|
||||
-- If you want to customize diagnostics for discussions you can make special config
|
||||
-- for namespace `gitlab_discussion`. See :h vim.diagnostic.config
|
||||
enabled = true,
|
||||
severity = vim.diagnostic.severity.INFO,
|
||||
code = nil, -- see :h diagnostic-structure
|
||||
display_opts = {}, -- this is dirrectly used as opts in vim.diagnostic.set, see :h vim.diagnostic.config.
|
||||
virtual_text = false,
|
||||
icons = {
|
||||
comment = "→|",
|
||||
range = " |",
|
||||
},
|
||||
skip_old_revision_discussion = false,
|
||||
priority = 100,
|
||||
},
|
||||
pipeline = {
|
||||
created = "",
|
||||
@@ -253,7 +236,7 @@ M.setPluginConfiguration = function()
|
||||
end
|
||||
|
||||
M.settings.auth_token = file_properties.auth_token or os.getenv("GITLAB_TOKEN")
|
||||
M.settings.gitlab_url = file_properties.gitlab_url or os.getenv("GITLAB_URL") or "https://gitlab.com"
|
||||
M.settings.gitlab_url = u.trim_slash(file_properties.gitlab_url or os.getenv("GITLAB_URL") or "https://gitlab.com")
|
||||
|
||||
if M.settings.auth_token == nil then
|
||||
vim.notify(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
local List = require("gitlab.utils.list")
|
||||
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
|
||||
local M = {}
|
||||
|
||||
@@ -28,6 +29,14 @@ M.get_last_word = function(sentence, divider)
|
||||
return words[#words] or ""
|
||||
end
|
||||
|
||||
---Returns whether a string ends with a substring
|
||||
---@param str string
|
||||
---@param ending string
|
||||
---@return boolean
|
||||
M.ends_with = function(str, ending)
|
||||
return ending == "" or str:sub(-#ending) == ending
|
||||
end
|
||||
|
||||
M.filter = function(input_table, value_to_remove)
|
||||
local resultTable = {}
|
||||
for _, v in ipairs(input_table) do
|
||||
@@ -58,6 +67,21 @@ M.merge = function(defaults, overrides)
|
||||
return vim.tbl_deep_extend("force", defaults, overrides)
|
||||
end
|
||||
|
||||
---Combines two list-like (non associative) tables, keeping values from both
|
||||
---@param t1 table The first table
|
||||
---@param ... table[] The first table
|
||||
---@return table
|
||||
M.combine = function(t1, ...)
|
||||
local result = t1
|
||||
local tables = { ... }
|
||||
for _, t in ipairs(tables) do
|
||||
for _, v in ipairs(t) do
|
||||
table.insert(result, v)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Pluralizes the input word, e.g. "3 cows"
|
||||
---@param num integer The count of the item/word
|
||||
---@param word string The word to pluralize
|
||||
@@ -373,26 +397,6 @@ M.difference = function(a, b)
|
||||
return not_included
|
||||
end
|
||||
|
||||
M.jump_to_file = function(filename, line_number)
|
||||
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)
|
||||
return
|
||||
end
|
||||
|
||||
-- If buffer is not already open, open it
|
||||
vim.cmd("edit " .. filename)
|
||||
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||
end
|
||||
|
||||
M.jump_to_buffer = function(bufnr, line_number)
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||
end
|
||||
|
||||
---Get the popup view_opts
|
||||
---@param title string The string to appear on top of the popup
|
||||
---@param settings table User defined popup settings
|
||||
@@ -484,14 +488,10 @@ M.get_window_id_by_buffer_id = function(buffer_id)
|
||||
local tabpage = vim.api.nvim_get_current_tabpage()
|
||||
local windows = vim.api.nvim_tabpage_list_wins(tabpage)
|
||||
|
||||
for _, win_id in ipairs(windows) do
|
||||
return List.new(windows):find(function(win_id)
|
||||
local buf_id = vim.api.nvim_win_get_buf(win_id)
|
||||
if buf_id == buffer_id then
|
||||
return win_id
|
||||
end
|
||||
end
|
||||
|
||||
return nil -- Buffer ID not found in any window
|
||||
return buf_id == buffer_id
|
||||
end)
|
||||
end
|
||||
|
||||
M.list_files_in_folder = function(folder_path)
|
||||
@@ -507,166 +507,21 @@ M.list_files_in_folder = function(folder_path)
|
||||
|
||||
local files = {}
|
||||
if folder ~= nil then
|
||||
for _, file in ipairs(folder) do
|
||||
local file_path = folder_path .. M.path_separator .. file
|
||||
local timestamp = vim.fn.getftime(file_path)
|
||||
table.insert(files, { name = file, timestamp = timestamp })
|
||||
end
|
||||
files = List.new(folder)
|
||||
:map(function(file)
|
||||
local file_path = folder_path .. M.path_separator .. file
|
||||
local timestamp = vim.fn.getftime(file_path)
|
||||
return { name = file, timestamp = timestamp }
|
||||
end)
|
||||
:sort(function(a, b)
|
||||
return a.timestamp > b.timestamp
|
||||
end)
|
||||
:map(function(file)
|
||||
return file.name
|
||||
end)
|
||||
end
|
||||
|
||||
-- Sort the table by timestamp in descending order (newest first)
|
||||
table.sort(files, function(a, b)
|
||||
return a.timestamp > b.timestamp
|
||||
end)
|
||||
|
||||
local result = {}
|
||||
for _, file in ipairs(files) do
|
||||
table.insert(result, file.name)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@class Hunk
|
||||
---@field old_line integer
|
||||
---@field old_range integer
|
||||
---@field new_line integer
|
||||
---@field new_range integer
|
||||
|
||||
---@class HunksAndDiff
|
||||
---@field hunks Hunk[] list of hunks
|
||||
---@field all_diff_output table The data from the git diff command
|
||||
|
||||
---Turn hunk line into Lua table
|
||||
---@param line table
|
||||
---@return Hunk|nil
|
||||
M.parse_possible_hunk_headers = function(line)
|
||||
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*) @@+")
|
||||
|
||||
return {
|
||||
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
|
||||
|
||||
---Parse git diff hunks.
|
||||
---@param file_path string Path to file.
|
||||
---@param base_branch string Git base branch of merge request.
|
||||
---@return HunksAndDiff
|
||||
M.parse_hunk_headers = function(file_path, base_branch)
|
||||
local hunks = {}
|
||||
local all_diff_output = {}
|
||||
|
||||
local Job = require("plenary.job")
|
||||
|
||||
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
|
||||
all_diff_output = j:result()
|
||||
for _, line in ipairs(all_diff_output) do
|
||||
local hunk = M.parse_possible_hunk_headers(line)
|
||||
if hunk ~= nil then
|
||||
table.insert(hunks, hunk)
|
||||
end
|
||||
end
|
||||
else
|
||||
M.notify("Failed to get git diff: " .. j:stderr(), vim.log.levels.WARN)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
diff_job:sync()
|
||||
|
||||
return { hunks = hunks, all_diff_output = all_diff_output }
|
||||
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
|
||||
return files
|
||||
end
|
||||
|
||||
---Check if current mode is visual mode
|
||||
@@ -707,6 +562,14 @@ M.get_icon = function(filename)
|
||||
end
|
||||
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
|
||||
|
||||
M.make_comma_separated_readable = function(str)
|
||||
return string.gsub(str, ",", ", ")
|
||||
end
|
||||
@@ -731,7 +594,7 @@ M.get_all_git_branches = function(remote)
|
||||
end
|
||||
handle:close()
|
||||
else
|
||||
print("Error running 'git branch' command.")
|
||||
M.notify("Error running 'git branch' command.", vim.log.levels.ERROR)
|
||||
end
|
||||
|
||||
return branches
|
||||
@@ -753,4 +616,11 @@ M.open_in_browser = function(url)
|
||||
end
|
||||
end
|
||||
|
||||
---Trims the trailing slash from a URL
|
||||
---@param s string
|
||||
---@return string
|
||||
M.trim_slash = function(s)
|
||||
return (s:gsub("/+$", ""))
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
74
lua/gitlab/utils/list.lua
Normal file
74
lua/gitlab/utils/list.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
local List = {}
|
||||
List.__index = List
|
||||
|
||||
function List.new(t)
|
||||
local list = t or {}
|
||||
setmetatable(list, List)
|
||||
return list
|
||||
end
|
||||
|
||||
---Mutates a given list
|
||||
---@generic T
|
||||
---@param func fun(v: T):T
|
||||
---@return List<T> @Returns a new list of elements mutated by func
|
||||
function List:map(func)
|
||||
local result = List.new()
|
||||
for _, v in ipairs(self) do
|
||||
table.insert(result, func(v))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Filters a given list
|
||||
---@generic T
|
||||
---@param func fun(v: T):boolean
|
||||
---@return List<T> @Returns a new list of elements for which func returns true
|
||||
function List:filter(func)
|
||||
local result = List.new()
|
||||
for _, v in ipairs(self) do
|
||||
if func(v) == true then
|
||||
table.insert(result, v)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function List:reduce(func, agg)
|
||||
for i, v in ipairs(self) do
|
||||
agg = func(agg, v, i)
|
||||
end
|
||||
return agg
|
||||
end
|
||||
|
||||
function List:sort(func)
|
||||
local result = List.new(self)
|
||||
table.sort(result, func)
|
||||
return result
|
||||
end
|
||||
|
||||
function List:find(func)
|
||||
for _, v in ipairs(self) do
|
||||
if func(v) == true then
|
||||
return v
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function List:slice(first, last, step)
|
||||
local sliced = List.new()
|
||||
for i = first or 1, last or #self, step or 1 do
|
||||
sliced[#sliced + 1] = self[i]
|
||||
end
|
||||
return sliced
|
||||
end
|
||||
|
||||
function List:values()
|
||||
local result = {}
|
||||
for _, v in ipairs(self) do
|
||||
table.insert(result, v)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return List
|
||||
Reference in New Issue
Block a user