* 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. 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: 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: branches:
- main - main
- develop - develop
paths:
- 'cmd/**' # Ignore changes to the Lua code
- 'go.sum'
- 'go.mod'
jobs: jobs:
go_lint: go_lint:
name: Lint Go 💅 name: Lint Go 💅

View File

@@ -4,6 +4,8 @@ on:
branches: branches:
- main - main
- develop - develop
paths:
- 'lua/**' # Ignore changes to the Go code
jobs: jobs:
lua_lint: lua_lint:
name: Lint Lua 💅 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 - View and manage pipeline Jobs
- Upload files, jump to the browser, and a lot more! - 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-12-08 at 5 43 53PM](https://github.com/user-attachments/assets/cb9e94e3-3817-4846-ba44-16ec06ea7654)
![Screenshot 2024-01-13 at 10 43 17AM](https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/079842de-e8a4-45c5-98c2-dcafc799c904)
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335-afe1-d554e3804372 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>: With <a href="https://github.com/folke/lazy.nvim">Lazy</a>:
```lua ```lua
return { {
"harrisoncramer/gitlab.nvim", "harrisoncramer/gitlab.nvim",
dependencies = { dependencies = {
"MunifTanjim/nui.nvim", "MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim", "nvim-lua/plenary.nvim",
"sindrets/diffview.nvim", "sindrets/diffview.nvim",
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers. "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 build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
config = function() config = function()
require("gitlab").setup() 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 ```lua
use { {
"harrisoncramer/gitlab.nvim", "harrisoncramer/gitlab.nvim",
requires = { requires = {
"MunifTanjim/nui.nvim", "MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim", "nvim-lua/plenary.nvim",
"sindrets/diffview.nvim" "sindrets/diffview.nvim",
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers. "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.
}, },
build = function() run = function() require("gitlab.server").build() end, -- Builds the Go binary
require("gitlab.server").build()
end,
branch = "develop",
config = function() config = function()
require("diffview") -- We require some global state from diffview require("diffview") -- We require some global state from diffview
local gitlab = require("gitlab") require("gitlab").setup()
gitlab.setup()
end, 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 ## 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. 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. 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` 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 source $VIMRUNTIME/syntax/markdown.vim
endif endif
syntax match Date "\v\d+\s+\w+\s+ago" let expanders = '^\s*\%(' . g:gitlab_discussion_tree_expander_open . '\|' . g:gitlab_discussion_tree_expander_closed . '\)'
highlight link Date GitlabDate let username = '@[a-zA-Z0-9.]\+'
execute 'syntax match Unresolved /\s' . g:gitlab_discussion_tree_unresolved . '\s\?/' " Covers times like '14 days ago', 'just now', as well as 'October 3, 2024'
highlight link Unresolved GitlabUnresolved 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\?/' let published = date . ' \%(' . g:gitlab_discussion_tree_resolved . '\|' . g:gitlab_discussion_tree_unresolved . '\|' . g:gitlab_discussion_tree_unlinked . '\)\?'
highlight link Resolved GitlabResolved let state = ' \%(' . published . '\|' . g:gitlab_discussion_tree_draft . '\)'
execute 'syntax match GitlabDiscussionOpen /^\s*' . g:gitlab_discussion_tree_expander_open . '/' execute 'syntax match GitlabNoteHeader "' . expanders . username . state . '" contains=GitlabDate,GitlabUnresolved,GitlabUnlinked,GitlabResolved,GitlabExpander,GitlabDraft,GitlabUsername'
highlight link GitlabDiscussionOpen GitlabExpander
execute 'syntax match GitlabDiscussionClosed /^\s*' . g:gitlab_discussion_tree_expander_closed . '/' execute 'syntax match GitlabDate "' . date . '" contained'
highlight link GitlabDiscussionClosed GitlabExpander execute 'syntax match GitlabUnresolved "' . g:gitlab_discussion_tree_unresolved . '" contained'
execute 'syntax match GitlabUnlinked "' . g:gitlab_discussion_tree_unlinked . '" contained'
execute 'syntax match Draft /' . g:gitlab_discussion_tree_draft . '/' execute 'syntax match GitlabResolved "' . g:gitlab_discussion_tree_resolved . '" contained'
highlight link Draft GitlabDraft execute 'syntax match GitlabExpander "' . expanders . '" contained'
execute 'syntax match GitlabDraft "' . g:gitlab_discussion_tree_draft . '" contained'
execute 'syntax match Username "@[a-zA-Z0-9.]\+"' execute 'syntax match GitlabUsername "' . username . '" contained'
highlight link Username GitlabUsername execute 'syntax match GitlabMention "' . username . '"'
execute 'syntax match Mention "\%(' . g:gitlab_discussion_tree_expander_open . '\|'
\ . g:gitlab_discussion_tree_expander_closed . '\)\@<!@[a-zA-Z0-9.]*"'
highlight link Mention GitlabMention
let b:current_syntax = 'gitlab' let b:current_syntax = 'gitlab'

View File

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

View File

@@ -21,8 +21,14 @@ func (f fakeDiscussionsLister) ListMergeRequestDiscussions(pid interface{}, merg
if err != nil { if err != nil {
return nil, nil, err 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 { type Author struct {
ID int `json:"id"` ID int `json:"id"`
@@ -35,8 +41,18 @@ func (f fakeDiscussionsLister) ListMergeRequestDiscussions(pid interface{}, merg
} }
testListDiscussionsResponse := []*gitlab.Discussion{ testListDiscussionsResponse := []*gitlab.Discussion{
{Notes: []*gitlab.Note{{CreatedAt: &now, Type: "DiffNote", Author: Author{Username: "hcramer"}}}}, {Notes: []*gitlab.Note{
{Notes: []*gitlab.Note{{CreatedAt: &newer, Type: "DiffNote", Author: Author{Username: "hcramer2"}}}}, {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 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) { func TestListDiscussions(t *testing.T) {
t.Run("Returns sorted discussions", func(t *testing.T) { t.Run("Returns discussions sorted by latest reply", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}}) request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}, SortBy: "latest_reply"})
svc := middleware( svc := middleware(
discussionsListerService{testProjectData, fakeDiscussionsLister{}}, discussionsListerService{testProjectData, fakeDiscussionsLister{}},
withMr(testProjectData, fakeMergeRequestLister{}), withMr(testProjectData, fakeMergeRequestLister{}),
@@ -76,12 +92,28 @@ func TestListDiscussions(t *testing.T) {
) )
data := getDiscussionsList(t, svc, request) data := getDiscussionsList(t, svc, request)
assert(t, data.Message, "Discussions retrieved") assert(t, data.Message, "Discussions retrieved")
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2") /* Sorting applied */ assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer4") /* Sorting applied */
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer") 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) { 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( svc := middleware(
discussionsListerService{testProjectData, fakeDiscussionsLister{}}, discussionsListerService{testProjectData, fakeDiscussionsLister{}},
withMr(testProjectData, fakeMergeRequestLister{}), withMr(testProjectData, fakeMergeRequestLister{}),
@@ -90,8 +122,9 @@ func TestListDiscussions(t *testing.T) {
) )
data := getDiscussionsList(t, svc, request) data := getDiscussionsList(t, svc, request)
assert(t, data.SuccessResponse.Message, "Discussions retrieved") assert(t, data.SuccessResponse.Message, "Discussions retrieved")
assert(t, len(data.Discussions), 1) assert(t, len(data.Discussions), 2)
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2") 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) { t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}}) request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})

View File

@@ -59,25 +59,24 @@ INSTALLATION *gitlab.nvim.installation*
With Lazy: With Lazy:
>lua >lua
return { {
"harrisoncramer/gitlab.nvim", "harrisoncramer/gitlab.nvim",
dependencies = { dependencies = {
"MunifTanjim/nui.nvim", "MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim", "nvim-lua/plenary.nvim",
"sindrets/diffview.nvim", "sindrets/diffview.nvim",
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers. "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 build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
config = function() config = function()
require("gitlab").setup() require("gitlab").setup()
end, end,
} }
< <
And with Packer: And with pckr.nvim:
>lua >lua
use { {
"harrisoncramer/gitlab.nvim", "harrisoncramer/gitlab.nvim",
requires = { requires = {
"MunifTanjim/nui.nvim", "MunifTanjim/nui.nvim",
@@ -86,14 +85,10 @@ And with Packer:
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers. "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.
}, },
build = function() run = function() require("gitlab.server").build() end, -- Builds the Go binary
require("gitlab.server").build()
end,
branch = "develop",
config = function() config = function()
require("diffview") -- We require some global state from diffview require("diffview") -- We require some global state from diffview
local gitlab = require("gitlab") require("gitlab").setup()
gitlab.setup()
end, 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|. next_field = "<Tab>", -- Cycle to the next field. Accepts |count|.
prev_field = "<S-Tab>", -- Cycle to the previous 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_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`) 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 = { 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" toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
publish_draft = "P", -- Publish the currently focused note/comment 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_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_node = "t", -- Open or close the discussion
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
toggle_resolved_discussions = "R", -- Open or close all resolved discussions toggle_resolved_discussions = "R", -- Open or close all resolved discussions
@@ -234,16 +230,20 @@ you call this function with no values the defaults will be used:
}, },
}, },
popup = { -- The popup for comment creation, editing, and replying popup = { -- The popup for comment creation, editing, and replying
width = "40%", width = "40%", -- Can be a percentage (string or decimal, "40%" = 0.4) of editor screen width, or an integer (number of columns)
height = "60%", 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" border = "rounded", -- One of "rounded", "single", "double", "solid"
opacity = 1.0, -- From 0.0 (fully transparent) to 1.0 (fully opaque) 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 }, comment = nil, -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 },
edit = nil, edit = nil,
note = 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, reply = nil,
squash_message = 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`) 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 discussion_tree = { -- The discussion tree that holds all comments
@@ -252,18 +252,22 @@ you call this function with no values the defaults will be used:
collapsed = " ", -- Icon for collapsed discussion thread collapsed = " ", -- Icon for collapsed discussion thread
indentation = " ", -- Indentation Icon indentation = " ", -- Indentation Icon
}, },
spinner_chars = { "/", "|", "\\", "-" }, -- Characters for the refresh animation
auto_open = true, -- Automatically open when the reviewer is opened 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) 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 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 size = "20%", -- Size of split
relative = "editor", -- Position of tree split relative to "editor" or "window" relative = "editor", -- Position of tree split relative to "editor" or "window"
resolved = '✓', -- Symbol to show next to resolved discussions resolved = '✓', -- Symbol to show next to resolved discussions
unresolved = '-', -- Symbol to show next to unresolved 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 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 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. -- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
}, },
emojis = { 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 squash = false, -- Whether the commits will be marked for squashing
fork = { fork = {
enabled = false, -- If making an MR from a 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 title_input = { -- Default settings for MR title input window
width = 40, width = 40,
@@ -349,6 +353,9 @@ you call this function with no values the defaults will be used:
resolved = "DiagnosticSignOk", resolved = "DiagnosticSignOk",
unresolved = "DiagnosticSignWarn", unresolved = "DiagnosticSignWarn",
draft = "DiffviewNonText", 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 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. 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.nvim.add_assignee*
gitlab.add_assignee() ~ gitlab.add_assignee() ~

View File

@@ -1,13 +1,26 @@
local job = require("gitlab.job") local job = require("gitlab.job")
local state = require("gitlab.state")
local u = require("gitlab.utils")
local M = {} 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() M.approve = function()
job.run_job("/mr/approve", "POST") job.run_job("/mr/approve", "POST", nil, function(data)
refresh_status_state(data)
end)
end end
M.revoke = function() M.revoke = function()
job.run_job("/mr/revoke", "POST") job.run_job("/mr/revoke", "POST", nil, function(data)
refresh_status_state(data)
end)
end end
return M return M

View File

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

View File

@@ -3,10 +3,10 @@
--- to this module the data required to make the API calls --- to this module the data required to make the API calls
local Popup = require("nui.popup") local Popup = require("nui.popup")
local Layout = require("nui.layout") local Layout = require("nui.layout")
local diffview_lib = require("diffview.lib")
local state = require("gitlab.state") local state = require("gitlab.state")
local job = require("gitlab.job") local job = require("gitlab.job")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local git = require("gitlab.git") local git = require("gitlab.git")
local discussions = require("gitlab.actions.discussions") local discussions = require("gitlab.actions.discussions")
local draft_notes = require("gitlab.actions.draft_notes") local draft_notes = require("gitlab.actions.draft_notes")
@@ -46,6 +46,18 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
return return
end 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) -- Creating a note (unlinked comment)
if unlinked and discussion_id == nil then if unlinked and discussion_id == nil then
local body = { comment = text } local body = { comment = text }
@@ -89,18 +101,6 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
line_range = location_data.line_range, 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) -- Creating a new comment (linked to specific changes)
local body = u.merge({ type = "text", comment = text }, position_data) local body = u.merge({ type = "text", comment = text }, position_data)
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment" 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 ---multi-line comment. It also sets up the basic keybindings for switching between
---window panes, and for the non-primary sections. ---window panes, and for the non-primary sections.
---@param opts LayoutOpts ---@param opts LayoutOpts
---@return NuiLayout|nil ---@return NuiLayout
M.create_comment_layout = function(opts) M.create_comment_layout = function(opts)
if opts.unlinked ~= true and opts.discussion_id == nil then local popup_settings = state.settings.popup
-- Check that diffview is initialized local title
if reviewer.tabnr == nil then local user_settings
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR) if opts.discussion_id ~= nil then
return 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 end
local settings = u.merge(popup_settings, user_settings or {})
-- 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
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
M.current_win = vim.api.nvim_get_current_win() M.current_win = vim.api.nvim_get_current_win()
M.comment_popup = Popup(u.create_popup_state(title, settings)) M.comment_popup = Popup(popup.create_popup_state(title, settings))
M.draft_popup = Popup(u.create_box_popup_state("Draft", false)) M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings))
M.start_line, M.end_line = u.get_visual_selection_boundaries() M.start_line, M.end_line = u.get_visual_selection_boundaries()
local internal_layout = Layout.Box({ local internal_layout = Layout.Box({
@@ -211,98 +185,69 @@ M.create_comment_layout = function(opts)
}, { dir = "col" }) }, { dir = "col" })
local layout = Layout({ local layout = Layout({
position = "50%", position = settings.position,
relative = "editor", relative = "editor",
size = { size = {
width = "50%", width = settings.width,
height = "55%", height = settings.height,
}, },
}, internal_layout) }, 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 range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
local unlinked = opts.unlinked or false local unlinked = opts.unlinked or false
---Keybinding for focus on draft section ---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) local text = u.get_buffer_text(M.comment_popup.bufnr)
confirm_create_comment(text, range, unlinked, opts.discussion_id) confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win) 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 ---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) confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win) 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() vim.schedule(function()
local draft_mode = state.settings.discussion_tree.draft_mode 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) }) vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
end) 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 return layout
end end
--- This function will open a comment popup in order to create a comment on the changed/updated --- This function will open a comment popup in order to create a comment on the changed/updated
--- line in the current MR --- line in the current MR
M.create_comment = function() M.create_comment = function()
local has_clean_tree, err = git.has_clean_tree() if not M.can_create_comment(false) then
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
return return
end end
local layout = M.create_comment_layout({ ranged = false, unlinked = false }) local layout = M.create_comment_layout({ ranged = false, unlinked = false })
if layout ~= nil then
layout:mount() layout:mount()
end
end end
--- This function will open a multi-line comment popup in order to create a multi-line comment --- 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 --- on the changed/updated line in the current MR
M.create_multiline_comment = function() M.create_multiline_comment = function()
if not u.check_visual_mode() then if not M.can_create_comment(true) then
return u.press_escape()
end
if not M.sha_exists() then
return return
end end
local layout = M.create_comment_layout({ ranged = true, unlinked = false }) local layout = M.create_comment_layout({ ranged = true, unlinked = false })
if layout ~= nil then
layout:mount() layout:mount()
end
end end
--- This function will open a a popup to create a "note" (e.g. unlinked comment) --- This function will open a a popup to create a "note" (e.g. unlinked comment)
--- on the changed/updated line in the current MR --- on the changed/updated line in the current MR
M.create_note = function() M.create_note = function()
local layout = M.create_comment_layout({ ranged = false, unlinked = true }) local layout = M.create_comment_layout({ ranged = false, unlinked = true })
if layout ~= nil then
layout:mount() layout:mount()
end
end end
---Given the current visually selected area of text, builds text to fill in the ---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 --- on the changed/updated line in the current MR
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html --- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
M.create_comment_suggestion = function() M.create_comment_suggestion = function()
if not u.check_visual_mode() then if not M.can_create_comment(true) then
return u.press_escape()
end
if not M.sha_exists() then
return return
end end
local suggestion_lines, range_length = build_suggestion() local suggestion_lines, range_length = build_suggestion()
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false }) local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
if layout ~= nil then
layout:mount() layout:mount()
else
return -- Failure in creating the comment layout
end
vim.schedule(function() vim.schedule(function()
if suggestion_lines then if suggestion_lines then
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines) 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)
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 ---Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
---buffers as 'null' ---buffers as 'null'
---@return boolean ---@return boolean

View File

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

View File

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

View File

@@ -7,12 +7,12 @@ local Popup = require("nui.popup")
local NuiTree = require("nui.tree") local NuiTree = require("nui.tree")
local job = require("gitlab.job") local job = require("gitlab.job")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local state = require("gitlab.state") local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer") local reviewer = require("gitlab.reviewer")
local common = require("gitlab.actions.common") local common = require("gitlab.actions.common")
local List = require("gitlab.utils.list") local List = require("gitlab.utils.list")
local tree_utils = require("gitlab.actions.discussions.tree") local tree_utils = require("gitlab.actions.discussions.tree")
local miscellaneous = require("gitlab.actions.miscellaneous")
local discussions_tree = require("gitlab.actions.discussions.tree") local discussions_tree = require("gitlab.actions.discussions.tree")
local draft_notes = require("gitlab.actions.draft_notes") local draft_notes = require("gitlab.actions.draft_notes")
local diffview_lib = require("diffview.lib") local diffview_lib = require("diffview.lib")
@@ -48,13 +48,15 @@ M.rebuild_view = function(unlinked, all)
else else
M.rebuild_discussion_tree() M.rebuild_discussion_tree()
end end
M.refresh_diagnostics_and_winbar() state.discussion_tree.last_updated = os.time()
M.refresh_diagnostics()
end) end)
end end
---Makes API call to get the discussion data, stores it in the state, and calls the callback ---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil ---@param callback function|nil
M.load_discussions = function(callback) M.load_discussions = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("discussion_data", function(data) state.load_new_state("discussion_data", function(data)
if not state.DISCUSSION_DATA then if not state.DISCUSSION_DATA then
state.DISCUSSION_DATA = {} state.DISCUSSION_DATA = {}
@@ -70,9 +72,10 @@ end
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc. ---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
M.initialize_discussions = function() M.initialize_discussions = function()
state.discussion_tree.last_updated = os.time()
signs.setup_signs() signs.setup_signs()
reviewer.set_callback_for_file_changed(function() reviewer.set_callback_for_file_changed(function()
M.refresh_diagnostics_and_winbar() M.refresh_diagnostics()
M.modifiable(false) M.modifiable(false)
reviewer.set_reviewer_keymaps() reviewer.set_reviewer_keymaps()
end) end)
@@ -102,11 +105,10 @@ M.modifiable = function(bool)
end end
--- Take existing data and refresh the diagnostics, the winbar, and the signs --- 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 if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics() diagnostics.refresh_diagnostics()
end end
winbar.update_winbar()
common.add_empty_titles() common.add_empty_titles()
end end
@@ -154,7 +156,7 @@ M.open = function(callback)
end end
vim.schedule(function() vim.schedule(function()
M.refresh_diagnostics_and_winbar() M.refresh_diagnostics()
end) end)
end end
@@ -251,9 +253,7 @@ M.reply = function(tree)
reply = true, reply = true,
}) })
if layout then
layout:mount() layout:mount()
end
end end
-- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment -- 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 -- 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) 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 current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node) local note_node = common.get_note_node(tree, current_node)
local root_node = common.get_root_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 return
end end
popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
edit_popup:mount() edit_popup:mount()
-- Gather all lines from immediate children that aren't note nodes -- 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 -- Draft notes module handles edits for draft notes
if M.is_draft_note(tree) then if M.is_draft_note(tree) then
state.set_popup_keymaps( popup.set_popup_keymaps(
edit_popup, edit_popup,
draft_notes.confirm_edit_draft_note(note_node.id, unlinked), draft_notes.confirm_edit_draft_note(note_node.id, unlinked),
nil, nil,
miscellaneous.editable_popup_opts popup.editable_popup_opts
) )
else else
local comment = require("gitlab.actions.comment") local comment = require("gitlab.actions.comment")
state.set_popup_keymaps( popup.set_popup_keymaps(
edit_popup, edit_popup,
comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked), comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
nil, nil,
miscellaneous.editable_popup_opts popup.editable_popup_opts
) )
end end
end end
@@ -585,7 +587,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.jump_to_reviewer then if keymaps.discussion_tree.jump_to_reviewer then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function() vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
if M.is_current_node_note(tree) then 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
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait }) end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
end end
@@ -603,7 +605,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.refresh_data then if keymaps.discussion_tree.refresh_data then
vim.keymap.set("n", keymaps.discussion_tree.refresh_data, function() vim.keymap.set("n", keymaps.discussion_tree.refresh_data, function()
u.notify("Refreshing data...", vim.log.levels.INFO)
draft_notes.rebuild_view(unlinked, false) draft_notes.rebuild_view(unlinked, false)
end, { end, {
buffer = bufnr, buffer = bufnr,
@@ -646,6 +647,16 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
}) })
end 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 if keymaps.discussion_tree.toggle_resolved then
vim.keymap.set("n", keymaps.discussion_tree.toggle_resolved, function() 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 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 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 if keymaps.discussion_tree.add_emoji then
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function() vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
M.add_emoji_to_note(tree, unlinked) 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) ---Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
M.toggle_draft_mode = function() M.toggle_draft_mode = function()
state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode 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() winbar.update_winbar()
M.rebuild_view(false, true)
end end
---Indicates whether the node under the cursor is a draft note or not ---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 end
local resolve_symbol = "" local symbol = ""
local is_draft = note.note ~= nil
if resolve_info ~= nil and resolve_info.resolvable then 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 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 end
local noteHeader = common.build_note_header(note) .. " " .. resolve_symbol local noteHeader = common.build_note_header(note) .. " " .. symbol
return noteHeader, text_nodes return noteHeader, text_nodes
end end
@@ -454,8 +457,10 @@ M.restore_cursor_position = function(winid, tree, original_node, root_node)
end end
end end
if line_number ~= nil then if line_number ~= nil then
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_set_cursor(winid, { line_number, 0 }) vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
end end
end
end end
---This function expands a node and its children. ---This function expands a node and its children.

