diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 98d3c074..d220b816 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -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:
diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml
index 2786b74b..73f5ad13 100644
--- a/.github/workflows/go.yaml
+++ b/.github/workflows/go.yaml
@@ -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 💅
diff --git a/.github/workflows/lua.yaml b/.github/workflows/lua.yaml
index 75042618..a8998468 100644
--- a/.github/workflows/lua.yaml
+++ b/.github/workflows/lua.yaml
@@ -4,6 +4,8 @@ on:
branches:
- main
- develop
+ paths:
+ - 'lua/**' # Ignore changes to the Go code
jobs:
lua_lint:
name: Lint Lua 💅
diff --git a/README.md b/README.md
index 4660110d..1b6841af 100644
--- a/README.md
+++ b/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!
-![Screenshot 2024-01-13 at 10 43 32 AM](https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/8dd8b961-a6b5-4e09-b87f-dc4a17b14149)
-![Screenshot 2024-01-13 at 10 43 17 AM](https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/079842de-e8a4-45c5-98c2-dcafc799c904)
+![Screenshot 2024-12-08 at 5 43 53 PM](https://github.com/user-attachments/assets/cb9e94e3-3817-4846-ba44-16ec06ea7654)
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335-afe1-d554e3804372
@@ -36,16 +35,15 @@ For more detailed information about the Lua APIs please run `:h gitlab.nvim.api`
With 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()
@@ -53,30 +51,32 @@ return {
}
```
-And with Packer:
+And with pckr.nvim:
```lua
- use {
- "harrisoncramer/gitlab.nvim",
- requires = {
- "MunifTanjim/nui.nvim",
- "nvim-lua/plenary.nvim",
- "sindrets/diffview.nvim"
- "stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
- "nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
- },
- build = function()
- require("gitlab.server").build()
- end,
- branch = "develop",
- config = function()
- require("diffview") -- We require some global state from diffview
- local gitlab = require("gitlab")
- gitlab.setup()
- end,
- }
+{
+ "harrisoncramer/gitlab.nvim",
+ requires = {
+ "MunifTanjim/nui.nvim",
+ "nvim-lua/plenary.nvim",
+ "sindrets/diffview.nvim",
+ "stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
+ "nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
+ },
+ run = function() require("gitlab.server").build() end, -- Builds the Go binary
+ config = function()
+ require("diffview") -- We require some global state from diffview
+ require("gitlab").setup()
+ end,
+}
```
+Add `branch = "develop",` to your configuration if you want to use the (possibly unstable) development version of `gitlab.nvim`.
+
+## Contributing
+
+Contributions to the plugin are welcome. Please read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start working on a pull request.
+
## Connecting to Gitlab
This plugin requires an auth token 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.
diff --git a/after/syntax/gitlab.vim b/after/syntax/gitlab.vim
index 3acf898a..d19182c5 100644
--- a/after/syntax/gitlab.vim
+++ b/after/syntax/gitlab.vim
@@ -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
-
-execute 'syntax match Unresolved /\s' . g:gitlab_discussion_tree_unresolved . '\s\?/'
-highlight link Unresolved GitlabUnresolved
-
-execute 'syntax match Resolved /\s' . g:gitlab_discussion_tree_resolved . '\s\?/'
-highlight link Resolved GitlabResolved
-
-execute 'syntax match GitlabDiscussionOpen /^\s*' . g:gitlab_discussion_tree_expander_open . '/'
-highlight link GitlabDiscussionOpen GitlabExpander
-
-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 . '\)\@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 = "", -- Cycle to the next field. Accepts |count|.
prev_field = "", -- Cycle to the previous field. Accepts |count|.
perform_action = "ZZ", -- Once in normal mode, does action (like saving comment or applying description edit, etc)
- perform_linewise_action = "ZA", -- Once in normal mode, does the linewise action (see logs for this job, etc)
+ perform_linewise_action, = "ZA", -- Once in normal mode, does the linewise action (see logs for this job, etc)
discard_changes = "ZQ", -- Quit the popup discarding changes, the popup content is not saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
},
discussion_tree = {
@@ -219,6 +214,7 @@ you call this function with no values the defaults will be used:
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
publish_draft = "P", -- Publish the currently focused note/comment
toggle_draft_mode = "D", -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
+ toggle_sort_method = "st", -- Toggle whether discussions are sorted by the "latest_reply", or by "original_comment", see `:h gitlab.nvim.toggle_sort_method`
toggle_node = "t", -- Open or close the discussion
toggle_all_discussions = "T", -- Open or close separately both resolved and unresolved discussions
toggle_resolved_discussions = "R", -- Open or close all resolved discussions
@@ -234,36 +230,44 @@ you call this function with no values the defaults will be used:
},
},
popup = { -- The popup for comment creation, editing, and replying
- width = "40%",
- height = "60%",
+ width = "40%", -- Can be a percentage (string or decimal, "40%" = 0.4) of editor screen width, or an integer (number of columns)
+ height = "60%", -- Can be a percentage (string or decimal, "60%" = 0.6) of editor screen width, or an integer (number of rows)
+ position = "50%", -- Position (from the top left corner), either a number or percentage string that applies to both horizontal and vertical position, or a table that specifies them separately, e.g., { row = "90%", col = "100%" } places popups in the bottom right corner while leaving the status line visible
border = "rounded", -- One of "rounded", "single", "double", "solid"
opacity = 1.0, -- From 0.0 (fully transparent) to 1.0 (fully opaque)
comment = nil, -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 },
edit = nil,
note = nil,
- pipeline = nil,
+ help = nil, -- Width and height are calculated automatically and cannot be overridden
+ pipeline = nil, -- Width and height are calculated automatically and cannot be overridden
reply = nil,
squash_message = nil,
+ create_mr = { width = "95%", height = "95%" },
+ summary = { width = "95%", height = "95%" },
temp_registers = {}, -- List of registers for backing up popup content (see `:h gitlab.nvim.temp-registers`)
},
discussion_tree = { -- The discussion tree that holds all comments
expanders = { -- Discussion tree icons
expanded = " ", -- Icon for expanded discussion thread
- collapsed = "ï‘ ", -- Icon for collapsed discussion thread
+ collapsed = "ï‘ ", -- Icon for collapsed discussion thread
indentation = " ", -- Indentation Icon
},
+ spinner_chars = { "/", "|", "\\", "-" }, -- Characters for the refresh animation
auto_open = true, -- Automatically open when the reviewer is opened
- default_view = "discussions" -- Show "discussions" or "notes" by default
+ default_view = "discussions", -- Show "discussions" or "notes" by default
blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc)
+ sort_by = "latest_reply", -- Sort discussion tree by the "latest_reply", or by "original_comment", see `:h gitlab.nvim.toggle_sort_method`
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
- position = "left", -- "top", "right", "bottom" or "left"
+ position = "bottom", -- "top", "right", "bottom" or "left"
size = "20%", -- Size of split
relative = "editor", -- Position of tree split relative to "editor" or "window"
resolved = '✓', -- Symbol to show next to resolved discussions
unresolved = '-', -- Symbol to show next to unresolved discussions
+ unlinked = "󰌸", -- Symbol to show next to unliked comments (i.e., not threads)
+ draft = "✎", -- Symbol to show next to draft comments/notes
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
draft_mode = false, -- Whether comments are posted as drafts as part of a review
- winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
+ winbar = nil, -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
-- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
},
emojis = {
@@ -330,7 +334,7 @@ you call this function with no values the defaults will be used:
squash = false, -- Whether the commits will be marked for squashing
fork = {
enabled = false, -- If making an MR from a fork
- forked_project_id = nil -- The ID of the project you are merging into. If nil, will be prompted.
+ forked_project_id = nil, -- The ID of the project you are merging into. If nil, will be prompted.
},
title_input = { -- Default settings for MR title input window
width = 40,
@@ -349,6 +353,9 @@ you call this function with no values the defaults will be used:
resolved = "DiagnosticSignOk",
unresolved = "DiagnosticSignWarn",
draft = "DiffviewNonText",
+ draft_mode = "DiagnosticWarn",
+ live_mode = "DiagnosticOk",
+ sort_method = "Keyword",
}
}
})
@@ -929,6 +936,13 @@ gitlab.toggle_draft_mode() ~
Toggles between draft mode, where comments and notes are added to a review as
drafts, and regular (or live) mode, where comments are posted immediately.
+ *gitlab.nvim.toggle_sort_method*
+gitlab.toggle_sort_method() ~
+
+Toggles whether the discussion tree is sorted by the "latest_reply", with
+threads with the most recent activity on top (the default), or by
+"original_comment", with the oldest threads on top.
+
*gitlab.nvim.add_assignee*
gitlab.add_assignee() ~
diff --git a/lua/gitlab/actions/approvals.lua b/lua/gitlab/actions/approvals.lua
index 8ca7669c..12b3e09d 100644
--- a/lua/gitlab/actions/approvals.lua
+++ b/lua/gitlab/actions/approvals.lua
@@ -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
diff --git a/lua/gitlab/actions/assignees_and_reviewers.lua b/lua/gitlab/actions/assignees_and_reviewers.lua
index 329f3825..77716d1e 100644
--- a/lua/gitlab/actions/assignees_and_reviewers.lua
+++ b/lua/gitlab/actions/assignees_and_reviewers.lua
@@ -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
diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua
index fbc3ad01..29fd08ca 100644
--- a/lua/gitlab/actions/comment.lua
+++ b/lua/gitlab/actions/comment.lua
@@ -3,10 +3,10 @@
--- to this module the data required to make the API calls
local Popup = require("nui.popup")
local Layout = require("nui.layout")
-local diffview_lib = require("diffview.lib")
local state = require("gitlab.state")
local job = require("gitlab.job")
local u = require("gitlab.utils")
+local popup = require("gitlab.popup")
local git = require("gitlab.git")
local discussions = require("gitlab.actions.discussions")
local draft_notes = require("gitlab.actions.draft_notes")
@@ -46,6 +46,18 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
return
end
+ -- Creating a draft reply, in response to a discussion ID
+ if discussion_id ~= nil and is_draft then
+ local body = { comment = text, discussion_id = discussion_id }
+ job.run_job("/mr/draft_notes/", "POST", body, function()
+ u.notify("Draft reply created!", vim.log.levels.INFO)
+ draft_notes.load_draft_notes(function()
+ discussions.rebuild_view(unlinked)
+ end)
+ end)
+ return
+ end
+
-- Creating a note (unlinked comment)
if unlinked and discussion_id == nil then
local body = { comment = text }
@@ -89,18 +101,6 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
line_range = location_data.line_range,
}
- -- Creating a draft reply, in response to a discussion ID
- if discussion_id ~= nil and is_draft then
- local body = { comment = text, discussion_id = discussion_id, position = position_data }
- job.run_job("/mr/draft_notes/", "POST", body, function()
- u.notify("Draft reply created!", vim.log.levels.INFO)
- draft_notes.load_draft_notes(function()
- discussions.rebuild_view(unlinked)
- end)
- end)
- return
- end
-
-- Creating a new comment (linked to specific changes)
local body = u.merge({ type = "text", comment = text }, position_data)
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
@@ -157,52 +157,26 @@ end
---multi-line comment. It also sets up the basic keybindings for switching between
---window panes, and for the non-primary sections.
---@param opts LayoutOpts
----@return NuiLayout|nil
+---@return NuiLayout
M.create_comment_layout = function(opts)
- if opts.unlinked ~= true and opts.discussion_id == nil then
- -- Check that diffview is initialized
- if reviewer.tabnr == nil then
- u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
- return
- end
-
- -- Check that Diffview is the current view
- local view = diffview_lib.get_current_view()
- if view == nil and not opts.reply then
- u.notify("Comments should be left in the reviewer pane", vim.log.levels.ERROR)
- return
- end
-
- -- Check that we are in the diffview tab
- local tabnr = vim.api.nvim_get_current_tabpage()
- if tabnr ~= reviewer.tabnr then
- u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
- return
- end
-
- -- Check that the file has not been renamed
- if reviewer.is_file_renamed() and not reviewer.does_file_have_changes() then
- u.notify("Commenting on (unchanged) renamed or moved files is not supported", vim.log.levels.WARN)
- return
- end
-
- -- Check that we are hovering over the code
- local filetype = vim.bo[0].filetype
- if not opts.reply and (filetype == "DiffviewFiles" or filetype == "gitlab") then
- u.notify(
- "Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
- vim.log.levels.ERROR
- )
- return
- end
+ local popup_settings = state.settings.popup
+ local title
+ local user_settings
+ if opts.discussion_id ~= nil then
+ title = "Reply"
+ user_settings = popup_settings.reply
+ elseif opts.unlinked then
+ title = "Note"
+ user_settings = popup_settings.note
+ else
+ title = "Comment"
+ user_settings = popup_settings.comment
end
-
- local title = opts.discussion_id and "Reply" or "Comment"
- local settings = opts.discussion_id ~= nil and state.settings.popup.reply or state.settings.popup.comment
+ local settings = u.merge(popup_settings, user_settings or {})
M.current_win = vim.api.nvim_get_current_win()
- M.comment_popup = Popup(u.create_popup_state(title, settings))
- M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
+ M.comment_popup = Popup(popup.create_popup_state(title, settings))
+ M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings))
M.start_line, M.end_line = u.get_visual_selection_boundaries()
local internal_layout = Layout.Box({
@@ -211,98 +185,69 @@ M.create_comment_layout = function(opts)
}, { dir = "col" })
local layout = Layout({
- position = "50%",
+ position = settings.position,
relative = "editor",
size = {
- width = "50%",
- height = "55%",
+ width = settings.width,
+ height = settings.height,
},
}, internal_layout)
- miscellaneous.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
+ popup.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
+ popup.set_up_autocommands(M.comment_popup, layout, M.current_win)
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
local unlinked = opts.unlinked or false
---Keybinding for focus on draft section
- state.set_popup_keymaps(M.draft_popup, function()
+ popup.set_popup_keymaps(M.draft_popup, function()
local text = u.get_buffer_text(M.comment_popup.bufnr)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
- end, miscellaneous.toggle_bool, miscellaneous.non_editable_popup_opts)
+ end, miscellaneous.toggle_bool, popup.non_editable_popup_opts)
---Keybinding for focus on text section
- state.set_popup_keymaps(M.comment_popup, function(text)
+ popup.set_popup_keymaps(M.comment_popup, function(text)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
- end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
+ end, miscellaneous.attach_file, popup.editable_popup_opts)
vim.schedule(function()
local draft_mode = state.settings.discussion_tree.draft_mode
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
end)
- --Send back to previous window on close
- vim.api.nvim_create_autocmd("BufHidden", {
- buffer = M.draft_popup.bufnr,
- callback = function()
- vim.api.nvim_set_current_win(M.current_win)
- end,
- })
-
return layout
end
--- This function will open a comment popup in order to create a comment on the changed/updated
--- line in the current MR
M.create_comment = function()
- local has_clean_tree, err = git.has_clean_tree()
- if err ~= nil then
- return
- end
-
- local is_modified = vim.bo[0].modified
- if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
- u.notify(
- "Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
- vim.log.levels.WARN
- )
- return
- end
-
- if not M.sha_exists() then
+ if not M.can_create_comment(false) then
return
end
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
- if layout ~= nil then
- layout:mount()
- end
+ layout:mount()
end
--- This function will open a multi-line comment popup in order to create a multi-line comment
--- on the changed/updated line in the current MR
M.create_multiline_comment = function()
- if not u.check_visual_mode() then
- return
- end
- if not M.sha_exists() then
+ if not M.can_create_comment(true) then
+ u.press_escape()
return
end
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
- if layout ~= nil then
- layout:mount()
- end
+ layout:mount()
end
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
--- on the changed/updated line in the current MR
M.create_note = function()
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
- if layout ~= nil then
- layout:mount()
- end
+ layout:mount()
end
---Given the current visually selected area of text, builds text to fill in the
@@ -347,21 +292,16 @@ end
--- on the changed/updated line in the current MR
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
M.create_comment_suggestion = function()
- if not u.check_visual_mode() then
- return
- end
- if not M.sha_exists() then
+ if not M.can_create_comment(true) then
+ u.press_escape()
return
end
local suggestion_lines, range_length = build_suggestion()
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
- if layout ~= nil then
- layout:mount()
- else
- return -- Failure in creating the comment layout
- end
+ layout:mount()
+
vim.schedule(function()
if suggestion_lines then
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
@@ -369,6 +309,68 @@ M.create_comment_suggestion = function()
end)
end
+---Returns true if it's possible to create an Inline Comment
+---@param must_be_visual boolean True if current mode must be visual
+---@return boolean
+M.can_create_comment = function(must_be_visual)
+ -- Check that diffview is initialized
+ if reviewer.tabnr == nil then
+ u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
+ return false
+ end
+
+ -- Check that we are in the Diffview tab
+ local tabnr = vim.api.nvim_get_current_tabpage()
+ if tabnr ~= reviewer.tabnr then
+ u.notify("Comments can only be left in the reviewer pane", vim.log.levels.ERROR)
+ return false
+ end
+
+ -- Check that we are hovering over the code
+ local filetype = vim.bo[0].filetype
+ if filetype == "DiffviewFiles" or filetype == "gitlab" then
+ u.notify(
+ "Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
+ vim.log.levels.ERROR
+ )
+ return false
+ end
+
+ -- Check that the file has not been renamed
+ if reviewer.is_file_renamed() and not reviewer.does_file_have_changes() then
+ u.notify("Commenting on (unchanged) renamed or moved files is not supported", vim.log.levels.ERROR)
+ return false
+ end
+
+ -- Check that we are in a valid buffer
+ if not M.sha_exists() then
+ return false
+ end
+
+ -- Check that there aren't saved modifications
+ local file = reviewer.get_current_file_path()
+ if file == nil then
+ return false
+ end
+ local has_changes, err = git.has_changes(file)
+ if err ~= nil then
+ return false
+ end
+ -- Check that there aren't unsaved modifications
+ local is_modified = vim.bo[0].modified
+ if state.settings.reviewer_settings.diffview.imply_local and (is_modified or has_changes) then
+ u.notify("Cannot leave comments on changed files, please stash or commit and push", vim.log.levels.ERROR)
+ return false
+ end
+
+ -- Check we're in visual mode for code suggestions and multiline comments
+ if must_be_visual and not u.check_visual_mode() then
+ return false
+ end
+
+ return true
+end
+
---Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
---buffers as 'null'
---@return boolean
diff --git a/lua/gitlab/actions/common.lua b/lua/gitlab/actions/common.lua
index ace7bbe4..a2caed0d 100644
--- a/lua/gitlab/actions/common.lua
+++ b/lua/gitlab/actions/common.lua
@@ -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
@@ -293,6 +293,13 @@ M.jump_to_file = function(tree)
u.notify("This comment was not left on a particular location", vim.log.levels.WARN)
return
end
+ if vim.fn.filereadable(root_node.file_name) == 0 then
+ u.notify(
+ string.format("The file %s for which the comment was made doesn't exist in HEAD.", root_node.file_name),
+ vim.log.levels.WARN
+ )
+ return
+ end
vim.cmd.tabnew()
local line_number = get_new_line(root_node) or get_old_line(root_node)
if line_number == nil then
diff --git a/lua/gitlab/actions/create_mr.lua b/lua/gitlab/actions/create_mr.lua
index 2e366946..7e6f35d4 100644
--- a/lua/gitlab/actions/create_mr.lua
+++ b/lua/gitlab/actions/create_mr.lua
@@ -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,
diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua
index 40f03acd..788ca398 100644
--- a/lua/gitlab/actions/discussions/init.lua
+++ b/lua/gitlab/actions/discussions/init.lua
@@ -7,12 +7,12 @@ local Popup = require("nui.popup")
local NuiTree = require("nui.tree")
local job = require("gitlab.job")
local u = require("gitlab.utils")
+local popup = require("gitlab.popup")
local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local common = require("gitlab.actions.common")
local List = require("gitlab.utils.list")
local tree_utils = require("gitlab.actions.discussions.tree")
-local miscellaneous = require("gitlab.actions.miscellaneous")
local discussions_tree = require("gitlab.actions.discussions.tree")
local draft_notes = require("gitlab.actions.draft_notes")
local diffview_lib = require("diffview.lib")
@@ -48,13 +48,15 @@ M.rebuild_view = function(unlinked, all)
else
M.rebuild_discussion_tree()
end
- M.refresh_diagnostics_and_winbar()
+ state.discussion_tree.last_updated = os.time()
+ M.refresh_diagnostics()
end)
end
---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil
M.load_discussions = function(callback)
+ state.discussion_tree.last_updated = nil
state.load_new_state("discussion_data", function(data)
if not state.DISCUSSION_DATA then
state.DISCUSSION_DATA = {}
@@ -70,9 +72,10 @@ end
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
M.initialize_discussions = function()
+ state.discussion_tree.last_updated = os.time()
signs.setup_signs()
reviewer.set_callback_for_file_changed(function()
- M.refresh_diagnostics_and_winbar()
+ M.refresh_diagnostics()
M.modifiable(false)
reviewer.set_reviewer_keymaps()
end)
@@ -102,11 +105,10 @@ M.modifiable = function(bool)
end
--- Take existing data and refresh the diagnostics, the winbar, and the signs
-M.refresh_diagnostics_and_winbar = function()
+M.refresh_diagnostics = function()
if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics()
end
- winbar.update_winbar()
common.add_empty_titles()
end
@@ -154,7 +156,7 @@ M.open = function(callback)
end
vim.schedule(function()
- M.refresh_diagnostics_and_winbar()
+ M.refresh_diagnostics()
end)
end
@@ -251,9 +253,7 @@ M.reply = function(tree)
reply = true,
})
- if layout then
- layout:mount()
- end
+ layout:mount()
end
-- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment
@@ -284,7 +284,7 @@ end
-- This function (settings.keymaps.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
M.edit_comment = function(tree, unlinked)
- local edit_popup = Popup(u.create_popup_state("Edit Comment", state.settings.popup.edit))
+ local edit_popup = Popup(popup.create_popup_state("Edit Comment", state.settings.popup.edit))
local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node)
local root_node = common.get_root_node(tree, current_node)
@@ -293,6 +293,8 @@ M.edit_comment = function(tree, unlinked)
return
end
+ popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
+
edit_popup:mount()
-- Gather all lines from immediate children that aren't note nodes
@@ -310,19 +312,19 @@ M.edit_comment = function(tree, unlinked)
-- Draft notes module handles edits for draft notes
if M.is_draft_note(tree) then
- state.set_popup_keymaps(
+ popup.set_popup_keymaps(
edit_popup,
draft_notes.confirm_edit_draft_note(note_node.id, unlinked),
nil,
- miscellaneous.editable_popup_opts
+ popup.editable_popup_opts
)
else
local comment = require("gitlab.actions.comment")
- state.set_popup_keymaps(
+ popup.set_popup_keymaps(
edit_popup,
comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
nil,
- miscellaneous.editable_popup_opts
+ popup.editable_popup_opts
)
end
end
@@ -585,7 +587,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.jump_to_reviewer then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
if M.is_current_node_note(tree) then
- common.jump_to_reviewer(tree, M.refresh_diagnostics_and_winbar)
+ common.jump_to_reviewer(tree, M.refresh_diagnostics)
end
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
end
@@ -603,7 +605,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.refresh_data then
vim.keymap.set("n", keymaps.discussion_tree.refresh_data, function()
- u.notify("Refreshing data...", vim.log.levels.INFO)
draft_notes.rebuild_view(unlinked, false)
end, {
buffer = bufnr,
@@ -646,6 +647,16 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
})
end
+ if keymaps.discussion_tree.toggle_sort_method then
+ vim.keymap.set("n", keymaps.discussion_tree.toggle_sort_method, function()
+ M.toggle_sort_method()
+ end, {
+ buffer = bufnr,
+ desc = "Toggle sort method",
+ nowait = keymaps.discussion_tree.toggle_sort_method_nowait,
+ })
+ end
+
if keymaps.discussion_tree.toggle_resolved then
vim.keymap.set("n", keymaps.discussion_tree.toggle_resolved, function()
if M.is_current_node_note(tree) and not M.is_draft_note(tree) then
@@ -746,16 +757,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
})
end
- if keymaps.discussion_tree.print_node then
- vim.keymap.set("n", keymaps.discussion_tree.print_node, function()
- common.print_node(tree)
- end, {
- buffer = bufnr,
- desc = "Print current node (for debugging)",
- nowait = keymaps.discussion_tree.print_node_nowait,
- })
- end
-
if keymaps.discussion_tree.add_emoji then
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
M.add_emoji_to_note(tree, unlinked)
@@ -792,7 +793,18 @@ end
---Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
M.toggle_draft_mode = function()
state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode
+end
+
+---Toggle between sorting by "original comment" (oldest at the top) or "latest reply" (newest at the
+---top).
+M.toggle_sort_method = function()
+ if state.settings.discussion_tree.sort_by == "original_comment" then
+ state.settings.discussion_tree.sort_by = "latest_reply"
+ else
+ state.settings.discussion_tree.sort_by = "original_comment"
+ end
winbar.update_winbar()
+ M.rebuild_view(false, true)
end
---Indicates whether the node under the cursor is a draft note or not
diff --git a/lua/gitlab/actions/discussions/tree.lua b/lua/gitlab/actions/discussions/tree.lua
index a34f5112..7451a802 100644
--- a/lua/gitlab/actions/discussions/tree.lua
+++ b/lua/gitlab/actions/discussions/tree.lua
@@ -277,13 +277,16 @@ local function build_note_body(note, resolve_info)
)
end
- local resolve_symbol = ""
+ local symbol = ""
+ local is_draft = note.note ~= nil
if resolve_info ~= nil and resolve_info.resolvable then
- resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
+ symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
or state.settings.discussion_tree.unresolved
+ elseif not is_draft and resolve_info and not resolve_info.resolvable then
+ symbol = state.settings.discussion_tree.unlinked
end
- local noteHeader = common.build_note_header(note) .. " " .. resolve_symbol
+ local noteHeader = common.build_note_header(note) .. " " .. symbol
return noteHeader, text_nodes
end
@@ -454,7 +457,9 @@ M.restore_cursor_position = function(winid, tree, original_node, root_node)
end
end
if line_number ~= nil then
- vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
+ if vim.api.nvim_win_is_valid(winid) then
+ vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
+ end
end
end
diff --git a/lua/gitlab/actions/discussions/winbar.lua b/lua/gitlab/actions/discussions/winbar.lua
index 5c831846..83f41357 100644
--- a/lua/gitlab/actions/discussions/winbar.lua
+++ b/lua/gitlab/actions/discussions/winbar.lua
@@ -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(" ", "") .. " " 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
diff --git a/lua/gitlab/actions/draft_notes/init.lua b/lua/gitlab/actions/draft_notes/init.lua
index ea7b5b42..e23665e9 100755
--- a/lua/gitlab/actions/draft_notes/init.lua
+++ b/lua/gitlab/actions/draft_notes/init.lua
@@ -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()
diff --git a/lua/gitlab/actions/help.lua b/lua/gitlab/actions/help.lua
index d76c7b5e..991a1623 100644
--- a/lua/gitlab/actions/help.lua
+++ b/lua/gitlab/actions/help.lua
@@ -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)
diff --git a/lua/gitlab/actions/labels.lua b/lua/gitlab/actions/labels.lua
index da9447f6..e182df73 100644
--- a/lua/gitlab/actions/labels.lua
+++ b/lua/gitlab/actions/labels.lua
@@ -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
diff --git a/lua/gitlab/actions/merge.lua b/lua/gitlab/actions/merge.lua
index 81d23846..81eed3f6 100644
--- a/lua/gitlab/actions/merge.lua
+++ b/lua/gitlab/actions/merge.lua
@@ -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
diff --git a/lua/gitlab/actions/merge_requests.lua b/lua/gitlab/actions/merge_requests.lua
index 0af9db0c..d9af5e29 100644
--- a/lua/gitlab/actions/merge_requests.lua
+++ b/lua/gitlab/actions/merge_requests.lua
@@ -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
diff --git a/lua/gitlab/actions/miscellaneous.lua b/lua/gitlab/actions/miscellaneous.lua
index df080665..368c198b 100644
--- a/lua/gitlab/actions/miscellaneous.lua
+++ b/lua/gitlab/actions/miscellaneous.lua
@@ -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()
diff --git a/lua/gitlab/actions/pipeline.lua b/lua/gitlab/actions/pipeline.lua
index bddc1949..00dc5c29 100644
--- a/lua/gitlab/actions/pipeline.lua
+++ b/lua/gitlab/actions/pipeline.lua
@@ -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
diff --git a/lua/gitlab/actions/summary.lua b/lua/gitlab/actions/summary.lua
index 6d034be1..f23d0d8d 100644
--- a/lua/gitlab/actions/summary.lua
+++ b/lua/gitlab/actions/summary.lua
@@ -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)
diff --git a/lua/gitlab/annotations.lua b/lua/gitlab/annotations.lua
index 9b1ae923..b1ade3c8 100644
--- a/lua/gitlab/annotations.lua
+++ b/lua/gitlab/annotations.lua
@@ -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
diff --git a/lua/gitlab/colors.lua b/lua/gitlab/colors.lua
index a5b92f10..54d557a5 100644
--- a/lua/gitlab/colors.lua
+++ b/lua/gitlab/colors.lua
@@ -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,
})
diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua
index aa74abc0..a633c86d 100644
--- a/lua/gitlab/git.lua
+++ b/lua/gitlab/git.lua
@@ -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
diff --git a/lua/gitlab/init.lua b/lua/gitlab/init.lua
index 25378a74..f17c6320 100644
--- a/lua/gitlab/init.lua
+++ b/lua/gitlab/init.lua
@@ -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 🤷
diff --git a/lua/gitlab/job.lua b/lua/gitlab/job.lua
index 7f5f4d8e..128591be 100644
--- a/lua/gitlab/job.lua
+++ b/lua/gitlab/job.lua
@@ -17,6 +17,7 @@ M.run_job = function(endpoint, method, body, callback)
-- This handler will handle all responses from the Go server. Anything with a successful
-- status will call the callback (if it is supplied for the job). Otherwise, it will print out the
-- success message or error message and details from the Go server.
+ local stderr = {}
Job:new({
command = "curl",
args = args,
@@ -55,13 +56,20 @@ M.run_job = function(endpoint, method, body, callback)
end
end, 0)
end,
- on_stderr = function()
- vim.defer_fn(function()
- u.notify("Could not run command!", vim.log.levels.ERROR)
- end, 0)
+ on_stderr = function(_, data)
+ if data then
+ table.insert(stderr, data)
+ end
end,
- on_exit = function(_, status)
+ on_exit = function(code, status)
vim.defer_fn(function()
+ if #stderr ~= 0 then
+ u.notify(
+ string.format("Could not run command `%s %s`! Stderr was:", code.command, table.concat(code.args, " ")),
+ vim.log.levels.ERROR
+ )
+ vim.notify(string.format("%s", table.concat(stderr, "\n")), vim.log.levels.ERROR)
+ end
if status ~= 0 then
u.notify(string.format("Go server exited with non-zero code: %d", status), vim.log.levels.ERROR)
end
diff --git a/lua/gitlab/popup.lua b/lua/gitlab/popup.lua
new file mode 100644
index 00000000..8ab0b513
--- /dev/null
+++ b/lua/gitlab/popup.lua
@@ -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
diff --git a/lua/gitlab/reviewer/init.lua b/lua/gitlab/reviewer/init.lua
index 41c00478..b03f99fc 100644
--- a/lua/gitlab/reviewer/init.lua
+++ b/lua/gitlab/reviewer/init.lua
@@ -42,12 +42,21 @@ M.open = function()
end
local diffview_open_command = "DiffviewOpen"
- local has_clean_tree, err = git.has_clean_tree()
- if err ~= nil then
- return
- end
- if state.settings.reviewer_settings.diffview.imply_local and has_clean_tree then
- diffview_open_command = diffview_open_command .. " --imply-local"
+
+ if state.settings.reviewer_settings.diffview.imply_local then
+ local has_clean_tree, err = git.has_clean_tree()
+ if err ~= nil then
+ return
+ end
+ if has_clean_tree then
+ diffview_open_command = diffview_open_command .. " --imply-local"
+ else
+ u.notify(
+ "Your working tree has changes, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
+ vim.log.levels.WARN
+ )
+ state.settings.reviewer_settings.diffview.imply_local = false
+ end
end
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
@@ -55,13 +64,6 @@ M.open = function()
M.is_open = true
M.tabnr = vim.api.nvim_get_current_tabpage()
- if state.settings.reviewer_settings.diffview.imply_local and not has_clean_tree then
- u.notify(
- "There are uncommited changes in the working tree, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
- vim.log.levels.WARN
- )
- end
-
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
u.notify(
"Diagnostics are now configured as settings.discussion_signs, see :h gitlab.nvim.signs-and-diagnostics",
@@ -107,7 +109,6 @@ M.jump = function(file_name, line_number, new_buffer)
return
end
vim.api.nvim_set_current_tabpage(M.tabnr)
- vim.cmd("DiffviewFocusFiles")
local view = diffview_lib.get_current_view()
if view == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
@@ -118,6 +119,13 @@ M.jump = function(file_name, line_number, new_buffer)
local file = List.new(files):find(function(file)
return file.path == file_name
end)
+ if file == nil then
+ u.notify(
+ string.format("The file %s for which the comment was made doesn't exist in HEAD.", file_name),
+ vim.log.levels.WARN
+ )
+ return
+ end
async.await(view:set_file(file))
local layout = view.cur_layout
@@ -289,12 +297,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
@@ -319,7 +331,8 @@ local set_keymaps = function(bufnr, keymaps)
if keymaps.reviewer.create_comment ~= false then
-- Set keymap for repeated operator keybinding
vim.keymap.set("o", keymaps.reviewer.create_comment, function()
- vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { tostring(vim.v.count1) .. "$" } }, {})
+ -- The "V" in "V%d$" forces linewise motion, see `:h o_V`
+ vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { string.format("V%d$", vim.v.count1) } }, {})
end, {
buffer = bufnr,
desc = "Create comment for [count] lines",
@@ -349,7 +362,8 @@ local set_keymaps = function(bufnr, keymaps)
if keymaps.reviewer.create_suggestion ~= false then
-- Set keymap for repeated operator keybinding
vim.keymap.set("o", keymaps.reviewer.create_suggestion, function()
- vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { tostring(vim.v.count1) .. "$" } }, {})
+ -- The "V" in "V%d$" forces linewise motion, see `:h o_V`
+ vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { string.format("V%d$", vim.v.count1) } }, {})
end, {
buffer = bufnr,
desc = "Create suggestion for [count] lines",
diff --git a/lua/gitlab/server.lua b/lua/gitlab/server.lua
index 6e77c8ac..094100a1 100644
--- a/lua/gitlab/server.lua
+++ b/lua/gitlab/server.lua
@@ -26,7 +26,11 @@ M.start = function(callback)
state.chosen_mr_iid = 0 -- Do not let this interfere with subsequent reviewer.open() calls
local settings = vim.json.encode(go_server_settings)
- local command = string.format("%s '%s'", state.settings.bin, settings)
+ if vim.fn.has("win32") then
+ settings = settings:gsub('"', '\\"')
+ end
+
+ local command = string.format('"%s" "%s"', state.settings.bin, settings)
local job_id = vim.fn.jobstart(command, {
on_stdout = function(_, data)
diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua
index 2989cd9a..e615ee32 100644
--- a/lua/gitlab/state.lua
+++ b/lua/gitlab/state.lua
@@ -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,
},
diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua
index aa71136b..48a01df0 100644
--- a/lua/gitlab/utils/init.lua
+++ b/lua/gitlab/utils/init.lua
@@ -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("", false, true, true), "n", false)
end
+M.press_escape = function()
+ vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", 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("", 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