* Feat: Enable sorting discussions by original comment (#422)
* Feat: Improve popup UX (#426)
* Feat: Automatically update MR summary details (#427)
* Feat: Show update progress in winbar (#432)
* Feat: Abbreviate winbar (#439)
* Fix: Note Creation Bug (#441)
* Fix: Checking whether comment can be created (#434)
* Fix: Syntax in discussion tree (#433)
* fix: improve indication of resolved threads and drafts (#442)
* Docs: Various minor improvements (#445)

---------

Co-authored-by: Jakub F. Bortlík <jakub.bortlik@proton.me>
This commit is contained in:
Harrison (Harry) Cramer
2024-12-11 14:21:50 -05:00
committed by GitHub
parent be027331e1
commit 495e64c8bc
32 changed files with 880 additions and 564 deletions

View File

@@ -6,7 +6,7 @@ Thank you for taking time to contribute to this plugin! Please follow these step
It's possible that the feature you want is already implemented, or does not belong in `gitlab.nvim` at all. By creating an issue first you can have a conversation with the maintainers about the functionality first. While this is not strictly necessary, it greatly increases the likelihood that your merge request will be accepted.
2. Fork the repository, and create a new feature branch for your desired functionality. Make your changes.
2. Fork the repository, and create a new feature branch off the `develop` branch for your desired functionality. Make your changes.
If you are using Lazy as a plugin manager, the easiest way to work on changes is by setting a specific path for the plugin that points to your repository locally. This is what I do:

View File

@@ -4,6 +4,10 @@ on:
branches:
- main
- develop
paths:
- 'cmd/**' # Ignore changes to the Lua code
- 'go.sum'
- 'go.mod'
jobs:
go_lint:
name: Lint Go 💅

View File

@@ -4,6 +4,8 @@ on:
branches:
- main
- develop
paths:
- 'lua/**' # Ignore changes to the Go code
jobs:
lua_lint:
name: Lint Lua 💅

View File

@@ -10,8 +10,7 @@ This Neovim plugin is designed to make it easy to review Gitlab MRs from within
- View and manage pipeline Jobs
- Upload files, jump to the browser, and a lot more!
![Screenshot 2024-01-13 at 10 43 32AM](https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/8dd8b961-a6b5-4e09-b87f-dc4a17b14149)
![Screenshot 2024-01-13 at 10 43 17AM](https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/079842de-e8a4-45c5-98c2-dcafc799c904)
![Screenshot 2024-12-08 at 5 43 53PM](https://github.com/user-attachments/assets/cb9e94e3-3817-4846-ba44-16ec06ea7654)
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335-afe1-d554e3804372
@@ -36,16 +35,15 @@ For more detailed information about the Lua APIs please run `:h gitlab.nvim.api`
With <a href="https://github.com/folke/lazy.nvim">Lazy</a>:
```lua
return {
{
"harrisoncramer/gitlab.nvim",
dependencies = {
"MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim",
"sindrets/diffview.nvim",
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
"nvim-tree/nvim-web-devicons" -- Recommended but not required. Icons in discussion tree.
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
},
enabled = true,
build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
config = function()
require("gitlab").setup()
@@ -53,30 +51,32 @@ return {
}
```
And with Packer:
And with <a href="https://github.com/lewis6991/pckr.nvim">pckr.nvim</a>:
```lua
use {
"harrisoncramer/gitlab.nvim",
requires = {
"MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim",
"sindrets/diffview.nvim"
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
},
build = function()
require("gitlab.server").build()
end,
branch = "develop",
config = function()
require("diffview") -- We require some global state from diffview
local gitlab = require("gitlab")
gitlab.setup()
end,
}
{
"harrisoncramer/gitlab.nvim",
requires = {
"MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim",
"sindrets/diffview.nvim",
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
},
run = function() require("gitlab.server").build() end, -- Builds the Go binary
config = function()
require("diffview") -- We require some global state from diffview
require("gitlab").setup()
end,
}
```
Add `branch = "develop",` to your configuration if you want to use the (possibly unstable) development version of `gitlab.nvim`.
## Contributing
Contributions to the plugin are welcome. Please read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start working on a pull request.
## Connecting to Gitlab
This plugin requires an <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token">auth token</a> to connect to Gitlab. The token can be set in the root directory of the project in a `.gitlab.nvim` environment file, or can be set via a shell environment variable called `GITLAB_TOKEN` instead. If both are present, the `.gitlab.nvim` file will take precedence.
@@ -122,7 +122,3 @@ For a list of all these settings please run `:h gitlab.nvim.configuring-the-plug
The plugin sets up a number of useful keybindings in the special buffers it creates, and some global keybindings as well. Refer to the relevant section of the manual `:h gitlab.nvim.keybindings` for more details.
For more information about each of these commands, and about the APIs in general, run `:h gitlab.nvim.api`
## Contributing
Contributions to the plugin are welcome. Please read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start working on a pull request.

View File

@@ -2,29 +2,26 @@ if filereadable($VIMRUNTIME . '/syntax/markdown.vim')
source $VIMRUNTIME/syntax/markdown.vim
endif
syntax match Date "\v\d+\s+\w+\s+ago"
highlight link Date GitlabDate
let expanders = '^\s*\%(' . g:gitlab_discussion_tree_expander_open . '\|' . g:gitlab_discussion_tree_expander_closed . '\)'
let username = '@[a-zA-Z0-9.]\+'
execute 'syntax match Unresolved /\s' . g:gitlab_discussion_tree_unresolved . '\s\?/'
highlight link Unresolved GitlabUnresolved
" Covers times like '14 days ago', 'just now', as well as 'October 3, 2024'
let time_ago = '\d\+ \w\+ ago'
let formatted_date = '\w\+ \{1,2}\d\{1,2}, \d\{4}'
let date = '\%(' . time_ago . '\|' . formatted_date . '\|just now\)'
execute 'syntax match Resolved /\s' . g:gitlab_discussion_tree_resolved . '\s\?/'
highlight link Resolved GitlabResolved
let published = date . ' \%(' . g:gitlab_discussion_tree_resolved . '\|' . g:gitlab_discussion_tree_unresolved . '\|' . g:gitlab_discussion_tree_unlinked . '\)\?'
let state = ' \%(' . published . '\|' . g:gitlab_discussion_tree_draft . '\)'
execute 'syntax match GitlabDiscussionOpen /^\s*' . g:gitlab_discussion_tree_expander_open . '/'
highlight link GitlabDiscussionOpen GitlabExpander
execute 'syntax match GitlabNoteHeader "' . expanders . username . state . '" contains=GitlabDate,GitlabUnresolved,GitlabUnlinked,GitlabResolved,GitlabExpander,GitlabDraft,GitlabUsername'
execute 'syntax match GitlabDiscussionClosed /^\s*' . g:gitlab_discussion_tree_expander_closed . '/'
highlight link GitlabDiscussionClosed GitlabExpander
execute 'syntax match Draft /' . g:gitlab_discussion_tree_draft . '/'
highlight link Draft GitlabDraft
execute 'syntax match Username "@[a-zA-Z0-9.]\+"'
highlight link Username GitlabUsername
execute 'syntax match Mention "\%(' . g:gitlab_discussion_tree_expander_open . '\|'
\ . g:gitlab_discussion_tree_expander_closed . '\)\@<!@[a-zA-Z0-9.]*"'
highlight link Mention GitlabMention
execute 'syntax match GitlabDate "' . date . '" contained'
execute 'syntax match GitlabUnresolved "' . g:gitlab_discussion_tree_unresolved . '" contained'
execute 'syntax match GitlabUnlinked "' . g:gitlab_discussion_tree_unlinked . '" contained'
execute 'syntax match GitlabResolved "' . g:gitlab_discussion_tree_resolved . '" contained'
execute 'syntax match GitlabExpander "' . expanders . '" contained'
execute 'syntax match GitlabDraft "' . g:gitlab_discussion_tree_draft . '" contained'
execute 'syntax match GitlabUsername "' . username . '" contained'
execute 'syntax match GitlabMention "' . username . '"'
let b:current_syntax = 'gitlab'

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"sort"
"sync"
"time"
"encoding/json"
@@ -19,8 +20,16 @@ func Contains[T comparable](elems []T, v T) bool {
return false
}
type SortBy string
const (
SortByLatestReply SortBy = "latest_reply"
SortByOriginalComment SortBy = "original_comment"
)
type DiscussionsRequest struct {
Blacklist []string `json:"blacklist" validate:"required"`
SortBy SortBy `json:"sort_by"`
}
type DiscussionsResponse struct {
@@ -30,20 +39,30 @@ type DiscussionsResponse struct {
Emojis map[int][]*gitlab.AwardEmoji `json:"emojis"`
}
type SortableDiscussions []*gitlab.Discussion
func (n SortableDiscussions) Len() int {
return len(n)
type SortableDiscussions struct {
Discussions []*gitlab.Discussion
SortBy SortBy
}
func (d SortableDiscussions) Less(i int, j int) bool {
iTime := d[i].Notes[len(d[i].Notes)-1].CreatedAt
jTime := d[j].Notes[len(d[j].Notes)-1].CreatedAt
return iTime.After(*jTime)
func (d SortableDiscussions) Len() int {
return len(d.Discussions)
}
func (n SortableDiscussions) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
func (d SortableDiscussions) Less(i, j int) bool {
var iTime, jTime *time.Time
if d.SortBy == SortByOriginalComment {
iTime = d.Discussions[i].Notes[0].CreatedAt
jTime = d.Discussions[j].Notes[0].CreatedAt
return iTime.Before(*jTime)
} else { // SortByLatestReply
iTime = d.Discussions[i].Notes[len(d.Discussions[i].Notes)-1].CreatedAt
jTime = d.Discussions[j].Notes[len(d.Discussions[j].Notes)-1].CreatedAt
return iTime.After(*jTime)
}
}
func (d SortableDiscussions) Swap(i, j int) {
d.Discussions[i], d.Discussions[j] = d.Discussions[j], d.Discussions[i]
}
type DiscussionsLister interface {
@@ -115,8 +134,14 @@ func (a discussionsListerService) ServeHTTP(w http.ResponseWriter, r *http.Reque
return
}
sortedLinkedDiscussions := SortableDiscussions(linkedDiscussions)
sortedUnlinkedDiscussions := SortableDiscussions(unlinkedDiscussions)
sortedLinkedDiscussions := SortableDiscussions{
Discussions: linkedDiscussions,
SortBy: request.SortBy,
}
sortedUnlinkedDiscussions := SortableDiscussions{
Discussions: unlinkedDiscussions,
SortBy: request.SortBy,
}
sort.Sort(sortedLinkedDiscussions)
sort.Sort(sortedUnlinkedDiscussions)

View File

@@ -21,8 +21,14 @@ func (f fakeDiscussionsLister) ListMergeRequestDiscussions(pid interface{}, merg
if err != nil {
return nil, nil, err
}
now := time.Now()
newer := now.Add(time.Second * 100)
timePointers := make([]*time.Time, 6)
timePointers[0] = new(time.Time)
*timePointers[0] = time.Now()
for i := 1; i < len(timePointers); i++ {
timePointers[i] = new(time.Time)
*timePointers[i] = timePointers[i-1].Add(time.Second * 100)
}
type Author struct {
ID int `json:"id"`
@@ -35,8 +41,18 @@ func (f fakeDiscussionsLister) ListMergeRequestDiscussions(pid interface{}, merg
}
testListDiscussionsResponse := []*gitlab.Discussion{
{Notes: []*gitlab.Note{{CreatedAt: &now, Type: "DiffNote", Author: Author{Username: "hcramer"}}}},
{Notes: []*gitlab.Note{{CreatedAt: &newer, Type: "DiffNote", Author: Author{Username: "hcramer2"}}}},
{Notes: []*gitlab.Note{
{CreatedAt: timePointers[0], Type: "DiffNote", Author: Author{Username: "hcramer0"}},
{CreatedAt: timePointers[4], Type: "DiffNote", Author: Author{Username: "hcramer1"}},
}},
{Notes: []*gitlab.Note{
{CreatedAt: timePointers[2], Type: "DiffNote", Author: Author{Username: "hcramer2"}},
{CreatedAt: timePointers[3], Type: "DiffNote", Author: Author{Username: "hcramer3"}},
}},
{Notes: []*gitlab.Note{
{CreatedAt: timePointers[1], Type: "DiffNote", Author: Author{Username: "hcramer4"}},
{CreatedAt: timePointers[5], Type: "DiffNote", Author: Author{Username: "hcramer5"}},
}},
}
return testListDiscussionsResponse, resp, err
}
@@ -66,8 +82,8 @@ func getDiscussionsList(t *testing.T, svc http.Handler, request *http.Request) D
}
func TestListDiscussions(t *testing.T) {
t.Run("Returns sorted discussions", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
t.Run("Returns discussions sorted by latest reply", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}, SortBy: "latest_reply"})
svc := middleware(
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
withMr(testProjectData, fakeMergeRequestLister{}),
@@ -76,12 +92,28 @@ func TestListDiscussions(t *testing.T) {
)
data := getDiscussionsList(t, svc, request)
assert(t, data.Message, "Discussions retrieved")
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2") /* Sorting applied */
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer")
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer4") /* Sorting applied */
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer0")
assert(t, data.Discussions[2].Notes[0].Author.Username, "hcramer2")
})
t.Run("Returns discussions sorted by original comment", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}, SortBy: "original_comment"})
svc := middleware(
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
withMr(testProjectData, fakeMergeRequestLister{}),
withPayloadValidation(methodToPayload{http.MethodPost: newPayload[DiscussionsRequest]}),
withMethodCheck(http.MethodPost),
)
data := getDiscussionsList(t, svc, request)
assert(t, data.Message, "Discussions retrieved")
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer0") /* Sorting applied */
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer4")
assert(t, data.Discussions[2].Notes[0].Author.Username, "hcramer2")
})
t.Run("Uses blacklist to filter unwanted authors", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer0"}, SortBy: "latest_reply"})
svc := middleware(
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
withMr(testProjectData, fakeMergeRequestLister{}),
@@ -90,8 +122,9 @@ func TestListDiscussions(t *testing.T) {
)
data := getDiscussionsList(t, svc, request)
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
assert(t, len(data.Discussions), 1)
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2")
assert(t, len(data.Discussions), 2)
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer4")
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer2")
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})

View File

@@ -59,25 +59,24 @@ INSTALLATION *gitlab.nvim.installation*
With Lazy:
>lua
return {
{
"harrisoncramer/gitlab.nvim",
dependencies = {
"MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim",
"sindrets/diffview.nvim",
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
"nvim-tree/nvim-web-devicons" -- Recommended but not required. Icons in discussion tree.
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
},
enabled = true,
build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
config = function()
require("gitlab").setup()
end,
}
<
And with Packer:
And with pckr.nvim:
>lua
use {
{
"harrisoncramer/gitlab.nvim",
requires = {
"MunifTanjim/nui.nvim",
@@ -86,14 +85,10 @@ And with Packer:
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
},
build = function()
require("gitlab.server").build()
end,
branch = "develop",
run = function() require("gitlab.server").build() end, -- Builds the Go binary
config = function()
require("diffview") -- We require some global state from diffview
local gitlab = require("gitlab")
gitlab.setup()
require("gitlab").setup()
end,
}
<
@@ -200,7 +195,7 @@ you call this function with no values the defaults will be used:
next_field = "<Tab>", -- Cycle to the next field. Accepts |count|.
prev_field = "<S-Tab>", -- Cycle to the previous field. Accepts |count|.
perform_action = "ZZ", -- Once in normal mode, does action (like saving comment or applying description edit, etc)
perform_linewise_action = "ZA", -- Once in normal mode, does the linewise action (see logs for this job, etc)
perform_linewise_action, = "ZA", -- Once in normal mode, does the linewise action (see logs for this job, etc)
discard_changes = "ZQ", -- Quit the popup discarding changes, the popup content is not saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
},
discussion_tree = {
@@ -219,6 +214,7 @@ you call this function with no values the defaults will be used:
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
publish_draft = "P", -- Publish the currently focused note/comment
toggle_draft_mode = "D", -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
toggle_sort_method = "st", -- Toggle whether discussions are sorted by the "latest_reply", or by "original_comment", see `:h gitlab.nvim.toggle_sort_method`
toggle_node = "t", -- Open or close the discussion
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
toggle_resolved_discussions = "R", -- Open or close all resolved discussions
@@ -234,36 +230,44 @@ you call this function with no values the defaults will be used:
},
},
popup = { -- The popup for comment creation, editing, and replying
width = "40%",
height = "60%",
width = "40%", -- Can be a percentage (string or decimal, "40%" = 0.4) of editor screen width, or an integer (number of columns)
height = "60%", -- Can be a percentage (string or decimal, "60%" = 0.6) of editor screen width, or an integer (number of rows)
position = "50%", -- Position (from the top left corner), either a number or percentage string that applies to both horizontal and vertical position, or a table that specifies them separately, e.g., { row = "90%", col = "100%" } places popups in the bottom right corner while leaving the status line visible
border = "rounded", -- One of "rounded", "single", "double", "solid"
opacity = 1.0, -- From 0.0 (fully transparent) to 1.0 (fully opaque)
comment = nil, -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 },
edit = nil,
note = nil,
pipeline = nil,
help = nil, -- Width and height are calculated automatically and cannot be overridden
pipeline = nil, -- Width and height are calculated automatically and cannot be overridden
reply = nil,
squash_message = nil,
create_mr = { width = "95%", height = "95%" },
summary = { width = "95%", height = "95%" },
temp_registers = {}, -- List of registers for backing up popup content (see `:h gitlab.nvim.temp-registers`)
},
discussion_tree = { -- The discussion tree that holds all comments
expanders = { -- Discussion tree icons
expanded = " ", -- Icon for expanded discussion thread
collapsed = " ", -- Icon for collapsed discussion thread
collapsed = " ", -- Icon for collapsed discussion thread
indentation = " ", -- Indentation Icon
},
spinner_chars = { "/", "|", "\\", "-" }, -- Characters for the refresh animation
auto_open = true, -- Automatically open when the reviewer is opened
default_view = "discussions" -- Show "discussions" or "notes" by default
default_view = "discussions", -- Show "discussions" or "notes" by default
blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc)
sort_by = "latest_reply", -- Sort discussion tree by the "latest_reply", or by "original_comment", see `:h gitlab.nvim.toggle_sort_method`
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
position = "left", -- "top", "right", "bottom" or "left"
position = "bottom", -- "top", "right", "bottom" or "left"
size = "20%", -- Size of split
relative = "editor", -- Position of tree split relative to "editor" or "window"
resolved = '✓', -- Symbol to show next to resolved discussions
unresolved = '-', -- Symbol to show next to unresolved discussions
unlinked = "󰌸", -- Symbol to show next to unliked comments (i.e., not threads)
draft = "✎", -- Symbol to show next to draft comments/notes
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
draft_mode = false, -- Whether comments are posted as drafts as part of a review
winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
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.
},
emojis = {
@@ -330,7 +334,7 @@ you call this function with no values the defaults will be used:
squash = false, -- Whether the commits will be marked for squashing
fork = {
enabled = false, -- If making an MR from a fork
forked_project_id = nil -- The ID of the project you are merging into. If nil, will be prompted.
forked_project_id = nil, -- The ID of the project you are merging into. If nil, will be prompted.
},
title_input = { -- Default settings for MR title input window
width = 40,
@@ -349,6 +353,9 @@ you call this function with no values the defaults will be used:
resolved = "DiagnosticSignOk",
unresolved = "DiagnosticSignWarn",
draft = "DiffviewNonText",
draft_mode = "DiagnosticWarn",
live_mode = "DiagnosticOk",
sort_method = "Keyword",
}
}
})
@@ -929,6 +936,13 @@ gitlab.toggle_draft_mode() ~
Toggles between draft mode, where comments and notes are added to a review as
drafts, and regular (or live) mode, where comments are posted immediately.
*gitlab.nvim.toggle_sort_method*
gitlab.toggle_sort_method() ~
Toggles whether the discussion tree is sorted by the "latest_reply", with
threads with the most recent activity on top (the default), or by
"original_comment", with the oldest threads on top.
*gitlab.nvim.add_assignee*
gitlab.add_assignee() ~

View File

@@ -1,13 +1,26 @@
local job = require("gitlab.job")
local state = require("gitlab.state")
local u = require("gitlab.utils")
local M = {}
local refresh_status_state = function(data)
u.notify(data.message, vim.log.levels.INFO)
state.load_new_state("info", function()
require("gitlab.actions.summary").update_summary_details()
end)
end
M.approve = function()
job.run_job("/mr/approve", "POST")
job.run_job("/mr/approve", "POST", nil, function(data)
refresh_status_state(data)
end)
end
M.revoke = function()
job.run_job("/mr/revoke", "POST")
job.run_job("/mr/revoke", "POST", nil, function(data)
refresh_status_state(data)
end)
end
return M

View File

@@ -22,6 +22,12 @@ M.delete_reviewer = function()
M.delete_popup("reviewer")
end
local refresh_user_state = function(type, data, message)
u.notify(message, vim.log.levels.INFO)
state.INFO[type] = data
require("gitlab.actions.summary").update_summary_details()
end
M.add_popup = function(type)
local plural = type .. "s"
local current = state.INFO[plural]
@@ -39,8 +45,7 @@ M.add_popup = function(type)
table.insert(current_ids, choice.id)
local body = { ids = current_ids }
job.run_job("/mr/" .. type, "PUT", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
state.INFO[plural] = data[plural]
refresh_user_state(plural, data[plural], data.message)
end)
end)
end
@@ -61,7 +66,7 @@ M.delete_popup = function(type)
local body = { ids = ids }
job.run_job("/mr/" .. type, "PUT", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
state.INFO[plural] = data[plural]
refresh_user_state(plural, data[plural], data.message)
end)
end)
end

View File

@@ -3,10 +3,10 @@
--- to this module the data required to make the API calls
local Popup = require("nui.popup")
local Layout = require("nui.layout")
local diffview_lib = require("diffview.lib")
local state = require("gitlab.state")
local job = require("gitlab.job")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local git = require("gitlab.git")
local discussions = require("gitlab.actions.discussions")
local draft_notes = require("gitlab.actions.draft_notes")
@@ -46,6 +46,18 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
return
end
-- Creating a draft reply, in response to a discussion ID
if discussion_id ~= nil and is_draft then
local body = { comment = text, discussion_id = discussion_id }
job.run_job("/mr/draft_notes/", "POST", body, function()
u.notify("Draft reply created!", vim.log.levels.INFO)
draft_notes.load_draft_notes(function()
discussions.rebuild_view(unlinked)
end)
end)
return
end
-- Creating a note (unlinked comment)
if unlinked and discussion_id == nil then
local body = { comment = text }
@@ -89,18 +101,6 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
line_range = location_data.line_range,
}
-- Creating a draft reply, in response to a discussion ID
if discussion_id ~= nil and is_draft then
local body = { comment = text, discussion_id = discussion_id, position = position_data }
job.run_job("/mr/draft_notes/", "POST", body, function()
u.notify("Draft reply created!", vim.log.levels.INFO)
draft_notes.load_draft_notes(function()
discussions.rebuild_view(unlinked)
end)
end)
return
end
-- Creating a new comment (linked to specific changes)
local body = u.merge({ type = "text", comment = text }, position_data)
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
@@ -157,52 +157,26 @@ end
---multi-line comment. It also sets up the basic keybindings for switching between
---window panes, and for the non-primary sections.
---@param opts LayoutOpts
---@return NuiLayout|nil
---@return NuiLayout
M.create_comment_layout = function(opts)
if opts.unlinked ~= true and opts.discussion_id == nil then
-- Check that diffview is initialized
if reviewer.tabnr == nil then
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
return
end
-- Check that Diffview is the current view
local view = diffview_lib.get_current_view()
if view == nil and not opts.reply then
u.notify("Comments should be left in the reviewer pane", vim.log.levels.ERROR)
return
end
-- Check that we are in the diffview tab
local tabnr = vim.api.nvim_get_current_tabpage()
if tabnr ~= reviewer.tabnr then
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
return
end
-- Check that the file has not been renamed
if reviewer.is_file_renamed() and not reviewer.does_file_have_changes() then
u.notify("Commenting on (unchanged) renamed or moved files is not supported", vim.log.levels.WARN)
return
end
-- Check that we are hovering over the code
local filetype = vim.bo[0].filetype
if not opts.reply and (filetype == "DiffviewFiles" or filetype == "gitlab") then
u.notify(
"Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
vim.log.levels.ERROR
)
return
end
local popup_settings = state.settings.popup
local title
local user_settings
if opts.discussion_id ~= nil then
title = "Reply"
user_settings = popup_settings.reply
elseif opts.unlinked then
title = "Note"
user_settings = popup_settings.note
else
title = "Comment"
user_settings = popup_settings.comment
end
local title = opts.discussion_id and "Reply" or "Comment"
local settings = opts.discussion_id ~= nil and state.settings.popup.reply or state.settings.popup.comment
local settings = u.merge(popup_settings, user_settings or {})
M.current_win = vim.api.nvim_get_current_win()
M.comment_popup = Popup(u.create_popup_state(title, settings))
M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
M.comment_popup = Popup(popup.create_popup_state(title, settings))
M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings))
M.start_line, M.end_line = u.get_visual_selection_boundaries()
local internal_layout = Layout.Box({
@@ -211,98 +185,69 @@ M.create_comment_layout = function(opts)
}, { dir = "col" })
local layout = Layout({
position = "50%",
position = settings.position,
relative = "editor",
size = {
width = "50%",
height = "55%",
width = settings.width,
height = settings.height,
},
}, internal_layout)
miscellaneous.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
popup.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
popup.set_up_autocommands(M.comment_popup, layout, M.current_win)
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
local unlinked = opts.unlinked or false
---Keybinding for focus on draft section
state.set_popup_keymaps(M.draft_popup, function()
popup.set_popup_keymaps(M.draft_popup, function()
local text = u.get_buffer_text(M.comment_popup.bufnr)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
end, miscellaneous.toggle_bool, miscellaneous.non_editable_popup_opts)
end, miscellaneous.toggle_bool, popup.non_editable_popup_opts)
---Keybinding for focus on text section
state.set_popup_keymaps(M.comment_popup, function(text)
popup.set_popup_keymaps(M.comment_popup, function(text)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
end, miscellaneous.attach_file, popup.editable_popup_opts)
vim.schedule(function()
local draft_mode = state.settings.discussion_tree.draft_mode
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
end)
--Send back to previous window on close
vim.api.nvim_create_autocmd("BufHidden", {
buffer = M.draft_popup.bufnr,
callback = function()
vim.api.nvim_set_current_win(M.current_win)
end,
})
return layout
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, err = git.has_clean_tree()
if err ~= nil then
return
end
local is_modified = vim.bo[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
if not M.sha_exists() then
if not M.can_create_comment(false) then
return
end
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
if layout ~= nil then
layout:mount()
end
layout:mount()
end
--- This function will open a multi-line comment popup in order to create a multi-line comment
--- on the changed/updated line in the current MR
M.create_multiline_comment = function()
if not u.check_visual_mode() then
return
end
if not M.sha_exists() then
if not M.can_create_comment(true) then
u.press_escape()
return
end
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
if layout ~= nil then
layout:mount()
end
layout:mount()
end
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
--- on the changed/updated line in the current MR
M.create_note = function()
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
if layout ~= nil then
layout:mount()
end
layout:mount()
end
---Given the current visually selected area of text, builds text to fill in the
@@ -347,21 +292,16 @@ end
--- on the changed/updated line in the current MR
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
M.create_comment_suggestion = function()
if not u.check_visual_mode() then
return
end
if not M.sha_exists() then
if not M.can_create_comment(true) then
u.press_escape()
return
end
local suggestion_lines, range_length = build_suggestion()
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
if layout ~= nil then
layout:mount()
else
return -- Failure in creating the comment layout
end
layout:mount()
vim.schedule(function()
if suggestion_lines then
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
@@ -369,6 +309,68 @@ M.create_comment_suggestion = function()
end)
end
---Returns true if it's possible to create an Inline Comment
---@param must_be_visual boolean True if current mode must be visual
---@return boolean
M.can_create_comment = function(must_be_visual)
-- Check that diffview is initialized
if reviewer.tabnr == nil then
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
return false
end
-- Check that we are in the Diffview tab
local tabnr = vim.api.nvim_get_current_tabpage()
if tabnr ~= reviewer.tabnr then
u.notify("Comments can only be left in the reviewer pane", vim.log.levels.ERROR)
return false
end
-- Check that we are hovering over the code
local filetype = vim.bo[0].filetype
if filetype == "DiffviewFiles" or filetype == "gitlab" then
u.notify(
"Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
vim.log.levels.ERROR
)
return false
end
-- Check that the file has not been renamed
if reviewer.is_file_renamed() and not reviewer.does_file_have_changes() then
u.notify("Commenting on (unchanged) renamed or moved files is not supported", vim.log.levels.ERROR)
return false
end
-- Check that we are in a valid buffer
if not M.sha_exists() then
return false
end
-- Check that there aren't saved modifications
local file = reviewer.get_current_file_path()
if file == nil then
return false
end
local has_changes, err = git.has_changes(file)
if err ~= nil then
return false
end
-- Check that there aren't unsaved modifications
local is_modified = vim.bo[0].modified
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or has_changes) then
u.notify("Cannot leave comments on changed files, please stash or commit and push", vim.log.levels.ERROR)
return false
end
-- Check we're in visual mode for code suggestions and multiline comments
if must_be_visual and not u.check_visual_mode() then
return false
end
return true
end
---Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
---buffers as 'null'
---@return boolean

View File

@@ -13,7 +13,7 @@ local M = {}
---@return string
M.build_note_header = function(note)
if note.note then
return "@" .. state.USER.username .. " " .. ""
return "@" .. state.USER.username .. " " .. state.settings.discussion_tree.draft
end
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
end

View File

@@ -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 popup = require("gitlab.popup")
local git = require("gitlab.git")
local state = require("gitlab.state")
local common = require("gitlab.actions.common")
@@ -277,13 +278,13 @@ M.open_confirmation_popup = function(mr)
action_before_exit = true,
}
state.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts)
state.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts)
state.set_popup_keymaps(target_popup, M.create_mr, M.select_new_target, popup_opts)
state.set_popup_keymaps(delete_branch_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
state.set_popup_keymaps(squash_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
state.set_popup_keymaps(forked_project_id_popup, M.create_mr, nil, popup_opts)
miscellaneous.set_cycle_popups_keymaps(popups)
popup.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts)
popup.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts)
popup.set_popup_keymaps(target_popup, M.create_mr, M.select_new_target, popup_opts)
popup.set_popup_keymaps(delete_branch_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
popup.set_popup_keymaps(squash_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
popup.set_popup_keymaps(forked_project_id_popup, M.create_mr, nil, popup_opts)
popup.set_cycle_popups_keymaps(popups)
vim.api.nvim_set_current_buf(M.description_bufnr)
end)
@@ -328,19 +329,20 @@ M.create_mr = function()
end
M.create_layout = function()
local title_popup = Popup(u.create_box_popup_state("Title", false))
local settings = u.merge(state.settings.popup, state.settings.popup.create_mr or {})
local title_popup = Popup(popup.create_box_popup_state("Title", false, settings))
M.title_bufnr = title_popup.bufnr
local description_popup = Popup(u.create_box_popup_state("Description", true))
local description_popup = Popup(popup.create_popup_state("Description", settings))
M.description_bufnr = description_popup.bufnr
local target_branch_popup = Popup(u.create_box_popup_state("Target branch", false))
local target_branch_popup = Popup(popup.create_box_popup_state("Target branch", false, settings))
M.target_bufnr = target_branch_popup.bufnr
local delete_title = vim.o.columns > 110 and "Delete source branch" or "Delete source"
local delete_branch_popup = Popup(u.create_box_popup_state(delete_title, false))
local delete_branch_popup = Popup(popup.create_box_popup_state(delete_title, false, settings))
M.delete_branch_bufnr = delete_branch_popup.bufnr
local squash_title = vim.o.columns > 110 and "Squash commits" or "Squash"
local squash_popup = Popup(u.create_box_popup_state(squash_title, false))
local squash_popup = Popup(popup.create_box_popup_state(squash_title, false, settings))
M.squash_bufnr = squash_popup.bufnr
local forked_project_id_popup = Popup(u.create_box_popup_state("Forked Project ID", false))
local forked_project_id_popup = Popup(popup.create_box_popup_state("Forked Project ID", false, settings))
M.forked_project_id_bufnr = forked_project_id_popup.bufnr
local boxes = {}
@@ -360,14 +362,16 @@ M.create_layout = function()
}, { dir = "col" })
local layout = Layout({
position = "50%",
position = settings.position,
relative = "editor",
size = {
width = "95%",
height = "95%",
width = settings.width,
height = settings.height,
},
}, internal_layout)
popup.set_up_autocommands(description_popup, layout, vim.api.nvim_get_current_win())
layout:mount()
return layout,

View File

@@ -7,12 +7,12 @@ local Popup = require("nui.popup")
local NuiTree = require("nui.tree")
local job = require("gitlab.job")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local common = require("gitlab.actions.common")
local List = require("gitlab.utils.list")
local tree_utils = require("gitlab.actions.discussions.tree")
local miscellaneous = require("gitlab.actions.miscellaneous")
local discussions_tree = require("gitlab.actions.discussions.tree")
local draft_notes = require("gitlab.actions.draft_notes")
local diffview_lib = require("diffview.lib")
@@ -48,13 +48,15 @@ M.rebuild_view = function(unlinked, all)
else
M.rebuild_discussion_tree()
end
M.refresh_diagnostics_and_winbar()
state.discussion_tree.last_updated = os.time()
M.refresh_diagnostics()
end)
end
---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil
M.load_discussions = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("discussion_data", function(data)
if not state.DISCUSSION_DATA then
state.DISCUSSION_DATA = {}
@@ -70,9 +72,10 @@ end
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
M.initialize_discussions = function()
state.discussion_tree.last_updated = os.time()
signs.setup_signs()
reviewer.set_callback_for_file_changed(function()
M.refresh_diagnostics_and_winbar()
M.refresh_diagnostics()
M.modifiable(false)
reviewer.set_reviewer_keymaps()
end)
@@ -102,11 +105,10 @@ M.modifiable = function(bool)
end
--- Take existing data and refresh the diagnostics, the winbar, and the signs
M.refresh_diagnostics_and_winbar = function()
M.refresh_diagnostics = function()
if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics()
end
winbar.update_winbar()
common.add_empty_titles()
end
@@ -154,7 +156,7 @@ M.open = function(callback)
end
vim.schedule(function()
M.refresh_diagnostics_and_winbar()
M.refresh_diagnostics()
end)
end
@@ -251,9 +253,7 @@ M.reply = function(tree)
reply = true,
})
if layout then
layout:mount()
end
layout:mount()
end
-- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment
@@ -284,7 +284,7 @@ end
-- This function (settings.keymaps.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
M.edit_comment = function(tree, unlinked)
local edit_popup = Popup(u.create_popup_state("Edit Comment", state.settings.popup.edit))
local edit_popup = Popup(popup.create_popup_state("Edit Comment", state.settings.popup.edit))
local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node)
local root_node = common.get_root_node(tree, current_node)
@@ -293,6 +293,8 @@ M.edit_comment = function(tree, unlinked)
return
end
popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
edit_popup:mount()
-- Gather all lines from immediate children that aren't note nodes
@@ -310,19 +312,19 @@ M.edit_comment = function(tree, unlinked)
-- Draft notes module handles edits for draft notes
if M.is_draft_note(tree) then
state.set_popup_keymaps(
popup.set_popup_keymaps(
edit_popup,
draft_notes.confirm_edit_draft_note(note_node.id, unlinked),
nil,
miscellaneous.editable_popup_opts
popup.editable_popup_opts
)
else
local comment = require("gitlab.actions.comment")
state.set_popup_keymaps(
popup.set_popup_keymaps(
edit_popup,
comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
nil,
miscellaneous.editable_popup_opts
popup.editable_popup_opts
)
end
end
@@ -585,7 +587,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.jump_to_reviewer then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
if M.is_current_node_note(tree) then
common.jump_to_reviewer(tree, M.refresh_diagnostics_and_winbar)
common.jump_to_reviewer(tree, M.refresh_diagnostics)
end
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
end
@@ -603,7 +605,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.refresh_data then
vim.keymap.set("n", keymaps.discussion_tree.refresh_data, function()
u.notify("Refreshing data...", vim.log.levels.INFO)
draft_notes.rebuild_view(unlinked, false)
end, {
buffer = bufnr,
@@ -646,6 +647,16 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
})
end
if keymaps.discussion_tree.toggle_sort_method then
vim.keymap.set("n", keymaps.discussion_tree.toggle_sort_method, function()
M.toggle_sort_method()
end, {
buffer = bufnr,
desc = "Toggle sort method",
nowait = keymaps.discussion_tree.toggle_sort_method_nowait,
})
end
if keymaps.discussion_tree.toggle_resolved then
vim.keymap.set("n", keymaps.discussion_tree.toggle_resolved, function()
if M.is_current_node_note(tree) and not M.is_draft_note(tree) then
@@ -746,16 +757,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
})
end
if keymaps.discussion_tree.print_node then
vim.keymap.set("n", keymaps.discussion_tree.print_node, function()
common.print_node(tree)
end, {
buffer = bufnr,
desc = "Print current node (for debugging)",
nowait = keymaps.discussion_tree.print_node_nowait,
})
end
if keymaps.discussion_tree.add_emoji then
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
M.add_emoji_to_note(tree, unlinked)
@@ -792,7 +793,18 @@ end
---Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
M.toggle_draft_mode = function()
state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode
end
---Toggle between sorting by "original comment" (oldest at the top) or "latest reply" (newest at the
---top).
M.toggle_sort_method = function()
if state.settings.discussion_tree.sort_by == "original_comment" then
state.settings.discussion_tree.sort_by = "latest_reply"
else
state.settings.discussion_tree.sort_by = "original_comment"
end
winbar.update_winbar()
M.rebuild_view(false, true)
end
---Indicates whether the node under the cursor is a draft note or not

View File

@@ -277,13 +277,16 @@ local function build_note_body(note, resolve_info)
)
end
local resolve_symbol = ""
local symbol = ""
local is_draft = note.note ~= nil
if resolve_info ~= nil and resolve_info.resolvable then
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
or state.settings.discussion_tree.unresolved
elseif not is_draft and resolve_info and not resolve_info.resolvable then
symbol = state.settings.discussion_tree.unlinked
end
local noteHeader = common.build_note_header(note) .. " " .. resolve_symbol
local noteHeader = common.build_note_header(note) .. " " .. symbol
return noteHeader, text_nodes
end
@@ -454,7 +457,9 @@ M.restore_cursor_position = function(winid, tree, original_node, root_node)
end
end
if line_number ~= nil then
vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
end
end
end

View File

@@ -54,7 +54,19 @@ local get_data = function(nodes)
return total_resolvable, total_resolved, total_non_resolvable
end
local spinner_index = 0
state.discussion_tree.last_updated = nil
local function content()
local updated
if state.discussion_tree.last_updated then
local last_update = tostring(os.date("!%Y-%m-%dT%H:%M:%S", state.discussion_tree.last_updated))
updated = u.time_since(last_update) .. ""
else
spinner_index = (spinner_index % #state.settings.discussion_tree.spinner_chars) + 1
updated = state.settings.discussion_tree.spinner_chars[spinner_index]
end
local resolvable_discussions, resolved_discussions, non_resolvable_discussions =
get_data(state.DISCUSSION_DATA.discussions)
local resolvable_notes, resolved_notes, non_resolvable_notes = get_data(state.DISCUSSION_DATA.unlinked_discussions)
@@ -82,9 +94,10 @@ local function content()
resolved_notes = resolved_notes,
non_resolvable_notes = non_resolvable_notes,
help_keymap = state.settings.keymaps.help,
updated = updated,
}
return M.make_winbar(t)
return state.settings.discussion_tree.winbar and state.settings.discussion_tree.winbar(t) or M.make_winbar(t)
end
---This function updates the winbar
@@ -108,7 +121,7 @@ M.update_winbar = function()
end
local function get_connector(base_title)
return string.match(base_title, "%($") and "" or "; "
return string.match(base_title, "%($") and "" or " "
end
---Builds the title string for both sections, using the count of resolvable and draft nodes
@@ -116,84 +129,128 @@ end
---@param resolvable_count integer
---@param resolved_count integer
---@param drafts_count integer
---@param focused boolean
---@return string
local add_drafts_and_resolvable = function(
base_title,
resolvable_count,
resolved_count,
drafts_count,
non_resolvable_count
non_resolvable_count,
focused
)
if resolvable_count == 0 and drafts_count == 0 and non_resolvable_count == 0 then
return base_title
end
base_title = base_title .. " ("
if non_resolvable_count ~= 0 then
base_title = base_title .. u.pluralize(non_resolvable_count, "comment")
end
if resolvable_count ~= 0 then
base_title = base_title
.. get_connector(base_title)
.. string.format("%d/%s", resolved_count, u.pluralize(resolvable_count, "thread"))
base_title = base_title .. M.get_resolved_text(focused, resolved_count, resolvable_count)
end
if non_resolvable_count ~= 0 then
base_title = base_title .. M.get_nonresolveable_text(base_title, non_resolvable_count, focused)
end
if drafts_count ~= 0 then
base_title = base_title .. get_connector(base_title) .. u.pluralize(drafts_count, "draft")
base_title = base_title .. M.get_drafts_text(base_title, drafts_count, focused)
end
base_title = base_title .. ")"
return base_title
end
---@param t WinbarTable
M.make_winbar = function(t)
local discussion_title = add_drafts_and_resolvable(
"Inline Comments",
local discussions_focused = M.current_view_type == "discussions"
local discussion_text = add_drafts_and_resolvable(
"Inline Comments:",
t.resolvable_discussions,
t.resolved_discussions,
t.inline_draft_notes,
t.non_resolvable_discussions
t.non_resolvable_discussions,
discussions_focused
)
local notes_title = add_drafts_and_resolvable(
"Notes",
local notes_text = add_drafts_and_resolvable(
"Notes:",
t.resolvable_notes,
t.resolved_notes,
t.unlinked_draft_notes,
t.non_resolvable_notes
t.non_resolvable_notes,
not discussions_focused
)
-- Colorize the active tab
if M.current_view_type == "discussions" then
discussion_title = "%#Text#" .. discussion_title
notes_title = "%#Comment#" .. notes_title
elseif M.current_view_type == "notes" then
discussion_title = "%#Comment#" .. discussion_title
notes_title = "%#Text#" .. notes_title
if discussions_focused then
discussion_text = "%#Text#" .. discussion_text
notes_text = "%#Comment#" .. notes_text
else
discussion_text = "%#Comment#" .. discussion_text
notes_text = "%#Text#" .. notes_text
end
local sort_method = M.get_sort_method()
local mode = M.get_mode()
-- Join everything together and return it
local separator = "%#Comment#|"
local end_section = "%="
local updated = "%#Text#" .. t.updated
local help = "%#Comment#Help: " .. (t.help_keymap and t.help_keymap:gsub(" ", "<space>") .. " " or "unmapped")
return string.format(
" %s %s %s %s %s %s %s",
discussion_title,
" %s %s %s %s %s %s %s %s %s %s %s",
discussion_text,
separator,
notes_title,
notes_text,
end_section,
updated,
separator,
sort_method,
separator,
mode,
separator,
help
)
end
---Returns a string for the winbar indicating the sort method
---@return string
M.get_sort_method = function()
local sort_method = state.settings.discussion_tree.sort_by == "original_comment" and "↓ by thread" or "↑ by reply"
return "%#GitlabSortMethod#" .. sort_method .. "%#Comment#"
end
M.get_resolved_text = function(focused, resolved_count, resolvable_count)
local text = focused and ("%#GitlabResolved#" .. state.settings.discussion_tree.resolved .. "%#Text#")
or state.settings.discussion_tree.resolved
return " " .. string.format("%d%s/%d", resolved_count, text, resolvable_count)
end
M.get_drafts_text = function(base_title, drafts_count, focused)
return get_connector(base_title)
.. string.format(
"%d%s",
drafts_count,
(
focused and ("%#GitlabDraft#" .. state.settings.discussion_tree.draft .. "%#Text#")
or state.settings.discussion_tree.draft
)
)
end
M.get_nonresolveable_text = function(base_title, non_resolvable_count, focused)
return get_connector(base_title)
.. string.format(
"%d%s",
non_resolvable_count,
(
focused and ("%#GitlabUnlinked#" .. state.settings.discussion_tree.unlinked .. "%#Text#")
or state.settings.discussion_tree.unlinked
)
)
end
---Returns a string for the winbar indicating the mode type, live or draft
---@return string
M.get_mode = function()
if state.settings.discussion_tree.draft_mode then
return "%#DiagnosticWarn#Draft Mode"
return "%#GitlabDraftMode#Draft"
else
return "%#DiagnosticOK#Live Mode"
return "%#GitlabLiveMode#Live"
end
end
@@ -215,4 +272,8 @@ M.switch_view_type = function(override)
M.update_winbar()
end
-- Set up a timer to update the winbar periodically
local timer = vim.uv.new_timer()
timer:start(0, 100, vim.schedule_wrap(M.update_winbar))
return M

View File

@@ -25,6 +25,7 @@ end
---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil
M.load_draft_notes = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("draft_notes", function()
if callback ~= nil then
callback()

View File

@@ -1,6 +1,6 @@
local M = {}
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local event = require("nui.utils.autocmd").event
local state = require("gitlab.state")
local List = require("gitlab.utils.list")
@@ -16,15 +16,31 @@ M.open = function()
end
return agg
end, {})
table.insert(help_content_lines, "")
table.insert(
help_content_lines,
string.format(
"%s = draft; %s = unlinked comment; %s = resolved",
state.settings.discussion_tree.draft,
state.settings.discussion_tree.unlinked,
state.settings.discussion_tree.resolved
)
)
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))
local opts = { "Help", state.settings.popup.help, longest_line + 3, #help_content_lines, 70 }
local help_popup = Popup(popup.create_popup_state(unpack(opts)))
help_popup:on(event.BufLeave, function()
help_popup:unmount()
end)
popup.set_up_autocommands(help_popup, nil, vim.api.nvim_get_current_win(), opts)
help_popup:mount()
state.set_popup_keymaps(help_popup, "Help", nil)
popup.set_popup_keymaps(help_popup, "Help", nil)
local currentBuffer = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(currentBuffer, 0, #help_content_lines, false, help_content_lines)
u.switch_can_edit_buf(currentBuffer, false)

View File

@@ -14,8 +14,10 @@ M.delete_label = function()
M.delete_popup("label")
end
local refresh_label_state = function(labels)
local refresh_label_state = function(labels, message)
u.notify(message, vim.log.levels.INFO)
state.INFO.labels = labels
require("gitlab.actions.summary").update_summary_details()
end
local get_current_labels = function()
@@ -41,9 +43,7 @@ M.add_popup = function(type)
table.insert(current_labels, choice)
local body = { labels = current_labels }
job.run_job("/mr/" .. type, "PUT", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
refresh_label_state(data.labels)
refresh_label_state(data.labels, data.message)
end)
end)
end
@@ -59,8 +59,7 @@ M.delete_popup = function(type)
local filtered_labels = u.filter(current_labels, choice)
local body = { labels = filtered_labels }
job.run_job("/mr/" .. type, "PUT", body, function(data)
u.notify(data.message, vim.log.levels.INFO)
refresh_label_state(data.labels)
refresh_label_state(data.labels, data.message)
end)
end)
end

View File

@@ -1,14 +1,14 @@
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local Popup = require("nui.popup")
local state = require("gitlab.state")
local job = require("gitlab.job")
local reviewer = require("gitlab.reviewer")
local miscellaneous = require("gitlab.actions.miscellaneous")
local M = {}
local function create_squash_message_popup()
return Popup(u.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
return Popup(popup.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
end
---@class MergeOpts
@@ -31,10 +31,11 @@ M.merge = function(opts)
if merge_body.squash then
local squash_message_popup = create_squash_message_popup()
popup.set_up_autocommands(squash_message_popup, nil, vim.api.nvim_get_current_win())
squash_message_popup:mount()
state.set_popup_keymaps(squash_message_popup, function(text)
popup.set_popup_keymaps(squash_message_popup, function(text)
M.confirm_merge(merge_body, text)
end, nil, miscellaneous.editable_popup_opts)
end, nil, popup.editable_popup_opts)
else
M.confirm_merge(merge_body)
end

View File

@@ -12,14 +12,6 @@ local M = {}
---Opens up a select menu that lets you choose a different merge request.
---@param opts ChooseMergeRequestOptions|nil
M.choose_merge_request = function(opts)
local has_clean_tree, clean_tree_err = git.has_clean_tree()
if clean_tree_err ~= nil then
return
elseif has_clean_tree ~= "" then
u.notify("Your local branch has changes, please stash or commit and push", vim.log.levels.ERROR)
return
end
if opts == nil then
opts = state.settings.choose_merge_request
end
@@ -38,6 +30,19 @@ M.choose_merge_request = function(opts)
reviewer.close()
end
if choice.source_branch ~= git.get_current_branch() then
local has_clean_tree, clean_tree_err = git.has_clean_tree()
if clean_tree_err ~= nil then
return
elseif not has_clean_tree then
u.notify(
"Cannot switch branch when working tree has changes, please stash or commit and push",
vim.log.levels.ERROR
)
return
end
end
vim.schedule(function()
local _, branch_switch_err = git.switch_branch(choice.source_branch)
if branch_switch_err ~= nil then

View File

@@ -34,70 +34,6 @@ M.attach_file = function()
end)
end
M.editable_popup_opts = {
action_before_close = true,
action_before_exit = false,
save_to_temp_register = true,
}
M.non_editable_popup_opts = {
action_before_close = true,
action_before_exit = false,
save_to_temp_register = false,
}
-- Get the index of the next popup when cycling forward
local function next_index(i, n, count)
count = count > 0 and count or 1
for _ = 1, count do
if i < n then
i = i + 1
elseif i == n then
i = 1
end
end
return i
end
---Get the index of the previous popup when cycling backward
---@param i integer The current index
---@param n integer The total number of popups
---@param count integer The count used with the keymap (replaced with 1 if no count was given)
local function prev_index(i, n, count)
count = count > 0 and count or 1
for _ = 1, count do
if i > 1 then
i = i - 1
elseif i == 1 then
i = n
end
end
return i
end
---Setup keymaps for cycling popups. The keymap accepts count.
---@param popups table Table of Popups
M.set_cycle_popups_keymaps = function(popups)
local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.popup.disable_all then
return
end
local number_of_popups = #popups
for i, popup in ipairs(popups) do
if keymaps.popup.next_field then
popup:map("n", keymaps.popup.next_field, function()
vim.api.nvim_set_current_win(popups[next_index(i, number_of_popups, vim.v.count)].winid)
end, { desc = "Go to next field (accepts count)", nowait = keymaps.popup.next_field_nowait })
end
if keymaps.popup.prev_field then
popup:map("n", keymaps.popup.prev_field, function()
vim.api.nvim_set_current_win(popups[prev_index(i, number_of_popups, vim.v.count)].winid)
end, { desc = "Go to previous field (accepts count)", nowait = keymaps.popup.prev_field_nowait })
end
end
end
---Toggle the value in a "Boolean buffer"
M.toggle_bool = function()
local bufnr = vim.api.nvim_get_current_buf()

View File

@@ -5,6 +5,7 @@ local Popup = require("nui.popup")
local state = require("gitlab.state")
local job = require("gitlab.job")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local M = {
pipeline_jobs = nil,
latest_pipeline = nil,
@@ -40,7 +41,8 @@ M.open = function()
local height = 6 + #M.pipeline_jobs + 3
local pipeline_popup =
Popup(u.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60))
Popup(popup.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60))
popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win())
M.pipeline_popup = pipeline_popup
pipeline_popup:mount()
@@ -91,7 +93,7 @@ M.open = function()
end
pipeline_popup.border:set_text("top", "Pipeline Status", "center")
state.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
popup.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
u.switch_can_edit_buf(bufnr, false)
end)
end

View File

@@ -7,6 +7,7 @@ local git = require("gitlab.git")
local job = require("gitlab.job")
local common = require("gitlab.actions.common")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local List = require("gitlab.utils.list")
local state = require("gitlab.state")
local miscellaneous = require("gitlab.actions.miscellaneous")
@@ -34,6 +35,9 @@ M.summary = function()
local info_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
layout:mount()
local popups = {
title_popup,
description_popup,
@@ -41,6 +45,9 @@ M.summary = function()
}
M.layout = layout
M.info_popup = info_popup
M.title_popup = title_popup
M.description_popup = description_popup
M.layout_buf = layout.bufnr
M.layout_visible = true
@@ -54,30 +61,28 @@ M.summary = function()
vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { title })
if info_popup then
vim.api.nvim_buf_set_lines(info_popup.bufnr, 0, -1, false, info_lines)
u.switch_can_edit_buf(info_popup.bufnr, false)
M.color_details(info_popup.bufnr) -- Color values in details popup
M.update_details_popup(info_popup.bufnr, info_lines)
end
state.set_popup_keymaps(
popup.set_popup_keymaps(
description_popup,
M.edit_summary,
miscellaneous.attach_file,
{ cb = exit, action_before_close = true, action_before_exit = true, save_to_temp_register = true }
)
state.set_popup_keymaps(
popup.set_popup_keymaps(
title_popup,
M.edit_summary,
nil,
{ cb = exit, action_before_close = true, action_before_exit = true }
)
state.set_popup_keymaps(
popup.set_popup_keymaps(
info_popup,
M.edit_summary,
nil,
{ cb = exit, action_before_close = true, action_before_exit = true }
)
miscellaneous.set_cycle_popups_keymaps(popups)
popup.set_cycle_popups_keymaps(popups)
vim.api.nvim_set_current_buf(description_popup.bufnr)
end)
@@ -86,6 +91,23 @@ M.summary = function()
git.check_mr_in_good_condition()
end
M.update_summary_details = function()
if not M.info_popup or not M.info_popup.bufnr then
return
end
local details_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
local internal_layout = M.create_internal_layout(details_lines, M.title_popup, M.description_popup, M.info_popup)
M.layout:update(M.get_outer_layout_config(), internal_layout)
M.update_details_popup(M.info_popup.bufnr, details_lines)
end
M.update_details_popup = function(bufnr, info_lines)
u.switch_can_edit_buf(bufnr, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, info_lines)
u.switch_can_edit_buf(bufnr, false)
M.color_details(bufnr) -- Color values in details popup
end
-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
-- lines that users include in their state.settings.info.fields list.
M.build_info_lines = function()
@@ -165,16 +187,37 @@ M.edit_summary = function()
end)
end
---Create the Summary layout and individual popups that make up the Layout.
---@return NuiLayout, NuiPopup, NuiPopup, NuiPopup
M.create_layout = function(info_lines)
local title_popup = Popup(u.create_box_popup_state(nil, false))
local settings = u.merge(state.settings.popup, state.settings.popup.summary or {})
local title_popup = Popup(popup.create_box_popup_state(nil, false, settings))
M.title_bufnr = title_popup.bufnr
local description_popup = Popup(u.create_box_popup_state("Description", true))
local description_popup = Popup(popup.create_popup_state("Description", settings))
M.description_bufnr = description_popup.bufnr
local details_popup
if state.settings.info.enabled then
details_popup = Popup(popup.create_box_popup_state("Details", false, settings))
end
local internal_layout = M.create_internal_layout(info_lines, title_popup, description_popup, details_popup)
local layout = Layout(M.get_outer_layout_config(), internal_layout)
popup.set_up_autocommands(description_popup, layout, vim.api.nvim_get_current_win())
return layout, title_popup, description_popup, details_popup
end
---Create the internal layout of the Summary and individual popups that make up the Layout.
---@param info_lines string[] Table of strings that make up the details content
---@param title_popup NuiPopup
---@param description_popup NuiPopup
---@param details_popup NuiPopup
---@return NuiLayout.Box
M.create_internal_layout = function(info_lines, title_popup, description_popup, details_popup)
local internal_layout
if state.settings.info.enabled then
details_popup = Popup(u.create_box_popup_state("Details", false))
if state.settings.info.horizontal then
local longest_line = u.get_longest_string(info_lines)
internal_layout = Layout.Box({
@@ -182,7 +225,7 @@ M.create_layout = function(info_lines)
Layout.Box({
Layout.Box(details_popup, { size = longest_line + 3 }),
Layout.Box(description_popup, { grow = 1 }),
}, { dir = "row", size = "100%" }),
}, { dir = "row", size = "95%" }),
}, { dir = "col" })
else
internal_layout = Layout.Box({
@@ -197,18 +240,21 @@ M.create_layout = function(info_lines)
Layout.Box(description_popup, { grow = 1 }),
}, { dir = "col" })
end
return internal_layout
end
local layout = Layout({
position = "50%",
---Create the config for the outer Layout of the Summary
---@return nui_layout_options
M.get_outer_layout_config = function()
local settings = u.merge(state.settings.popup, state.settings.popup.summary or {})
return {
position = settings.position,
relative = "editor",
size = {
width = "95%",
height = "95%",
width = settings.width,
height = settings.height,
},
}, internal_layout)
layout:mount()
return layout, title_popup, description_popup, details_popup
}
end
M.color_details = function(bufnr)

View File

@@ -92,6 +92,7 @@
---@field resolved_notes number
---@field non_resolvable_notes number
---@field help_keymap string
---@field updated string
---
---@class SignTable
---@field name string

View File

@@ -3,12 +3,13 @@ local state = require("gitlab.state")
local colors = state.settings.colors
-- Set icons into global vim variables for syntax matching
local expanders = state.settings.discussion_tree.expanders
vim.g.gitlab_discussion_tree_expander_open = expanders.expanded
vim.g.gitlab_discussion_tree_expander_closed = expanders.collapsed
vim.g.gitlab_discussion_tree_draft = ""
vim.g.gitlab_discussion_tree_resolved = ""
vim.g.gitlab_discussion_tree_unresolved = "-"
local discussion_tree = state.settings.discussion_tree
vim.g.gitlab_discussion_tree_expander_open = discussion_tree.expanders.expanded
vim.g.gitlab_discussion_tree_expander_closed = discussion_tree.expanders.collapsed
vim.g.gitlab_discussion_tree_draft = discussion_tree.draft
vim.g.gitlab_discussion_tree_resolved = discussion_tree.resolved
vim.g.gitlab_discussion_tree_unresolved = discussion_tree.unresolved
vim.g.gitlab_discussion_tree_unlinked = discussion_tree.unlinked
local discussion = colors.discussion_tree
@@ -17,7 +18,6 @@ local function get_colors_for_group(group)
local normal_bg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(group)), "bg")
return { fg = normal_fg, bg = normal_bg }
end
vim.api.nvim_create_autocmd("VimEnter", {
callback = function()
vim.api.nvim_set_hl(0, "GitlabUsername", get_colors_for_group(discussion.username))
@@ -29,6 +29,10 @@ vim.api.nvim_create_autocmd("VimEnter", {
vim.api.nvim_set_hl(0, "GitlabFileName", get_colors_for_group(discussion.file_name))
vim.api.nvim_set_hl(0, "GitlabResolved", get_colors_for_group(discussion.resolved))
vim.api.nvim_set_hl(0, "GitlabUnresolved", get_colors_for_group(discussion.unresolved))
vim.api.nvim_set_hl(0, "GitlabUnlinked", get_colors_for_group(discussion.unlinked))
vim.api.nvim_set_hl(0, "GitlabDraft", get_colors_for_group(discussion.draft))
vim.api.nvim_set_hl(0, "GitlabDraftMode", get_colors_for_group(discussion.draft_mode))
vim.api.nvim_set_hl(0, "GitlabLiveMode", get_colors_for_group(discussion.live_mode))
vim.api.nvim_set_hl(0, "GitlabSortMethod", get_colors_for_group(discussion.sort_method))
end,
})

View File

@@ -25,10 +25,19 @@ M.branches = function(args)
return run_system(u.combine({ "git", "branch" }, args or {}))
end
---Checks whether the tree has any changes that haven't been pushed to the remote
---@return string|nil, string|nil
---Returns true if the working tree hasn't got any changes that haven't been commited
---@return boolean, string|nil
M.has_clean_tree = function()
return run_system({ "git", "status", "--short", "--untracked-files=no" })
local changes, err = run_system({ "git", "status", "--short", "--untracked-files=no" })
return changes == "", err
end
---Returns true if the `file` has got any uncommitted changes
---@param file string File to check for changes
---@return boolean, string|nil
M.has_changes = function(file)
local changes, err = run_system({ "git", "status", "--short", "--untracked-files=no", "--", file })
return changes ~= "", err
end
---Gets the base directory of the current project

View File

@@ -92,10 +92,10 @@ return {
end
end,
toggle_draft_mode = discussions.toggle_draft_mode,
toggle_sort_method = discussions.toggle_sort_method,
publish_all_drafts = draft_notes.publish_all_drafts,
refresh_data = function()
-- This also rebuilds the regular views
u.notify("Refreshing data...", vim.log.levels.INFO)
draft_notes.rebuild_view(false, true)
end,
-- Other functions 🤷

239
lua/gitlab/popup.lua Normal file
View File

@@ -0,0 +1,239 @@
local u = require("gitlab.utils")
local M = {}
---Get the popup view_opts
---@param title string The string to appear on top of the popup
---@param user_settings table|nil User-defined popup settings
---@param width number? Override default width
---@param height number? Override default height
---@param zindex number? Override default zindex
---@return table
M.create_popup_state = function(title, user_settings, width, height, zindex)
local settings = u.merge(require("gitlab.state").settings.popup, user_settings or {})
local view_opts = {
buf_options = {
filetype = "markdown",
},
relative = "editor",
enter = true,
focusable = true,
zindex = zindex or 50,
border = {
style = settings.border,
text = {
top = title,
},
},
position = settings.position,
size = {
width = width and math.min(width, vim.o.columns - 2) or settings.width,
height = height and math.min(height, vim.o.lines - 3) or settings.height,
},
opacity = settings.opacity,
}
return view_opts
end
---Create view_opts for Box popups used inside popup Layouts
---@param title string|nil The string to appear on top of the popup
---@param enter boolean Whether the pop should be focused after creation
---@param settings table User defined popup settings
---@return table
M.create_box_popup_state = function(title, enter, settings)
return {
buf_options = {
filetype = "markdown",
},
enter = enter or false,
focusable = true,
border = {
style = settings.border,
text = {
top = title,
},
},
opacity = settings.opacity,
}
end
local function exit(popup, opts)
if opts.action_before_exit and opts.cb ~= nil then
opts.cb()
popup:unmount()
else
popup:unmount()
if opts.cb ~= nil then
opts.cb()
end
end
end
-- These keymaps are buffer specific and are set dynamically when popups mount
M.set_popup_keymaps = function(popup, action, linewise_action, opts)
local settings = require("gitlab.state").settings
if settings.keymaps.disable_all or settings.keymaps.popup.disable_all then
return
end
if opts == nil then
opts = {}
end
if action ~= "Help" and settings.keymaps.help then -- Don't show help on the help popup
vim.keymap.set("n", settings.keymaps.help, function()
local help = require("gitlab.actions.help")
help.open()
end, { buffer = popup.bufnr, desc = "Open help", nowait = settings.keymaps.help_nowait })
end
if action ~= nil and settings.keymaps.popup.perform_action then
vim.keymap.set("n", settings.keymaps.popup.perform_action, function()
local text = u.get_buffer_text(popup.bufnr)
if opts.action_before_close then
action(text, popup.bufnr)
exit(popup, opts)
else
exit(popup, opts)
action(text, popup.bufnr)
end
end, { buffer = popup.bufnr, desc = "Perform action", nowait = settings.keymaps.popup.perform_action_nowait })
end
if linewise_action ~= nil and settings.keymaps.popup.perform_action then
vim.keymap.set("n", settings.keymaps.popup.perform_linewise_action, function()
local bufnr = vim.api.nvim_get_current_buf()
local linnr = vim.api.nvim_win_get_cursor(0)[1]
local text = u.get_line_content(bufnr, linnr)
linewise_action(text)
end, {
buffer = popup.bufnr,
desc = "Perform linewise action",
nowait = settings.keymaps.popup.perform_linewise_action_nowait,
})
end
if settings.keymaps.popup.discard_changes then
vim.keymap.set("n", settings.keymaps.popup.discard_changes, function()
local temp_registers = settings.popup.temp_registers
settings.popup.temp_registers = {}
vim.cmd("quit!")
settings.popup.temp_registers = temp_registers
end, {
buffer = popup.bufnr,
desc = "Quit discarding changes",
nowait = settings.keymaps.popup.discard_changes_nowait,
})
end
if opts.save_to_temp_register then
vim.api.nvim_create_autocmd("BufWinLeave", {
buffer = popup.bufnr,
callback = function()
local text = u.get_buffer_text(popup.bufnr)
for _, register in ipairs(settings.popup.temp_registers) do
vim.fn.setreg(register, text)
end
end,
})
end
if opts.action_before_exit then
vim.api.nvim_create_autocmd("BufWinLeave", {
buffer = popup.bufnr,
callback = function()
exit(popup, opts)
end,
})
end
end
--- Setup autocommands for the popup
--- @param popup NuiPopup
--- @param layout NuiLayout|nil
--- @param previous_window number|nil Number of window active before the popup was opened
--- @param opts table|nil Table with options for updating the popup
M.set_up_autocommands = function(popup, layout, previous_window, opts)
-- Make the popup/layout resizable
popup:on("VimResized", function()
if layout ~= nil then
layout:update()
else
popup:update_layout(opts and M.create_popup_state(unpack(opts)))
end
end)
-- After closing the popup, refocus the previously active window
if previous_window ~= nil then
popup:on("BufHidden", function()
vim.schedule(function()
vim.api.nvim_set_current_win(previous_window)
end)
end)
end
end
M.editable_popup_opts = {
action_before_close = true,
action_before_exit = false,
save_to_temp_register = true,
}
M.non_editable_popup_opts = {
action_before_close = true,
action_before_exit = false,
save_to_temp_register = false,
}
-- Get the index of the next popup when cycling forward
local function next_index(i, n, count)
count = count > 0 and count or 1
for _ = 1, count do
if i < n then
i = i + 1
elseif i == n then
i = 1
end
end
return i
end
---Get the index of the previous popup when cycling backward
---@param i integer The current index
---@param n integer The total number of popups
---@param count integer The count used with the keymap (replaced with 1 if no count was given)
local function prev_index(i, n, count)
count = count > 0 and count or 1
for _ = 1, count do
if i > 1 then
i = i - 1
elseif i == 1 then
i = n
end
end
return i
end
---Setup keymaps for cycling popups. The keymap accepts count.
---@param popups table Table of Popups
M.set_cycle_popups_keymaps = function(popups)
local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.popup.disable_all then
return
end
local number_of_popups = #popups
for i, popup in ipairs(popups) do
if keymaps.popup.next_field then
popup:map("n", keymaps.popup.next_field, function()
vim.api.nvim_set_current_win(popups[next_index(i, number_of_popups, vim.v.count)].winid)
end, { desc = "Go to next field (accepts count)", nowait = keymaps.popup.next_field_nowait })
end
if keymaps.popup.prev_field then
popup:map("n", keymaps.popup.prev_field, function()
vim.api.nvim_set_current_win(popups[prev_index(i, number_of_popups, vim.v.count)].winid)
end, { desc = "Go to previous field (accepts count)", nowait = keymaps.popup.prev_field_nowait })
end
end
end
return M

View File

@@ -42,12 +42,21 @@ M.open = function()
end
local diffview_open_command = "DiffviewOpen"
local has_clean_tree, err = git.has_clean_tree()
if err ~= nil then
return
end
if state.settings.reviewer_settings.diffview.imply_local and has_clean_tree then
diffview_open_command = diffview_open_command .. " --imply-local"
if state.settings.reviewer_settings.diffview.imply_local then
local has_clean_tree, err = git.has_clean_tree()
if err ~= nil then
return
end
if has_clean_tree then
diffview_open_command = diffview_open_command .. " --imply-local"
else
u.notify(
"Your working tree has changes, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
vim.log.levels.WARN
)
state.settings.reviewer_settings.diffview.imply_local = false
end
end
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
@@ -55,13 +64,6 @@ M.open = function()
M.is_open = true
M.tabnr = vim.api.nvim_get_current_tabpage()
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
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
u.notify(
"Diagnostics are now configured as settings.discussion_signs, see :h gitlab.nvim.signs-and-diagnostics",
@@ -289,12 +291,16 @@ end
M.execute_callback = function(callback)
return function()
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { "'[V']" } }, {})
vim.api.nvim_cmd(
local _, err = pcall(
vim.api.nvim_cmd,
{ cmd = "lua", args = { ("require'gitlab'.%s()"):format(callback) }, mods = { lockmarks = true } },
{}
)
vim.api.nvim_win_set_cursor(M.old_winnr, M.old_cursor_position)
vim.opt.operatorfunc = M.old_opfunc
if err ~= "" then
u.notify_vim_error(err, vim.log.levels.ERROR)
end
end
end

View File

@@ -116,6 +116,7 @@ M.settings = {
toggle_tree_type = "i",
publish_draft = "P",
toggle_draft_mode = "D",
toggle_sort_method = "st",
toggle_node = "t",
toggle_all_discussions = "T",
toggle_resolved_discussions = "R",
@@ -133,15 +134,18 @@ M.settings = {
popup = {
width = "40%",
height = "60%",
position = "50%",
border = "rounded",
opacity = 1.0,
edit = nil,
comment = nil,
edit = nil,
note = nil,
help = nil,
pipeline = nil,
reply = nil,
squash_message = nil,
create_mr = { width = "95%", height = "95%" },
summary = { width = "95%", height = "95%" },
temp_registers = {},
},
discussion_tree = {
@@ -150,15 +154,19 @@ M.settings = {
collapsed = "",
indentation = " ",
},
spinner_chars = { "-", "\\", "|", "/" },
auto_open = true,
default_view = "discussions",
blacklist = {},
sort_by = "latest_reply",
keep_current_open = false,
position = "left",
position = "bottom",
size = "20%",
relative = "editor",
resolved = "",
unresolved = "-",
unlinked = "󰌸",
draft = "",
tree_type = "simple",
draft_mode = false,
},
@@ -233,13 +241,17 @@ M.settings = {
username = "Keyword",
mention = "WarningMsg",
date = "Comment",
unlinked = "DiffviewNonText",
expander = "DiffviewNonText",
directory = "Directory",
directory_icon = "DiffviewFolderSign",
file_name = "Normal",
resolved = "DiagnosticSignOk",
unresolved = "DiagnosticSignWarn",
draft = "DiffviewNonText",
draft = "DiffviewReference",
draft_mode = "DiagnosticWarn",
live_mode = "DiagnosticOk",
sort_method = "Keyword",
},
},
}
@@ -427,94 +439,6 @@ M.setPluginConfiguration = function()
return true
end
local function exit(popup, opts)
if opts.action_before_exit and opts.cb ~= nil then
opts.cb()
popup:unmount()
else
popup:unmount()
if opts.cb ~= nil then
opts.cb()
end
end
end
-- These keymaps are buffer specific and are set dynamically when popups mount
M.set_popup_keymaps = function(popup, action, linewise_action, opts)
if M.settings.keymaps.disable_all or M.settings.keymaps.popup.disable_all then
return
end
if opts == nil then
opts = {}
end
if action ~= "Help" and M.settings.keymaps.help then -- Don't show help on the help popup
vim.keymap.set("n", M.settings.keymaps.help, function()
local help = require("gitlab.actions.help")
help.open()
end, { buffer = popup.bufnr, desc = "Open help", nowait = M.settings.keymaps.help_nowait })
end
if action ~= nil and M.settings.keymaps.popup.perform_action then
vim.keymap.set("n", M.settings.keymaps.popup.perform_action, function()
local text = u.get_buffer_text(popup.bufnr)
if opts.action_before_close then
action(text, popup.bufnr)
exit(popup, opts)
else
exit(popup, opts)
action(text, popup.bufnr)
end
end, { buffer = popup.bufnr, desc = "Perform action", nowait = M.settings.keymaps.popup.perform_action_nowait })
end
if linewise_action ~= nil and M.settings.keymaps.popup.perform_action then
vim.keymap.set("n", M.settings.keymaps.popup.perform_linewise_action, function()
local bufnr = vim.api.nvim_get_current_buf()
local linnr = vim.api.nvim_win_get_cursor(0)[1]
local text = u.get_line_content(bufnr, linnr)
linewise_action(text)
end, {
buffer = popup.bufnr,
desc = "Perform linewise action",
nowait = M.settings.keymaps.popup.perform_linewise_action_nowait,
})
end
if M.settings.keymaps.popup.discard_changes then
vim.keymap.set("n", M.settings.keymaps.popup.discard_changes, function()
local temp_registers = M.settings.popup.temp_registers
M.settings.popup.temp_registers = {}
vim.cmd("quit!")
M.settings.popup.temp_registers = temp_registers
end, {
buffer = popup.bufnr,
desc = "Quit discarding changes",
nowait = M.settings.keymaps.popup.discard_changes_nowait,
})
end
if opts.save_to_temp_register then
vim.api.nvim_create_autocmd("BufWinLeave", {
buffer = popup.bufnr,
callback = function()
local text = u.get_buffer_text(popup.bufnr)
for _, register in ipairs(M.settings.popup.temp_registers) do
vim.fn.setreg(register, text)
end
end,
})
end
if opts.action_before_exit then
vim.api.nvim_create_autocmd("BufWinLeave", {
buffer = popup.bufnr,
callback = function()
exit(popup, opts)
end,
})
end
end
-- Dependencies
-- These tables are passed to the async.sequence function, which calls them in sequence
-- before calling an action. They are used to set global state that's required
@@ -606,6 +530,7 @@ M.dependencies = {
body = function()
return {
blacklist = M.settings.discussion_tree.blacklist,
sort_by = M.settings.discussion_tree.sort_by,
}
end,
},

View File

@@ -122,7 +122,7 @@ M.time_since = function(date_string, current_date_table)
local time_diff = current_date - date
if time_diff < 60 then
return M.pluralize(time_diff, "second") .. " ago"
return "just now"
elseif time_diff < 3600 then
return M.pluralize(math.floor(time_diff / 60), "minute") .. " ago"
elseif time_diff < 86400 then
@@ -335,6 +335,11 @@ M.notify = function(msg, lvl)
vim.notify("gitlab.nvim: " .. msg, lvl)
end
-- Re-raise Vimscript error message after removing existing message prefixes
M.notify_vim_error = function(msg, lvl)
M.notify(msg:gsub("^Vim:", ""):gsub("^gitlab.nvim: ", ""), lvl)
end
M.get_current_line_number = function()
return vim.api.nvim_call_function("line", { "." })
end
@@ -427,6 +432,10 @@ M.press_enter = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false)
end
M.press_escape = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", false, true, true), "nx", false)
end
---Return timestamp from ISO 8601 formatted date string.
---@param date_string string ISO 8601 formatted date string
---@return integer timestamp
@@ -480,62 +489,6 @@ M.difference = function(a, b)
return not_included
end
---Get the popup view_opts
---@param title string The string to appear on top of the popup
---@param settings table|nil User defined popup settings
---@param width number? Override default width
---@param height number? Override default height
---@return table
M.create_popup_state = function(title, settings, width, height, zindex)
local default_settings = require("gitlab.state").settings.popup
local user_settings = settings or {}
local view_opts = {
buf_options = {
filetype = "markdown",
},
relative = "editor",
enter = true,
focusable = true,
zindex = zindex or 50,
border = {
style = user_settings.border or default_settings.border,
text = {
top = title,
},
},
position = "50%",
size = {
width = user_settings.width or width or default_settings.width,
height = user_settings.height or height or default_settings.height,
},
opacity = user_settings.opacity or default_settings.opacity,
}
return view_opts
end
---Create view_opts for Box popups used inside popup Layouts
---@param title string|nil The string to appear on top of the popup
---@param enter boolean Whether the pop should be focused after creation
---@return table
M.create_box_popup_state = function(title, enter)
local settings = require("gitlab.state").settings.popup
return {
buf_options = {
filetype = "markdown",
},
enter = enter or false,
focusable = true,
border = {
style = settings.border,
text = {
top = title,
},
},
opacity = settings.opacity,
}
end
M.read_file = function(file_path, opts)
local file = io.open(file_path, "r")
if file == nil then
@@ -634,7 +587,7 @@ end
M.check_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode
if mode ~= "v" and mode ~= "V" then
M.notify("Code suggestions are only available in visual mode", vim.log.levels.WARN)
M.notify("Code suggestions and multiline comments are only available in visual mode", vim.log.levels.WARN)
return false
end
return true
@@ -644,7 +597,7 @@ end
---Exists visual mode in order to access marks "<" , ">"
---@return integer start,integer end Start line and end line
M.get_visual_selection_boundaries = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", false, true, true), "nx", false)
M.press_escape()
local start_line = vim.api.nvim_buf_get_mark(0, "<")[1]
local end_line = vim.api.nvim_buf_get_mark(0, ">")[1]
return start_line, end_line