View File

@@ -54,7 +54,19 @@ local get_data = function(nodes)
return total_resolvable, total_resolved, total_non_resolvable return total_resolvable, total_resolved, total_non_resolvable
end end
local spinner_index = 0
state.discussion_tree.last_updated = nil
local function content() 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 = local resolvable_discussions, resolved_discussions, non_resolvable_discussions =
get_data(state.DISCUSSION_DATA.discussions) get_data(state.DISCUSSION_DATA.discussions)
local resolvable_notes, resolved_notes, non_resolvable_notes = get_data(state.DISCUSSION_DATA.unlinked_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, resolved_notes = resolved_notes,
non_resolvable_notes = non_resolvable_notes, non_resolvable_notes = non_resolvable_notes,
help_keymap = state.settings.keymaps.help, 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 end
---This function updates the winbar ---This function updates the winbar
@@ -108,7 +121,7 @@ M.update_winbar = function()
end end
local function get_connector(base_title) local function get_connector(base_title)
return string.match(base_title, "%($") and "" or "; " return string.match(base_title, "%($") and "" or " "
end end
---Builds the title string for both sections, using the count of resolvable and draft nodes ---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 resolvable_count integer
---@param resolved_count integer ---@param resolved_count integer
---@param drafts_count integer ---@param drafts_count integer
---@param focused boolean
---@return string ---@return string
local add_drafts_and_resolvable = function( local add_drafts_and_resolvable = function(
base_title, base_title,
resolvable_count, resolvable_count,
resolved_count, resolved_count,
drafts_count, drafts_count,
non_resolvable_count non_resolvable_count,
focused
) )
if resolvable_count == 0 and drafts_count == 0 and non_resolvable_count == 0 then if resolvable_count == 0 and drafts_count == 0 and non_resolvable_count == 0 then
return base_title return base_title
end 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 if resolvable_count ~= 0 then
base_title = base_title base_title = base_title .. M.get_resolved_text(focused, resolved_count, resolvable_count)
.. get_connector(base_title) end
.. string.format("%d/%s", resolved_count, u.pluralize(resolvable_count, "thread")) if non_resolvable_count ~= 0 then
base_title = base_title .. M.get_nonresolveable_text(base_title, non_resolvable_count, focused)
end end
if drafts_count ~= 0 then 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 end
base_title = base_title .. ")"
return base_title return base_title
end end
---@param t WinbarTable ---@param t WinbarTable
M.make_winbar = function(t) M.make_winbar = function(t)
local discussion_title = add_drafts_and_resolvable( local discussions_focused = M.current_view_type == "discussions"
"Inline Comments", local discussion_text = add_drafts_and_resolvable(
"Inline Comments:",
t.resolvable_discussions, t.resolvable_discussions,
t.resolved_discussions, t.resolved_discussions,
t.inline_draft_notes, t.inline_draft_notes,
t.non_resolvable_discussions t.non_resolvable_discussions,
discussions_focused
) )
local notes_title = add_drafts_and_resolvable( local notes_text = add_drafts_and_resolvable(
"Notes", "Notes:",
t.resolvable_notes, t.resolvable_notes,
t.resolved_notes, t.resolved_notes,
t.unlinked_draft_notes, t.unlinked_draft_notes,
t.non_resolvable_notes t.non_resolvable_notes,
not discussions_focused
) )
-- Colorize the active tab -- Colorize the active tab
if M.current_view_type == "discussions" then if discussions_focused then
discussion_title = "%#Text#" .. discussion_title discussion_text = "%#Text#" .. discussion_text
notes_title = "%#Comment#" .. notes_title notes_text = "%#Comment#" .. notes_text
elseif M.current_view_type == "notes" then else
discussion_title = "%#Comment#" .. discussion_title discussion_text = "%#Comment#" .. discussion_text
notes_title = "%#Text#" .. notes_title notes_text = "%#Text#" .. notes_text
end end
local sort_method = M.get_sort_method()
local mode = M.get_mode() local mode = M.get_mode()
-- Join everything together and return it -- Join everything together and return it
local separator = "%#Comment#|" local separator = "%#Comment#|"
local end_section = "%=" local end_section = "%="
local updated = "%#Text#" .. t.updated
local help = "%#Comment#Help: " .. (t.help_keymap and t.help_keymap:gsub(" ", "<space>") .. " " or "unmapped") local help = "%#Comment#Help: " .. (t.help_keymap and t.help_keymap:gsub(" ", "<space>") .. " " or "unmapped")
return string.format( return string.format(
" %s %s %s %s %s %s %s", " %s %s %s %s %s %s %s %s %s %s %s",
discussion_title, discussion_text,
separator, separator,
notes_title, notes_text,
end_section, end_section,
updated,
separator,
sort_method,
separator,
mode, mode,
separator, separator,
help help
) )
end 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 ---Returns a string for the winbar indicating the mode type, live or draft
---@return string ---@return string
M.get_mode = function() M.get_mode = function()
if state.settings.discussion_tree.draft_mode then if state.settings.discussion_tree.draft_mode then
return "%#DiagnosticWarn#Draft Mode" return "%#GitlabDraftMode#Draft"
else else
return "%#DiagnosticOK#Live Mode" return "%#GitlabLiveMode#Live"
end end
end end
@@ -215,4 +272,8 @@ M.switch_view_type = function(override)
M.update_winbar() M.update_winbar()
end 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 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 ---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil ---@param callback function|nil
M.load_draft_notes = function(callback) M.load_draft_notes = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("draft_notes", function() state.load_new_state("draft_notes", function()
if callback ~= nil then if callback ~= nil then
callback() callback()

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
local u = require("gitlab.utils") local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local Popup = require("nui.popup") local Popup = require("nui.popup")
local state = require("gitlab.state") local state = require("gitlab.state")
local job = require("gitlab.job") local job = require("gitlab.job")
local reviewer = require("gitlab.reviewer") local reviewer = require("gitlab.reviewer")
local miscellaneous = require("gitlab.actions.miscellaneous")
local M = {} local M = {}
local function create_squash_message_popup() 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 end
---@class MergeOpts ---@class MergeOpts
@@ -31,10 +31,11 @@ M.merge = function(opts)
if merge_body.squash then if merge_body.squash then
local squash_message_popup = create_squash_message_popup() 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() 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) M.confirm_merge(merge_body, text)
end, nil, miscellaneous.editable_popup_opts) end, nil, popup.editable_popup_opts)
else else
M.confirm_merge(merge_body) M.confirm_merge(merge_body)
end end

