Release (#440)
* 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:
committed by
GitHub
parent
be027331e1
commit
495e64c8bc
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ Thank you for taking time to contribute to this plugin! Please follow these step
|
||||
|
||||
It's possible that the feature you want is already implemented, or does not belong in `gitlab.nvim` at all. By creating an issue first you can have a conversation with the maintainers about the functionality first. While this is not strictly necessary, it greatly increases the likelihood that your merge request will be accepted.
|
||||
|
||||
2. Fork the repository, and create a new feature branch for your desired functionality. Make your changes.
|
||||
2. Fork the repository, and create a new feature branch off the `develop` branch for your desired functionality. Make your changes.
|
||||
|
||||
If you are using Lazy as a plugin manager, the easiest way to work on changes is by setting a specific path for the plugin that points to your repository locally. This is what I do:
|
||||
|
||||
|
||||
4
.github/workflows/go.yaml
vendored
4
.github/workflows/go.yaml
vendored
@@ -4,6 +4,10 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
- 'cmd/**' # Ignore changes to the Lua code
|
||||
- 'go.sum'
|
||||
- 'go.mod'
|
||||
jobs:
|
||||
go_lint:
|
||||
name: Lint Go 💅
|
||||
|
||||
2
.github/workflows/lua.yaml
vendored
2
.github/workflows/lua.yaml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
- 'lua/**' # Ignore changes to the Go code
|
||||
jobs:
|
||||
lua_lint:
|
||||
name: Lint Lua 💅
|
||||
|
||||
34
README.md
34
README.md
@@ -10,8 +10,7 @@ This Neovim plugin is designed to make it easy to review Gitlab MRs from within
|
||||
- View and manage pipeline Jobs
|
||||
- Upload files, jump to the browser, and a lot more!
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335-afe1-d554e3804372
|
||||
|
||||
@@ -36,16 +35,15 @@ For more detailed information about the Lua APIs please run `:h gitlab.nvim.api`
|
||||
With <a href="https://github.com/folke/lazy.nvim">Lazy</a>:
|
||||
|
||||
```lua
|
||||
return {
|
||||
{
|
||||
"harrisoncramer/gitlab.nvim",
|
||||
dependencies = {
|
||||
"MunifTanjim/nui.nvim",
|
||||
"nvim-lua/plenary.nvim",
|
||||
"sindrets/diffview.nvim",
|
||||
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
||||
"nvim-tree/nvim-web-devicons" -- Recommended but not required. Icons in discussion tree.
|
||||
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
||||
},
|
||||
enabled = true,
|
||||
build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
|
||||
config = function()
|
||||
require("gitlab").setup()
|
||||
@@ -53,30 +51,32 @@ return {
|
||||
}
|
||||
```
|
||||
|
||||
And with Packer:
|
||||
And with <a href="https://github.com/lewis6991/pckr.nvim">pckr.nvim</a>:
|
||||
|
||||
```lua
|
||||
use {
|
||||
{
|
||||
"harrisoncramer/gitlab.nvim",
|
||||
requires = {
|
||||
"MunifTanjim/nui.nvim",
|
||||
"nvim-lua/plenary.nvim",
|
||||
"sindrets/diffview.nvim"
|
||||
"sindrets/diffview.nvim",
|
||||
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
||||
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
||||
},
|
||||
build = function()
|
||||
require("gitlab.server").build()
|
||||
end,
|
||||
branch = "develop",
|
||||
run = function() require("gitlab.server").build() end, -- Builds the Go binary
|
||||
config = function()
|
||||
require("diffview") -- We require some global state from diffview
|
||||
local gitlab = require("gitlab")
|
||||
gitlab.setup()
|
||||
require("gitlab").setup()
|
||||
end,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add `branch = "develop",` to your configuration if you want to use the (possibly unstable) development version of `gitlab.nvim`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to the plugin are welcome. Please read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start working on a pull request.
|
||||
|
||||
## Connecting to Gitlab
|
||||
|
||||
This plugin requires an <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token">auth token</a> to connect to Gitlab. The token can be set in the root directory of the project in a `.gitlab.nvim` environment file, or can be set via a shell environment variable called `GITLAB_TOKEN` instead. If both are present, the `.gitlab.nvim` file will take precedence.
|
||||
@@ -122,7 +122,3 @@ For a list of all these settings please run `:h gitlab.nvim.configuring-the-plug
|
||||
The plugin sets up a number of useful keybindings in the special buffers it creates, and some global keybindings as well. Refer to the relevant section of the manual `:h gitlab.nvim.keybindings` for more details.
|
||||
|
||||
For more information about each of these commands, and about the APIs in general, run `:h gitlab.nvim.api`
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to the plugin are welcome. Please read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start working on a pull request.
|
||||
|
||||
@@ -2,29 +2,26 @@ if filereadable($VIMRUNTIME . '/syntax/markdown.vim')
|
||||
source $VIMRUNTIME/syntax/markdown.vim
|
||||
endif
|
||||
|
||||
syntax match Date "\v\d+\s+\w+\s+ago"
|
||||
highlight link Date GitlabDate
|
||||
let expanders = '^\s*\%(' . g:gitlab_discussion_tree_expander_open . '\|' . g:gitlab_discussion_tree_expander_closed . '\)'
|
||||
let username = '@[a-zA-Z0-9.]\+'
|
||||
|
||||
execute 'syntax match Unresolved /\s' . g:gitlab_discussion_tree_unresolved . '\s\?/'
|
||||
highlight link Unresolved GitlabUnresolved
|
||||
" Covers times like '14 days ago', 'just now', as well as 'October 3, 2024'
|
||||
let time_ago = '\d\+ \w\+ ago'
|
||||
let formatted_date = '\w\+ \{1,2}\d\{1,2}, \d\{4}'
|
||||
let date = '\%(' . time_ago . '\|' . formatted_date . '\|just now\)'
|
||||
|
||||
execute 'syntax match Resolved /\s' . g:gitlab_discussion_tree_resolved . '\s\?/'
|
||||
highlight link Resolved GitlabResolved
|
||||
let published = date . ' \%(' . g:gitlab_discussion_tree_resolved . '\|' . g:gitlab_discussion_tree_unresolved . '\|' . g:gitlab_discussion_tree_unlinked . '\)\?'
|
||||
let state = ' \%(' . published . '\|' . g:gitlab_discussion_tree_draft . '\)'
|
||||
|
||||
execute 'syntax match GitlabDiscussionOpen /^\s*' . g:gitlab_discussion_tree_expander_open . '/'
|
||||
highlight link GitlabDiscussionOpen GitlabExpander
|
||||
execute 'syntax match GitlabNoteHeader "' . expanders . username . state . '" contains=GitlabDate,GitlabUnresolved,GitlabUnlinked,GitlabResolved,GitlabExpander,GitlabDraft,GitlabUsername'
|
||||
|
||||
execute 'syntax match GitlabDiscussionClosed /^\s*' . g:gitlab_discussion_tree_expander_closed . '/'
|
||||
highlight link GitlabDiscussionClosed GitlabExpander
|
||||
|
||||
execute 'syntax match Draft /' . g:gitlab_discussion_tree_draft . '/'
|
||||
highlight link Draft GitlabDraft
|
||||
|
||||
execute 'syntax match Username "@[a-zA-Z0-9.]\+"'
|
||||
highlight link Username GitlabUsername
|
||||
|
||||
execute 'syntax match Mention "\%(' . g:gitlab_discussion_tree_expander_open . '\|'
|
||||
\ . g:gitlab_discussion_tree_expander_closed . '\)\@<!@[a-zA-Z0-9.]*"'
|
||||
highlight link Mention GitlabMention
|
||||
execute 'syntax match GitlabDate "' . date . '" contained'
|
||||
execute 'syntax match GitlabUnresolved "' . g:gitlab_discussion_tree_unresolved . '" contained'
|
||||
execute 'syntax match GitlabUnlinked "' . g:gitlab_discussion_tree_unlinked . '" contained'
|
||||
execute 'syntax match GitlabResolved "' . g:gitlab_discussion_tree_resolved . '" contained'
|
||||
execute 'syntax match GitlabExpander "' . expanders . '" contained'
|
||||
execute 'syntax match GitlabDraft "' . g:gitlab_discussion_tree_draft . '" contained'
|
||||
execute 'syntax match GitlabUsername "' . username . '" contained'
|
||||
execute 'syntax match GitlabMention "' . username . '"'
|
||||
|
||||
let b:current_syntax = 'gitlab'
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
@@ -19,8 +20,16 @@ func Contains[T comparable](elems []T, v T) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type SortBy string
|
||||
|
||||
const (
|
||||
SortByLatestReply SortBy = "latest_reply"
|
||||
SortByOriginalComment SortBy = "original_comment"
|
||||
)
|
||||
|
||||
type DiscussionsRequest struct {
|
||||
Blacklist []string `json:"blacklist" validate:"required"`
|
||||
SortBy SortBy `json:"sort_by"`
|
||||
}
|
||||
|
||||
type DiscussionsResponse struct {
|
||||
@@ -30,20 +39,30 @@ type DiscussionsResponse struct {
|
||||
Emojis map[int][]*gitlab.AwardEmoji `json:"emojis"`
|
||||
}
|
||||
|
||||
type SortableDiscussions []*gitlab.Discussion
|
||||
|
||||
func (n SortableDiscussions) Len() int {
|
||||
return len(n)
|
||||
type SortableDiscussions struct {
|
||||
Discussions []*gitlab.Discussion
|
||||
SortBy SortBy
|
||||
}
|
||||
|
||||
func (d SortableDiscussions) Less(i int, j int) bool {
|
||||
iTime := d[i].Notes[len(d[i].Notes)-1].CreatedAt
|
||||
jTime := d[j].Notes[len(d[j].Notes)-1].CreatedAt
|
||||
func (d SortableDiscussions) Len() int {
|
||||
return len(d.Discussions)
|
||||
}
|
||||
|
||||
func (d SortableDiscussions) Less(i, j int) bool {
|
||||
var iTime, jTime *time.Time
|
||||
if d.SortBy == SortByOriginalComment {
|
||||
iTime = d.Discussions[i].Notes[0].CreatedAt
|
||||
jTime = d.Discussions[j].Notes[0].CreatedAt
|
||||
return iTime.Before(*jTime)
|
||||
} else { // SortByLatestReply
|
||||
iTime = d.Discussions[i].Notes[len(d.Discussions[i].Notes)-1].CreatedAt
|
||||
jTime = d.Discussions[j].Notes[len(d.Discussions[j].Notes)-1].CreatedAt
|
||||
return iTime.After(*jTime)
|
||||
}
|
||||
}
|
||||
|
||||
func (n SortableDiscussions) Swap(i, j int) {
|
||||
n[i], n[j] = n[j], n[i]
|
||||
func (d SortableDiscussions) Swap(i, j int) {
|
||||
d.Discussions[i], d.Discussions[j] = d.Discussions[j], d.Discussions[i]
|
||||
}
|
||||
|
||||
type DiscussionsLister interface {
|
||||
@@ -115,8 +134,14 @@ func (a discussionsListerService) ServeHTTP(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
sortedLinkedDiscussions := SortableDiscussions(linkedDiscussions)
|
||||
sortedUnlinkedDiscussions := SortableDiscussions(unlinkedDiscussions)
|
||||
sortedLinkedDiscussions := SortableDiscussions{
|
||||
Discussions: linkedDiscussions,
|
||||
SortBy: request.SortBy,
|
||||
}
|
||||
sortedUnlinkedDiscussions := SortableDiscussions{
|
||||
Discussions: unlinkedDiscussions,
|
||||
SortBy: request.SortBy,
|
||||
}
|
||||
|
||||
sort.Sort(sortedLinkedDiscussions)
|
||||
sort.Sort(sortedUnlinkedDiscussions)
|
||||
|
||||
@@ -21,8 +21,14 @@ func (f fakeDiscussionsLister) ListMergeRequestDiscussions(pid interface{}, merg
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
newer := now.Add(time.Second * 100)
|
||||
|
||||
timePointers := make([]*time.Time, 6)
|
||||
timePointers[0] = new(time.Time)
|
||||
*timePointers[0] = time.Now()
|
||||
for i := 1; i < len(timePointers); i++ {
|
||||
timePointers[i] = new(time.Time)
|
||||
*timePointers[i] = timePointers[i-1].Add(time.Second * 100)
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
ID int `json:"id"`
|
||||
@@ -35,8 +41,18 @@ func (f fakeDiscussionsLister) ListMergeRequestDiscussions(pid interface{}, merg
|
||||
}
|
||||
|
||||
testListDiscussionsResponse := []*gitlab.Discussion{
|
||||
{Notes: []*gitlab.Note{{CreatedAt: &now, Type: "DiffNote", Author: Author{Username: "hcramer"}}}},
|
||||
{Notes: []*gitlab.Note{{CreatedAt: &newer, Type: "DiffNote", Author: Author{Username: "hcramer2"}}}},
|
||||
{Notes: []*gitlab.Note{
|
||||
{CreatedAt: timePointers[0], Type: "DiffNote", Author: Author{Username: "hcramer0"}},
|
||||
{CreatedAt: timePointers[4], Type: "DiffNote", Author: Author{Username: "hcramer1"}},
|
||||
}},
|
||||
{Notes: []*gitlab.Note{
|
||||
{CreatedAt: timePointers[2], Type: "DiffNote", Author: Author{Username: "hcramer2"}},
|
||||
{CreatedAt: timePointers[3], Type: "DiffNote", Author: Author{Username: "hcramer3"}},
|
||||
}},
|
||||
{Notes: []*gitlab.Note{
|
||||
{CreatedAt: timePointers[1], Type: "DiffNote", Author: Author{Username: "hcramer4"}},
|
||||
{CreatedAt: timePointers[5], Type: "DiffNote", Author: Author{Username: "hcramer5"}},
|
||||
}},
|
||||
}
|
||||
return testListDiscussionsResponse, resp, err
|
||||
}
|
||||
@@ -66,8 +82,8 @@ func getDiscussionsList(t *testing.T, svc http.Handler, request *http.Request) D
|
||||
}
|
||||
|
||||
func TestListDiscussions(t *testing.T) {
|
||||
t.Run("Returns sorted discussions", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
|
||||
t.Run("Returns discussions sorted by latest reply", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}, SortBy: "latest_reply"})
|
||||
svc := middleware(
|
||||
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
|
||||
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||
@@ -76,12 +92,28 @@ func TestListDiscussions(t *testing.T) {
|
||||
)
|
||||
data := getDiscussionsList(t, svc, request)
|
||||
assert(t, data.Message, "Discussions retrieved")
|
||||
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2") /* Sorting applied */
|
||||
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer")
|
||||
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer4") /* Sorting applied */
|
||||
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer0")
|
||||
assert(t, data.Discussions[2].Notes[0].Author.Username, "hcramer2")
|
||||
})
|
||||
|
||||
t.Run("Returns discussions sorted by original comment", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}, SortBy: "original_comment"})
|
||||
svc := middleware(
|
||||
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
|
||||
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||
withPayloadValidation(methodToPayload{http.MethodPost: newPayload[DiscussionsRequest]}),
|
||||
withMethodCheck(http.MethodPost),
|
||||
)
|
||||
data := getDiscussionsList(t, svc, request)
|
||||
assert(t, data.Message, "Discussions retrieved")
|
||||
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer0") /* Sorting applied */
|
||||
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer4")
|
||||
assert(t, data.Discussions[2].Notes[0].Author.Username, "hcramer2")
|
||||
})
|
||||
|
||||
t.Run("Uses blacklist to filter unwanted authors", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer0"}, SortBy: "latest_reply"})
|
||||
svc := middleware(
|
||||
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
|
||||
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||
@@ -90,8 +122,9 @@ func TestListDiscussions(t *testing.T) {
|
||||
)
|
||||
data := getDiscussionsList(t, svc, request)
|
||||
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
|
||||
assert(t, len(data.Discussions), 1)
|
||||
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2")
|
||||
assert(t, len(data.Discussions), 2)
|
||||
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer4")
|
||||
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer2")
|
||||
})
|
||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
|
||||
|
||||
@@ -59,25 +59,24 @@ INSTALLATION *gitlab.nvim.installation*
|
||||
|
||||
With Lazy:
|
||||
>lua
|
||||
return {
|
||||
{
|
||||
"harrisoncramer/gitlab.nvim",
|
||||
dependencies = {
|
||||
"MunifTanjim/nui.nvim",
|
||||
"nvim-lua/plenary.nvim",
|
||||
"sindrets/diffview.nvim",
|
||||
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
||||
"nvim-tree/nvim-web-devicons" -- Recommended but not required. Icons in discussion tree.
|
||||
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
||||
},
|
||||
enabled = true,
|
||||
build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
|
||||
config = function()
|
||||
require("gitlab").setup()
|
||||
end,
|
||||
}
|
||||
<
|
||||
And with Packer:
|
||||
And with pckr.nvim:
|
||||
>lua
|
||||
use {
|
||||
{
|
||||
"harrisoncramer/gitlab.nvim",
|
||||
requires = {
|
||||
"MunifTanjim/nui.nvim",
|
||||
@@ -86,14 +85,10 @@ And with Packer:
|
||||
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
||||
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
||||
},
|
||||
build = function()
|
||||
require("gitlab.server").build()
|
||||
end,
|
||||
branch = "develop",
|
||||
run = function() require("gitlab.server").build() end, -- Builds the Go binary
|
||||
config = function()
|
||||
require("diffview") -- We require some global state from diffview
|
||||
local gitlab = require("gitlab")
|
||||
gitlab.setup()
|
||||
require("gitlab").setup()
|
||||
end,
|
||||
}
|
||||
<
|
||||
@@ -200,7 +195,7 @@ you call this function with no values the defaults will be used:
|
||||
next_field = "<Tab>", -- Cycle to the next field. Accepts |count|.
|
||||
prev_field = "<S-Tab>", -- Cycle to the previous field. Accepts |count|.
|
||||
perform_action = "ZZ", -- Once in normal mode, does action (like saving comment or applying description edit, etc)
|
||||
perform_linewise_action = "ZA", -- Once in normal mode, does the linewise action (see logs for this job, etc)
|
||||
perform_linewise_action, = "ZA", -- Once in normal mode, does the linewise action (see logs for this job, etc)
|
||||
discard_changes = "ZQ", -- Quit the popup discarding changes, the popup content is not saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
|
||||
},
|
||||
discussion_tree = {
|
||||
@@ -219,6 +214,7 @@ you call this function with no values the defaults will be used:
|
||||
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
|
||||
publish_draft = "P", -- Publish the currently focused note/comment
|
||||
toggle_draft_mode = "D", -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
|
||||
toggle_sort_method = "st", -- Toggle whether discussions are sorted by the "latest_reply", or by "original_comment", see `:h gitlab.nvim.toggle_sort_method`
|
||||
toggle_node = "t", -- Open or close the discussion
|
||||
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
|
||||
toggle_resolved_discussions = "R", -- Open or close all resolved discussions
|
||||
@@ -234,16 +230,20 @@ you call this function with no values the defaults will be used:
|
||||
},
|
||||
},
|
||||
popup = { -- The popup for comment creation, editing, and replying
|
||||
width = "40%",
|
||||
height = "60%",
|
||||
width = "40%", -- Can be a percentage (string or decimal, "40%" = 0.4) of editor screen width, or an integer (number of columns)
|
||||
height = "60%", -- Can be a percentage (string or decimal, "60%" = 0.6) of editor screen width, or an integer (number of rows)
|
||||
position = "50%", -- Position (from the top left corner), either a number or percentage string that applies to both horizontal and vertical position, or a table that specifies them separately, e.g., { row = "90%", col = "100%" } places popups in the bottom right corner while leaving the status line visible
|
||||
border = "rounded", -- One of "rounded", "single", "double", "solid"
|
||||
opacity = 1.0, -- From 0.0 (fully transparent) to 1.0 (fully opaque)
|
||||
comment = nil, -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 },
|
||||
edit = nil,
|
||||
note = nil,
|
||||
pipeline = nil,
|
||||
help = nil, -- Width and height are calculated automatically and cannot be overridden
|
||||
pipeline = nil, -- Width and height are calculated automatically and cannot be overridden
|
||||
reply = nil,
|
||||
squash_message = nil,
|
||||
create_mr = { width = "95%", height = "95%" },
|
||||
summary = { width = "95%", height = "95%" },
|
||||
temp_registers = {}, -- List of registers for backing up popup content (see `:h gitlab.nvim.temp-registers`)
|
||||
},
|
||||
discussion_tree = { -- The discussion tree that holds all comments
|
||||
@@ -252,18 +252,22 @@ you call this function with no values the defaults will be used:
|
||||
collapsed = " ", -- Icon for collapsed discussion thread
|
||||
indentation = " ", -- Indentation Icon
|
||||
},
|
||||
spinner_chars = { "/", "|", "\\", "-" }, -- Characters for the refresh animation
|
||||
auto_open = true, -- Automatically open when the reviewer is opened
|
||||
default_view = "discussions" -- Show "discussions" or "notes" by default
|
||||
default_view = "discussions", -- Show "discussions" or "notes" by default
|
||||
blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc)
|
||||
sort_by = "latest_reply", -- Sort discussion tree by the "latest_reply", or by "original_comment", see `:h gitlab.nvim.toggle_sort_method`
|
||||
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
|
||||
position = "left", -- "top", "right", "bottom" or "left"
|
||||
position = "bottom", -- "top", "right", "bottom" or "left"
|
||||
size = "20%", -- Size of split
|
||||
relative = "editor", -- Position of tree split relative to "editor" or "window"
|
||||
resolved = '✓', -- Symbol to show next to resolved discussions
|
||||
unresolved = '-', -- Symbol to show next to unresolved discussions
|
||||
unlinked = "", -- Symbol to show next to unliked comments (i.e., not threads)
|
||||
draft = "✎", -- Symbol to show next to draft comments/notes
|
||||
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
|
||||
draft_mode = false, -- Whether comments are posted as drafts as part of a review
|
||||
winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
|
||||
winbar = nil, -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
|
||||
-- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
|
||||
},
|
||||
emojis = {
|
||||
@@ -330,7 +334,7 @@ you call this function with no values the defaults will be used:
|
||||
squash = false, -- Whether the commits will be marked for squashing
|
||||
fork = {
|
||||
enabled = false, -- If making an MR from a fork
|
||||
forked_project_id = nil -- The ID of the project you are merging into. If nil, will be prompted.
|
||||
forked_project_id = nil, -- The ID of the project you are merging into. If nil, will be prompted.
|
||||
},
|
||||
title_input = { -- Default settings for MR title input window
|
||||
width = 40,
|
||||
@@ -349,6 +353,9 @@ you call this function with no values the defaults will be used:
|
||||
resolved = "DiagnosticSignOk",
|
||||
unresolved = "DiagnosticSignWarn",
|
||||
draft = "DiffviewNonText",
|
||||
draft_mode = "DiagnosticWarn",
|
||||
live_mode = "DiagnosticOk",
|
||||
sort_method = "Keyword",
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -929,6 +936,13 @@ gitlab.toggle_draft_mode() ~
|
||||
Toggles between draft mode, where comments and notes are added to a review as
|
||||
drafts, and regular (or live) mode, where comments are posted immediately.
|
||||
|
||||
*gitlab.nvim.toggle_sort_method*
|
||||
gitlab.toggle_sort_method() ~
|
||||
|
||||
Toggles whether the discussion tree is sorted by the "latest_reply", with
|
||||
threads with the most recent activity on top (the default), or by
|
||||
"original_comment", with the oldest threads on top.
|
||||
|
||||
*gitlab.nvim.add_assignee*
|
||||
gitlab.add_assignee() ~
|
||||
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
local job = require("gitlab.job")
|
||||
local state = require("gitlab.state")
|
||||
local u = require("gitlab.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
local refresh_status_state = function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
state.load_new_state("info", function()
|
||||
require("gitlab.actions.summary").update_summary_details()
|
||||
end)
|
||||
end
|
||||
|
||||
M.approve = function()
|
||||
job.run_job("/mr/approve", "POST")
|
||||
job.run_job("/mr/approve", "POST", nil, function(data)
|
||||
refresh_status_state(data)
|
||||
end)
|
||||
end
|
||||
|
||||
M.revoke = function()
|
||||
job.run_job("/mr/revoke", "POST")
|
||||
job.run_job("/mr/revoke", "POST", nil, function(data)
|
||||
refresh_status_state(data)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -22,6 +22,12 @@ M.delete_reviewer = function()
|
||||
M.delete_popup("reviewer")
|
||||
end
|
||||
|
||||
local refresh_user_state = function(type, data, message)
|
||||
u.notify(message, vim.log.levels.INFO)
|
||||
state.INFO[type] = data
|
||||
require("gitlab.actions.summary").update_summary_details()
|
||||
end
|
||||
|
||||
M.add_popup = function(type)
|
||||
local plural = type .. "s"
|
||||
local current = state.INFO[plural]
|
||||
@@ -39,8 +45,7 @@ M.add_popup = function(type)
|
||||
table.insert(current_ids, choice.id)
|
||||
local body = { ids = current_ids }
|
||||
job.run_job("/mr/" .. type, "PUT", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
state.INFO[plural] = data[plural]
|
||||
refresh_user_state(plural, data[plural], data.message)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -61,7 +66,7 @@ M.delete_popup = function(type)
|
||||
local body = { ids = ids }
|
||||
job.run_job("/mr/" .. type, "PUT", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
state.INFO[plural] = data[plural]
|
||||
refresh_user_state(plural, data[plural], data.message)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
--- to this module the data required to make the API calls
|
||||
local Popup = require("nui.popup")
|
||||
local Layout = require("nui.layout")
|
||||
local diffview_lib = require("diffview.lib")
|
||||
local state = require("gitlab.state")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local popup = require("gitlab.popup")
|
||||
local git = require("gitlab.git")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
local draft_notes = require("gitlab.actions.draft_notes")
|
||||
@@ -46,6 +46,18 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
|
||||
return
|
||||
end
|
||||
|
||||
-- Creating a draft reply, in response to a discussion ID
|
||||
if discussion_id ~= nil and is_draft then
|
||||
local body = { comment = text, discussion_id = discussion_id }
|
||||
job.run_job("/mr/draft_notes/", "POST", body, function()
|
||||
u.notify("Draft reply created!", vim.log.levels.INFO)
|
||||
draft_notes.load_draft_notes(function()
|
||||
discussions.rebuild_view(unlinked)
|
||||
end)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- Creating a note (unlinked comment)
|
||||
if unlinked and discussion_id == nil then
|
||||
local body = { comment = text }
|
||||
@@ -89,18 +101,6 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
|
||||
line_range = location_data.line_range,
|
||||
}
|
||||
|
||||
-- Creating a draft reply, in response to a discussion ID
|
||||
if discussion_id ~= nil and is_draft then
|
||||
local body = { comment = text, discussion_id = discussion_id, position = position_data }
|
||||
job.run_job("/mr/draft_notes/", "POST", body, function()
|
||||
u.notify("Draft reply created!", vim.log.levels.INFO)
|
||||
draft_notes.load_draft_notes(function()
|
||||
discussions.rebuild_view(unlinked)
|
||||
end)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- Creating a new comment (linked to specific changes)
|
||||
local body = u.merge({ type = "text", comment = text }, position_data)
|
||||
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
|
||||
@@ -157,52 +157,26 @@ end
|
||||
---multi-line comment. It also sets up the basic keybindings for switching between
|
||||
---window panes, and for the non-primary sections.
|
||||
---@param opts LayoutOpts
|
||||
---@return NuiLayout|nil
|
||||
---@return NuiLayout
|
||||
M.create_comment_layout = function(opts)
|
||||
if opts.unlinked ~= true and opts.discussion_id == nil then
|
||||
-- Check that diffview is initialized
|
||||
if reviewer.tabnr == nil then
|
||||
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
|
||||
return
|
||||
local popup_settings = state.settings.popup
|
||||
local title
|
||||
local user_settings
|
||||
if opts.discussion_id ~= nil then
|
||||
title = "Reply"
|
||||
user_settings = popup_settings.reply
|
||||
elseif opts.unlinked then
|
||||
title = "Note"
|
||||
user_settings = popup_settings.note
|
||||
else
|
||||
title = "Comment"
|
||||
user_settings = popup_settings.comment
|
||||
end
|
||||
|
||||
-- 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
|
||||
local settings = u.merge(popup_settings, user_settings or {})
|
||||
|
||||
M.current_win = vim.api.nvim_get_current_win()
|
||||
M.comment_popup = Popup(u.create_popup_state(title, settings))
|
||||
M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
|
||||
M.comment_popup = Popup(popup.create_popup_state(title, settings))
|
||||
M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings))
|
||||
M.start_line, M.end_line = u.get_visual_selection_boundaries()
|
||||
|
||||
local internal_layout = Layout.Box({
|
||||
@@ -211,98 +185,69 @@ M.create_comment_layout = function(opts)
|
||||
}, { dir = "col" })
|
||||
|
||||
local layout = Layout({
|
||||
position = "50%",
|
||||
position = settings.position,
|
||||
relative = "editor",
|
||||
size = {
|
||||
width = "50%",
|
||||
height = "55%",
|
||||
width = settings.width,
|
||||
height = settings.height,
|
||||
},
|
||||
}, internal_layout)
|
||||
|
||||
miscellaneous.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
|
||||
popup.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
|
||||
popup.set_up_autocommands(M.comment_popup, layout, M.current_win)
|
||||
|
||||
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
|
||||
local unlinked = opts.unlinked or false
|
||||
|
||||
---Keybinding for focus on draft section
|
||||
state.set_popup_keymaps(M.draft_popup, function()
|
||||
popup.set_popup_keymaps(M.draft_popup, function()
|
||||
local text = u.get_buffer_text(M.comment_popup.bufnr)
|
||||
confirm_create_comment(text, range, unlinked, opts.discussion_id)
|
||||
vim.api.nvim_set_current_win(M.current_win)
|
||||
end, miscellaneous.toggle_bool, miscellaneous.non_editable_popup_opts)
|
||||
end, miscellaneous.toggle_bool, popup.non_editable_popup_opts)
|
||||
|
||||
---Keybinding for focus on text section
|
||||
state.set_popup_keymaps(M.comment_popup, function(text)
|
||||
popup.set_popup_keymaps(M.comment_popup, function(text)
|
||||
confirm_create_comment(text, range, unlinked, opts.discussion_id)
|
||||
vim.api.nvim_set_current_win(M.current_win)
|
||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
||||
end, miscellaneous.attach_file, popup.editable_popup_opts)
|
||||
|
||||
vim.schedule(function()
|
||||
local draft_mode = state.settings.discussion_tree.draft_mode
|
||||
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
|
||||
end)
|
||||
|
||||
--Send back to previous window on close
|
||||
vim.api.nvim_create_autocmd("BufHidden", {
|
||||
buffer = M.draft_popup.bufnr,
|
||||
callback = function()
|
||||
vim.api.nvim_set_current_win(M.current_win)
|
||||
end,
|
||||
})
|
||||
|
||||
return layout
|
||||
end
|
||||
|
||||
--- This function will open a comment popup in order to create a comment on the changed/updated
|
||||
--- line in the current MR
|
||||
M.create_comment = function()
|
||||
local has_clean_tree, err = git.has_clean_tree()
|
||||
if err ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
local is_modified = vim.bo[0].modified
|
||||
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
||||
u.notify(
|
||||
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if not M.sha_exists() then
|
||||
if not M.can_create_comment(false) then
|
||||
return
|
||||
end
|
||||
|
||||
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
|
||||
if layout ~= nil then
|
||||
layout:mount()
|
||||
end
|
||||
end
|
||||
|
||||
--- This function will open a multi-line comment popup in order to create a multi-line comment
|
||||
--- on the changed/updated line in the current MR
|
||||
M.create_multiline_comment = function()
|
||||
if not u.check_visual_mode() then
|
||||
return
|
||||
end
|
||||
if not M.sha_exists() then
|
||||
if not M.can_create_comment(true) then
|
||||
u.press_escape()
|
||||
return
|
||||
end
|
||||
|
||||
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
|
||||
if layout ~= nil then
|
||||
layout:mount()
|
||||
end
|
||||
end
|
||||
|
||||
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
|
||||
--- on the changed/updated line in the current MR
|
||||
M.create_note = function()
|
||||
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
|
||||
if layout ~= nil then
|
||||
layout:mount()
|
||||
end
|
||||
end
|
||||
|
||||
---Given the current visually selected area of text, builds text to fill in the
|
||||
@@ -347,21 +292,16 @@ end
|
||||
--- on the changed/updated line in the current MR
|
||||
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
|
||||
M.create_comment_suggestion = function()
|
||||
if not u.check_visual_mode() then
|
||||
return
|
||||
end
|
||||
if not M.sha_exists() then
|
||||
if not M.can_create_comment(true) then
|
||||
u.press_escape()
|
||||
return
|
||||
end
|
||||
|
||||
local suggestion_lines, range_length = build_suggestion()
|
||||
|
||||
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
|
||||
if layout ~= nil then
|
||||
layout:mount()
|
||||
else
|
||||
return -- Failure in creating the comment layout
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
if suggestion_lines then
|
||||
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
|
||||
@@ -369,6 +309,68 @@ M.create_comment_suggestion = function()
|
||||
end)
|
||||
end
|
||||
|
||||
---Returns true if it's possible to create an Inline Comment
|
||||
---@param must_be_visual boolean True if current mode must be visual
|
||||
---@return boolean
|
||||
M.can_create_comment = function(must_be_visual)
|
||||
-- Check that diffview is initialized
|
||||
if reviewer.tabnr == nil then
|
||||
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check that we are in the Diffview tab
|
||||
local tabnr = vim.api.nvim_get_current_tabpage()
|
||||
if tabnr ~= reviewer.tabnr then
|
||||
u.notify("Comments can only be left in the reviewer pane", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check that we are hovering over the code
|
||||
local filetype = vim.bo[0].filetype
|
||||
if filetype == "DiffviewFiles" or filetype == "gitlab" then
|
||||
u.notify(
|
||||
"Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check that the file has not been renamed
|
||||
if reviewer.is_file_renamed() and not reviewer.does_file_have_changes() then
|
||||
u.notify("Commenting on (unchanged) renamed or moved files is not supported", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check that we are in a valid buffer
|
||||
if not M.sha_exists() then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check that there aren't saved modifications
|
||||
local file = reviewer.get_current_file_path()
|
||||
if file == nil then
|
||||
return false
|
||||
end
|
||||
local has_changes, err = git.has_changes(file)
|
||||
if err ~= nil then
|
||||
return false
|
||||
end
|
||||
-- Check that there aren't unsaved modifications
|
||||
local is_modified = vim.bo[0].modified
|
||||
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or has_changes) then
|
||||
u.notify("Cannot leave comments on changed files, please stash or commit and push", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check we're in visual mode for code suggestions and multiline comments
|
||||
if must_be_visual and not u.check_visual_mode() then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
|
||||
---buffers as 'null'
|
||||
---@return boolean
|
||||
|
||||
@@ -13,7 +13,7 @@ local M = {}
|
||||
---@return string
|
||||
M.build_note_header = function(note)
|
||||
if note.note then
|
||||
return "@" .. state.USER.username .. " " .. ""
|
||||
return "@" .. state.USER.username .. " " .. state.settings.discussion_tree.draft
|
||||
end
|
||||
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ local Input = require("nui.input")
|
||||
local Popup = require("nui.popup")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local popup = require("gitlab.popup")
|
||||
local git = require("gitlab.git")
|
||||
local state = require("gitlab.state")
|
||||
local common = require("gitlab.actions.common")
|
||||
@@ -277,13 +278,13 @@ M.open_confirmation_popup = function(mr)
|
||||
action_before_exit = true,
|
||||
}
|
||||
|
||||
state.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts)
|
||||
state.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts)
|
||||
state.set_popup_keymaps(target_popup, M.create_mr, M.select_new_target, popup_opts)
|
||||
state.set_popup_keymaps(delete_branch_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
|
||||
state.set_popup_keymaps(squash_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
|
||||
state.set_popup_keymaps(forked_project_id_popup, M.create_mr, nil, popup_opts)
|
||||
miscellaneous.set_cycle_popups_keymaps(popups)
|
||||
popup.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts)
|
||||
popup.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts)
|
||||
popup.set_popup_keymaps(target_popup, M.create_mr, M.select_new_target, popup_opts)
|
||||
popup.set_popup_keymaps(delete_branch_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
|
||||
popup.set_popup_keymaps(squash_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
|
||||
popup.set_popup_keymaps(forked_project_id_popup, M.create_mr, nil, popup_opts)
|
||||
popup.set_cycle_popups_keymaps(popups)
|
||||
|
||||
vim.api.nvim_set_current_buf(M.description_bufnr)
|
||||
end)
|
||||
@@ -328,19 +329,20 @@ M.create_mr = function()
|
||||
end
|
||||
|
||||
M.create_layout = function()
|
||||
local title_popup = Popup(u.create_box_popup_state("Title", false))
|
||||
local settings = u.merge(state.settings.popup, state.settings.popup.create_mr or {})
|
||||
local title_popup = Popup(popup.create_box_popup_state("Title", false, settings))
|
||||
M.title_bufnr = title_popup.bufnr
|
||||
local description_popup = Popup(u.create_box_popup_state("Description", true))
|
||||
local description_popup = Popup(popup.create_popup_state("Description", settings))
|
||||
M.description_bufnr = description_popup.bufnr
|
||||
local target_branch_popup = Popup(u.create_box_popup_state("Target branch", false))
|
||||
local target_branch_popup = Popup(popup.create_box_popup_state("Target branch", false, settings))
|
||||
M.target_bufnr = target_branch_popup.bufnr
|
||||
local delete_title = vim.o.columns > 110 and "Delete source branch" or "Delete source"
|
||||
local delete_branch_popup = Popup(u.create_box_popup_state(delete_title, false))
|
||||
local delete_branch_popup = Popup(popup.create_box_popup_state(delete_title, false, settings))
|
||||
M.delete_branch_bufnr = delete_branch_popup.bufnr
|
||||
local squash_title = vim.o.columns > 110 and "Squash commits" or "Squash"
|
||||
local squash_popup = Popup(u.create_box_popup_state(squash_title, false))
|
||||
local squash_popup = Popup(popup.create_box_popup_state(squash_title, false, settings))
|
||||
M.squash_bufnr = squash_popup.bufnr
|
||||
local forked_project_id_popup = Popup(u.create_box_popup_state("Forked Project ID", false))
|
||||
local forked_project_id_popup = Popup(popup.create_box_popup_state("Forked Project ID", false, settings))
|
||||
M.forked_project_id_bufnr = forked_project_id_popup.bufnr
|
||||
|
||||
local boxes = {}
|
||||
@@ -360,14 +362,16 @@ M.create_layout = function()
|
||||
}, { dir = "col" })
|
||||
|
||||
local layout = Layout({
|
||||
position = "50%",
|
||||
position = settings.position,
|
||||
relative = "editor",
|
||||
size = {
|
||||
width = "95%",
|
||||
height = "95%",
|
||||
width = settings.width,
|
||||
height = settings.height,
|
||||
},
|
||||
}, internal_layout)
|
||||
|
||||
popup.set_up_autocommands(description_popup, layout, vim.api.nvim_get_current_win())
|
||||
|
||||
layout:mount()
|
||||
|
||||
return layout,
|
||||
|
||||
@@ -7,12 +7,12 @@ local Popup = require("nui.popup")
|
||||
local NuiTree = require("nui.tree")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local popup = require("gitlab.popup")
|
||||
local state = require("gitlab.state")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local common = require("gitlab.actions.common")
|
||||
local List = require("gitlab.utils.list")
|
||||
local tree_utils = require("gitlab.actions.discussions.tree")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
local discussions_tree = require("gitlab.actions.discussions.tree")
|
||||
local draft_notes = require("gitlab.actions.draft_notes")
|
||||
local diffview_lib = require("diffview.lib")
|
||||
@@ -48,13 +48,15 @@ M.rebuild_view = function(unlinked, all)
|
||||
else
|
||||
M.rebuild_discussion_tree()
|
||||
end
|
||||
M.refresh_diagnostics_and_winbar()
|
||||
state.discussion_tree.last_updated = os.time()
|
||||
M.refresh_diagnostics()
|
||||
end)
|
||||
end
|
||||
|
||||
---Makes API call to get the discussion data, stores it in the state, and calls the callback
|
||||
---@param callback function|nil
|
||||
M.load_discussions = function(callback)
|
||||
state.discussion_tree.last_updated = nil
|
||||
state.load_new_state("discussion_data", function(data)
|
||||
if not state.DISCUSSION_DATA then
|
||||
state.DISCUSSION_DATA = {}
|
||||
@@ -70,9 +72,10 @@ end
|
||||
|
||||
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
|
||||
M.initialize_discussions = function()
|
||||
state.discussion_tree.last_updated = os.time()
|
||||
signs.setup_signs()
|
||||
reviewer.set_callback_for_file_changed(function()
|
||||
M.refresh_diagnostics_and_winbar()
|
||||
M.refresh_diagnostics()
|
||||
M.modifiable(false)
|
||||
reviewer.set_reviewer_keymaps()
|
||||
end)
|
||||
@@ -102,11 +105,10 @@ M.modifiable = function(bool)
|
||||
end
|
||||
|
||||
--- Take existing data and refresh the diagnostics, the winbar, and the signs
|
||||
M.refresh_diagnostics_and_winbar = function()
|
||||
M.refresh_diagnostics = function()
|
||||
if state.settings.discussion_signs.enabled then
|
||||
diagnostics.refresh_diagnostics()
|
||||
end
|
||||
winbar.update_winbar()
|
||||
common.add_empty_titles()
|
||||
end
|
||||
|
||||
@@ -154,7 +156,7 @@ M.open = function(callback)
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
M.refresh_diagnostics_and_winbar()
|
||||
M.refresh_diagnostics()
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -251,9 +253,7 @@ M.reply = function(tree)
|
||||
reply = true,
|
||||
})
|
||||
|
||||
if layout then
|
||||
layout:mount()
|
||||
end
|
||||
end
|
||||
|
||||
-- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment
|
||||
@@ -284,7 +284,7 @@ end
|
||||
|
||||
-- This function (settings.keymaps.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
|
||||
M.edit_comment = function(tree, unlinked)
|
||||
local edit_popup = Popup(u.create_popup_state("Edit Comment", state.settings.popup.edit))
|
||||
local edit_popup = Popup(popup.create_popup_state("Edit Comment", state.settings.popup.edit))
|
||||
local current_node = tree:get_node()
|
||||
local note_node = common.get_note_node(tree, current_node)
|
||||
local root_node = common.get_root_node(tree, current_node)
|
||||
@@ -293,6 +293,8 @@ M.edit_comment = function(tree, unlinked)
|
||||
return
|
||||
end
|
||||
|
||||
popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
|
||||
|
||||
edit_popup:mount()
|
||||
|
||||
-- Gather all lines from immediate children that aren't note nodes
|
||||
@@ -310,19 +312,19 @@ M.edit_comment = function(tree, unlinked)
|
||||
|
||||
-- Draft notes module handles edits for draft notes
|
||||
if M.is_draft_note(tree) then
|
||||
state.set_popup_keymaps(
|
||||
popup.set_popup_keymaps(
|
||||
edit_popup,
|
||||
draft_notes.confirm_edit_draft_note(note_node.id, unlinked),
|
||||
nil,
|
||||
miscellaneous.editable_popup_opts
|
||||
popup.editable_popup_opts
|
||||
)
|
||||
else
|
||||
local comment = require("gitlab.actions.comment")
|
||||
state.set_popup_keymaps(
|
||||
popup.set_popup_keymaps(
|
||||
edit_popup,
|
||||
comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
|
||||
nil,
|
||||
miscellaneous.editable_popup_opts
|
||||
popup.editable_popup_opts
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -585,7 +587,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||
if keymaps.discussion_tree.jump_to_reviewer then
|
||||
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
|
||||
if M.is_current_node_note(tree) then
|
||||
common.jump_to_reviewer(tree, M.refresh_diagnostics_and_winbar)
|
||||
common.jump_to_reviewer(tree, M.refresh_diagnostics)
|
||||
end
|
||||
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
|
||||
end
|
||||
@@ -603,7 +605,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||
|
||||
if keymaps.discussion_tree.refresh_data then
|
||||
vim.keymap.set("n", keymaps.discussion_tree.refresh_data, function()
|
||||
u.notify("Refreshing data...", vim.log.levels.INFO)
|
||||
draft_notes.rebuild_view(unlinked, false)
|
||||
end, {
|
||||
buffer = bufnr,
|
||||
@@ -646,6 +647,16 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||
})
|
||||
end
|
||||
|
||||
if keymaps.discussion_tree.toggle_sort_method then
|
||||
vim.keymap.set("n", keymaps.discussion_tree.toggle_sort_method, function()
|
||||
M.toggle_sort_method()
|
||||
end, {
|
||||
buffer = bufnr,
|
||||
desc = "Toggle sort method",
|
||||
nowait = keymaps.discussion_tree.toggle_sort_method_nowait,
|
||||
})
|
||||
end
|
||||
|
||||
if keymaps.discussion_tree.toggle_resolved then
|
||||
vim.keymap.set("n", keymaps.discussion_tree.toggle_resolved, function()
|
||||
if M.is_current_node_note(tree) and not M.is_draft_note(tree) then
|
||||
@@ -746,16 +757,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||
})
|
||||
end
|
||||
|
||||
if keymaps.discussion_tree.print_node then
|
||||
vim.keymap.set("n", keymaps.discussion_tree.print_node, function()
|
||||
common.print_node(tree)
|
||||
end, {
|
||||
buffer = bufnr,
|
||||
desc = "Print current node (for debugging)",
|
||||
nowait = keymaps.discussion_tree.print_node_nowait,
|
||||
})
|
||||
end
|
||||
|
||||
if keymaps.discussion_tree.add_emoji then
|
||||
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
|
||||
M.add_emoji_to_note(tree, unlinked)
|
||||
@@ -792,7 +793,18 @@ end
|
||||
---Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
|
||||
M.toggle_draft_mode = function()
|
||||
state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode
|
||||
end
|
||||
|
||||
---Toggle between sorting by "original comment" (oldest at the top) or "latest reply" (newest at the
|
||||
---top).
|
||||
M.toggle_sort_method = function()
|
||||
if state.settings.discussion_tree.sort_by == "original_comment" then
|
||||
state.settings.discussion_tree.sort_by = "latest_reply"
|
||||
else
|
||||
state.settings.discussion_tree.sort_by = "original_comment"
|
||||
end
|
||||
winbar.update_winbar()
|
||||
M.rebuild_view(false, true)
|
||||
end
|
||||
|
||||
---Indicates whether the node under the cursor is a draft note or not
|
||||
|
||||
@@ -277,13 +277,16 @@ local function build_note_body(note, resolve_info)
|
||||
)
|
||||
end
|
||||
|
||||
local resolve_symbol = ""
|
||||
local symbol = ""
|
||||
local is_draft = note.note ~= nil
|
||||
if resolve_info ~= nil and resolve_info.resolvable then
|
||||
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
||||
symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
||||
or state.settings.discussion_tree.unresolved
|
||||
elseif not is_draft and resolve_info and not resolve_info.resolvable then
|
||||
symbol = state.settings.discussion_tree.unlinked
|
||||
end
|
||||
|
||||
local noteHeader = common.build_note_header(note) .. " " .. resolve_symbol
|
||||
local noteHeader = common.build_note_header(note) .. " " .. symbol
|
||||
|
||||
return noteHeader, text_nodes
|
||||
end
|
||||
@@ -454,8 +457,10 @@ M.restore_cursor_position = function(winid, tree, original_node, root_node)
|
||||
end
|
||||
end
|
||||
if line_number ~= nil then
|
||||
if vim.api.nvim_win_is_valid(winid) then
|
||||
vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---This function expands a node and its children.
|
||||
|
||||
@@ -54,7 +54,19 @@ local get_data = function(nodes)
|
||||
return total_resolvable, total_resolved, total_non_resolvable
|
||||
end
|
||||
|
||||
local spinner_index = 0
|
||||
state.discussion_tree.last_updated = nil
|
||||
|
||||
local function content()
|
||||
local updated
|
||||
if state.discussion_tree.last_updated then
|
||||
local last_update = tostring(os.date("!%Y-%m-%dT%H:%M:%S", state.discussion_tree.last_updated))
|
||||
updated = u.time_since(last_update) .. " ⟳"
|
||||
else
|
||||
spinner_index = (spinner_index % #state.settings.discussion_tree.spinner_chars) + 1
|
||||
updated = state.settings.discussion_tree.spinner_chars[spinner_index]
|
||||
end
|
||||
|
||||
local resolvable_discussions, resolved_discussions, non_resolvable_discussions =
|
||||
get_data(state.DISCUSSION_DATA.discussions)
|
||||
local resolvable_notes, resolved_notes, non_resolvable_notes = get_data(state.DISCUSSION_DATA.unlinked_discussions)
|
||||
@@ -82,9 +94,10 @@ local function content()
|
||||
resolved_notes = resolved_notes,
|
||||
non_resolvable_notes = non_resolvable_notes,
|
||||
help_keymap = state.settings.keymaps.help,
|
||||
updated = updated,
|
||||
}
|
||||
|
||||
return M.make_winbar(t)
|
||||
return state.settings.discussion_tree.winbar and state.settings.discussion_tree.winbar(t) or M.make_winbar(t)
|
||||
end
|
||||
|
||||
---This function updates the winbar
|
||||
@@ -108,7 +121,7 @@ M.update_winbar = function()
|
||||
end
|
||||
|
||||
local function get_connector(base_title)
|
||||
return string.match(base_title, "%($") and "" or "; "
|
||||
return string.match(base_title, "%($") and "" or " "
|
||||
end
|
||||
|
||||
---Builds the title string for both sections, using the count of resolvable and draft nodes
|
||||
@@ -116,84 +129,128 @@ end
|
||||
---@param resolvable_count integer
|
||||
---@param resolved_count integer
|
||||
---@param drafts_count integer
|
||||
---@param focused boolean
|
||||
---@return string
|
||||
local add_drafts_and_resolvable = function(
|
||||
base_title,
|
||||
resolvable_count,
|
||||
resolved_count,
|
||||
drafts_count,
|
||||
non_resolvable_count
|
||||
non_resolvable_count,
|
||||
focused
|
||||
)
|
||||
if resolvable_count == 0 and drafts_count == 0 and non_resolvable_count == 0 then
|
||||
return base_title
|
||||
end
|
||||
base_title = base_title .. " ("
|
||||
if non_resolvable_count ~= 0 then
|
||||
base_title = base_title .. u.pluralize(non_resolvable_count, "comment")
|
||||
end
|
||||
if resolvable_count ~= 0 then
|
||||
base_title = base_title
|
||||
.. get_connector(base_title)
|
||||
.. string.format("%d/%s", resolved_count, u.pluralize(resolvable_count, "thread"))
|
||||
base_title = base_title .. M.get_resolved_text(focused, resolved_count, resolvable_count)
|
||||
end
|
||||
if non_resolvable_count ~= 0 then
|
||||
base_title = base_title .. M.get_nonresolveable_text(base_title, non_resolvable_count, focused)
|
||||
end
|
||||
if drafts_count ~= 0 then
|
||||
base_title = base_title .. get_connector(base_title) .. u.pluralize(drafts_count, "draft")
|
||||
base_title = base_title .. M.get_drafts_text(base_title, drafts_count, focused)
|
||||
end
|
||||
base_title = base_title .. ")"
|
||||
return base_title
|
||||
end
|
||||
|
||||
---@param t WinbarTable
|
||||
M.make_winbar = function(t)
|
||||
local discussion_title = add_drafts_and_resolvable(
|
||||
"Inline Comments",
|
||||
local discussions_focused = M.current_view_type == "discussions"
|
||||
local discussion_text = add_drafts_and_resolvable(
|
||||
"Inline Comments:",
|
||||
t.resolvable_discussions,
|
||||
t.resolved_discussions,
|
||||
t.inline_draft_notes,
|
||||
t.non_resolvable_discussions
|
||||
t.non_resolvable_discussions,
|
||||
discussions_focused
|
||||
)
|
||||
local notes_title = add_drafts_and_resolvable(
|
||||
"Notes",
|
||||
local notes_text = add_drafts_and_resolvable(
|
||||
"Notes:",
|
||||
t.resolvable_notes,
|
||||
t.resolved_notes,
|
||||
t.unlinked_draft_notes,
|
||||
t.non_resolvable_notes
|
||||
t.non_resolvable_notes,
|
||||
not discussions_focused
|
||||
)
|
||||
|
||||
-- Colorize the active tab
|
||||
if M.current_view_type == "discussions" then
|
||||
discussion_title = "%#Text#" .. discussion_title
|
||||
notes_title = "%#Comment#" .. notes_title
|
||||
elseif M.current_view_type == "notes" then
|
||||
discussion_title = "%#Comment#" .. discussion_title
|
||||
notes_title = "%#Text#" .. notes_title
|
||||
if discussions_focused then
|
||||
discussion_text = "%#Text#" .. discussion_text
|
||||
notes_text = "%#Comment#" .. notes_text
|
||||
else
|
||||
discussion_text = "%#Comment#" .. discussion_text
|
||||
notes_text = "%#Text#" .. notes_text
|
||||
end
|
||||
|
||||
local sort_method = M.get_sort_method()
|
||||
local mode = M.get_mode()
|
||||
|
||||
-- Join everything together and return it
|
||||
local separator = "%#Comment#|"
|
||||
local end_section = "%="
|
||||
local updated = "%#Text#" .. t.updated
|
||||
local help = "%#Comment#Help: " .. (t.help_keymap and t.help_keymap:gsub(" ", "<space>") .. " " or "unmapped")
|
||||
return string.format(
|
||||
" %s %s %s %s %s %s %s",
|
||||
discussion_title,
|
||||
" %s %s %s %s %s %s %s %s %s %s %s",
|
||||
discussion_text,
|
||||
separator,
|
||||
notes_title,
|
||||
notes_text,
|
||||
end_section,
|
||||
updated,
|
||||
separator,
|
||||
sort_method,
|
||||
separator,
|
||||
mode,
|
||||
separator,
|
||||
help
|
||||
)
|
||||
end
|
||||
|
||||
---Returns a string for the winbar indicating the sort method
|
||||
---@return string
|
||||
M.get_sort_method = function()
|
||||
local sort_method = state.settings.discussion_tree.sort_by == "original_comment" and "↓ by thread" or "↑ by reply"
|
||||
return "%#GitlabSortMethod#" .. sort_method .. "%#Comment#"
|
||||
end
|
||||
|
||||
M.get_resolved_text = function(focused, resolved_count, resolvable_count)
|
||||
local text = focused and ("%#GitlabResolved#" .. state.settings.discussion_tree.resolved .. "%#Text#")
|
||||
or state.settings.discussion_tree.resolved
|
||||
return " " .. string.format("%d%s/%d", resolved_count, text, resolvable_count)
|
||||
end
|
||||
|
||||
M.get_drafts_text = function(base_title, drafts_count, focused)
|
||||
return get_connector(base_title)
|
||||
.. string.format(
|
||||
"%d%s",
|
||||
drafts_count,
|
||||
(
|
||||
focused and ("%#GitlabDraft#" .. state.settings.discussion_tree.draft .. "%#Text#")
|
||||
or state.settings.discussion_tree.draft
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
M.get_nonresolveable_text = function(base_title, non_resolvable_count, focused)
|
||||
return get_connector(base_title)
|
||||
.. string.format(
|
||||
"%d%s",
|
||||
non_resolvable_count,
|
||||
(
|
||||
focused and ("%#GitlabUnlinked#" .. state.settings.discussion_tree.unlinked .. "%#Text#")
|
||||
or state.settings.discussion_tree.unlinked
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
---Returns a string for the winbar indicating the mode type, live or draft
|
||||
---@return string
|
||||
M.get_mode = function()
|
||||
if state.settings.discussion_tree.draft_mode then
|
||||
return "%#DiagnosticWarn#Draft Mode"
|
||||
return "%#GitlabDraftMode#Draft"
|
||||
else
|
||||
return "%#DiagnosticOK#Live Mode"
|
||||
return "%#GitlabLiveMode#Live"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -215,4 +272,8 @@ M.switch_view_type = function(override)
|
||||
M.update_winbar()
|
||||
end
|
||||
|
||||
-- Set up a timer to update the winbar periodically
|
||||
local timer = vim.uv.new_timer()
|
||||
timer:start(0, 100, vim.schedule_wrap(M.update_winbar))
|
||||
|
||||
return M
|
||||
|
||||
@@ -25,6 +25,7 @@ end
|
||||
---Makes API call to get the discussion data, stores it in the state, and calls the callback
|
||||
---@param callback function|nil
|
||||
M.load_draft_notes = function(callback)
|
||||
state.discussion_tree.last_updated = nil
|
||||
state.load_new_state("draft_notes", function()
|
||||
if callback ~= nil then
|
||||
callback()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
local M = {}
|
||||
|
||||
local u = require("gitlab.utils")
|
||||
local popup = require("gitlab.popup")
|
||||
local event = require("nui.utils.autocmd").event
|
||||
local state = require("gitlab.state")
|
||||
local List = require("gitlab.utils.list")
|
||||
@@ -16,15 +16,31 @@ M.open = function()
|
||||
end
|
||||
return agg
|
||||
end, {})
|
||||
|
||||
table.insert(help_content_lines, "")
|
||||
table.insert(
|
||||
help_content_lines,
|
||||
string.format(
|
||||
"%s = draft; %s = unlinked comment; %s = resolved",
|
||||
state.settings.discussion_tree.draft,
|
||||
state.settings.discussion_tree.unlinked,
|
||||
state.settings.discussion_tree.resolved
|
||||
)
|
||||
)
|
||||
|
||||
local longest_line = u.get_longest_string(help_content_lines)
|
||||
local help_popup =
|
||||
Popup(u.create_popup_state("Help", state.settings.popup.help, longest_line + 3, #help_content_lines + 3, 60))
|
||||
local opts = { "Help", state.settings.popup.help, longest_line + 3, #help_content_lines, 70 }
|
||||
local help_popup = Popup(popup.create_popup_state(unpack(opts)))
|
||||
|
||||
help_popup:on(event.BufLeave, function()
|
||||
help_popup:unmount()
|
||||
end)
|
||||
|
||||
popup.set_up_autocommands(help_popup, nil, vim.api.nvim_get_current_win(), opts)
|
||||
|
||||
help_popup:mount()
|
||||
|
||||
state.set_popup_keymaps(help_popup, "Help", nil)
|
||||
popup.set_popup_keymaps(help_popup, "Help", nil)
|
||||
local currentBuffer = vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_buf_set_lines(currentBuffer, 0, #help_content_lines, false, help_content_lines)
|
||||
u.switch_can_edit_buf(currentBuffer, false)
|
||||
|
||||
@@ -14,8 +14,10 @@ M.delete_label = function()
|
||||
M.delete_popup("label")
|
||||
end
|
||||
|
||||
local refresh_label_state = function(labels)
|
||||
local refresh_label_state = function(labels, message)
|
||||
u.notify(message, vim.log.levels.INFO)
|
||||
state.INFO.labels = labels
|
||||
require("gitlab.actions.summary").update_summary_details()
|
||||
end
|
||||
|
||||
local get_current_labels = function()
|
||||
@@ -41,9 +43,7 @@ M.add_popup = function(type)
|
||||
table.insert(current_labels, choice)
|
||||
local body = { labels = current_labels }
|
||||
job.run_job("/mr/" .. type, "PUT", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
|
||||
refresh_label_state(data.labels)
|
||||
refresh_label_state(data.labels, data.message)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -59,8 +59,7 @@ M.delete_popup = function(type)
|
||||
local filtered_labels = u.filter(current_labels, choice)
|
||||
local body = { labels = filtered_labels }
|
||||
job.run_job("/mr/" .. type, "PUT", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
refresh_label_state(data.labels)
|
||||
refresh_label_state(data.labels, data.message)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
local u = require("gitlab.utils")
|
||||
local popup = require("gitlab.popup")
|
||||
local Popup = require("nui.popup")
|
||||
local state = require("gitlab.state")
|
||||
local job = require("gitlab.job")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
|
||||
local M = {}
|
||||
|
||||
local function create_squash_message_popup()
|
||||
return Popup(u.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
|
||||
return Popup(popup.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
|
||||
end
|
||||
|
||||
---@class MergeOpts
|
||||
@@ -31,10 +31,11 @@ M.merge = function(opts)
|
||||
|
||||
if merge_body.squash then
|
||||
local squash_message_popup = create_squash_message_popup()
|
||||
popup.set_up_autocommands(squash_message_popup, nil, vim.api.nvim_get_current_win())
|
||||
squash_message_popup:mount()
|
||||
state.set_popup_keymaps(squash_message_popup, function(text)
|
||||
popup.set_popup_keymaps(squash_message_popup, function(text)
|
||||
M.confirm_merge(merge_body, text)
|
||||
end, nil, miscellaneous.editable_popup_opts)
|
||||
end, nil, popup.editable_popup_opts)
|
||||
else
|
||||
M.confirm_merge(merge_body)
|
||||
end
|
||||
|
||||
@@ -12,14 +12,6 @@ local M = {}
|
||||
---Opens up a select menu that lets you choose a different merge request.
|
||||
---@param opts ChooseMergeRequestOptions|nil
|
||||
M.choose_merge_request = function(opts)
|
||||
local has_clean_tree, clean_tree_err = git.has_clean_tree()
|
||||
if clean_tree_err ~= nil then
|
||||
return
|
||||
elseif has_clean_tree ~= "" then
|
||||
u.notify("Your local branch has changes, please stash or commit and push", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if opts == nil then
|
||||
opts = state.settings.choose_merge_request
|
||||
end
|
||||
@@ -38,6 +30,19 @@ M.choose_merge_request = function(opts)
|
||||
reviewer.close()
|
||||
end
|
||||
|
||||
if choice.source_branch ~= git.get_current_branch() then
|
||||
local has_clean_tree, clean_tree_err = git.has_clean_tree()
|
||||
if clean_tree_err ~= nil then
|
||||
return
|
||||
elseif not has_clean_tree then
|
||||
u.notify(
|
||||
"Cannot switch branch when working tree has changes, please stash or commit and push",
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
local _, branch_switch_err = git.switch_branch(choice.source_branch)
|
||||
if branch_switch_err ~= nil then
|
||||
|
||||
@@ -34,70 +34,6 @@ M.attach_file = function()
|
||||
end)
|
||||
end
|
||||
|
||||
M.editable_popup_opts = {
|
||||
action_before_close = true,
|
||||
action_before_exit = false,
|
||||
save_to_temp_register = true,
|
||||
}
|
||||
|
||||
M.non_editable_popup_opts = {
|
||||
action_before_close = true,
|
||||
action_before_exit = false,
|
||||
save_to_temp_register = false,
|
||||
}
|
||||
|
||||
-- Get the index of the next popup when cycling forward
|
||||
local function next_index(i, n, count)
|
||||
count = count > 0 and count or 1
|
||||
for _ = 1, count do
|
||||
if i < n then
|
||||
i = i + 1
|
||||
elseif i == n then
|
||||
i = 1
|
||||
end
|
||||
end
|
||||
return i
|
||||
end
|
||||
|
||||
---Get the index of the previous popup when cycling backward
|
||||
---@param i integer The current index
|
||||
---@param n integer The total number of popups
|
||||
---@param count integer The count used with the keymap (replaced with 1 if no count was given)
|
||||
local function prev_index(i, n, count)
|
||||
count = count > 0 and count or 1
|
||||
for _ = 1, count do
|
||||
if i > 1 then
|
||||
i = i - 1
|
||||
elseif i == 1 then
|
||||
i = n
|
||||
end
|
||||
end
|
||||
return i
|
||||
end
|
||||
|
||||
---Setup keymaps for cycling popups. The keymap accepts count.
|
||||
---@param popups table Table of Popups
|
||||
M.set_cycle_popups_keymaps = function(popups)
|
||||
local keymaps = require("gitlab.state").settings.keymaps
|
||||
if keymaps.disable_all or keymaps.popup.disable_all then
|
||||
return
|
||||
end
|
||||
|
||||
local number_of_popups = #popups
|
||||
for i, popup in ipairs(popups) do
|
||||
if keymaps.popup.next_field then
|
||||
popup:map("n", keymaps.popup.next_field, function()
|
||||
vim.api.nvim_set_current_win(popups[next_index(i, number_of_popups, vim.v.count)].winid)
|
||||
end, { desc = "Go to next field (accepts count)", nowait = keymaps.popup.next_field_nowait })
|
||||
end
|
||||
if keymaps.popup.prev_field then
|
||||
popup:map("n", keymaps.popup.prev_field, function()
|
||||
vim.api.nvim_set_current_win(popups[prev_index(i, number_of_popups, vim.v.count)].winid)
|
||||
end, { desc = "Go to previous field (accepts count)", nowait = keymaps.popup.prev_field_nowait })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Toggle the value in a "Boolean buffer"
|
||||
M.toggle_bool = function()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
@@ -5,6 +5,7 @@ local Popup = require("nui.popup")
|
||||
local state = require("gitlab.state")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local popup = require("gitlab.popup")
|
||||
local M = {
|
||||
pipeline_jobs = nil,
|
||||
latest_pipeline = nil,
|
||||
@@ -40,7 +41,8 @@ M.open = function()
|
||||
local height = 6 + #M.pipeline_jobs + 3
|
||||
|
||||
local pipeline_popup =
|
||||
Popup(u.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60))
|
||||
Popup(popup.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60))
|
||||
popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win())
|
||||
M.pipeline_popup = pipeline_popup
|
||||
pipeline_popup:mount()
|
||||
|
||||
@@ -91,7 +93,7 @@ M.open = function()
|
||||
end
|
||||
|
||||
pipeline_popup.border:set_text("top", "Pipeline Status", "center")
|
||||
state.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
|
||||
popup.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
|
||||
u.switch_can_edit_buf(bufnr, false)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ local git = require("gitlab.git")
|
||||
local job = require("gitlab.job")
|
||||
local common = require("gitlab.actions.common")
|
||||
local u = require("gitlab.utils")
|
||||
local popup = require("gitlab.popup")
|
||||
local List = require("gitlab.utils.list")
|
||||
local state = require("gitlab.state")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
@@ -34,6 +35,9 @@ M.summary = function()
|
||||
local info_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
|
||||
|
||||
local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
|
||||
|
||||
layout:mount()
|
||||
|
||||
local popups = {
|
||||
title_popup,
|
||||
description_popup,
|
||||
@@ -41,6 +45,9 @@ M.summary = function()
|
||||
}
|
||||
|
||||
M.layout = layout
|
||||
M.info_popup = info_popup
|
||||
M.title_popup = title_popup
|
||||
M.description_popup = description_popup
|
||||
M.layout_buf = layout.bufnr
|
||||
M.layout_visible = true
|
||||
|
||||
@@ -54,30 +61,28 @@ M.summary = function()
|
||||
vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { title })
|
||||
|
||||
if info_popup then
|
||||
vim.api.nvim_buf_set_lines(info_popup.bufnr, 0, -1, false, info_lines)
|
||||
u.switch_can_edit_buf(info_popup.bufnr, false)
|
||||
M.color_details(info_popup.bufnr) -- Color values in details popup
|
||||
M.update_details_popup(info_popup.bufnr, info_lines)
|
||||
end
|
||||
|
||||
state.set_popup_keymaps(
|
||||
popup.set_popup_keymaps(
|
||||
description_popup,
|
||||
M.edit_summary,
|
||||
miscellaneous.attach_file,
|
||||
{ cb = exit, action_before_close = true, action_before_exit = true, save_to_temp_register = true }
|
||||
)
|
||||
state.set_popup_keymaps(
|
||||
popup.set_popup_keymaps(
|
||||
title_popup,
|
||||
M.edit_summary,
|
||||
nil,
|
||||
{ cb = exit, action_before_close = true, action_before_exit = true }
|
||||
)
|
||||
state.set_popup_keymaps(
|
||||
popup.set_popup_keymaps(
|
||||
info_popup,
|
||||
M.edit_summary,
|
||||
nil,
|
||||
{ cb = exit, action_before_close = true, action_before_exit = true }
|
||||
)
|
||||
miscellaneous.set_cycle_popups_keymaps(popups)
|
||||
popup.set_cycle_popups_keymaps(popups)
|
||||
|
||||
vim.api.nvim_set_current_buf(description_popup.bufnr)
|
||||
end)
|
||||
@@ -86,6 +91,23 @@ M.summary = function()
|
||||
git.check_mr_in_good_condition()
|
||||
end
|
||||
|
||||
M.update_summary_details = function()
|
||||
if not M.info_popup or not M.info_popup.bufnr then
|
||||
return
|
||||
end
|
||||
local details_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
|
||||
local internal_layout = M.create_internal_layout(details_lines, M.title_popup, M.description_popup, M.info_popup)
|
||||
M.layout:update(M.get_outer_layout_config(), internal_layout)
|
||||
M.update_details_popup(M.info_popup.bufnr, details_lines)
|
||||
end
|
||||
|
||||
M.update_details_popup = function(bufnr, info_lines)
|
||||
u.switch_can_edit_buf(bufnr, true)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, info_lines)
|
||||
u.switch_can_edit_buf(bufnr, false)
|
||||
M.color_details(bufnr) -- Color values in details popup
|
||||
end
|
||||
|
||||
-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
|
||||
-- lines that users include in their state.settings.info.fields list.
|
||||
M.build_info_lines = function()
|
||||
@@ -165,16 +187,37 @@ M.edit_summary = function()
|
||||
end)
|
||||
end
|
||||
|
||||
---Create the Summary layout and individual popups that make up the Layout.
|
||||
---@return NuiLayout, NuiPopup, NuiPopup, NuiPopup
|
||||
M.create_layout = function(info_lines)
|
||||
local title_popup = Popup(u.create_box_popup_state(nil, false))
|
||||
local settings = u.merge(state.settings.popup, state.settings.popup.summary or {})
|
||||
local title_popup = Popup(popup.create_box_popup_state(nil, false, settings))
|
||||
M.title_bufnr = title_popup.bufnr
|
||||
local description_popup = Popup(u.create_box_popup_state("Description", true))
|
||||
local description_popup = Popup(popup.create_popup_state("Description", settings))
|
||||
M.description_bufnr = description_popup.bufnr
|
||||
local details_popup
|
||||
if state.settings.info.enabled then
|
||||
details_popup = Popup(popup.create_box_popup_state("Details", false, settings))
|
||||
end
|
||||
|
||||
local internal_layout = M.create_internal_layout(info_lines, title_popup, description_popup, details_popup)
|
||||
|
||||
local layout = Layout(M.get_outer_layout_config(), internal_layout)
|
||||
|
||||
popup.set_up_autocommands(description_popup, layout, vim.api.nvim_get_current_win())
|
||||
|
||||
return layout, title_popup, description_popup, details_popup
|
||||
end
|
||||
|
||||
---Create the internal layout of the Summary and individual popups that make up the Layout.
|
||||
---@param info_lines string[] Table of strings that make up the details content
|
||||
---@param title_popup NuiPopup
|
||||
---@param description_popup NuiPopup
|
||||
---@param details_popup NuiPopup
|
||||
---@return NuiLayout.Box
|
||||
M.create_internal_layout = function(info_lines, title_popup, description_popup, details_popup)
|
||||
local internal_layout
|
||||
if state.settings.info.enabled then
|
||||
details_popup = Popup(u.create_box_popup_state("Details", false))
|
||||
if state.settings.info.horizontal then
|
||||
local longest_line = u.get_longest_string(info_lines)
|
||||
internal_layout = Layout.Box({
|
||||
@@ -182,7 +225,7 @@ M.create_layout = function(info_lines)
|
||||
Layout.Box({
|
||||
Layout.Box(details_popup, { size = longest_line + 3 }),
|
||||
Layout.Box(description_popup, { grow = 1 }),
|
||||
}, { dir = "row", size = "100%" }),
|
||||
}, { dir = "row", size = "95%" }),
|
||||
}, { dir = "col" })
|
||||
else
|
||||
internal_layout = Layout.Box({
|
||||
@@ -197,18 +240,21 @@ M.create_layout = function(info_lines)
|
||||
Layout.Box(description_popup, { grow = 1 }),
|
||||
}, { dir = "col" })
|
||||
end
|
||||
return internal_layout
|
||||
end
|
||||
|
||||
local layout = Layout({
|
||||
position = "50%",
|
||||
---Create the config for the outer Layout of the Summary
|
||||
---@return nui_layout_options
|
||||
M.get_outer_layout_config = function()
|
||||
local settings = u.merge(state.settings.popup, state.settings.popup.summary or {})
|
||||
return {
|
||||
position = settings.position,
|
||||
relative = "editor",
|
||||
size = {
|
||||
width = "95%",
|
||||
height = "95%",
|
||||
width = settings.width,
|
||||
height = settings.height,
|
||||
},
|
||||
}, internal_layout)
|
||||
|
||||
layout:mount()
|
||||
return layout, title_popup, description_popup, details_popup
|
||||
}
|
||||
end
|
||||
|
||||
M.color_details = function(bufnr)
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
---@field resolved_notes number
|
||||
---@field non_resolvable_notes number
|
||||
---@field help_keymap string
|
||||
---@field updated string
|
||||
---
|
||||
---@class SignTable
|
||||
---@field name string
|
||||
|
||||
@@ -3,12 +3,13 @@ local state = require("gitlab.state")
|
||||
local colors = state.settings.colors
|
||||
|
||||
-- Set icons into global vim variables for syntax matching
|
||||
local expanders = state.settings.discussion_tree.expanders
|
||||
vim.g.gitlab_discussion_tree_expander_open = expanders.expanded
|
||||
vim.g.gitlab_discussion_tree_expander_closed = expanders.collapsed
|
||||
vim.g.gitlab_discussion_tree_draft = ""
|
||||
vim.g.gitlab_discussion_tree_resolved = "✓"
|
||||
vim.g.gitlab_discussion_tree_unresolved = "-"
|
||||
local discussion_tree = state.settings.discussion_tree
|
||||
vim.g.gitlab_discussion_tree_expander_open = discussion_tree.expanders.expanded
|
||||
vim.g.gitlab_discussion_tree_expander_closed = discussion_tree.expanders.collapsed
|
||||
vim.g.gitlab_discussion_tree_draft = discussion_tree.draft
|
||||
vim.g.gitlab_discussion_tree_resolved = discussion_tree.resolved
|
||||
vim.g.gitlab_discussion_tree_unresolved = discussion_tree.unresolved
|
||||
vim.g.gitlab_discussion_tree_unlinked = discussion_tree.unlinked
|
||||
|
||||
local discussion = colors.discussion_tree
|
||||
|
||||
@@ -17,7 +18,6 @@ local function get_colors_for_group(group)
|
||||
local normal_bg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(group)), "bg")
|
||||
return { fg = normal_fg, bg = normal_bg }
|
||||
end
|
||||
|
||||
vim.api.nvim_create_autocmd("VimEnter", {
|
||||
callback = function()
|
||||
vim.api.nvim_set_hl(0, "GitlabUsername", get_colors_for_group(discussion.username))
|
||||
@@ -29,6 +29,10 @@ vim.api.nvim_create_autocmd("VimEnter", {
|
||||
vim.api.nvim_set_hl(0, "GitlabFileName", get_colors_for_group(discussion.file_name))
|
||||
vim.api.nvim_set_hl(0, "GitlabResolved", get_colors_for_group(discussion.resolved))
|
||||
vim.api.nvim_set_hl(0, "GitlabUnresolved", get_colors_for_group(discussion.unresolved))
|
||||
vim.api.nvim_set_hl(0, "GitlabUnlinked", get_colors_for_group(discussion.unlinked))
|
||||
vim.api.nvim_set_hl(0, "GitlabDraft", get_colors_for_group(discussion.draft))
|
||||
vim.api.nvim_set_hl(0, "GitlabDraftMode", get_colors_for_group(discussion.draft_mode))
|
||||
vim.api.nvim_set_hl(0, "GitlabLiveMode", get_colors_for_group(discussion.live_mode))
|
||||
vim.api.nvim_set_hl(0, "GitlabSortMethod", get_colors_for_group(discussion.sort_method))
|
||||
end,
|
||||
})
|
||||
|
||||
@@ -25,10 +25,19 @@ M.branches = function(args)
|
||||
return run_system(u.combine({ "git", "branch" }, args or {}))
|
||||
end
|
||||
|
||||
---Checks whether the tree has any changes that haven't been pushed to the remote
|
||||
---@return string|nil, string|nil
|
||||
---Returns true if the working tree hasn't got any changes that haven't been commited
|
||||
---@return boolean, string|nil
|
||||
M.has_clean_tree = function()
|
||||
return run_system({ "git", "status", "--short", "--untracked-files=no" })
|
||||
local changes, err = run_system({ "git", "status", "--short", "--untracked-files=no" })
|
||||
return changes == "", err
|
||||
end
|
||||
|
||||
---Returns true if the `file` has got any uncommitted changes
|
||||
---@param file string File to check for changes
|
||||
---@return boolean, string|nil
|
||||
M.has_changes = function(file)
|
||||
local changes, err = run_system({ "git", "status", "--short", "--untracked-files=no", "--", file })
|
||||
return changes ~= "", err
|
||||
end
|
||||
|
||||
---Gets the base directory of the current project
|
||||
|
||||
@@ -92,10 +92,10 @@ return {
|
||||
end
|
||||
end,
|
||||
toggle_draft_mode = discussions.toggle_draft_mode,
|
||||
toggle_sort_method = discussions.toggle_sort_method,
|
||||
publish_all_drafts = draft_notes.publish_all_drafts,
|
||||
refresh_data = function()
|
||||
-- This also rebuilds the regular views
|
||||
u.notify("Refreshing data...", vim.log.levels.INFO)
|
||||
draft_notes.rebuild_view(false, true)
|
||||
end,
|
||||
-- Other functions 🤷
|
||||
|
||||
239
lua/gitlab/popup.lua
Normal file
239
lua/gitlab/popup.lua
Normal 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
|
||||
@@ -42,12 +42,21 @@ M.open = function()
|
||||
end
|
||||
|
||||
local diffview_open_command = "DiffviewOpen"
|
||||
|
||||
if state.settings.reviewer_settings.diffview.imply_local then
|
||||
local has_clean_tree, err = git.has_clean_tree()
|
||||
if err ~= nil then
|
||||
return
|
||||
end
|
||||
if 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"
|
||||
else
|
||||
u.notify(
|
||||
"Your working tree has changes, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
state.settings.reviewer_settings.diffview.imply_local = false
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
|
||||
@@ -55,13 +64,6 @@ M.open = function()
|
||||
M.is_open = true
|
||||
M.tabnr = vim.api.nvim_get_current_tabpage()
|
||||
|
||||
if state.settings.reviewer_settings.diffview.imply_local and not has_clean_tree then
|
||||
u.notify(
|
||||
"There are uncommited changes in the working tree, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
|
||||
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
|
||||
u.notify(
|
||||
"Diagnostics are now configured as settings.discussion_signs, see :h gitlab.nvim.signs-and-diagnostics",
|
||||
@@ -289,12 +291,16 @@ end
|
||||
M.execute_callback = function(callback)
|
||||
return function()
|
||||
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { "'[V']" } }, {})
|
||||
vim.api.nvim_cmd(
|
||||
local _, err = pcall(
|
||||
vim.api.nvim_cmd,
|
||||
{ cmd = "lua", args = { ("require'gitlab'.%s()"):format(callback) }, mods = { lockmarks = true } },
|
||||
{}
|
||||
)
|
||||
vim.api.nvim_win_set_cursor(M.old_winnr, M.old_cursor_position)
|
||||
vim.opt.operatorfunc = M.old_opfunc
|
||||
if err ~= "" then
|
||||
u.notify_vim_error(err, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ M.settings = {
|
||||
toggle_tree_type = "i",
|
||||
publish_draft = "P",
|
||||
toggle_draft_mode = "D",
|
||||
toggle_sort_method = "st",
|
||||
toggle_node = "t",
|
||||
toggle_all_discussions = "T",
|
||||
toggle_resolved_discussions = "R",
|
||||
@@ -133,15 +134,18 @@ M.settings = {
|
||||
popup = {
|
||||
width = "40%",
|
||||
height = "60%",
|
||||
position = "50%",
|
||||
border = "rounded",
|
||||
opacity = 1.0,
|
||||
edit = nil,
|
||||
comment = nil,
|
||||
edit = nil,
|
||||
note = nil,
|
||||
help = nil,
|
||||
pipeline = nil,
|
||||
reply = nil,
|
||||
squash_message = nil,
|
||||
create_mr = { width = "95%", height = "95%" },
|
||||
summary = { width = "95%", height = "95%" },
|
||||
temp_registers = {},
|
||||
},
|
||||
discussion_tree = {
|
||||
@@ -150,15 +154,19 @@ M.settings = {
|
||||
collapsed = " ",
|
||||
indentation = " ",
|
||||
},
|
||||
spinner_chars = { "-", "\\", "|", "/" },
|
||||
auto_open = true,
|
||||
default_view = "discussions",
|
||||
blacklist = {},
|
||||
sort_by = "latest_reply",
|
||||
keep_current_open = false,
|
||||
position = "left",
|
||||
position = "bottom",
|
||||
size = "20%",
|
||||
relative = "editor",
|
||||
resolved = "✓",
|
||||
unresolved = "-",
|
||||
unlinked = "",
|
||||
draft = "✎",
|
||||
tree_type = "simple",
|
||||
draft_mode = false,
|
||||
},
|
||||
@@ -233,13 +241,17 @@ M.settings = {
|
||||
username = "Keyword",
|
||||
mention = "WarningMsg",
|
||||
date = "Comment",
|
||||
unlinked = "DiffviewNonText",
|
||||
expander = "DiffviewNonText",
|
||||
directory = "Directory",
|
||||
directory_icon = "DiffviewFolderSign",
|
||||
file_name = "Normal",
|
||||
resolved = "DiagnosticSignOk",
|
||||
unresolved = "DiagnosticSignWarn",
|
||||
draft = "DiffviewNonText",
|
||||
draft = "DiffviewReference",
|
||||
draft_mode = "DiagnosticWarn",
|
||||
live_mode = "DiagnosticOk",
|
||||
sort_method = "Keyword",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -427,94 +439,6 @@ M.setPluginConfiguration = function()
|
||||
return true
|
||||
end
|
||||
|
||||
local function exit(popup, opts)
|
||||
if opts.action_before_exit and opts.cb ~= nil then
|
||||
opts.cb()
|
||||
popup:unmount()
|
||||
else
|
||||
popup:unmount()
|
||||
if opts.cb ~= nil then
|
||||
opts.cb()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- These keymaps are buffer specific and are set dynamically when popups mount
|
||||
M.set_popup_keymaps = function(popup, action, linewise_action, opts)
|
||||
if M.settings.keymaps.disable_all or M.settings.keymaps.popup.disable_all then
|
||||
return
|
||||
end
|
||||
|
||||
if opts == nil then
|
||||
opts = {}
|
||||
end
|
||||
if action ~= "Help" and M.settings.keymaps.help then -- Don't show help on the help popup
|
||||
vim.keymap.set("n", M.settings.keymaps.help, function()
|
||||
local help = require("gitlab.actions.help")
|
||||
help.open()
|
||||
end, { buffer = popup.bufnr, desc = "Open help", nowait = M.settings.keymaps.help_nowait })
|
||||
end
|
||||
if action ~= nil and M.settings.keymaps.popup.perform_action then
|
||||
vim.keymap.set("n", M.settings.keymaps.popup.perform_action, function()
|
||||
local text = u.get_buffer_text(popup.bufnr)
|
||||
if opts.action_before_close then
|
||||
action(text, popup.bufnr)
|
||||
exit(popup, opts)
|
||||
else
|
||||
exit(popup, opts)
|
||||
action(text, popup.bufnr)
|
||||
end
|
||||
end, { buffer = popup.bufnr, desc = "Perform action", nowait = M.settings.keymaps.popup.perform_action_nowait })
|
||||
end
|
||||
|
||||
if linewise_action ~= nil and M.settings.keymaps.popup.perform_action then
|
||||
vim.keymap.set("n", M.settings.keymaps.popup.perform_linewise_action, function()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local linnr = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local text = u.get_line_content(bufnr, linnr)
|
||||
linewise_action(text)
|
||||
end, {
|
||||
buffer = popup.bufnr,
|
||||
desc = "Perform linewise action",
|
||||
nowait = M.settings.keymaps.popup.perform_linewise_action_nowait,
|
||||
})
|
||||
end
|
||||
|
||||
if M.settings.keymaps.popup.discard_changes then
|
||||
vim.keymap.set("n", M.settings.keymaps.popup.discard_changes, function()
|
||||
local temp_registers = M.settings.popup.temp_registers
|
||||
M.settings.popup.temp_registers = {}
|
||||
vim.cmd("quit!")
|
||||
M.settings.popup.temp_registers = temp_registers
|
||||
end, {
|
||||
buffer = popup.bufnr,
|
||||
desc = "Quit discarding changes",
|
||||
nowait = M.settings.keymaps.popup.discard_changes_nowait,
|
||||
})
|
||||
end
|
||||
|
||||
if opts.save_to_temp_register then
|
||||
vim.api.nvim_create_autocmd("BufWinLeave", {
|
||||
buffer = popup.bufnr,
|
||||
callback = function()
|
||||
local text = u.get_buffer_text(popup.bufnr)
|
||||
for _, register in ipairs(M.settings.popup.temp_registers) do
|
||||
vim.fn.setreg(register, text)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
if opts.action_before_exit then
|
||||
vim.api.nvim_create_autocmd("BufWinLeave", {
|
||||
buffer = popup.bufnr,
|
||||
callback = function()
|
||||
exit(popup, opts)
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Dependencies
|
||||
-- These tables are passed to the async.sequence function, which calls them in sequence
|
||||
-- before calling an action. They are used to set global state that's required
|
||||
@@ -606,6 +530,7 @@ M.dependencies = {
|
||||
body = function()
|
||||
return {
|
||||
blacklist = M.settings.discussion_tree.blacklist,
|
||||
sort_by = M.settings.discussion_tree.sort_by,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -122,7 +122,7 @@ M.time_since = function(date_string, current_date_table)
|
||||
local time_diff = current_date - date
|
||||
|
||||
if time_diff < 60 then
|
||||
return M.pluralize(time_diff, "second") .. " ago"
|
||||
return "just now"
|
||||
elseif time_diff < 3600 then
|
||||
return M.pluralize(math.floor(time_diff / 60), "minute") .. " ago"
|
||||
elseif time_diff < 86400 then
|
||||
@@ -335,6 +335,11 @@ M.notify = function(msg, lvl)
|
||||
vim.notify("gitlab.nvim: " .. msg, lvl)
|
||||
end
|
||||
|
||||
-- Re-raise Vimscript error message after removing existing message prefixes
|
||||
M.notify_vim_error = function(msg, lvl)
|
||||
M.notify(msg:gsub("^Vim:", ""):gsub("^gitlab.nvim: ", ""), lvl)
|
||||
end
|
||||
|
||||
M.get_current_line_number = function()
|
||||
return vim.api.nvim_call_function("line", { "." })
|
||||
end
|
||||
@@ -427,6 +432,10 @@ M.press_enter = function()
|
||||
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false)
|
||||
end
|
||||
|
||||
M.press_escape = function()
|
||||
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", false, true, true), "nx", false)
|
||||
end
|
||||
|
||||
---Return timestamp from ISO 8601 formatted date string.
|
||||
---@param date_string string ISO 8601 formatted date string
|
||||
---@return integer timestamp
|
||||
@@ -480,62 +489,6 @@ M.difference = function(a, b)
|
||||
return not_included
|
||||
end
|
||||
|
||||
---Get the popup view_opts
|
||||
---@param title string The string to appear on top of the popup
|
||||
---@param settings table|nil User defined popup settings
|
||||
---@param width number? Override default width
|
||||
---@param height number? Override default height
|
||||
---@return table
|
||||
M.create_popup_state = function(title, settings, width, height, zindex)
|
||||
local default_settings = require("gitlab.state").settings.popup
|
||||
local user_settings = settings or {}
|
||||
local view_opts = {
|
||||
buf_options = {
|
||||
filetype = "markdown",
|
||||
},
|
||||
relative = "editor",
|
||||
enter = true,
|
||||
focusable = true,
|
||||
zindex = zindex or 50,
|
||||
border = {
|
||||
style = user_settings.border or default_settings.border,
|
||||
text = {
|
||||
top = title,
|
||||
},
|
||||
},
|
||||
position = "50%",
|
||||
size = {
|
||||
width = user_settings.width or width or default_settings.width,
|
||||
height = user_settings.height or height or default_settings.height,
|
||||
},
|
||||
opacity = user_settings.opacity or default_settings.opacity,
|
||||
}
|
||||
|
||||
return view_opts
|
||||
end
|
||||
|
||||
---Create view_opts for Box popups used inside popup Layouts
|
||||
---@param title string|nil The string to appear on top of the popup
|
||||
---@param enter boolean Whether the pop should be focused after creation
|
||||
---@return table
|
||||
M.create_box_popup_state = function(title, enter)
|
||||
local settings = require("gitlab.state").settings.popup
|
||||
return {
|
||||
buf_options = {
|
||||
filetype = "markdown",
|
||||
},
|
||||
enter = enter or false,
|
||||
focusable = true,
|
||||
border = {
|
||||
style = settings.border,
|
||||
text = {
|
||||
top = title,
|
||||
},
|
||||
},
|
||||
opacity = settings.opacity,
|
||||
}
|
||||
end
|
||||
|
||||
M.read_file = function(file_path, opts)
|
||||
local file = io.open(file_path, "r")
|
||||
if file == nil then
|
||||
@@ -634,7 +587,7 @@ end
|
||||
M.check_visual_mode = function()
|
||||
local mode = vim.api.nvim_get_mode().mode
|
||||
if mode ~= "v" and mode ~= "V" then
|
||||
M.notify("Code suggestions are only available in visual mode", vim.log.levels.WARN)
|
||||
M.notify("Code suggestions and multiline comments are only available in visual mode", vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
@@ -644,7 +597,7 @@ end
|
||||
---Exists visual mode in order to access marks "<" , ">"
|
||||
---@return integer start,integer end Start line and end line
|
||||
M.get_visual_selection_boundaries = function()
|
||||
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", false, true, true), "nx", false)
|
||||
M.press_escape()
|
||||
local start_line = vim.api.nvim_buf_get_mark(0, "<")[1]
|
||||
local end_line = vim.api.nvim_buf_get_mark(0, ">")[1]
|
||||
return start_line, end_line
|
||||
|
||||
Reference in New Issue
Block a user