View File

@@ -12,14 +12,6 @@ local M = {}
---Opens up a select menu that lets you choose a different merge request. ---Opens up a select menu that lets you choose a different merge request.
---@param opts ChooseMergeRequestOptions|nil ---@param opts ChooseMergeRequestOptions|nil
M.choose_merge_request = function(opts) 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 if opts == nil then
opts = state.settings.choose_merge_request opts = state.settings.choose_merge_request
end end
@@ -38,6 +30,19 @@ M.choose_merge_request = function(opts)
reviewer.close() reviewer.close()
end 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() vim.schedule(function()
local _, branch_switch_err = git.switch_branch(choice.source_branch) local _, branch_switch_err = git.switch_branch(choice.source_branch)
if branch_switch_err ~= nil then if branch_switch_err ~= nil then

View File

@@ -34,70 +34,6 @@ M.attach_file = function()
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
---Toggle the value in a "Boolean buffer" ---Toggle the value in a "Boolean buffer"
M.toggle_bool = function() M.toggle_bool = function()
local bufnr = vim.api.nvim_get_current_buf() 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 state = require("gitlab.state")
local job = require("gitlab.job") local job = require("gitlab.job")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local M = { local M = {
pipeline_jobs = nil, pipeline_jobs = nil,
latest_pipeline = nil, latest_pipeline = nil,
@@ -40,7 +41,8 @@ M.open = function()
local height = 6 + #M.pipeline_jobs + 3 local height = 6 + #M.pipeline_jobs + 3
local pipeline_popup = 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 M.pipeline_popup = pipeline_popup
pipeline_popup:mount() pipeline_popup:mount()
@@ -91,7 +93,7 @@ M.open = function()
end end
pipeline_popup.border:set_text("top", "Pipeline Status", "center") 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) u.switch_can_edit_buf(bufnr, false)
end) end)
end end

View File

@@ -7,6 +7,7 @@ local git = require("gitlab.git")
local job = require("gitlab.job") local job = require("gitlab.job")
local common = require("gitlab.actions.common") local common = require("gitlab.actions.common")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local List = require("gitlab.utils.list") local List = require("gitlab.utils.list")
local state = require("gitlab.state") local state = require("gitlab.state")
local miscellaneous = require("gitlab.actions.miscellaneous") 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 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) local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
layout:mount()
local popups = { local popups = {
title_popup, title_popup,
description_popup, description_popup,
@@ -41,6 +45,9 @@ M.summary = function()
} }
M.layout = layout 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_buf = layout.bufnr
M.layout_visible = true M.layout_visible = true
@@ -54,30 +61,28 @@ M.summary = function()
vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { title }) vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { title })
if info_popup then if info_popup then
vim.api.nvim_buf_set_lines(info_popup.bufnr, 0, -1, false, info_lines) M.update_details_popup(info_popup.bufnr, info_lines)
u.switch_can_edit_buf(info_popup.bufnr, false)
M.color_details(info_popup.bufnr) -- Color values in details popup
end end
state.set_popup_keymaps( popup.set_popup_keymaps(
description_popup, description_popup,
M.edit_summary, M.edit_summary,
miscellaneous.attach_file, miscellaneous.attach_file,
{ cb = exit, action_before_close = true, action_before_exit = true, save_to_temp_register = true } { 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, title_popup,
M.edit_summary, M.edit_summary,
nil, nil,
{ cb = exit, action_before_close = true, action_before_exit = true } { cb = exit, action_before_close = true, action_before_exit = true }
) )
state.set_popup_keymaps( popup.set_popup_keymaps(
info_popup, info_popup,
M.edit_summary, M.edit_summary,
nil, nil,
{ cb = exit, action_before_close = true, action_before_exit = true } { 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) vim.api.nvim_set_current_buf(description_popup.bufnr)
end) end)
@@ -86,6 +91,23 @@ M.summary = function()
git.check_mr_in_good_condition() git.check_mr_in_good_condition()
end 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 -- 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. -- lines that users include in their state.settings.info.fields list.
M.build_info_lines = function() M.build_info_lines = function()
@@ -165,16 +187,37 @@ M.edit_summary = function()
end) end)
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) 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 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 M.description_bufnr = description_popup.bufnr
local details_popup 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 local internal_layout
if state.settings.info.enabled then if state.settings.info.enabled then
details_popup = Popup(u.create_box_popup_state("Details", false))
if state.settings.info.horizontal then if state.settings.info.horizontal then
local longest_line = u.get_longest_string(info_lines) local longest_line = u.get_longest_string(info_lines)
internal_layout = Layout.Box({ internal_layout = Layout.Box({
@@ -182,7 +225,7 @@ M.create_layout = function(info_lines)
Layout.Box({ Layout.Box({
Layout.Box(details_popup, { size = longest_line + 3 }), Layout.Box(details_popup, { size = longest_line + 3 }),
Layout.Box(description_popup, { grow = 1 }), Layout.Box(description_popup, { grow = 1 }),
}, { dir = "row", size = "100%" }), }, { dir = "row", size = "95%" }),
}, { dir = "col" }) }, { dir = "col" })
else else
internal_layout = Layout.Box({ internal_layout = Layout.Box({
@@ -197,18 +240,21 @@ M.create_layout = function(info_lines)
Layout.Box(description_popup, { grow = 1 }), Layout.Box(description_popup, { grow = 1 }),
}, { dir = "col" }) }, { dir = "col" })
end end
return internal_layout
end
local layout = Layout({ ---Create the config for the outer Layout of the Summary
position = "50%", ---@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", relative = "editor",
size = { size = {
width = "95%", width = settings.width,
height = "95%", height = settings.height,
}, },
}, internal_layout) }
layout:mount()
return layout, title_popup, description_popup, details_popup
end end
M.color_details = function(bufnr) M.color_details = function(bufnr)

View File

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

View File

@@ -3,12 +3,13 @@ local state = require("gitlab.state")
local colors = state.settings.colors local colors = state.settings.colors
-- Set icons into global vim variables for syntax matching -- Set icons into global vim variables for syntax matching
local expanders = state.settings.discussion_tree.expanders local discussion_tree = state.settings.discussion_tree
vim.g.gitlab_discussion_tree_expander_open = expanders.expanded vim.g.gitlab_discussion_tree_expander_open = discussion_tree.expanders.expanded
vim.g.gitlab_discussion_tree_expander_closed = expanders.collapsed vim.g.gitlab_discussion_tree_expander_closed = discussion_tree.expanders.collapsed
vim.g.gitlab_discussion_tree_draft = "" vim.g.gitlab_discussion_tree_draft = discussion_tree.draft
vim.g.gitlab_discussion_tree_resolved = "" vim.g.gitlab_discussion_tree_resolved = discussion_tree.resolved
vim.g.gitlab_discussion_tree_unresolved = "-" vim.g.gitlab_discussion_tree_unresolved = discussion_tree.unresolved
vim.g.gitlab_discussion_tree_unlinked = discussion_tree.unlinked
local discussion = colors.discussion_tree 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") local normal_bg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(group)), "bg")
return { fg = normal_fg, bg = normal_bg } return { fg = normal_fg, bg = normal_bg }
end end
vim.api.nvim_create_autocmd("VimEnter", { vim.api.nvim_create_autocmd("VimEnter", {
callback = function() callback = function()
vim.api.nvim_set_hl(0, "GitlabUsername", get_colors_for_group(discussion.username)) 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, "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, "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, "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, "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, end,
}) })

View File

@@ -25,10 +25,19 @@ M.branches = function(args)
return run_system(u.combine({ "git", "branch" }, args or {})) return run_system(u.combine({ "git", "branch" }, args or {}))
end end
---Checks whether the tree has any changes that haven't been pushed to the remote ---Returns true if the working tree hasn't got any changes that haven't been commited
---@return string|nil, string|nil ---@return boolean, string|nil
M.has_clean_tree = function() 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 end
---Gets the base directory of the current project ---Gets the base directory of the current project

View File

@@ -92,10 +92,10 @@ return {
end end
end, end,
toggle_draft_mode = discussions.toggle_draft_mode, toggle_draft_mode = discussions.toggle_draft_mode,
toggle_sort_method = discussions.toggle_sort_method,
publish_all_drafts = draft_notes.publish_all_drafts, publish_all_drafts = draft_notes.publish_all_drafts,
refresh_data = function() refresh_data = function()
-- This also rebuilds the regular views -- This also rebuilds the regular views
u.notify("Refreshing data...", vim.log.levels.INFO)
draft_notes.rebuild_view(false, true) draft_notes.rebuild_view(false, true)
end, end,
-- Other functions 🤷 -- 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 end
local diffview_open_command = "DiffviewOpen" local diffview_open_command = "DiffviewOpen"
if state.settings.reviewer_settings.diffview.imply_local then
local has_clean_tree, err = git.has_clean_tree() local has_clean_tree, err = git.has_clean_tree()
if err ~= nil then if err ~= nil then
return return
end end
if state.settings.reviewer_settings.diffview.imply_local and has_clean_tree then if has_clean_tree then
diffview_open_command = diffview_open_command .. " --imply-local" 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 end
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha)) 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.is_open = true
M.tabnr = vim.api.nvim_get_current_tabpage() 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 if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
u.notify( u.notify(
"Diagnostics are now configured as settings.discussion_signs, see :h gitlab.nvim.signs-and-diagnostics", "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) M.execute_callback = function(callback)
return function() return function()
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { "'[V']" } }, {}) 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 } }, { 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.api.nvim_win_set_cursor(M.old_winnr, M.old_cursor_position)
vim.opt.operatorfunc = M.old_opfunc vim.opt.operatorfunc = M.old_opfunc
if err ~= "" then
u.notify_vim_error(err, vim.log.levels.ERROR)
end
end end
end end

View File

@@ -116,6 +116,7 @@ M.settings = {
toggle_tree_type = "i", toggle_tree_type = "i",
publish_draft = "P", publish_draft = "P",
toggle_draft_mode = "D", toggle_draft_mode = "D",
toggle_sort_method = "st",
toggle_node = "t", toggle_node = "t",
toggle_all_discussions = "T", toggle_all_discussions = "T",
toggle_resolved_discussions = "R", toggle_resolved_discussions = "R",
@@ -133,15 +134,18 @@ M.settings = {
popup = { popup = {
width = "40%", width = "40%",
height = "60%", height = "60%",
position = "50%",
border = "rounded", border = "rounded",
opacity = 1.0, opacity = 1.0,
edit = nil,
comment = nil, comment = nil,
edit = nil,
note = nil, note = nil,
help = nil, help = nil,
pipeline = nil, pipeline = nil,
reply = nil, reply = nil,
squash_message = nil, squash_message = nil,
create_mr = { width = "95%", height = "95%" },
summary = { width = "95%", height = "95%" },
temp_registers = {}, temp_registers = {},
}, },
discussion_tree = { discussion_tree = {
@@ -150,15 +154,19 @@ M.settings = {
collapsed = "", collapsed = "",
indentation = " ", indentation = " ",
}, },
spinner_chars = { "-", "\\", "|", "/" },
auto_open = true, auto_open = true,
default_view = "discussions", default_view = "discussions",
blacklist = {}, blacklist = {},
sort_by = "latest_reply",
keep_current_open = false, keep_current_open = false,
position = "left", position = "bottom",
size = "20%", size = "20%",
relative = "editor", relative = "editor",
resolved = "", resolved = "",
unresolved = "-", unresolved = "-",
unlinked = "󰌸",
draft = "",
tree_type = "simple", tree_type = "simple",
draft_mode = false, draft_mode = false,
}, },
@@ -233,13 +241,17 @@ M.settings = {
username = "Keyword", username = "Keyword",
mention = "WarningMsg", mention = "WarningMsg",
date = "Comment", date = "Comment",
unlinked = "DiffviewNonText",
expander = "DiffviewNonText", expander = "DiffviewNonText",
directory = "Directory", directory = "Directory",
directory_icon = "DiffviewFolderSign", directory_icon = "DiffviewFolderSign",
file_name = "Normal", file_name = "Normal",
resolved = "DiagnosticSignOk", resolved = "DiagnosticSignOk",
unresolved = "DiagnosticSignWarn", unresolved = "DiagnosticSignWarn",
draft = "DiffviewNonText", draft = "DiffviewReference",
draft_mode = "DiagnosticWarn",
live_mode = "DiagnosticOk",
sort_method = "Keyword",
}, },
}, },
} }
@@ -427,94 +439,6 @@ M.setPluginConfiguration = function()
return true return true
end 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 -- Dependencies
-- These tables are passed to the async.sequence function, which calls them in sequence -- 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 -- before calling an action. They are used to set global state that's required
@@ -606,6 +530,7 @@ M.dependencies = {
body = function() body = function()
return { return {
blacklist = M.settings.discussion_tree.blacklist, blacklist = M.settings.discussion_tree.blacklist,
sort_by = M.settings.discussion_tree.sort_by,
} }
end, end,
}, },

View File

@@ -122,7 +122,7 @@ M.time_since = function(date_string, current_date_table)
local time_diff = current_date - date local time_diff = current_date - date
if time_diff < 60 then if time_diff < 60 then
return M.pluralize(time_diff, "second") .. " ago" return "just now"
elseif time_diff < 3600 then elseif time_diff < 3600 then
return M.pluralize(math.floor(time_diff / 60), "minute") .. " ago" return M.pluralize(math.floor(time_diff / 60), "minute") .. " ago"
elseif time_diff < 86400 then elseif time_diff < 86400 then
@@ -335,6 +335,11 @@ M.notify = function(msg, lvl)
vim.notify("gitlab.nvim: " .. msg, lvl) vim.notify("gitlab.nvim: " .. msg, lvl)
end 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() M.get_current_line_number = function()
return vim.api.nvim_call_function("line", { "." }) return vim.api.nvim_call_function("line", { "." })
end end
@@ -427,6 +432,10 @@ M.press_enter = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false) vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false)
end 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. ---Return timestamp from ISO 8601 formatted date string.
---@param date_string string ISO 8601 formatted date string ---@param date_string string ISO 8601 formatted date string
---@return integer timestamp ---@return integer timestamp
@@ -480,62 +489,6 @@ M.difference = function(a, b)
return not_included return not_included
end 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) M.read_file = function(file_path, opts)
local file = io.open(file_path, "r") local file = io.open(file_path, "r")
if file == nil then if file == nil then
@@ -634,7 +587,7 @@ end
M.check_visual_mode = function() M.check_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode local mode = vim.api.nvim_get_mode().mode
if mode ~= "v" and mode ~= "V" then 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 return false
end end
return true return true
@@ -644,7 +597,7 @@ end
---Exists visual mode in order to access marks "<" , ">" ---Exists visual mode in order to access marks "<" , ">"
---@return integer start,integer end Start line and end line ---@return integer start,integer end Start line and end line
M.get_visual_selection_boundaries = function() 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 start_line = vim.api.nvim_buf_get_mark(0, "<")[1]
local end_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 return start_line, end_line