diff --git a/README.md b/README.md
index f512b172..ca64337f 100644
--- a/README.md
+++ b/README.md
@@ -14,44 +14,58 @@ This Neovim plugin is designed to make it easy to review Gitlab MRs from within
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335-afe1-d554e3804372
-To view these help docs and to get more detailed help information, please run `:h gitlab.nvim`
+To view the help docs run `:h gitlab.nvim`.
## Requirements
- Neovim >= v0.10
-- Go >= v1.25.1
+- Go >= v1.25.1, or a compatible pre-built `gitlab.nvim` [local Go server](#local-go-server) binary
- Git
- Curl
- Cat (for displaying pipeline logs)
## Quick Start
-1. Install the required dependencies
-2. Add configuration (see [Installation](#installation) section)
+1. Install the required dependencies and the plugin (see the [Installation](#installation) section)
+2. Set up [connecting to Gitlab](#connecting-to-gitlab)
3. Open Neovim
4. Run `:lua require("gitlab").choose_merge_request()` or `:lua require("gitlab").review()` if already in review branch/worktree.
-This will checkout the branch locally, and open the plugin's reviewer pane.
+This will checkout the branch locally, and open the plugin's reviewer pane. Type `g?` in any of the plugin's windows to get help on context-specific keybindings. Read `:h gitlab.nvim.usage` for more information of what can be done with the plugin.
-For more detailed information about the Lua APIs please run `:h gitlab.nvim.api`
+For more detailed information about the Lua APIs run `:h gitlab.nvim.api`
## Installation
-With Lazy:
+With [vim.pack](https://neovim.io/doc/user/helptag.html?tag=vim.pack) (the built-in plugin
+manager on Neovim 0.12 and newer):
+```lua
+vim.pack.add({
+ "https://github.com/MunifTanjim/nui.nvim",
+ "https://github.com/dlyongemallo/diffview-plus.nvim", -- Maintained fork of "sindrets/diffview.nvim".
+ "https://github.com/stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
+ "https://github.com/nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
+ "https://github.com/harrisoncramer/gitlab.nvim",
+})
+---@type GitlabSettings
+local opts = {} -- Your configuration
+require("gitlab").setup(opts)
+```
+
+With [folke/lazy.nvim](https://github.com/folke/lazy.nvim):
```lua
{
"harrisoncramer/gitlab.nvim",
+ -- branch = "main", -- Uncomment to use a stable version. The default, possibly unstable, but more actively maintained branch is `develop`.
dependencies = {
"MunifTanjim/nui.nvim",
"dlyongemallo/diffview-plus.nvim", -- Maintained fork of "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(true) end, -- Builds the Go binary
- config = function()
- require("gitlab").setup()
- end,
+ ---@type GitlabSettings
+ opts = {}, -- Your configuration
}
```
@@ -60,27 +74,45 @@ And with pckr.nvim:
```lua
{
"harrisoncramer/gitlab.nvim",
+ -- branch = "main", -- Uncomment to use a stable version. The default, possibly unstable, but more actively maintained branch is `develop`.
requires = {
"MunifTanjim/nui.nvim",
"dlyongemallo/diffview-plus.nvim", -- Maintained fork of "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()
+ ---@type GitlabSettings
+ local opts = {} -- Your configuration
+ require("gitlab").setup(opts)
end,
}
```
-Add `branch = "develop",` to your configuration if you want to use the (possibly unstable) development version of `gitlab.nvim`.
+### Notes on dependencies
-`gitlab.nvim` uses the `diffview.nvim` plugin for showing the diffs in a MR. We recommend using `dlyongemallo`'s [diffview+](https://github.com/dlyongemallo/diffview-plus.nvim) fork which is the de-facto maintained version of the plugin with many fixes and improvements (e.g., marking files as viewed). The original [sindrets/diffview.nvim](https://github.com/sindrets/diffview.nvim) plugin will be supported as long as the maintenance remains feasible.
+`gitlab.nvim` uses the `diffview.nvim` plugin for showing the diffs in a MR. We recommend using `dlyongemallo`'s [diffview+](https://github.com/dlyongemallo/diffview-plus.nvim) fork which is the de-facto maintained version of the plugin with many fixes and improvements (e.g., marking files as viewed). The original [sindrets/diffview.nvim](https://github.com/sindrets/diffview.nvim) plugin will be supported by `gitlab.nvim` as long as the maintenance remains feasible.
-## Contributing
+Some plugin actions use Neovim’s `vim.ui.select()` picker, which looks much nicer if you use `dressing.nvim` or a similar UI plugin. To use Dressing with `gitlab.nvim`, enable it for `vim.ui.select()` like this:
+```lua
+require("dressing").setup({
+ select = {
+ enabled = true
+ }
+})
+```
-Contributions to the plugin are welcome. Please read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start working on a pull request.
+## Configuring the plugin
+
+The plugin expects you to call `require("gitlab").setup(opts)` and pass in a table of options. All of these values are optional, and if you call this function with no opts table the defaults will be used.
+
+For a list of all these settings run `:h gitlab.nvim.configuring-the-plugin` which will show you the help stored in [doc/gitlab.nvim.txt](doc/gitlab.nvim.txt).
+
+## Local Go server
+
+This plugin uses a local Go server to reach out to Gitlab. If you have an appropriate Go version available on your system, you can have the server built automatically. This is performed whenever the Go server binary is not found in the expected location or when the plugin is updated to a newer version.
+
+If you can't or don't want to install Go, you can instead pre-build the server binary (some people use Nix for this) and configure `gitlab.nvim` to use that binary instead, see the `server` section in `:h gitlab.nvim.configuring-the-plugin`.
## Connecting to Gitlab
@@ -95,15 +127,14 @@ export GITLAB_URL="https://my-personal-gitlab-instance.com/"
And as a `.gitlab.nvim` file:
-```
+```bash
auth_token=your_gitlab_token
gitlab_url=https://my-personal-gitlab-instance.com/
```
The plugin will look for the `.gitlab.nvim` file in the root of the current project by default. However, you may provide a custom path to the configuration file via the `config_path` option. This must be an absolute path to the directory that holds your `.gitlab.nvim` file.
-In case even more control over the auth config is needed, there is the possibility to override the `auth_provider` settings field. It should be
-a function that returns the `token` as well as the `gitlab_url` value, and a nilable error. If the `gitlab_url` is `nil`, `https://gitlab.com` is used as default.
+In case even more control over the auth config is needed, there is the possibility to override the `auth_provider` settings field. It should be a function that returns the `token` as well as the `gitlab_url` value, and a nilable error. If the `gitlab_url` is `nil`, `https://gitlab.com` is used as default.
Here an example how to use a custom `auth_provider`:
```lua
@@ -114,13 +145,7 @@ require("gitlab").setup({
}
```
-For more settings, please see `:h gitlab.nvim.connecting-to-gitlab`
-
-## Configuring the Plugin
-
-The plugin expects you to call `setup()` and pass in a table of options. All of these values are optional, and if you call this function with no values the defaults will be used.
-
-For a list of all these settings please run `:h gitlab.nvim.configuring-the-plugin` which will show you the help stored in [doc/gitlab.nvim.txt](doc/gitlab.nvim.txt).
+For more settings, see `:h gitlab.nvim.connecting-to-gitlab`
## Keybindings
@@ -134,82 +159,86 @@ For more information about each of these commands, and about the APIs in general
These keymaps are available globally (i.e., in any buffer).
-```
-g? Open a help popup for local keymaps
-glaa Add assignee
-glad Delete assignee
-glla Add label
-glld Delete label
-glra Add reviewer
-glrd Delete reviewer
-glA Approve MR
-glR Revoke MR approval
-glM Merge the feature branch to the target branch and close MR
-glm Set MR to merge automatically when the pipeline succeeds
-glrr Rebase the feature branch of the MR on the server (if not already rebased) and pull the new state
-glrs Same as `glrr`, but skip the CI pipeline
-glrf Same as `glrr`, but rebase even if MR already is rebased
-glC Create a new MR for currently checked-out feature branch
-glc Chose MR for review
-glS Start review for the currently checked-out branch
-gl Load new MR state from Gitlab and apply new diff refs to the diff view
-gls Show the editable summary of the MR
-glu Copy the URL of the MR to the system clipboard
-glo Open the URL of the MR in the default Internet browser
-gln Create a note (comment not linked to a specific line)
-glp Show the pipeline status
-gld Toggle the discussions window
-glD Toggle between draft mode and live mode
-glP Publish all draft comments/notes
-```
+| Keys | Action |
+| --------- | ------------------------------------------------------------------------------------------------- |
+| `g?` | Open a help popup for local keymaps |
+| `glaa` | Add assignee |
+| `glad` | Delete assignee |
+| `glla` | Add label |
+| `glld` | Delete label |
+| `glra` | Add reviewer |
+| `glrd` | Delete reviewer |
+| `glA` | Approve MR |
+| `glR` | Revoke MR approval |
+| `glM` | Merge the feature branch to the target branch and close MR |
+| `glm` | Set MR to merge automatically when the pipeline succeeds |
+| `glrr` | Rebase the feature branch of the MR on the server (if not already rebased) and pull the new state |
+| `glrs` | Same as `glrr`, but skip the CI pipeline |
+| `glrf` | Same as `glrr`, but rebase even if MR already is rebased |
+| `glC` | Create a new MR for currently checked-out feature branch |
+| `glc` | Chose MR for review |
+| `glS` | Start review for the currently checked-out branch |
+| `gl` | Load new MR state from Gitlab and apply new diff refs to the diff view |
+| `gls` | Show the editable summary of the MR |
+| `glu` | Copy the URL of the MR to the system clipboard |
+| `glo` | Open the URL of the MR in the default Internet browser |
+| `gln` | Create a note (comment not linked to a specific line) |
+| `glp` | Show the pipeline status |
+| `gld` | Toggle the discussions window |
+| `glD` | Toggle between draft mode and live mode |
+| `glP` | Publish all draft comments/notes |
#### Popup Keymaps
These `keymaps` are active in the popup windows (e.g., for creating comments, editing the summary, etc.).
-```
- Cycle to the next field
- Cycle to the previous field
-ZZ Perform action (e.g., save comment)
-ZA Perform linewise action
-ZQ Discard changes and quit the popup
-```
+| Keys | Action |
+| --------- | ----------------------------------- |
+| `` | Cycle to the next field |
+| `` | Cycle to the previous field |
+| `ZZ` | Perform action (e.g., save comment) |
+| `ZA` | Perform linewise action |
+| `ZQ` | Discard changes and quit the popup |
#### Discussion Tree Keymaps
These `keymaps` are active in the discussion tree window.
-```
-Ea Add an emoji to the note/comment
-Ed Remove an emoji from a note/comment
-dd Delete comment
-e Edit comment
-r Reply to comment
-- Toggle the resolved status of the whole discussion
-o Jump to comment location in file
-a Jump to the comment location in the reviewer window
-b Jump to the URL of the current note/discussion
-u Copy the URL of the current node to clipboard
-c Toggle between the notes and discussions views
-i Toggle type of discussion tree
-P Publish the currently focused note/comment
-dt Toggle between date formats
-D Toggle between draft mode and live mode
-st Toggle whether discussions are sorted by the "latest_reply", or by "original_comment"
-t Open or close the discussion
-T Open or close separately both resolved and unresolved discussions
-R Open or close all resolved discussions
-U Open or close all unresolved discussions
- Refresh the data in the view
-p Print the current node (for debugging)
-```
+| Keys | Action |
+| ----------- | ------------------------------------------------------------------------------------- |
+| `Ea` | Add an emoji to the note/comment |
+| `Ed` | Remove an emoji from a note/comment |
+| `dd` | Delete comment |
+| `e` | Edit comment |
+| `r` | Reply to comment |
+| `-` | Toggle the resolved status of the whole discussion |
+| `o` | Jump to comment location in file |
+| `a` | Jump to the comment location in the reviewer window |
+| `b` | Jump to the URL of the current note/discussion |
+| `u` | Copy the URL of the current node to clipboard |
+| `c` | Toggle between the notes and discussions views |
+| `i` | Toggle type of discussion tree |
+| `P` | Publish the currently focused note/comment |
+| `dt` | Toggle between date formats |
+| `D` | Toggle between draft mode and live mode |
+| `st` | Toggle whether discussions are sorted by the "latest_reply", or by "original_comment" |
+| `t` | Open or close the discussion |
+| `T` | Open or close separately both resolved and unresolved discussions |
+| `R` | Open or close all resolved discussions |
+| `U` | Open or close all unresolved discussions |
+| `` | Refresh the data in the view |
+| `p` | Print the current node (for debugging) |
#### Reviewer Keymaps
These `keymaps` are active in the reviewer window (the diff view).
-```
-c Create a comment for the lines that the following {motion} moves over
-s Create a suggestion for the lines that the following {motion} moves over
-a Jump to the comment in the discussion tree
-```
+| Keys | Action |
+| ---- | ------------------------------------------------------------------------ |
+| `c` | Create a comment for the lines that the following {motion} moves over |
+| `s` | Create a suggestion for the lines that the following {motion} moves over |
+| `a` | Jump to the comment in the discussion tree |
+
+## 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/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt
index c9a12373..3549def8 100644
--- a/doc/gitlab.nvim.txt
+++ b/doc/gitlab.nvim.txt
@@ -6,26 +6,31 @@ Table of Contents *gitlab.nvim.table-of-contents*
- Requirements |gitlab.nvim.requirements|
- Quick Start |gitlab.nvim.quick-start|
- Installation |gitlab.nvim.installation|
+ - Notes on dependencies |gitlab.nvim.dependencies|
+ - Local Go server |gitlab.nvim.local-go-server|
- Connecting to Gitlab |gitlab.nvim.connecting-to-gitlab|
- - Configuring the Plugin |gitlab.nvim.configuring-the-plugin|
+ - Configuring the plugin |gitlab.nvim.configuring-the-plugin|
- Usage |gitlab.nvim.usage|
- The Summary view |gitlab.nvim.summary-view|
- Reviewing an MR |gitlab.nvim.reviewing-an-mr|
+ - Draft notes |gitlab.nvim.draft-comments|
- Temporary registers |gitlab.nvim.temp-registers|
- Discussions and Notes |gitlab.nvim.discussions-and-notes|
- Labels |gitlab.nvim.labels|
- Signs and diagnostics |gitlab.nvim.signs-and-diagnostics|
- Emojis |gitlab.nvim.emojis|
- - Uploading Files |gitlab.nvim.uploading-files|
+ - Uploading files |gitlab.nvim.uploading-files|
- MR Approvals |gitlab.nvim.mr-approvals|
- Merging an MR |gitlab.nvim.merging-an-mr|
+ - Rebasing an MR |gitlab.nvim.rebasing-an-mr|
- Creating an MR |gitlab.nvim.creating-an-mr|
- Pipelines |gitlab.nvim.pipelines|
- Reviewers and Assignees |gitlab.nvim.reviewers-and-assignees|
- - Restarting The Go Server |gitlab.nvim.restarting|
+ - Restarting the Go server |gitlab.nvim.restarting|
- Keybindings |gitlab.nvim.keybindings|
- Troubleshooting |gitlab.nvim.troubleshooting|
- Api |gitlab.nvim.api|
+ - Contributing |gitlab.nvim.contributing|
OVERVIEW *gitlab.nvim.overview*
@@ -45,7 +50,8 @@ the editor. This means you can do things like:
REQUIREMENTS *gitlab.nvim.requirements*
- Neovim >= v0.10
-- Go >= v1.25.1
+- Go >= v1.25.1, or a compatible pre-built `gitlab.nvim` server binary, see
+ |gitlab.nvim.local-go-server|
- Git
- Curl
- Cat (for displaying pipeline logs)
@@ -53,50 +59,102 @@ REQUIREMENTS *gitlab.nvim.requirements*
QUICK START *gitlab.nvim.quick-start*
-1. Install the required dependencies
-2. Add configuration (see |gitlab.nvim.installation| section)
+1. Install the required dependencies and the plugin (see the
+ |gitlab.nvim.installation| section)
+2. Set up connecting to Gitlab |gitlab.nvim.connecting-to-gitlab|
3. Open Neovim
4. Run `:lua require("gitlab").choose_merge_request()` or
`:lua require("gitlab").review()` if already in review branch/worktree.
This will checkout the branch locally, and open the plugin's reviewer pane.
+Type `g?` in any of the plugin's windows to get help on context-specific
+keybindings. Read |gitlab.nvim.usage| for more information of what can be done
+with the plugin.
+
+For more detailed information about the Lua APIs see |gitlab.nvim.api|.
+
INSTALLATION *gitlab.nvim.installation*
-With Lazy:
+With |vim.pack| (the built-in plugin manager on Neovim 0.12 and newer):
>lua
- {
- "harrisoncramer/gitlab.nvim",
- dependencies = {
- "MunifTanjim/nui.nvim",
- "dlyongemallo/diffview-plus.nvim", -- Maintained fork of "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(true) end, -- Builds the Go binary
- config = function()
- require("gitlab").setup()
- end,
- }
+ vim.pack.add({
+ "https://github.com/MunifTanjim/nui.nvim",
+ "https://github.com/dlyongemallo/diffview-plus.nvim", -- Maintained fork of "sindrets/diffview.nvim".
+ "https://github.com/stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
+ "https://github.com/nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
+ "https://github.com/harrisoncramer/gitlab.nvim",
+ })
+ ---@type GitlabSettings
+ local opts = {} -- Your configuration
+ require("gitlab").setup(opts)
+<
+
+With folke/lazy.nvim:
+>lua
+ {
+ "harrisoncramer/gitlab.nvim",
+ -- branch = "main", -- Uncomment to use a stable version. The default, possibly unstable, but more actively maintained branch is `develop`.
+ dependencies = {
+ "MunifTanjim/nui.nvim",
+ "dlyongemallo/diffview-plus.nvim", -- Maintained fork of "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.
+ },
+ ---@type GitlabSettings
+ opts = {}, -- Your configuration
+ }
<
And with pckr.nvim:
>lua
- {
- "harrisoncramer/gitlab.nvim",
- requires = {
- "MunifTanjim/nui.nvim",
- "dlyongemallo/diffview-plus.nvim", -- Maintained fork of "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,
+ {
+ "harrisoncramer/gitlab.nvim",
+ -- branch = "main", -- Uncomment to use a stable version. The default, possibly unstable, but more actively maintained branch is `develop`.
+ requires = {
+ "MunifTanjim/nui.nvim",
+ "dlyongemallo/diffview-plus.nvim", -- Maintained fork of "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.
+ },
+ config = function()
+ ---@type GitlabSettings
+ local opts = {} -- Your configuration
+ require("gitlab").setup(opts)
+ end,
+ }
+<
+
+NOTES ON DEPENDENCIES *gitlab.nvim.dependencies*
+
+`gitlab.nvim` uses the `diffview.nvim` plugin for showing the diffs in a MR.
+We recommend using `dlyongemallo`'s `diffview+` fork which is the de-facto
+maintained version of the plugin with many fixes and improvements (e.g.,
+marking files as viewed). The original `sindrets/diffview.nvim` plugin will be
+supported by `gitlab.nvim` as long as the maintenance remains feasible.
+
+Some plugin actions use Neovim’s |vim.ui.select()| picker, which looks much
+nicer if you use `dressing.nvim` or a similar UI plugin. To use Dressing,
+enable it for |vim.ui.select()| like this:
+>lua
+ require("dressing").setup({
+ select = {
+ enabled = true
}
+ })
<
+LOCAL GO SERVER *gitlab.nvim.local-go-server*
+
+This plugin uses a local Go server to reach out to Gitlab. If you have an
+appropriate Go version available on your system, you can have the server built
+automatically. This is performed whenever the Go server binary is not found in
+the expected location or when the plugin is updated to a newer version.
+
+If you can't or don't want to install Go, you can instead pre-build the server
+binary (some peple use Nix for this) and configure `gitlab.nvim` to use that
+binary instead, see the `server` section in |gitlab.nvim.configuring-the-plugin|.
+
+
CONNECTING TO GITLAB *gitlab.nvim.connecting-to-gitlab*
This plugin requires an auth token to connect to Gitlab. The token can be set
@@ -109,13 +167,13 @@ the `.gitlab.nvim` file) to connect to a self-hosted Gitlab instance. This is
optional, use ONLY for self-hosted instances. Here’s what they’d look like
as environment variables:
>bash
- export GITLAB_TOKEN="your_gitlab_token"
- export GITLAB_URL="https://my-personal-gitlab-instance.com/"
+ export GITLAB_TOKEN="your_gitlab_token"
+ export GITLAB_URL="https://my-personal-gitlab-instance.com/"
<
And as a `.gitlab.nvim` file:
->
- auth_token=your_gitlab_token
- gitlab_url=https://my-personal-gitlab-instance.com/
+>bash
+ auth_token=your_gitlab_token
+ gitlab_url=https://my-personal-gitlab-instance.com/
<
The plugin will look for the `.gitlab.nvim` file in the root of the current
project by default. However, you may provide a custom path to the configuration
@@ -126,314 +184,312 @@ The `connection_settings` block in the `state.lua` file will be used to
configure your connection to Gitlab.
In case even more control over the auth config is needed, there is the
-possibility to override the `auth_provider` settings field. It should be
-a function that returns the `token` as well as the `gitlab_url` value and
-a nilable error value.
-
-If the `gitlab_url` is `nil`, `https://gitlab.com` is used as default.
+possibility to override the `auth_provider` settings field. It should be a
+function that returns the `token` as well as the `gitlab_url` value and a
+nilable error value. If the `gitlab_url` is `nil`, `https://gitlab.com` is
+used as default.
Here an example how to use a custom `auth_provider`:
>lua
- require("gitlab").setup({
- auth_provider = function()
- return "my_token", "https://custom.gitlab.instance.url", nil
- end,
- }
+ require("gitlab").setup({
+ auth_provider = function()
+ return "my_token", "https://custom.gitlab.instance.url", nil
+ end,
+ }
<
-
CONFIGURING THE PLUGIN *gitlab.nvim.configuring-the-plugin*
Here is the default setup function. All of these values are optional, and if
you call this function with no values the defaults will be used:
>lua
- require("gitlab").setup({
- server = {
- binary = nil, -- The path to the server binary. If omitted or nil, the server will be built
- port = nil, -- The port of the Go server, which runs in the background. If omitted or `nil` the port will be chosen automatically
- },
- log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server
- config_path = nil, -- Custom path for `.gitlab.nvim` file, please read the "Connecting to Gitlab" section
- debug = {
- request = false, -- Requests to/from Go server
- response = false,
- gitlab_request = false, -- Requests to/from Gitlab
- gitlab_response = false,
- },
- attachment_dir = nil, -- The local directory for files (see the "summary" section)
- reviewer_settings = {
- jump_with_no_diagnostics = false, -- Jump to last position in discussion tree if true, otherwise stay in reviewer and show warning.
- diffview = {
- imply_local = false, -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen|
- },
+ require("gitlab").setup({
+ server = {
+ binary = nil, -- The path to the server binary. If omitted or nil, the server will be built
+ port = nil, -- The port of the Go server, which runs in the background. If omitted or `nil` the port will be chosen automatically
+ },
+ log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server
+ config_path = nil, -- Custom path for `.gitlab.nvim` file, see the "Connecting to Gitlab" section for more information
+ debug = {
+ request = false, -- Requests to/from Go server
+ response = false,
+ gitlab_request = false, -- Requests to/from Gitlab
+ gitlab_response = false,
+ },
+ attachment_dir = nil, -- The local directory for files (see the "summary" section)
+ reviewer_settings = {
+ jump_with_no_diagnostics = false, -- Jump to last position in discussion tree if true, otherwise stay in reviewer and show warning.
+ diffview = {
+ imply_local = false, -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen|
},
- connection_settings = {
- proxy = "", -- Configure a proxy URL to use when connecting to GitLab. Supports URL schemes: http, https, socks5
- insecure = false, -- Like curl's --insecure option, ignore bad x509 certificates on connection
- remote = "origin", -- The default remote that your MRs target
+ },
+ connection_settings = {
+ proxy = "", -- Configure a proxy URL to use when connecting to GitLab. Supports URL schemes: http, https, socks5
+ insecure = false, -- Like curl's --insecure option, ignore bad x509 certificates on connection
+ remote = "origin", -- The default remote that your MRs target
+ },
+ keymaps = {
+ disable_all = false, -- Disable all mappings created by the plugin
+ help = "g?", -- Open a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc)
+ global = {
+ disable_all = false, -- Disable all global mappings created by the plugin
+ add_assignee = "glaa",
+ delete_assignee = "glad",
+ add_label = "glla",
+ delete_label = "glld",
+ add_reviewer = "glra",
+ delete_reviewer = "glrd",
+ approve = "glA", -- Approve MR
+ revoke = "glR", -- Revoke MR approval
+ merge = "glM", -- Merge the feature branch to the target branch and close MR
+ set_auto_merge = "glm", -- Set MR to merge automatically when the pipeline succeeds
+ rebase = "glrr", -- Rebase the feature branch of the MR on the server and pull the new state
+ rebase_skip_ci = "glrs", -- Same as `rebase`, but skip the CI pipeline
+ rebase_force = "glrf", -- Same as `rebase`, but rebase even if MR already is rebased
+ create_mr = "glC", -- Create a new MR for currently checked-out feature branch
+ choose_merge_request = "glc", -- Chose MR for review (if necessary check out the feature branch)
+ start_review = "glS", -- Start review for the currently checked-out branch
+ reload_review = "gl", -- Load new MR state from Gitlab and apply new diff refs to the diff view
+ summary = "gls", -- Show the editable summary of the MR
+ copy_mr_url = "glu", -- Copy the URL of the MR to the system clipboard
+ open_in_browser = "glo", -- Openthe URL of the MR in the default Internet browser
+ create_note = "gln", -- Create a note (comment not linked to a specific line)
+ pipeline = "glp", -- Show the pipeline status
+ toggle_discussions = "gld", -- Toggle the discussions window
+ toggle_draft_mode = "glD", -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
+ publish_all_drafts = "glP", -- Publish all draft comments/notes
},
- keymaps = {
- disable_all = false, -- Disable all mappings created by the plugin
- help = "g?", -- Open a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc)
- global = {
- disable_all = false, -- Disable all global mappings created by the plugin
- add_assignee = "glaa",
- delete_assignee = "glad",
- add_label = "glla",
- delete_label = "glld",
- add_reviewer = "glra",
- delete_reviewer = "glrd",
- approve = "glA", -- Approve MR
- revoke = "glR", -- Revoke MR approval
- merge = "glM", -- Merge the feature branch to the target branch and close MR
- set_auto_merge = "glm", -- Set MR to merge automatically when the pipeline succeeds
- rebase = "glrr", -- Rebase the feature branch of the MR on the server and pull the new state
- rebase_skip_ci = "glrs", -- Same as `rebase`, but skip the CI pipeline
- rebase_force = "glrf", -- Same as `rebase`, but rebase even if MR already is rebased
- create_mr = "glC", -- Create a new MR for currently checked-out feature branch
- choose_merge_request = "glc", -- Chose MR for review (if necessary check out the feature branch)
- start_review = "glS", -- Start review for the currently checked-out branch
- reload_review = "gl", -- Load new MR state from Gitlab and apply new diff refs to the diff view
- summary = "gls", -- Show the editable summary of the MR
- copy_mr_url = "glu", -- Copy the URL of the MR to the system clipboard
- open_in_browser = "glo", -- Openthe URL of the MR in the default Internet browser
- create_note = "gln", -- Create a note (comment not linked to a specific line)
- pipeline = "glp", -- Show the pipeline status
- toggle_discussions = "gld", -- Toggle the discussions window
- toggle_draft_mode = "glD", -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
- publish_all_drafts = "glP", -- Publish all draft comments/notes
- },
- popup = {
- disable_all = false, -- Disable all default mappings for the popup windows (comments, summary, MR creation, etc.)
- 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)
- 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 = {
- disable_all = false, -- Disable all default mappings for the discussion tree window
- add_emoji = "Ea", -- Add an emoji to the note/comment
- delete_emoji = "Ed", -- Remove an emoji from a note/comment
- delete_comment = "dd", -- Delete comment
- edit_comment = "e", -- Edit comment
- reply = "r", -- Reply to comment
- toggle_resolved = "-", -- Toggle the resolved status of the whole discussion
- jump_to_file = "o", -- Jump to comment location in file
- jump_to_reviewer = "a", -- Jump to the comment location in the reviewer window
- open_in_browser = "b", -- Jump to the URL of the current note/discussion
- copy_node_url = "u", -- Copy the URL of the current node to clipboard
- switch_view = "c", -- Toggle between the notes and discussions views
- toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
- publish_draft = "P", -- Publish the currently focused note/comment
- toggle_date_format = "dt", -- Toggle between date formats: relative (e.g., "5 days ago", "just now", "October 13, 2024" for dates more than a month ago) and absolute (e.g., "03/01/2024 at 11:43")
- 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
- toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
- refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again
- print_node = "p", -- Print the current node (for debugging)
- },
- reviewer = {
- disable_all = false, -- Disable all default mappings for the reviewer windows
- create_comment = "c", -- Create a comment for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
- create_suggestion = "s", -- Create a suggestion for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
- move_to_discussion_tree = "a", -- Jump to the comment in the discussion tree
- },
+ popup = {
+ disable_all = false, -- Disable all default mappings for the popup windows (comments, summary, MR creation, etc.)
+ 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)
+ discard_changes = "ZQ", -- Quit the popup discarding changes, the popup content is not saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
},
- popup = { -- The popup for comment creation, editing, and replying
- 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,
- 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 = {
+ disable_all = false, -- Disable all default mappings for the discussion tree window
+ add_emoji = "Ea", -- Add an emoji to the note/comment
+ delete_emoji = "Ed", -- Remove an emoji from a note/comment
+ delete_comment = "dd", -- Delete comment
+ edit_comment = "e", -- Edit comment
+ reply = "r", -- Reply to comment
+ toggle_resolved = "-", -- Toggle the resolved status of the whole discussion
+ jump_to_file = "o", -- Jump to comment location in file
+ jump_to_reviewer = "a", -- Jump to the comment location in the reviewer window
+ open_in_browser = "b", -- Jump to the URL of the current note/discussion
+ copy_node_url = "u", -- Copy the URL of the current node to clipboard
+ switch_view = "c", -- Toggle between the notes and discussions views
+ toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
+ publish_draft = "P", -- Publish the currently focused note/comment
+ toggle_date_format = "dt", -- Toggle between date formats: relative (e.g., "5 days ago", "just now", "October 13, 2024" for dates more than a month ago) and absolute (e.g., "03/01/2024 at 11:43")
+ 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
+ toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
+ refresh_data = "", -- Refresh the data in the view by hitting Gitlab's APIs again
+ print_node = "p", -- Print the current node (for debugging)
},
- 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
- indentation = " ", -- Indentation Icon
- },
- spinner_chars = { "/", "|", "\\", "-" }, -- Characters for the refresh animation
- auto_open = true, -- Automatically open when the reviewer is opened
- focus_on_open = true, -- Automatically focus the discussion tree when it is opened
- 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 = "bottom", -- "top", "right", "bottom" or "left"
- size = "20%", -- Size of split
- relative = "editor", -- Position of tree split relative to "editor" or "win" (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
- relative_date = true, -- Whether to show relative time like "5 days ago" or absolute time like "03/01/2025 at 01:43"
- winopts = { -- Window-local options for the discussion tree split
- number = false,
- relativenumber = false,
- breakindent = true, -- Every wrapped line will continue visually indented
- showbreak = "+ ", -- String to put at the start of lines that have been wrapped
- }
- 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.
+ reviewer = {
+ disable_all = false, -- Disable all default mappings for the reviewer windows
+ create_comment = "c", -- Create a comment for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
+ create_suggestion = "s", -- Create a suggestion for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
+ move_to_discussion_tree = "a", -- Jump to the comment in the discussion tree
},
- emojis = {
- -- Function for modifying how emojis are displayed in the picker. This does not affect the actual selected emoji.
- -- The function is passed an emoji object as a paramter, e.g.,
- -- {"name": "thumbs up", "shortname": ":thumbsup:", "moji": "👍", "category": "People & Body"}
- -- This is useful if your editor/terminal/font/tmux does not render some emojis properly,
- -- e.g., you can remove skin tones and additionally show the shortname and category with
- -- formatter = function(val)
- -- return string.format("%s %s %s (%s)", val.moji:gsub("[\240][\159][\143][\187-\191]", ""), val.shortname, val.name, val.category)
- -- end
- formatter = nil,
- -- The emoji version used by the Gitlab instance: https://{GITLAB_URL}/-/emojis/{VERSION}/emojis.json. Can be a function that receives the gitlab_url as an argument and returns the version string, e.g.:
- -- version = function(gitlab_url)
- -- return gitlab_url == "https://gitlab.com" and "4" or "3"
- -- end
- version = "4",
+ },
+ popup = { -- The popup for comment creation, editing, and replying
+ 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,
+ 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
+ indentation = " ", -- Indentation Icon
},
- choose_merge_request = {
- open_reviewer = true, -- Open the reviewer window automatically after switching merge requests
+ spinner_chars = { "/", "|", "\\", "-" }, -- Characters for the refresh animation
+ auto_open = true, -- Automatically open when the reviewer is opened
+ focus_on_open = true, -- Automatically focus the discussion tree when it is opened
+ 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 = "bottom", -- "top", "right", "bottom" or "left"
+ size = "20%", -- Size of split
+ relative = "editor", -- Position of tree split relative to "editor" or "win" (window)
+ resolved = '✓', -- Symbol to show next to resolved discussions
+ unresolved = '-', -- Symbol to show next to unresolved discussions
+ unlinked = "", -- Symbol to show next to unlinked 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
+ relative_date = true, -- Whether to show relative time like "5 days ago" or absolute time like "03/01/2025 at 01:43"
+ winopts = { -- Window-local options for the discussion tree split
+ number = false,
+ relativenumber = false,
+ breakindent = true, -- Every wrapped line will continue visually indented
+ showbreak = "+ ", -- String to put at the start of lines that have been wrapped
+ }
+ winbar = nil, -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
+ -- If using lualine, add "gitlab" to disabled file types, otherwise you will not see the winbar.
+ },
+ emojis = {
+ -- Function for modifying how emojis are displayed in the picker. This does not affect the actual selected emoji.
+ -- The function is passed an emoji object as a paramter, e.g.,
+ -- {"name": "thumbs up", "shortname": ":thumbsup:", "moji": "👍", "category": "People & Body"}
+ -- This is useful if your editor/terminal/font/tmux does not render some emojis properly,
+ -- e.g., you can remove skin tones and additionally show the shortname and category with
+ -- formatter = function(val)
+ -- return string.format("%s %s %s (%s)", val.moji:gsub("[\240][\159][\143][\187-\191]", ""), val.shortname, val.name, val.category)
+ -- end
+ formatter = nil,
+ -- The emoji version used by the Gitlab instance: https://{GITLAB_URL}/-/emojis/{VERSION}/emojis.json. Can be a function that receives the gitlab_url as an argument and returns the version string, e.g.:
+ -- version = function(gitlab_url)
+ -- return gitlab_url == "https://gitlab.com" and "4" or "3"
+ -- end
+ version = "4",
+ },
+ choose_merge_request = {
+ open_reviewer = true, -- Open the reviewer window automatically after switching merge requests
+ },
+ info = { -- Show additional fields in the summary view
+ enabled = true,
+ horizontal = false, -- Display metadata to the left of the summary rather than underneath
+ fields = { -- The fields listed here will be displayed, in whatever order you choose
+ "author",
+ "created_at",
+ "updated_at",
+ "merge_status",
+ "draft",
+ "conflicts",
+ "assignees",
+ "reviewers",
+ "pipeline",
+ "branch",
+ "target_branch",
+ "auto_merge",
+ "delete_branch",
+ "squash",
+ "labels",
+ "web_url",
+ "mergeability_checks", -- See more detailed configuration below
},
- info = { -- Show additional fields in the summary view
- enabled = true,
- horizontal = false, -- Display metadata to the left of the summary rather than underneath
- fields = { -- The fields listed here will be displayed, in whatever order you choose
- "author",
- "created_at",
- "updated_at",
- "merge_status",
- "draft",
- "conflicts",
- "assignees",
- "reviewers",
- "pipeline",
- "branch",
- "target_branch",
- "auto_merge",
- "delete_branch",
- "squash",
- "labels",
- "web_url",
- "mergeability_checks", -- See more detailed configuration below
- },
- -- Settings for the mergeability checks in the summary view
- -- https://docs.gitlab.com/api/graphql/reference/#mergeabilitycheckidentifier
- mergeability_checks = {
- -- Symbols for individual check statuses. Set values to `false` to hide checks with given status from summary
- statuses = {
- SUCCESS = "✅",
- CHECKING = "🔁",
- FAILED = "❌",
- WARNING = "⚠️",
- INACTIVE = "💤",
- },
- -- Descriptions for individual checks. Set values to `false` to hide given checks from summary
- checks = {
- CI_MUST_PASS = "Pipeline must succeed",
- COMMITS_STATUS = "Source branch exists and contains commits",
- CONFLICT = "Merge conflicts must be resolved",
- DISCUSSIONS_NOT_RESOLVED = "Open threads must be resolved",
- DRAFT_STATUS = "Merge request must not be draft",
- JIRA_ASSOCIATION_MISSING = "Title or description references a Jira issue",
- LOCKED_LFS_FILES = "All LFS files must be unlocked",
- LOCKED_PATHS = "All paths must be unlocked",
- MERGE_REQUEST_BLOCKED = "Merge request is not blocked",
- MERGE_TIME = "Merge is not blocked due to a scheduled merge time",
- NEED_REBASE = "Merge request must be rebased, fast-forward merge is not possible",
- NOT_APPROVED = "All required approvals must be given",
- NOT_OPEN = "Merge request must be open",
- REQUESTED_CHANGES = "Change requests must be approved by the requesting user",
- SECURITY_POLICY_PIPELINE_CHECK = "Security policy pipeline must succeed",
- SECURITY_POLICY_VIOLATIONS = "Security policies are satisfied",
- STATUS_CHECKS_MUST_PASS = "External status checks pass",
- TITLE_REGEX = "Title matches the expected regex",
- },
- },
+ },
+ -- Settings for the mergeability checks in the summary view
+ -- https://docs.gitlab.com/api/graphql/reference/#mergeabilitycheckidentifier
+ mergeability_checks = {
+ -- Symbols for individual check statuses. Set values to `false` to hide checks with given status from summary
+ statuses = {
+ SUCCESS = "✅",
+ CHECKING = "🔁",
+ FAILED = "❌",
+ WARNING = "⚠️",
+ INACTIVE = "💤",
},
- discussion_signs = {
- enabled = true, -- Show diagnostics for gitlab comments in the reviewer
- skip_resolved_discussion = false, -- Show diagnostics for resolved discussions
- severity = vim.diagnostic.severity.INFO, -- ERROR, WARN, INFO, or HINT
- virtual_text = false, -- Whether to show the comment text inline as floating virtual text
- use_diagnostic_signs = true, -- Show diagnostic sign (depending on the `severity` setting, e.g., I for INFO) along with the comment icon
- priority = 100, -- Higher will override LSP warnings, etc
- icons = {
- comment = "→|",
- range = " |",
- },
- skip_old_revision_discussion = false, -- Don't show diagnostics for discussions that were created for earlier MR revisions
+ -- Descriptions for individual checks. Set values to `false` to hide given checks from summary
+ checks = {
+ CI_MUST_PASS = "Pipeline must succeed",
+ COMMITS_STATUS = "Source branch exists and contains commits",
+ CONFLICT = "Merge conflicts must be resolved",
+ DISCUSSIONS_NOT_RESOLVED = "Open threads must be resolved",
+ DRAFT_STATUS = "Merge request must not be draft",
+ JIRA_ASSOCIATION_MISSING = "Title or description references a Jira issue",
+ LOCKED_LFS_FILES = "All LFS files must be unlocked",
+ LOCKED_PATHS = "All paths must be unlocked",
+ MERGE_REQUEST_BLOCKED = "Merge request is not blocked",
+ MERGE_TIME = "Merge is not blocked due to a scheduled merge time",
+ NEED_REBASE = "Merge request must be rebased, fast-forward merge is not possible",
+ NOT_APPROVED = "All required approvals must be given",
+ NOT_OPEN = "Merge request must be open",
+ REQUESTED_CHANGES = "Change requests must be approved by the requesting user",
+ SECURITY_POLICY_PIPELINE_CHECK = "Security policy pipeline must succeed",
+ SECURITY_POLICY_VIOLATIONS = "Security policies are satisfied",
+ STATUS_CHECKS_MUST_PASS = "External status checks pass",
+ TITLE_REGEX = "Title matches the expected regex",
},
- pipeline = {
- created = "",
- pending = "",
- preparing = "",
- scheduled = "",
- running = "",
- canceled = "↪",
- skipped = "↪",
- success = "✓",
- failed = "",
+ },
+ discussion_signs = {
+ enabled = true, -- Show diagnostics for gitlab comments in the reviewer
+ skip_resolved_discussion = false, -- Show diagnostics for resolved discussions
+ severity = vim.diagnostic.severity.INFO, -- ERROR, WARN, INFO, or HINT
+ virtual_text = false, -- Whether to show the comment text inline as floating virtual text
+ use_diagnostic_signs = true, -- Show diagnostic sign (depending on the `severity` setting, e.g., I for INFO) along with the comment icon
+ priority = 100, -- Higher will override LSP warnings, etc
+ icons = {
+ comment = "→|",
+ range = " |",
},
- create_mr = {
- target = nil, -- Default branch to target when creating an MR
- template_file = nil, -- Default MR template in .gitlab/merge_request_templates
- delete_branch = false, -- Whether the source branch will be marked for deletion
- 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.
- },
- title_input = { -- Default settings for MR title input window
- width = 40,
- border = "rounded",
- },
+ skip_old_revision_discussion = false, -- Don't show diagnostics for discussions that were created for earlier MR revisions
+ },
+ pipeline = {
+ created = "",
+ pending = "",
+ preparing = "",
+ scheduled = "",
+ running = "",
+ canceled = "↪",
+ skipped = "↪",
+ success = "✓",
+ failed = "",
+ },
+ create_mr = {
+ target = nil, -- Default branch to target when creating an MR
+ template_file = nil, -- Default MR template in .gitlab/merge_request_templates
+ delete_branch = false, -- Whether the source branch will be marked for deletion
+ 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.
},
- rebase_mr = {
- skip_ci = false, -- If true, a CI pipeline is not created. Can be overridden in gitlab.rebase call.
- force = false, -- If true, MR is rebased even if MR already is rebased. Can be overridden in gitlab.rebase call.
+ title_input = { -- Default settings for MR title input window
+ width = 40,
+ border = "rounded",
},
- colors = {
- discussion_tree = {
- username = "Keyword",
- mention = "WarningMsg",
- date = "Comment",
- expander = "DiffviewNonText",
- directory = "Directory",
- directory_icon = "DiffviewFolderSign",
- file_name = "Normal",
- resolved = "DiagnosticSignOk",
- unresolved = "DiagnosticSignWarn",
- draft = "DiffviewNonText",
- draft_mode = "DiagnosticWarn",
- live_mode = "DiagnosticOk",
- sort_method = "Keyword",
- }
+ },
+ rebase_mr = {
+ skip_ci = false, -- If true, a CI pipeline is not created. Can be overridden in gitlab.rebase call.
+ force = false, -- If true, MR is rebased even if MR already is rebased. Can be overridden in gitlab.rebase call.
+ },
+ colors = {
+ discussion_tree = {
+ username = "Keyword",
+ mention = "WarningMsg",
+ date = "Comment",
+ expander = "DiffviewNonText",
+ directory = "Directory",
+ directory_icon = "DiffviewFolderSign",
+ file_name = "Normal",
+ resolved = "DiagnosticSignOk",
+ unresolved = "DiagnosticSignWarn",
+ draft = "DiffviewNonText",
+ draft_mode = "DiagnosticWarn",
+ live_mode = "DiagnosticOk",
+ sort_method = "Keyword",
}
- })
+ }
+ })
<
USAGE *gitlab.nvim.usage*
First, check out the branch that you want to review locally:
->
- git checkout feature-branch
+>bash
+ git checkout feature-branch
<
Then open Neovim. To begin, try running the `summary` command or the `review`
command.
@@ -443,15 +499,15 @@ THE SUMMARY VIEW *gitlab.nvim.summary-view*
The `summary` action will open the MR title and description:
>lua
- require("gitlab").summary()
+ require("gitlab").summary()
<
After editing the description or title, you may save your changes via the
`keymaps.popup.perform_action` keybinding.
By default this plugin will also show additional metadata about the MR in a
separate pane underneath the description. This can be disabled, and these
-fields can be reordered or removed. Please see the `settings.info` section of
-the configuration.
+fields can be reordered or removed. See the `settings.info` section of the
+configuration for more information.
REVIEWING AN MR *gitlab.nvim.reviewing-an-mr*
@@ -461,10 +517,10 @@ using the `create_comment` action. In visual mode, add multiline comments with
the `create_multiline_comment` command, and add suggested changes with the
`create_comment_suggestion` command:
>lua
- require("gitlab").review()
- require("gitlab").create_comment()
- require("gitlab").create_multiline_comment()
- require("gitlab").create_comment_suggestion()
+ require("gitlab").review()
+ require("gitlab").create_comment()
+ require("gitlab").create_multiline_comment()
+ require("gitlab").create_comment_suggestion()
<
For suggesting changes you can use `create_comment_suggestion` in visual mode
which works similar to `create_multiline_comment` but prefills the comment
@@ -474,6 +530,7 @@ code block with prefilled code from the visual selection. Just like the
summary, all the different kinds of comments are saved via the
`keymaps.popup.perform_action` keybinding.
+
DRAFT NOTES *gitlab.nvim.draft-comments*
When you publish a "draft" of any of the above resources, the comment will be
@@ -487,6 +544,7 @@ you can publish an individual comment or note by pressing the
Draft notes do not support replying or emojis.
+
TEMPORARY REGISTERS *gitlab.nvim.temp-registers*
While writing a note/comment/suggestion/reply, you may need to interrupt the
@@ -511,6 +569,7 @@ If you don't want the popup contents to be saved to the `temp_registers`, quit
the popup using the `keymaps.pupup.discard_changes` keybinding, which is `ZQ`
by default (compare the builtin |ZQ| command).
+
DISCUSSIONS AND NOTES *gitlab.nvim.discussions-and-notes*
Gitlab groups threads of comments together into "discussions."
@@ -518,9 +577,8 @@ Gitlab groups threads of comments together into "discussions."
To display all discussions for the current MR, use the `toggle_discussions`
action, which will show the discussions in a split window:
>lua
- require("gitlab").toggle_discussions()
+ require("gitlab").toggle_discussions()
<
-
You can jump to the comment’s location in the reviewer window by using the
`keymaps.discussion_tree.jump_to_reviewer` keybinding, or to the actual file
with the `keymaps.discussion_tree.jump_to_file` keybinding.
@@ -532,26 +590,16 @@ If you’d like to create a note in an MR (like a comment, but not linked to a
specific line) use the `create_note` action. The same keybindings for
delete/edit/reply are available on the note tree.
>lua
- require("gitlab").create_note()
-<
-
-When the 'wrap' option is on, line wrapping can make the discussion tree
-harder to read. You can use the 'breakindent' option to visually indent tree
-nodes at the same level. If you don't want to set 'breakindent' globally, you
-can set this only for the `gitlab` file type (the file type of discussions and
-notes). One way of doing this is by creating a file called
-$XDG_CONFIG_HOME/nvim/after/ftplugin/gitlab.lua with the following contents:
->lua
- vim.o.breakindent = true
+ require("gitlab").create_note()
<
LABELS *gitlab.nvim.labels*
You can add or remove labels from the current MR.
>lua
- require("gitlab").add_label()
- require("gitlab").delete_label()
-
+ require("gitlab").add_label()
+ require("gitlab").delete_label()
+<
These labels will be visible in the summary panel, as long as you provide the
"fields" string in your setup function under the `setting.info.fields` block.
@@ -561,26 +609,26 @@ SIGNS AND DIAGNOSTICS *gitlab.nvim.signs-and-diagnostics*
By default when reviewing files, you will see diagnostics for comments that
have been added to a review. These are the default settings:
>lua
- discussion_signs = {
- enabled = true, -- Show diagnostics for gitlab comments in the reviewer
- skip_resolved_discussion = false, -- Show diagnostics for resolved discussions
- severity = vim.diagnostic.severity.INFO, -- ERROR, WARN, INFO, or HINT
- virtual_text = false, -- Whether to show the comment text inline as floating virtual text
- use_diagnostic_signs = true, -- Show diagnostic sign (depending on the `severity` setting, e.g., I for INFO) along with the comment icon
- priority = 100, -- Higher will override LSP warnings, etc
- icons = {
- comment = "→|",
- range = " |",
- },
+ discussion_signs = {
+ enabled = true, -- Show diagnostics for gitlab comments in the reviewer
+ skip_resolved_discussion = false, -- Show diagnostics for resolved discussions
+ severity = vim.diagnostic.severity.INFO, -- ERROR, WARN, INFO, or HINT
+ virtual_text = false, -- Whether to show the comment text inline as floating virtual text
+ use_diagnostic_signs = true, -- Show diagnostic sign (depending on the `severity` setting, e.g., I for INFO) along with the comment icon
+ priority = 100, -- Higher will override LSP warnings, etc
+ icons = {
+ comment = "→|",
+ range = " |",
},
-
+ },
+<
When the cursor is on a diagnostic line you can view the discussion thread by
using `vim.diagnostic.show()`.
You can also jump to the discussion tree for the given comment:
>lua
- require("gitlab").move_to_discussion_tree_from_diagnostic()
-
+ require("gitlab").move_to_discussion_tree_from_diagnostic()
+<
Since nvim 0.10 you can use these two function anywhere in the diagnostic
range. In previous versions, you have to move the cursor to the first line of
the diagnostic.
@@ -625,8 +673,8 @@ MR APPROVALS *gitlab.nvim.mr-approvals*
You can approve or revoke approval for an MR with the `approve` and `revoke`
actions respectively:
>lua
- require("gitlab").approve()
- require("gitlab").revoke()
+ require("gitlab").approve()
+ require("gitlab").revoke()
<
MERGING AN MR *gitlab.nvim.merging-an-mr*
@@ -634,21 +682,21 @@ MERGING AN MR *gitlab.nvim.merging-an-mr*
The `merge` action will merge an MR. The MR must be in a "mergeable" state for
this command to work.
>lua
- require("gitlab").merge()
- require("gitlab").merge({ squash = false, delete_branch = false })
+ require("gitlab").merge()
+ require("gitlab").merge({ squash = false, delete_branch = false })
<
See |gitlab.nvim.merge| for more help on this function.
-REBASING AN MR *gitlab.nvim.rebasing-an-mr*
+REBASING AN MR *gitlab.nvim.rebasing-an-mr*
The `rebase` action will rebase an MR on the server, wait for the rebase to
take effect and then update the local reviewer state. The MR must be in a
"rebasable" state for this command to work, i.e., the worktree must be clean
and there must be no conflicts with the target branch.
>lua
- require("gitlab").rebase()
- require("gitlab").rebase({ skip_ci = true, force = true })
+ require("gitlab").rebase()
+ require("gitlab").rebase({ skip_ci = true, force = true })
<
See |gitlab.nvim.rebase| for more help on this function.
@@ -659,9 +707,9 @@ To create an MR for the current branch, make sure you have the branch checked
out. Then, use the `create_mr` action. See |gitlab.nvim.create_mr| for more
help on this function
>lua
- require("gitlab").create_mr()
- require("gitlab").create_mr({ target = "main" })
- require("gitlab").create_mr({ template_file = "my-template.md", title = "Fix bug XYZ" })
+ require("gitlab").create_mr()
+ require("gitlab").create_mr({ target = "main" })
+ require("gitlab").create_mr({ template_file = "my-template.md", title = "Fix bug XYZ" })
<
PIPELINES *gitlab.nvim.pipelines*
@@ -669,7 +717,7 @@ PIPELINES *gitlab.nvim.pipelines*
You can view the status of the pipeline for the current MR with the `pipeline`
action.
>lua
- require("gitlab").pipeline()
+ require("gitlab").pipeline()
<
To re-trigger failed jobs in the pipeline manually, use the
`keymaps.popup.perform_action` keybinding. To open the log trace of a job in a
@@ -683,19 +731,10 @@ The `add_reviewer` and `delete_reviewer` actions, as well as the `add_assignee`
and `delete_assignee` functions, will let you choose from a list of users who
are available in the current project:
>lua
- require("gitlab").add_reviewer()
- require("gitlab").delete_reviewer()
- require("gitlab").add_assignee()
- require("gitlab").delete_assignee()
-<
-These actions use Neovim’s built in picker, which is much nicer if you
-install `dressing.nvim`. If you use Dressing, please enable it:
->lua
- require("dressing").setup({
- input = {
- enabled = true
- }
- })
+ require("gitlab").add_reviewer()
+ require("gitlab").delete_reviewer()
+ require("gitlab").add_assignee()
+ require("gitlab").delete_assignee()
<
RESTARTING THE GO SERVER *gitlab.nvim.restarting*
@@ -703,7 +742,7 @@ RESTARTING THE GO SERVER *gitlab.nvim.restarting*
The `gitlab.nvim` server will shut down automatically when you exit Neovim.
You may restart the server at any time via the the `restart` command:
>lua
- require("gitlab.server").restart()
+ require("gitlab.server").restart()
<
KEYBINDINGS *gitlab.nvim.keybindings*
@@ -729,8 +768,8 @@ The global keymaps all use the `gl` prefix as it does not have a special
meaning in normal mode. You can add your own global keybindings by calling
something like this after the `gitlab.setup()` call:
>lua
- local gitlab = require("gitlab")
- vim.keymap.set("n", "gl", gitlab.print_settings, { desc = "Print gitlab.nvim settings"})
+ local gitlab = require("gitlab")
+ vim.keymap.set("n", "gl", gitlab.print_settings, { desc = "Print gitlab.nvim settings"})
<
See |gitlab.nvim.api| for an overview of available API functions that you can
use in your mappings.
@@ -741,10 +780,9 @@ file. E.g., if you wanted to use the `j` and `k` keys to move to the next
discussion node instead of the next line, you could add to the |ftplugin| file
the following lines:
>lua
- vim.keymap.set("n", "j", [[call search('[] @')]], { buffer = 0, desc = "Go to next node" })
- vim.keymap.set("n", "k", [[call search('[] @', 'b')]], { buffer = 0, desc = "Go to previous node" })
+ vim.keymap.set("n", "j", [[call search('[] @')]], { buffer = 0, desc = "Go to next node" })
+ vim.keymap.set("n", "k", [[call search('[] @', 'b')]], { buffer = 0, desc = "Go to previous node" })
<
-
Reviewer keybindings ~
Most of the keybindings `gitlab.nvim` sets are normal mode mappings, with the
@@ -785,44 +823,44 @@ this buffer) by adding a special field called `nowait` to your keymap. For examp
if the name of the keybinding is `reviewer.create_comment`, then you add the
following to your config:
>lua
- keymaps = { reviewer = { create_comment_nowait = true }}
+ keymaps = { reviewer = { create_comment_nowait = true }}
<
TROUBLESHOOTING *gitlab.nvim.troubleshooting*
-To check the health of the plugin and related dependencies, please run:
+To check the health of the plugin and related dependencies run:
>vim
- :checkhealth gitlab
+ :checkhealth gitlab
<
To print your current settings:
>lua
- require("gitlab").print_settings()
+ require("gitlab").print_settings()
<
-This plugin uses a Go server to reach out to Gitlab. It’s possible that
+This plugin uses a local Go server to reach out to Gitlab. It’s possible that
something is going wrong when starting that server or connecting with Gitlab.
The Go server runs outside of Neovim, and can be interacted with directly in
order to troubleshoot. To start the server, check out your feature branch and
run these commands:
>lua
- require("gitlab.server").build(true)
- require("gitlab.server").start(function() print("Server started") end)
+ require("gitlab.server").build(true)
+ require("gitlab.server").start(function() print("Server started") end)
<
The easiest way to debug what’s going wrong is to turn on the `debug` options
in your setup function. This will allow you to see requests leaving the Go
server, and the responses coming back from Gitlab. Once the server is running,
you can also interact with the Go server like any other process:
->
- curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" localhost:21036/mr/info
+>bash
+ curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" localhost:21036/mr/info
<
==============================================================================
-LUA API *gitlab.nvim.api*
+LUA API *gitlab.nvim.api*
- *gitlab.nvim.setup*
+ *gitlab.nvim.setup*
gitlab.setup() ~
Call this first to initialize the plugin. With no arguments, it will use the
-default arguments outlined under "Configuring the Plugin".
+default arguments outlined under "Configuring the plugin".
>lua
require("gitlab").setup()
@@ -830,7 +868,7 @@ default arguments outlined under "Configuring the Plugin".
require("gitlab").setup({ discussion_tree = { blacklist = { "some_bot"} } })
<
- *gitlab.nvim.choose_merge_request*
+ *gitlab.nvim.choose_merge_request*
gitlab.choose_merge_request({opts}) ~
Choose a merge request from a list of those open in your current project to
@@ -849,13 +887,13 @@ By default, the endpoint will return all open merge requests.
require("gitlab").choose_merge_request({ ["[not]labels"] = {"exclude_mrs_with_label"} })
<
Parameters: ~
- • {opts}: (table|nil)
+ • {opts}: (table?)
• {open_reviewer}: (boolean) Whether to open the reviewer after
switching branches. True by default.
• {labels}: (table) Return merge requests with *including* matching labels
• Etc, see: https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests
- *gitlab.nvim.choose_merge_request_by_username*
+ *gitlab.nvim.choose_merge_request_by_username*
gitlab.choose_merge_request_by_username({opts}) ~
Choose a merge request based on the username provided. Like the `choose_merge_request`
@@ -865,12 +903,12 @@ the reviewer pane when the MR is chosen.
require("gitlab").choose_merge_request_by_username({ username = "hcramer" })
<
Parameters: ~
- • {opts}: (table|nil)
+ • {opts}: (table?)
• {username}: (string) The username of the Gitlab user, must be a
member of the current project.
• {state} [optional]: (string) The status of the MR, e.g. "opened" or "all"
- *gitlab.nvim.review*
+ *gitlab.nvim.review*
gitlab.review() ~
Opens the reviewer pane. Can be used from anywhere within Neovim after the
@@ -878,7 +916,7 @@ plugin is loaded. If run twice, will open a second reviewer pane.
>lua
require("gitlab").review()
<
- *gitlab.nvim.reload_review*
+ *gitlab.nvim.reload_review*
gitlab.reload_review() ~
Loads new MR state from Gitlab. Then if `diffview.api` is available (with the
@@ -889,7 +927,7 @@ https://github.com/sindrets/diffview.nvim) closes and re-opens the reviewer.
>lua
require("gitlab").reload_review()
<
- *gitlab.nvim.close_review*
+ *gitlab.nvim.close_review*
gitlab.close_review() ~
Closes the reviewer tab and discussion tree and cleans up (e.g., removes
@@ -897,7 +935,7 @@ winbar timer).
>lua
require("gitlab").close_review()
<
- *gitlab.nvim.summary*
+ *gitlab.nvim.summary*
gitlab.summary() ~
Opens the summary window with information about the current MR, such as the
@@ -909,7 +947,7 @@ in the setup call.
The summary can be edited. Once you have made changes, send them to Gitlab via
the `keymaps.popup.perform_action` keybinding.
- *gitlab.nvim.approve*
+ *gitlab.nvim.approve*
gitlab.approve() ~
Approves the current MR. Will error if the current user does not have
@@ -917,7 +955,7 @@ permission.
>lua
require("gitlab").approve()
<
- *gitlab.nvim.revoke*
+ *gitlab.nvim.revoke*
gitlab.revoke() ~
Revokes approval for the current MR. Will error if the current user does not
@@ -925,7 +963,7 @@ have permission or has not previously approved the MR.
>lua
require("gitlab").approve()
<
- *gitlab.nvim.create_comment*
+ *gitlab.nvim.create_comment*
gitlab.create_comment() ~
Opens a popup to create a comment on the current line. Must be called when focused on the
@@ -936,7 +974,7 @@ reviewer pane (see the gitlab.nvim.review command), otherwise it will error.
After the comment is typed, submit it to Gitlab via the
`keymaps.popup.perform_action` keybinding, by default `ZZ`.
- *gitlab.nvim.create_multiline_comment*
+ *gitlab.nvim.create_multiline_comment*
gitlab.create_multiline_comment() ~
Opens a popup to create a multi-line comment. May only be called in visual
@@ -947,7 +985,7 @@ mode, and will use the currently selected lines.
After the comment is typed, submit it to Gitlab via the
`keymaps.popup.perform_linewise_action` keybinding, by default `ZA`.
- *gitlab.nvim.create_comment_suggestion*
+ *gitlab.nvim.create_comment_suggestion*
gitlab.create_comment_suggestion() ~
Opens a popup to create a comment suggestion (aka a comment that makes a committable
@@ -958,7 +996,7 @@ change suggestion to the currently selected lines).
After the comment is typed, submit it to Gitlab via the
`keymaps.popup.perform_linewise_action` keybinding, by default `ZA`.
- *gitlab.nvim.create_mr*
+ *gitlab.nvim.create_mr*
gitlab.create_mr({opts}) ~
Starts the process of creating an MR for the currently checked out branch.
@@ -969,7 +1007,7 @@ Starts the process of creating an MR for the currently checked out branch.
require("gitlab").create_mr({ title = "Fix bug XYZ", description = "Closes #123" })
<
Parameters: ~
- • {opts}: (table|nil) Keyword arguments that can be used to skip
+ • {opts}: (table?) Keyword arguments that can be used to skip
certain steps in the MR creation process. `target` and
`template_file` can also be configured via the `gitlab.setup()`
function in the `settings.create_mr` field. If any field is missing,
@@ -997,7 +1035,7 @@ branch", "Squash commits", and "Target branch" fields, you can use the
value or to select a new target branch, respectively. Use the
`keymaps.popup.perform_action` keymap to POST the MR to Gitlab.
- *gitlab.nvim.move_to_discussion_tree_from_diagnostic*
+ *gitlab.nvim.move_to_discussion_tree_from_diagnostic*
gitlab.move_to_discussion_tree_from_diagnostic() ~
When hovering over a diagnostic in the reviewer pane, jumps to the relevant
@@ -1007,7 +1045,7 @@ node in the discussion tree.
If there are no diagnostics for the current line, shows a warning message.
- *gitlab.nvim.create_note*
+ *gitlab.nvim.create_note*
gitlab.create_note() ~
Opens a popup to create a note. Notes are like comments except they are not
@@ -1018,7 +1056,7 @@ tied to specific changes in an MR.
After the comment is typed, submit it to Gitlab via the
`keymaps.popup.perform_action` keybinding, by default `ZZ`.
- *gitlab.nvim.toggle_discussions*
+ *gitlab.nvim.toggle_discussions*
gitlab.toggle_discussions() ~
Toggles visibility of the discussion tree.
@@ -1026,11 +1064,11 @@ Toggles visibility of the discussion tree.
require("gitlab").toggle_discussions()
Once the discussion tree is open, a number of different keybindings are
-available for interacting with different discussions. Please see the
+available for interacting with different discussions. See the
`keymaps.discussion_tree` section of the setup call for more information about
different keybindings.
- *gitlab.nvim.publish_all_drafts*
+ *gitlab.nvim.publish_all_drafts*
gitlab.publish_all_drafts() ~
Publishes all unpublished draft notes. Used to finish a review and make all
@@ -1038,62 +1076,62 @@ notes and comments visible.
>lua
require("gitlab").publish_all_drafts()
<
- *gitlab.nvim.toggle_draft_mode*
+ *gitlab.nvim.toggle_draft_mode*
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.nvim.toggle_sort_method*
gitlab.toggle_sort_method() ~
Toggles whether the discussion tree is sorted by the "latest_reply", with
threads with the most recent activity on top (the default), or by
"original_comment", with the oldest threads on top.
- *gitlab.nvim.add_assignee*
+ *gitlab.nvim.add_assignee*
gitlab.add_assignee() ~
Opens up a select menu for choosing an assignee for the current merge request.
>lua
require("gitlab").add_assignee()
<
- *gitlab.nvim.delete_assignee*
+ *gitlab.nvim.delete_assignee*
gitlab.delete_assignee() ~
Opens up a select menu for removing an existing assignee for the current merge request.
>lua
require("gitlab").delete_assignee()
<
- *gitlab.nvim.add_reviewer*
+ *gitlab.nvim.add_reviewer*
gitlab.add_reviewer() ~
Opens up a select menu for adding a reviewer for the current merge request.
>lua
require("gitlab").add_reviewer()
<
- *gitlab.nvim.add_label*
+ *gitlab.nvim.add_label*
gitlab.add_label() ~
Opens up a select menu for adding a label to the current merge request.
>lua
require("gitlab").add_label()
<
- *gitlab.nvim.delete_label*
+ *gitlab.nvim.delete_label*
gitlab.delete_label() ~
Opens up a select menu for removing an existing label from the current merge request.
>lua
require("gitlab").delete_label()
<
- *gitlab.nvim.delete_reviewer*
+ *gitlab.nvim.delete_reviewer*
gitlab.delete_reviewer() ~
Opens up a select menu for removing an existing reviewer for the current merge request.
>lua
require("gitlab").delete_reviewer()
<
- *gitlab.nvim.pipeline*
+ *gitlab.nvim.pipeline*
gitlab.pipeline() ~
Opens up a popup with information about the pipeline for the current merge request.
@@ -1105,21 +1143,21 @@ To re-trigger failed jobs in the pipeline manually, use the
new Neovim buffer, use your `keymaps.popup.perform_linewise_action`
keybinding.
- *gitlab.nvim.open_in_browser*
+ *gitlab.nvim.open_in_browser*
gitlab.open_in_browser() ~
Opens the current MR in your default web browser.
>lua
require("gitlab").open_in_browser()
<
- *gitlab.nvim.copy_mr_url*
+ *gitlab.nvim.copy_mr_url*
gitlab.copy_mr_url() ~
Copies the URL of the current MR to system clipboard.
>lua
require("gitlab").copy_mr_url()
<
- *gitlab.nvim.merge*
+ *gitlab.nvim.merge*
gitlab.merge({opts}) ~
Merges a mergeable merge request into the target branch. The behaviour can be
@@ -1135,7 +1173,7 @@ request" page). You can see the current settings in the Summary view, see
})
<
Parameters: ~
- • {opts}: (table|nil) Keyword arguments that can be used to override
+ • {opts}: (table?) Keyword arguments that can be used to override
individual current settings.
• {auto_merge}: (bool) If true, the merge request is set to merge
automatically when the pipeline succeeds. If false, an immediate
@@ -1149,7 +1187,7 @@ request" page). You can see the current settings in the Summary view, see
Use the `keymaps.popup.perform_action` to merge the MR
with your message.
- *gitlab.nvim.rebase*
+ *gitlab.nvim.rebase*
gitlab.rebase({opts}) ~
Rebases the feature branch of the MR on the server, pulls the new state, and
@@ -1159,7 +1197,7 @@ updates the reviewer.
require("gitlab").rebase({ skip_ci = true, force = true })
<
Parameters: ~
- • {opts}: (table|nil) Keyword arguments that can be used to override
+ • {opts}: (table?) Keyword arguments that can be used to override
default behavior.
• {skip_ci}: (`bool?`, default: `false`) If true, a CI pipeline is
not created (this may lead to a failed Mergeability Check
@@ -1168,12 +1206,12 @@ updates the reviewer.
if MR already is rebased (this may run an unnecessary CI
pipeline).
- *gitlab.nvim.data*
+ *gitlab.nvim.data*
gitlab.data({resources}, {cb}) ~
The data function can be used to integrate `gitlab.nvim` with other plugins
and tooling, by fetching raw data about the current MR, including the summary
-information (title, description, etc); reviewers, assignees, pipeline status.
+information (title, description, etc), project members, pipeline status, etc.
>lua
require("gitlab").data({
{ type = "info", refresh = false },
@@ -1188,18 +1226,21 @@ execute and passed the data as an argument.
Parameters: ~
• {resources}: (table) A list of {resource} blocks to fetch.
- • {resource}: (table) A resource to fetch, such as job
+ • {resource}: (table) A resource to fetch, such as user
information, etc. The resource is defined by its {type}:
• {type}: (string) The type of resource, either:
+ • "info": Basic information about the MR.
• "user": Information about the currently authenticated
user.
• "labels": The labels available in the current project.
+ • "mergeability": The status of the mergeability checks.
• "project_members": The list of current project members.
• "revisions": Revision information about the MR.
• "pipeline": Information about the current branch's
pipeline. Returns and object with `latest_pipeline` and
`jobs` as fields.
- • "draft_notes": The current user's unpublished notes
+ • "draft_notes": The current user's unpublished notes.
+ • "discussion_data": The comments and notes in the MR.
• {refresh}: (bool) Whether to re-fetch the data from Gitlab
or use the cached data locally, if available.
• {cb}: (function) The callback function that runs after all of the
@@ -1207,7 +1248,7 @@ execute and passed the data as an argument.
with each resource as a key-value pair, with the key being it's
type.
- *gitlab.nvim.refresh_data*
+ *gitlab.nvim.refresh_data*
gitlab.refresh_data() ~
Fetches discussion tree data from Gitlab and refreshes the tree views. It can
@@ -1228,4 +1269,9 @@ least 10 seconds:
})
<
+CONTRIBUTING *gitlab.nvim.contributing*
+
+Contributions to the plugin are welcome. Please read `.github/CONTRIBUTING.md`
+before you start working on a pull request.
+
vim:tw=78:ts=4:sw=4:expandtab:ft=help:norl:
diff --git a/lua/gitlab/actions/approvals.lua b/lua/gitlab/actions/approvals.lua
index 739f00df..85844b4f 100644
--- a/lua/gitlab/actions/approvals.lua
+++ b/lua/gitlab/actions/approvals.lua
@@ -4,8 +4,8 @@ local u = require("gitlab.utils")
local M = {}
-local refresh_status_state = function(data)
- u.notify(data.message, vim.log.levels.INFO)
+---Load mergeability and info data from Gitlab and update the Status window.
+local refresh_status_state = function()
state.load_new_state("mergeability", function()
state.load_new_state("info", function()
require("gitlab.actions.summary").update_summary_details()
@@ -13,15 +13,19 @@ local refresh_status_state = function(data)
end)
end
+---Send the approval to Gitlab, notify user, and re-fresh state.
M.approve = function()
job.run_job("/mr/approve", "POST", nil, function(data)
- refresh_status_state(data)
+ u.notify(data.message, vim.log.levels.INFO)
+ refresh_status_state()
end)
end
+---Send the approval revocation to Gitlab, notify user, and re-fresh state.
M.revoke = function()
job.run_job("/mr/revoke", "POST", nil, function(data)
- refresh_status_state(data)
+ u.notify(data.message, vim.log.levels.INFO)
+ refresh_status_state()
end)
end
diff --git a/lua/gitlab/actions/assignees_and_reviewers.lua b/lua/gitlab/actions/assignees_and_reviewers.lua
index 77716d1e..174561fd 100644
--- a/lua/gitlab/actions/assignees_and_reviewers.lua
+++ b/lua/gitlab/actions/assignees_and_reviewers.lua
@@ -1,5 +1,6 @@
-- This module is responsible for the assignment of reviewers
-- and assignees in Gitlab, those who must review an MR.
+
local u = require("gitlab.utils")
local job = require("gitlab.job")
local List = require("gitlab.utils.list")
@@ -22,12 +23,18 @@ M.delete_reviewer = function()
M.delete_popup("reviewer")
end
+---Refresh the assignees and reviewers state with new data.
+---@param type "assignee"|"reviewer" The type of data to refresh
+---@param data table
+---@param message string Message from the Go server
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
+---Prompt the user to select a new assignee/reviewer to add to the MR.
+---@param type "assignee"|"reviewer" The type of data to add to the MR
M.add_popup = function(type)
local plural = type .. "s"
local current = state.INFO[plural]
@@ -50,6 +57,8 @@ M.add_popup = function(type)
end)
end
+---Prompt the user to select an existing assignee/reviewer to remove from the MR.
+---@param type "assignee"|"reviewer" The type of data to delete from the MR
M.delete_popup = function(type)
local plural = type .. "s"
local current = state.INFO[plural]
@@ -71,6 +80,15 @@ M.delete_popup = function(type)
end)
end
+---@class HasId
+---@field id integer The ID of the item
+
+---Return a copy of `current` list of items without items in the `to_remove` list.
+---@generic T: HasId
+---@generic U: HasId
+---@param current T[] Original list of items
+---@param to_remove U[] List of items to remove
+---@return T[]
M.filter_eligible = function(current, to_remove)
local ids = u.extract(to_remove, "id")
return List.new(current):filter(function(member)
diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua
index 56617464..862521af 100644
--- a/lua/gitlab/actions/comment.lua
+++ b/lua/gitlab/actions/comment.lua
@@ -1,6 +1,6 @@
---- This module is responsible for creating new comments
---- in the reviewer's buffer. The reviewer will pass back
---- to this module the data required to make the API calls
+-- This module is responsible for creating new comments in the reviewer's buffer. The reviewer will
+-- pass back to this module the data required to make the API calls.
+
local Popup = require("nui.popup")
local Layout = require("nui.layout")
local state = require("gitlab.state")
@@ -21,11 +21,10 @@ local M = {
comment_popup = nil,
}
----Fires the API that sends the comment data to the Go server, called when you "confirm" creation
----via the M.settings.keymaps.popup.perform_action keybinding
+---Fire the API to send the comment data to the Go server.
---@param text string comment text
---@param unlinked boolean if true, the comment is not linked to a line
----@param discussion_id string | nil The ID of the discussion to which the reply is responding, nil if not a reply
+---@param discussion_id? string The ID of the discussion when replying in a thread, otherwise nil
local confirm_create_comment = function(text, unlinked, discussion_id)
if text == nil then
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
@@ -100,8 +99,7 @@ local confirm_create_comment = function(text, unlinked, discussion_id)
end)
end
--- This function will actually send the deletion to Gitlab when you make a selection,
--- and re-render the tree
+---Send comment deletion request to the Go server and re-render the tree.
---@param note_id integer
---@param discussion_id string
---@param unlinked boolean
@@ -113,7 +111,7 @@ M.confirm_delete_comment = function(note_id, discussion_id, unlinked)
end)
end
----This function sends the edited comment to the Go server
+---Send edited comment to the Go server.
---@param discussion_id string
---@param note_id integer
---@param unlinked boolean
@@ -133,13 +131,13 @@ end
---@class LayoutOpts
---@field unlinked boolean
----@field discussion_id string|nil
----@field reply boolean|nil
----@field file_name string|nil
+---@field discussion_id? string
+---@field reply? boolean
+---@field file_name? string
----This function sets up the layout and popups needed to create a comment, note and
----multi-line comment. It also sets up the basic keybindings for switching between
----window panes, and for the non-primary sections.
+---Set up the layout and popups needed to create a comment, note and multi-line comment.
+---Also set up basic keybindings for the non-primary sections, and for switching between
+---sections.
---@param opts LayoutOpts
---@return NuiLayout
M.create_comment_layout = function(opts)
@@ -163,7 +161,7 @@ M.create_comment_layout = function(opts)
local settings = u.merge(popup_settings, user_settings or {})
local current_win = vim.api.nvim_get_current_win()
- M.comment_popup = Popup(popup.create_popup_state(title, settings))
+ M.comment_popup = Popup(popup.create_popup_state({ title = title, user_settings = settings }))
M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings))
local internal_layout = Layout.Box({
@@ -206,8 +204,8 @@ M.create_comment_layout = function(opts)
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
+---Open a comment popup in order to create a comment on the changed/updated line in the
+---current MR.
M.create_comment = function()
M.location = Location.new()
if not M.can_create_comment(false) then
@@ -218,8 +216,8 @@ M.create_comment = function()
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
+---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()
M.location = Location.new()
if not M.can_create_comment(true) then
@@ -231,8 +229,8 @@ M.create_multiline_comment = function()
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
+---Open 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({ unlinked = true })
layout:mount()
@@ -240,7 +238,7 @@ end
---Given the current visually selected area of text, builds text to fill in the
---comment popup with a suggested change
----@return LineRange|nil
+---@return LineRange?
local build_suggestion = function()
local current_line = vim.api.nvim_win_get_cursor(0)[1]
local range_length = M.location.visual_range.end_line - M.location.visual_range.start_line
@@ -273,9 +271,8 @@ local build_suggestion = function()
return suggestion_lines
end
---- This function will open a a popup to create a suggestion comment
---- on the changed/updated line in the current MR
---- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
+---Open a popup to create a suggestion comment 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()
M.location = Location.new()
if not M.can_create_comment(true) then
@@ -295,7 +292,7 @@ M.create_comment_suggestion = function()
end)
end
----Returns true if it's possible to create an Inline Comment
+---Return 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)
@@ -366,8 +363,8 @@ M.can_create_comment = function(must_be_visual)
return true
end
----Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
----buffers as 'null'
+---Check whether user is commenting on a valid buffer.
+---Tightly coupled to how the Diffview plugin names non-existent buffers!
---@return boolean
M.sha_exists = function()
if vim.fn.expand("%") == "diffview://null" then
diff --git a/lua/gitlab/actions/common.lua b/lua/gitlab/actions/common.lua
index d8e66fbe..d328440a 100644
--- a/lua/gitlab/actions/common.lua
+++ b/lua/gitlab/actions/common.lua
@@ -1,6 +1,7 @@
-- This module contains code shared between at least two modules. This includes
-- actions common to multiple tree types, as well as general utility functions
--- that are specific to actions (like jumping to a file or opening a URL)
+-- that are specific to actions (like jumping to a file or opening a URL).
+
local List = require("gitlab.utils.list")
local u = require("gitlab.utils")
local reviewer = require("gitlab.reviewer")
@@ -8,7 +9,7 @@ local indicators_common = require("gitlab.indicators.common")
local state = require("gitlab.state")
local M = {}
----Build note header from note
+---Build note header from note.
---@param note Note|DraftNote
---@return string
M.build_note_header = function(note)
@@ -20,6 +21,9 @@ M.build_note_header = function(note)
return "@" .. note.author.username .. " " .. time
end
+---Switch the options that control if buffers can be modified.
+---@param bool boolean The value to set
+---@param ... integer List of buffer numbers
M.switch_can_edit_bufs = function(bool, ...)
local bufnrs = { ... }
---@param v integer
@@ -28,15 +32,18 @@ M.switch_can_edit_bufs = function(bool, ...)
end
end
----Takes in a chunk of text separated by new line characters and returns a lua table
+---Transform a string containing new line characters into a table of lines.
+---TODO: Replace this function with direct calls of u.lines_into_table, as the extra
+---line is not necessary!
---@param content string
----@return table
+---@return string[]
M.build_content = function(content)
local description_lines = u.lines_into_table(content)
table.insert(description_lines, "")
return description_lines
end
+---Fill discussion tree buffers with warnings, if there are no linked or unlinked comments.
M.add_empty_titles = function()
local draft_notes = require("gitlab.actions.draft_notes")
local discussions = require("gitlab.actions.discussions")
@@ -88,7 +95,9 @@ M.add_empty_titles = function()
end
end
+---Return the URL for the current note.
---@param tree NuiTree
+---@return string?
M.get_url = function(tree)
local current_node = tree:get_node()
local note_node = M.get_note_node(tree, current_node)
@@ -103,6 +112,7 @@ M.get_url = function(tree)
return url
end
+---Open the current note in the browser.
---@param tree NuiTree
M.open_in_browser = function(tree)
local url = M.get_url(tree)
@@ -111,6 +121,7 @@ M.open_in_browser = function(tree)
end
end
+---Copy the current note's URL to the clipboard.
---@param tree NuiTree
M.copy_node_url = function(tree)
local url = M.get_url(tree)
@@ -120,13 +131,14 @@ M.copy_node_url = function(tree)
end
end
--- For developers!
+---For developers!
+---@param tree NuiTree
M.print_node = function(tree)
local current_node = tree:get_node()
vim.print(current_node)
end
----Check if type of node is note or note body
+---Return true if type of node is note or note body (as opposed to e.g. "file_name").
---@param node? NuiTree.Node
---@return boolean
M.is_node_note = function(node)
@@ -137,7 +149,7 @@ M.is_node_note = function(node)
end
end
----Get root node
+---Get root node at the current cursor position.
---@param tree NuiTree
---@param node? NuiTree.Node
---@return NuiTree.Node?
@@ -153,7 +165,7 @@ M.get_root_node = function(tree, node)
end
end
----Get note node
+---Get the main note node at the current cursor position.
---@param tree NuiTree
---@param node? NuiTree.Node
---@return NuiTree.Node?
@@ -173,12 +185,14 @@ M.get_note_node = function(tree, node)
end
end
----Takes a node and returns the line where the note is positioned in the new SHA. If
----the line is not in the new SHA, returns nil
+---Return the line where the note is positioned in the new SHA.
+---If the line is not in the new SHA, return nil for a single-line comment.
+---TODO: For ranged comments this function may return 0 for removed lines. Shouldn't it
+---return nil instead?
---@param node NuiTree.Node
----@return number|nil
-local function get_new_line(node)
- ---@type GitlabLineRange|nil
+---@return integer?
+local function get_new_linenr(node)
+ ---@type GitlabLineRange?
local range = node.range
if range == nil then
return node.new_line
@@ -188,12 +202,14 @@ local function get_new_line(node)
return new_start_line
end
----Takes a node and returns the line where the note is positioned in the old SHA. If
----the line is not in the old SHA, returns nil
+---Return the line where the note is positioned in the old SHA.
+---If the line is not in the old SHA, return nil for a single-line comment.
+---TODO: For ranged comments this function may return 0 for added lines. Shouldn't it
+---return nil instead?
---@param node NuiTree.Node
----@return number|nil
-local function get_old_line(node)
- ---@type GitlabLineRange|nil
+---@return integer?
+local function get_old_linenr(node)
+ ---@type GitlabLineRange?
local range = node.range
if range == nil then
return node.old_line
@@ -203,11 +219,14 @@ local function get_old_line(node)
return old_start_line
end
+---Get the line number of a single-line comment.
+---TODO: Consider if this can be removed if all comments would be created as "multi-line".
+---See lua/gitlab/reviewer/location.lua:74.
---@param id string|integer
----@return integer|nil line_number
+---@return integer? line_number
---@return boolean is_new_sha True if line number refers to NEW SHA
M.get_line_number = function(id)
- ---@type Discussion|DraftNote|nil
+ ---@type (Discussion|DraftNote)?
local d_or_n
d_or_n = List.new(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {}):find(function(d)
return d.id == id
@@ -224,10 +243,11 @@ M.get_line_number = function(id)
return ((is_new_sha and first_note.position.new_line or first_note.position.old_line) or 1), is_new_sha
end
----Return the start and end line numbers for the note range. The range is calculated from the line
----codes but the position itself is based on either the `new_line` or `old_line`.
----@param old_line integer|nil The line number in the OLD version
----@param new_line integer|nil The line number in the NEW version
+---Return start and end line numbers for the note range, and whether comment is on "NEW SHA".
+---The range is calculated from the line codes but the position itself is based on
+---either the `new_line` or `old_line`.
+---@param old_line? integer The line number in the OLD version
+---@param new_line? integer The line number in the NEW version
---@param start_line_code string The line code for the start of the range
---@param end_line_code string The line code for the end of the range
---@return integer start_line
@@ -250,8 +270,9 @@ M.get_line_numbers_for_range = function(old_line, new_line, start_line_code, end
end
end
+---Return line number for jumping to reviewer.
---@param root_node NuiTree.Node
----@return integer|nil line_number
+---@return integer? line_number
---@return boolean is_new_sha True if line number refers to NEW SHA
M.get_line_number_from_node = function(root_node)
if root_node.range then
@@ -267,7 +288,8 @@ M.get_line_number_from_node = function(root_node)
end
end
--- This function (settings.keymaps.discussion_tree.jump_to_reviewer) will jump the cursor to the reviewer's location associated with the note. The implementation depends on the reviewer
+---Move the cursor to the reviewer's location associated with the note.
+---@param tree NuiTree
M.jump_to_reviewer = function(tree)
local node = tree:get_node()
local root_node = M.get_root_node(tree, node)
@@ -283,7 +305,8 @@ M.jump_to_reviewer = function(tree)
reviewer.jump(root_node.file_name, root_node.old_file_name, line_number, is_new_sha)
end
--- This function (settings.keymaps.discussion_tree.jump_to_file) will jump to the file changed in a new tab
+---Jump to the file in a new tab.
+---@param tree NuiTree
M.jump_to_file = function(tree)
local node = tree:get_node()
local root_node = M.get_root_node(tree, node)
@@ -303,7 +326,7 @@ M.jump_to_file = function(tree)
return
end
vim.cmd.tabnew()
- local line_number = get_new_line(root_node) or get_old_line(root_node)
+ local line_number = get_new_linenr(root_node) or get_old_linenr(root_node)
if line_number == nil or line_number == 0 then
line_number = 1
end
diff --git a/lua/gitlab/actions/create_mr.lua b/lua/gitlab/actions/create_mr.lua
index 5ea71fb7..39c6f1f7 100644
--- a/lua/gitlab/actions/create_mr.lua
+++ b/lua/gitlab/actions/create_mr.lua
@@ -1,5 +1,6 @@
-- This module is responsible for creating am MR
-- for the current branch
+
local Layout = require("nui.layout")
local Input = require("nui.input")
local Popup = require("nui.popup")
@@ -36,6 +37,7 @@ local M = {
},
}
+---Reset the state associated with the MR creation process.
M.reset_state = function()
M.started = false
M.mr.title = ""
@@ -44,10 +46,10 @@ M.reset_state = function()
M.mr.forked_project_id = nil
end
----1. If the user has already begun writing an MR, prompt them to
---- continue working on it.
+---1. If the user has already begun writing an MR, prompt them to continue working on it.
---@param args? Mr
M.start = function(args)
+ args = args or {}
if not git.check_current_branch_up_to_date_on_remote(vim.log.levels.ERROR) then
return
end
@@ -67,12 +69,9 @@ M.start = function(args)
end
end
----2. Pick the target branch
----@param mr? Mr
+---2. Pick the target branch.
+---@param mr Mr
M.pick_target = function(mr)
- if not mr then
- mr = {}
- end
if mr.target ~= nil then
M.pick_template(mr)
return
@@ -91,21 +90,18 @@ M.pick_target = function(mr)
end)
end
+---Return the path to the template file.
+---@param t string The template file basename
+---@return string template_path The absolute path to the template file in this repository
local function make_template_path(t)
local base_dir, err = git.base_dir()
if err ~= nil then
- return
+ return ""
end
- return base_dir
- .. state.settings.file_separator
- .. ".gitlab"
- .. state.settings.file_separator
- .. "merge_request_templates"
- .. state.settings.file_separator
- .. t
+ return table.concat({ base_dir, ".gitlab", "merge_request_templates", t }, u.path_separator)
end
----3. Pick template (if applicable). This is used as the description
+---3. Pick template (if applicable) to be used as the MR description.
---@param mr Mr
M.pick_template = function(mr)
if mr.description ~= nil then
@@ -120,17 +116,17 @@ M.pick_template = function(mr)
return
end
- local all_templates = u.list_files_in_folder(".gitlab" .. state.settings.file_separator .. "merge_request_templates")
+ local all_templates = u.list_files_in_folder(".gitlab" .. u.path_separator .. "merge_request_templates")
if all_templates == nil then
M.add_title(mr)
return
end
- local opts = { "Blank Template" }
+ local items = { "Blank Template" }
for _, v in ipairs(all_templates) do
- table.insert(opts, v)
+ table.insert(items, v)
end
- vim.ui.select(opts, {
+ vim.ui.select(items, {
prompt = "Choose Template",
}, function(choice)
if choice and choice ~= "Blank Template" then
@@ -140,7 +136,7 @@ M.pick_template = function(mr)
end)
end
----4. Prompts the user for the title of the MR
+---4. Prompt the user for the title of the MR.
---@param mr Mr
M.add_title = function(mr)
if mr.title ~= nil then
@@ -176,7 +172,7 @@ M.add_title = function(mr)
input:mount()
end
----Sets the ID of the base project when working from a fork
+---Set the ID of the base project when working from a fork.
---@param mr Mr
M.open_fork_popup = function(mr)
local input = Input({
@@ -204,8 +200,8 @@ M.open_fork_popup = function(mr)
end
---5. Show the final popup.
----The function will render a popup containing the MR title and MR description,
----target branch, and the "delete_branch" and "squash" options. All fields are editable.
+---Render a popup containing the MR title and description, target branch, and the
+---"delete_branch" and "squash" options. All fields are editable.
---@param mr Mr
M.open_confirmation_popup = function(mr)
M.started = true
@@ -255,6 +251,8 @@ M.open_confirmation_popup = function(mr)
end
local description_lines = mr.description and common.build_content(mr.description) or { "" }
+ -- TODO: replace `u.get_first_non_nil_value` with properly merging user-provided MR
+ -- opts with defaults when starting MR creation.
local delete_branch = u.get_first_non_nil_value({ mr.delete_branch, state.settings.create_mr.delete_branch })
local squash = u.get_first_non_nil_value({ mr.squash, state.settings.create_mr.squash })
@@ -293,7 +291,7 @@ M.open_confirmation_popup = function(mr)
end)
end
----Prompts for interactive selection of a new target among remote-tracking branches
+---Prompt for interactive selection of a new target among remote-tracking branches.
M.select_new_target = function()
local bufnr = vim.api.nvim_get_current_buf()
u.select_target_branch(function(target)
@@ -305,7 +303,7 @@ M.select_new_target = function()
end)
end
----This function will POST the new MR to create it
+---Get data from the confirmation popup and POST the new MR to Gitlab.
M.create_mr = function()
local description = u.get_buffer_text(M.description_bufnr)
local title = u.get_buffer_text(M.title_bufnr):gsub("\n", " ")
@@ -331,11 +329,20 @@ M.create_mr = function()
end)
end
+---Create the layout and popups for the main MR confirmation.
+---@return NuiLayout layout The main MR confirmation popup layout
+---@return NuiPopup title_popup
+---@return NuiPopup description_popup
+---@return NuiPopup target_branch_popup
+---@return NuiPopup source_branch_popup
+---@return NuiPopup delete_branch_popup
+---@return NuiPopup squash_popup
+---@return NuiPopup forked_project_id_popup
M.create_layout = function()
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(popup.create_popup_state("Description", settings))
+ local description_popup = Popup(popup.create_popup_state({ title = "Description", user_settings = settings }))
M.description_bufnr = description_popup.bufnr
local target_branch_popup = Popup(popup.create_box_popup_state("Target branch", false, settings))
M.target_bufnr = target_branch_popup.bufnr
diff --git a/lua/gitlab/actions/data.lua b/lua/gitlab/actions/data.lua
index 1959bf52..3f68ac69 100644
--- a/lua/gitlab/actions/data.lua
+++ b/lua/gitlab/actions/data.lua
@@ -12,6 +12,14 @@ local revisions = state.dependencies.revisions
local latest_pipeline = state.dependencies.latest_pipeline
local draft_notes = state.dependencies.draft_notes
+---@class GitlabResource
+---@field type "info"|"user"|"labels"|"mergeability"|"project_members"|"revisions"|"pipeline"|"draft_notes"
+---@field refresh boolean If true, refresh the data by hitting Gitlab's APIs again.
+
+---Load the data about the current MR, and execute callback with the date as an argument.
+---@param resources GitlabResource[]
+---@param cb fun(data)
+---@return nil
M.data = function(resources, cb)
if type(resources) ~= "table" or type(cb) ~= "function" then
u.notify("The data function must be passed a resources table and a callback function", vim.log.levels.ERROR)
@@ -39,6 +47,7 @@ M.data = function(resources, cb)
-- in parallel where possible to speed up this API
return async.sequence(api_calls, function()
local data = {}
+ -- FIXME: Shouldn't this really be `for k, v in pairs(api_calls) do`?
for k, v in pairs(all_resources) do
data[k] = state[v.state]
end
diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua
index cfed643f..37fe68fc 100644
--- a/lua/gitlab/actions/discussions/init.lua
+++ b/lua/gitlab/actions/discussions/init.lua
@@ -2,6 +2,7 @@
-- That includes things like editing existing notes in the tree,
-- replying to notes in the tree, and marking discussions as resolved/unresolved.
-- Draft notes are managed separately, under lua/gitlab/actions/draft_notes/init.lua
+
local Split = require("nui.split")
local Popup = require("nui.popup")
local NuiTree = require("nui.tree")
@@ -28,15 +29,16 @@ local M = {
linked_bufnr = nil,
---@type number
unlinked_bufnr = nil,
- ---@type NuiTree|nil
+ ---@type NuiTree?
discussion_tree = nil,
- ---@type NuiTree|nil
+ ---@type NuiTree?
unlinked_discussion_tree = nil,
}
----Re-fetches all discussions and re-renders the relevant view
+---Re-fetch all discussions and re-render the relevant view.
+---TODO: simplify the function signature - "unlinked" and "all" should not be two booleans
---@param unlinked boolean
----@param all boolean|nil
+---@param all? boolean
M.rebuild_view = function(unlinked, all)
M.load_discussions(function()
if all then
@@ -52,8 +54,8 @@ M.rebuild_view = function(unlinked, all)
end)
end
----Makes API call to get the discussion data, stores it in the state, and calls the callback
----@param callback function|nil
+---Make API call to get the discussion data, stores it in the state, and calls the callback.
+---@param callback? fun()
M.load_discussions = function(callback)
local git = require("gitlab.git")
require("gitlab.git_async").get_ahead_behind(
@@ -100,7 +102,7 @@ M.initialize_discussions = function()
end)
end
---- Take existing data and refresh the diagnostics and the signs
+---Take existing data and refresh the diagnostics and the signs.
M.refresh_diagnostics = function()
if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics()
@@ -108,10 +110,9 @@ M.refresh_diagnostics = function()
common.add_empty_titles()
end
----Opens the discussion tree, sets the keybindings. It also
----creates the tree for notes (which are not linked to specific lines of code)
+---Open the discussion and unlinked note trees and set the keybindings.
---@param callback? function
----@param view_type "discussions"|"notes" Defines the view type to select (useful for overriding the default view type when jumping to discussion tree when it's closed).
+---@param view_type "discussions"|"notes" Defines the view type to select (useful for overriding the default view type when jumping to discussion tree when it's closed)
M.open = function(callback, view_type)
local original_window = vim.api.nvim_get_current_win() -- The window from which ther user called M.open
@@ -179,7 +180,7 @@ M.open = function(callback, view_type)
end
end
--- Clears the discussion state and unmounts the split
+---Clear the discussion state and unmounts the split.
M.close = function()
if M.split then
M.split:unmount()
@@ -201,7 +202,7 @@ M.move_to_discussion_tree = function()
-- All diagnostics in `diagnotics_namespace` have diagnostic_id
local discussion_id = diagnostic.user_data.discussion_id
local discussion_node, line_number = M.discussion_tree:get_node("-" .. discussion_id)
- if discussion_node == {} or discussion_node == nil then
+ if discussion_node == nil or next(discussion_node) == nil then
u.notify("Discussion not found", vim.log.levels.WARN)
return
end
@@ -249,7 +250,8 @@ M.move_to_discussion_tree = function()
end
end
--- The reply popup will mount in a window when you trigger it (settings.keymaps.discussion_tree.reply) when hovering over a node in the discussion tree.
+---Open a reply popup for a note in the discussion tree.
+---@param tree NuiTree
M.reply = function(tree)
if M.is_draft_note(tree) then
u.notify("Gitlab does not support replying to draft notes", vim.log.levels.WARN)
@@ -277,7 +279,9 @@ M.reply = function(tree)
layout:mount()
end
--- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment
+---Open a popup prompting the user to delete the current comment.
+---@param tree NuiTree
+---@param unlinked boolean
M.delete_comment = function(tree, unlinked)
vim.ui.select({ "Confirm", "Cancel" }, {
prompt = "Delete comment?",
@@ -303,7 +307,9 @@ M.delete_comment = function(tree, unlinked)
end)
end
--- This function (settings.keymaps.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
+---Open the edit popup for the current comment in the discussion tree.
+---@param tree NuiTree
+---@param unlinked boolean
M.edit_comment = function(tree, unlinked)
local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node)
@@ -314,7 +320,7 @@ M.edit_comment = function(tree, unlinked)
end
local title = "Edit Comment"
title = root_node.file_name ~= nil and string.format("%s [%s]", title, root_node.file_name) or title
- local edit_popup = Popup(popup.create_popup_state(title, state.settings.popup.edit))
+ local edit_popup = Popup(popup.create_popup_state({ title = title, user_settings = state.settings.popup.edit }))
popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
@@ -323,7 +329,7 @@ M.edit_comment = function(tree, unlinked)
-- Gather all lines from immediate children that aren't note nodes
local lines = List.new(note_node:get_child_ids()):reduce(function(agg, child_id)
local child_node = tree:get_node(child_id)
- if not child_node:has_children() then
+ if child_node and not child_node:has_children() then
local line = tree:get_node(child_id).text
table.insert(agg, line)
end
@@ -345,14 +351,15 @@ M.edit_comment = function(tree, unlinked)
local comment = require("gitlab.actions.comment")
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),
+ comment.confirm_edit_comment(tostring(root_node.id), note_node.root_note_id or note_node.id, unlinked),
nil,
popup.editable_popup_opts
)
end
end
--- This function (settings.keymaps.discussion_tree.toggle_discussion_resolved) will toggle the resolved status of the current discussion and send the change to the Go server
+---Toggle the resolved status of the current discussion and send the change to the Go server.
+---@param tree NuiTree
M.toggle_discussion_resolved = function(tree)
local note = tree:get_node()
if note == nil then
@@ -379,14 +386,19 @@ M.toggle_discussion_resolved = function(tree)
end)
end
----Opens a popup prompting the user to choose an emoji to attach to the current node
+---Open a popup prompting the user to choose an emoji to attach to the current node.
---@param tree any
---@param unlinked boolean
M.add_emoji_to_note = function(tree, unlinked)
local node = tree:get_node()
local note_node = common.get_note_node(tree, node)
- local root_node = common.get_root_node(tree, node)
- local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
+
+ if note_node == nil then
+ u.notify("Could not get note", vim.log.levels.ERROR)
+ return
+ end
+
+ local note_id = note_node.root_note_id or note_node.id
local emojis = require("gitlab.emoji").emoji_list
emoji.pick_emoji(emojis, function(name)
local body = { emoji = name, note_id = note_id }
@@ -397,14 +409,19 @@ M.add_emoji_to_note = function(tree, unlinked)
end)
end
----Opens a popup prompting the user to choose an emoji to remove from the current node
+---Open a popup prompting the user to choose an emoji to remove from the current node.
---@param tree any
---@param unlinked boolean
M.delete_emoji_from_note = function(tree, unlinked)
local node = tree:get_node()
local note_node = common.get_note_node(tree, node)
- local root_node = common.get_root_node(tree, node)
- local note_id = tonumber(note_node.is_root and root_node.root_note_id or note_node.id)
+
+ if note_node == nil then
+ u.notify("Could not get note", vim.log.levels.ERROR)
+ return
+ end
+
+ local note_id = note_node.root_note_id or note_node.id
local note_id_str = tostring(note_id)
local e = require("gitlab.emoji")
@@ -436,9 +453,8 @@ end
-- 🌲 Helper Functions
--
----Used to collect all nodes in a tree prior to rebuilding it, so that they
----can be re-expanded before render
----@param tree any
+---Collect all nodes in a tree prior to rebuilding it, so they can be re-expanded before render.
+---@param tree? NuiTree
---@return table
M.gather_expanded_node_ids = function(tree)
-- Gather all nodes for later expansion, after rebuild
@@ -451,8 +467,8 @@ M.gather_expanded_node_ids = function(tree)
return ids
end
----Rebuilds the discussion tree, which contains all comments and draft comments
----linked to specific places in the code.
+---Rebuild the discussion tree, which contains all comments and draft comments linked to
+---specific places in the code.
M.rebuild_discussion_tree = function()
if M.linked_bufnr == nil then
return
@@ -491,7 +507,7 @@ M.rebuild_discussion_tree = function()
state.discussion_tree.unresolved_expanded = false
end
----Rebuilds the unlinked discussion tree, which contains all notes and draft notes.
+---Rebuild the unlinked discussion tree, which contains all notes and draft notes.
M.rebuild_unlinked_discussion_tree = function()
if M.unlinked_bufnr == nil then
return
@@ -530,25 +546,7 @@ M.rebuild_unlinked_discussion_tree = function()
state.unlinked_discussion_tree.unresolved_expanded = false
end
----Adds a discussion to the global state. Works for both notes (unlinked) and diff-linked comments,
-M.add_discussion = function(arg)
- local discussion = arg.data.discussion
- if arg.unlinked then
- if type(state.DISCUSSION_DATA.unlinked_discussions) ~= "table" then
- state.DISCUSSION_DATA.unlinked_discussions = {}
- end
- table.insert(state.DISCUSSION_DATA.unlinked_discussions, 1, discussion)
- M.rebuild_unlinked_discussion_tree()
- else
- if type(state.DISCUSSION_DATA.discussions) ~= "table" then
- state.DISCUSSION_DATA.discussions = {}
- end
- table.insert(state.DISCUSSION_DATA.discussions, 1, discussion)
- M.rebuild_discussion_tree()
- end
-end
-
----Creates the split for the discussion tree and returns it, with both buffer numbers
+---Create the split for the discussion tree and returns it, with both buffer numbers.
---@return NuiSplit
---@return integer
---@return integer
@@ -584,13 +582,17 @@ M.create_split_and_bufs = function()
return split, linked_bufnr, unlinked_bufnr
end
----Check if type of current node is note or note body
+---Check if type of current node is note or note body.
---@param tree NuiTree
---@return boolean
M.is_current_node_note = function(tree)
return common.is_node_note(tree:get_node())
end
+---Set the discussion tree keymaps.
+---@param tree NuiTree The current discussion tree
+---@param bufnr integer The number of the buffer that holds the discussion tree
+---@param unlinked boolean If true, the comment is not linked to a line
M.set_tree_keymaps = function(tree, bufnr, unlinked)
-- Require keymaps only after user settings have been merged with defaults
local keymaps = require("gitlab.state").settings.keymaps
@@ -824,8 +826,8 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
emoji.init_popup(tree, bufnr)
end
----Toggles the current view type (or sets it to `override`) and then updates the view.
----@param override? "discussions"|"notes" The view type to select.
+---Toggle the current view type (or sets it to `override`) and update the view.
+---@param override? "discussions"|"notes" The view type to select
M.switch_view_type = function(override)
vim.api.nvim_set_option_value("winfixbuf", false, { win = M.split.winid })
if override == "discussions" or M.current_view_type == "notes" then
@@ -839,7 +841,7 @@ M.switch_view_type = function(override)
winbar.update_winbar()
end
----Toggle comments tree type between "simple" and "by_file_name"
+---Toggle comments tree type between "simple" and "by_file_name".
M.toggle_tree_type = function()
if state.settings.discussion_tree.tree_type == "simple" then
state.settings.discussion_tree.tree_type = "by_file_name"
@@ -849,13 +851,12 @@ M.toggle_tree_type = function()
M.rebuild_discussion_tree()
end
----Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
+---Toggle between creating comments as drafts and publishing 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).
+---Toggle between sorting by "original comment" (oldest first) or "latest reply" (newest first).
M.toggle_sort_method = function()
if state.settings.discussion_tree.sort_by == "original_comment" then
state.settings.discussion_tree.sort_by = "latest_reply"
@@ -866,14 +867,14 @@ M.toggle_sort_method = function()
M.rebuild_view(false, true)
end
----Toggle between displaying relative time (e.g., "5 days ago") and absolute time (e.g., "04/10/2025 at 22:49")
+---Toggle between displaying relative time ("5 days ago") and absolute ("04/10/2025 at 22:49").
M.toggle_date_format = function()
state.settings.discussion_tree.relative_date = not state.settings.discussion_tree.relative_date
M.rebuild_unlinked_discussion_tree()
M.rebuild_discussion_tree()
end
----Indicates whether the node under the cursor is a draft note or not
+---Indicate whether the node under the cursor is a draft note or not.
---@param tree NuiTree
---@return boolean
M.is_draft_note = function(tree)
diff --git a/lua/gitlab/actions/discussions/tree.lua b/lua/gitlab/actions/discussions/tree.lua
index 91c76c42..4672b3ac 100644
--- a/lua/gitlab/actions/discussions/tree.lua
+++ b/lua/gitlab/actions/discussions/tree.lua
@@ -1,5 +1,6 @@
--- This module contains tree code specific to the discussion tree, that
--- is not used in the draft notes tree
+-- This module contains tree code specific to the discussion tree, that is not used in the draft
+-- notes tree.
+
local u = require("gitlab.utils")
local common = require("gitlab.actions.common")
local List = require("gitlab.utils.list")
@@ -9,7 +10,7 @@ local NuiLine = require("nui.line")
local M = {}
----Create nodes for NuiTree from discussions
+---Create nodes for NuiTree from discussions.
---@param items Discussion[]
---@param unlinked? boolean False or nil means that discussions are linked to code lines
---@return NuiTree.Node[]
@@ -24,8 +25,8 @@ M.add_discussions_to_table = function(items, unlinked)
-- These properties are filled in by the first note
---@type string?
local root_text = ""
- ---@type string?
- local root_note_id = ""
+ ---@type integer?
+ local root_note_id
---@type string?
local root_file_name = ""
---@type string?
@@ -34,7 +35,7 @@ M.add_discussions_to_table = function(items, unlinked)
local root_id
local root_text_nodes = {}
local resolvable = false
- ---@type GitlabLineRange|nil
+ ---@type GitlabLineRange?
local range = nil
local resolved = false
local root_new_line = nil
@@ -49,7 +50,7 @@ M.add_discussions_to_table = function(items, unlinked)
root_new_line = (type(note.position) == "table" and note.position.new_line or nil)
root_old_line = (type(note.position) == "table" and note.position.old_line or nil)
root_id = discussion.id
- root_note_id = tostring(note.id)
+ root_note_id = note.id
resolvable = note.resolvable
resolved = note.resolved
root_url = state.INFO.web_url .. "#note_" .. note.id
@@ -99,7 +100,7 @@ M.add_discussions_to_table = function(items, unlinked)
return M.create_node_list_by_file_name(t)
end
----Create path node
+---Create path node.
---@param relative_path string
---@param full_path string
---@param child_nodes? NuiTree.Node[]
@@ -116,7 +117,7 @@ local function create_path_node(relative_path, full_path, child_nodes)
}, child_nodes or {})
end
----Sort list of nodes (in place) of type "path" or "file_name"
+---Sort list of nodes (in place) of type "path" or "file_name".
---@param nodes NuiTree.Node[]
local function sort_nodes(nodes)
table.sort(nodes, function(node1, node2)
@@ -132,7 +133,7 @@ local function sort_nodes(nodes)
end)
end
----Merge path nodes which have only single path child
+---Merge path nodes which have only single path child.
---@param node NuiTree.Node
local function flatten_nodes(node)
if node.type ~= "path" then
@@ -151,7 +152,7 @@ local function flatten_nodes(node)
sort_nodes(node.__children)
end
----Create file name node
+---Create file name node.
---@param file_name string
---@param full_file_path string
---@param child_nodes? NuiTree.Node[]
@@ -169,6 +170,9 @@ local function create_file_name_node(file_name, full_file_path, child_nodes)
}, child_nodes or {})
end
+---Return comments from `node_list` grouped by the file they belong to.
+---@param node_list NuiTree.Node[]
+---@return NuiTree.Node[]
local create_disscussions_by_file_name = function(node_list)
-- Create all the folder and file name nodes.
local discussion_by_file_name = {}
@@ -242,6 +246,11 @@ local create_disscussions_by_file_name = function(node_list)
return discussion_by_file_name
end
+---Return comments from `node_list` grouped by the file they belong to, with flattened
+---nodes when there is just a single path child, and sorted alphabetically by file
+---path/name.
+---@param node_list NuiTree.Node[]
+---@return NuiTree.Node[]
M.create_node_list_by_file_name = function(node_list)
-- Create all the folder and file name nodes.
local discussion_by_file_name = create_disscussions_by_file_name(node_list)
@@ -256,9 +265,13 @@ M.create_node_list_by_file_name = function(node_list)
return discussion_by_file_name
end
----Build note node body
+---@class ResolveInfo
+---@field resolved boolean Indicates whether a Note is resolved
+---@field resolvable boolean Indicates whether a Note can be resolved
+
+---Build note node body.
---@param note Note|DraftNote
----@param resolve_info? table
+---@param resolve_info? ResolveInfo Nil if the note is a child node
---@return string
---@return NuiTree.Node[]
local function build_note_body(note, resolve_info)
@@ -292,9 +305,9 @@ local function build_note_body(note, resolve_info)
return noteHeader, text_nodes
end
----Build note node
+---Build note node.
---@param note Note|DraftNote
----@param resolve_info? table
+---@param resolve_info? ResolveInfo Nil if the note is a child node
---@return NuiTree.Node
---@return string
---@return NuiTree.Node[]
@@ -315,6 +328,8 @@ M.build_note = function(note, resolve_info)
end
---Inspired by default func https://github.com/MunifTanjim/nui.nvim/blob/main/lua/nui/tree/util.lua#L38
+---@param node NuiTree.Node
+---@return NuiLine[]
M.nui_tree_prepare_node = function(node)
if not node.text then
error("missing node.text")
@@ -325,6 +340,7 @@ M.nui_tree_prepare_node = function(node)
texts = { node.text }
end
+ ---@type NuiLine[]
local lines = {}
for i, text in ipairs(texts) do
@@ -373,7 +389,7 @@ end
---@field toggle_unresolved boolean Whether to toggle unresolved discussions.
---@field keep_current_open boolean Whether to keep the current discussion open even if it should otherwise be closed.
----This function expands/collapses all nodes and their children according to the opts.
+---Expand/collapse all nodes and their children according to the opts.
---@param winid integer
---@param tree NuiTree
---@param unlinked boolean
@@ -427,9 +443,9 @@ M.toggle_nodes = function(winid, tree, unlinked, opts)
M.restore_cursor_position(winid, tree, current_cursor_column, current_node, root_node)
end
--- Get current node for restoring cursor position
+---Get current node for restoring cursor position.
---@param tree NuiTree The inline discussion tree or the unlinked discussion tree
----@param last_node NuiTree.Node|nil The last active discussion tree node in case we are not in any of the discussion trees
+---@param last_node? NuiTree.Node The last active discussion tree node in case we are not in any of the discussion trees
M.get_node_at_cursor = function(tree, last_node)
if tree == nil then
return
@@ -441,12 +457,12 @@ M.get_node_at_cursor = function(tree, last_node)
end
end
----Restore cursor position to the original node if possible
+---Restore cursor position to the original node if possible.
---@param winid integer Window number of the discussions split
---@param tree NuiTree The inline discussion tree or the unlinked discussion tree
---@param cursor_column integer The original column of the cursor
----@param original_node NuiTree.Node|nil The last node with the cursor
----@param root_node NuiTree.Node|nil The root node of the last node with the cursor
+---@param original_node? NuiTree.Node The last node with the cursor
+---@param root_node? NuiTree.Node The root node of the last node with the cursor
M.restore_cursor_position = function(winid, tree, cursor_column, original_node, root_node)
if original_node == nil or tree == nil then
return
@@ -465,10 +481,10 @@ M.restore_cursor_position = function(winid, tree, cursor_column, original_node,
end
end
----This function expands a node and its children.
+---Expand a node and its children.
---@param tree NuiTree
----@param node NuiTree.Node
----@param is_resolved boolean If true, expand resolved discussions. If false, expand unresolved discussions.
+---@param node? NuiTree.Node
+---@param is_resolved boolean If true, expand resolved discussions. If false, expand unresolved discussions
M.expand_recursively = function(tree, node, is_resolved)
if node == nil then
return
@@ -482,18 +498,18 @@ M.expand_recursively = function(tree, node, is_resolved)
end
end
----This function collapses a node and its children.
+---Collapse a node and its children.
---@param tree NuiTree
----@param node NuiTree.Node
----@param current_root_node NuiTree.Node The root node of the current node.
----@param keep_current_open boolean If true, the current node stays open, even if it should otherwise be collapsed.
----@param is_resolved boolean If true, collapse resolved discussions. If false, collapse unresolved discussions.
+---@param node? NuiTree.Node
+---@param current_root_node? NuiTree.Node The root node of the current node
+---@param keep_current_open boolean If true, the current node stays open, even if it should otherwise be collapsed
+---@param is_resolved boolean If true, collapse resolved discussions. If false, collapse unresolved discussions
M.collapse_recursively = function(tree, node, current_root_node, keep_current_open, is_resolved)
if node == nil then
return
end
local root_node = common.get_root_node(tree, node)
- if common.is_node_note(node) and root_node.resolved == is_resolved then
+ if common.is_node_note(node) and root_node and root_node.resolved == is_resolved then
if keep_current_open and root_node == current_root_node then
return
end
@@ -505,7 +521,7 @@ M.collapse_recursively = function(tree, node, current_root_node, keep_current_op
end
end
----Expands a given node in a given tree by it's ID
+---Expand a given node in a given tree by its ID.
---@param tree NuiTree
---@param id string
M.open_node_by_id = function(tree, id)
@@ -515,9 +531,9 @@ M.open_node_by_id = function(tree, id)
end
end
--- This function (settings.keymaps.discussion_tree.toggle_node) expands/collapses the current node and its children
----@param winid integer The id if the tree split.
----@param tree NuiTree The current discussion tree.
+---Expand or collapse the current node and its children.
+---@param winid integer The id if the tree split
+---@param tree NuiTree The current discussion tree
M.toggle_node = function(winid, tree)
local node = tree:get_node()
local current_cursor_column = vim.api.nvim_win_get_cursor(winid)[2]
diff --git a/lua/gitlab/actions/discussions/winbar.lua b/lua/gitlab/actions/discussions/winbar.lua
index cae784ad..f59dbee6 100644
--- a/lua/gitlab/actions/discussions/winbar.lua
+++ b/lua/gitlab/actions/discussions/winbar.lua
@@ -4,8 +4,11 @@ local state = require("gitlab.state")
local M = {}
----@param nodes Discussion[]|UnlinkedDiscussion[]|nil
----@return number, number, number
+---Return the number of resolvable, resolved, and non-resolvable comments.
+---@param nodes? Discussion[]|UnlinkedDiscussion[]
+---@return integer total_resolvable
+---@return integer total_resolved
+---@return integer total_non_resolvable
local get_data = function(nodes)
local total_resolvable = 0
local total_resolved = 0
@@ -14,24 +17,24 @@ local get_data = function(nodes)
return total_resolvable, total_resolved, total_non_resolvable
end
- total_resolvable = List.new(nodes):reduce(function(agg, d)
- local first_child = d.notes[1]
+ total_resolvable = List.new(nodes):reduce(function(agg, node)
+ local first_child = node.notes[1]
if first_child and first_child.resolvable then
agg = agg + 1
end
return agg
end, 0)
- total_non_resolvable = List.new(nodes):reduce(function(agg, d)
- local first_child = d.notes[1]
+ total_non_resolvable = List.new(nodes):reduce(function(agg, node)
+ local first_child = node.notes[1]
if first_child and not first_child.resolvable then
agg = agg + 1
end
return agg
end, 0)
- total_resolved = List.new(nodes):reduce(function(agg, d)
- local first_child = d.notes[1]
+ total_resolved = List.new(nodes):reduce(function(agg, node)
+ local first_child = node.notes[1]
if first_child and first_child.resolved then
agg = agg + 1
end
@@ -44,6 +47,8 @@ end
local spinner_index = 0
state.discussion_tree.last_updated = nil
+---Return the raw content of the winbar.
+---@return string
local function content()
local updated
if state.discussion_tree.last_updated then
@@ -89,7 +94,7 @@ local function content()
return state.settings.discussion_tree.winbar and state.settings.discussion_tree.winbar(t) or M.make_winbar(t)
end
----This function updates the winbar
+---Update the winbar.
M.update_winbar = function()
local d = require("gitlab.actions.discussions")
if d.split == nil then
@@ -109,6 +114,7 @@ M.update_winbar = function()
vim.api.nvim_set_option_value("winbar", c, { scope = "local", win = win_id })
end
+---TODO: remove this function and hardcode " " where called
local function get_connector(base_title)
return string.match(base_title, "%($") and "" or " "
end
@@ -144,6 +150,7 @@ local add_drafts_and_resolvable = function(
end
---@param t WinbarTable
+---@return string winbar The raw content of the winbar
M.make_winbar = function(t)
local discussions_focused = require("gitlab.actions.discussions").current_view_type == "discussions"
local discussion_text = add_drafts_and_resolvable(
@@ -199,19 +206,29 @@ M.make_winbar = function(t)
)
end
----Returns a string for the winbar indicating the sort method
+---Return 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
+---Return the winbar component for resolvable discussions.
+---@param focused boolean Whether the particular section (Comments x Notes) is focused
+---@param resolved_count integer The number of resolved discussions
+---@param resolvable_count integer The total number of resolvable discussions
+---@return string winbar_component
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
+---Return the winbar component for draft discussions.
+---@param base_title string
+---@param drafts_count integer The number of draft discussions
+---@param focused boolean Whether the particular section (Comments x Notes) is focused
+---@return string winbar_component
M.get_drafts_text = function(base_title, drafts_count, focused)
return get_connector(base_title)
.. string.format(
@@ -224,6 +241,11 @@ M.get_drafts_text = function(base_title, drafts_count, focused)
)
end
+---Return the winbar component for non-resolvable discussions.
+---@param base_title string
+---@param non_resolvable_count integer The number of non-resolvable discussions
+---@param focused boolean Whether the particular section (Comments x Notes) is focused
+---@return string winbar_component
M.get_nonresolveable_text = function(base_title, non_resolvable_count, focused)
return get_connector(base_title)
.. string.format(
@@ -236,7 +258,7 @@ M.get_nonresolveable_text = function(base_title, non_resolvable_count, focused)
)
end
----Returns a string for the winbar indicating the mode type, live or draft
+---Return 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
@@ -246,8 +268,10 @@ M.get_mode = function()
end
end
----@param ahead number|nil
----@param behind number|nil
+---Return the winbar component for number of ahead and behind commits.
+---@param ahead? number
+---@param behind? number
+---@return string winbar_component
M.get_ahead_behind = function(ahead, behind)
local a = ahead == nil and "?" or tostring(ahead)
local b = behind == nil and "?" or tostring(behind)
@@ -256,7 +280,7 @@ M.get_ahead_behind = function(ahead, behind)
return a .. "↑ " .. b .. "↓"
end
----Set up a timer to update the winbar periodically
+---Set up a timer to update the winbar periodically.
M.start_timer = function()
M.cleanup_timer()
---@type nil|uv_timer_t
@@ -264,7 +288,7 @@ M.start_timer = function()
M.timer:start(0, 100, vim.schedule_wrap(M.update_winbar))
end
---Stop and close the timer
+---Stop and close the timer.
M.cleanup_timer = function()
if M.timer ~= nil then
M.timer:stop()
diff --git a/lua/gitlab/actions/draft_notes/init.lua b/lua/gitlab/actions/draft_notes/init.lua
index 9538678e..ec2b2aec 100755
--- a/lua/gitlab/actions/draft_notes/init.lua
+++ b/lua/gitlab/actions/draft_notes/init.lua
@@ -2,6 +2,7 @@
-- That includes things like editing existing draft notes in the tree, and
-- and deleting them. Normal notes and comments are managed separately,
-- under lua/gitlab/actions/discussions/init.lua
+
local common = require("gitlab.actions.common")
local discussion_tree = require("gitlab.actions.discussions.tree")
local git = require("gitlab.git")
@@ -15,9 +16,9 @@ local branch_not_in_sync_comment = " (even if local branch not in sync with remo
local M = {}
----Re-fetches all draft notes (and non-draft notes) and re-renders the relevant views
+---Re-fetch all draft notes (and non-draft notes) and re-render the relevant views.
---@param unlinked boolean
----@param all boolean|nil
+---@param all? boolean
M.rebuild_view = function(unlinked, all)
M.load_draft_notes(function()
local discussions = require("gitlab.actions.discussions")
@@ -25,8 +26,8 @@ M.rebuild_view = function(unlinked, all)
end)
end
----Makes API call to get the discussion data, stores it in the state, and calls the callback
----@param callback function|nil
+---Make API call to get the discussion data, store it in the state, and call the callback.
+---@param callback? fun()
M.load_draft_notes = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("draft_notes", function()
@@ -36,7 +37,7 @@ M.load_draft_notes = function(callback)
end)
end
----Will actually send the edits to Gitlab and refresh the draft_notes tree
+---Send the edits to Gitlab and refresh the draft_notes tree.
---@param note_id integer
---@param unlinked boolean
---@return function
@@ -54,7 +55,7 @@ M.confirm_edit_draft_note = function(note_id, unlinked)
end
end
----This function will actually send the deletion to Gitlab when you make a selection, and re-render the tree
+---Send the deletion to Gitlab when the user makes a selection, and re-render the tree.
---@param note_id integer
---@param unlinked boolean
M.confirm_delete_draft_note = function(note_id, unlinked)
@@ -64,7 +65,8 @@ M.confirm_delete_draft_note = function(note_id, unlinked)
end)
end
--- This function will trigger a popup prompting you to publish the current draft comment
+---Trigger a popup prompting the user to publish the current draft comment.
+---@param tree NuiTree
M.publish_draft = function(tree)
local branch_in_sync = git.check_current_branch_up_to_date_on_remote(vim.log.levels.ERROR)
local sync_comment = branch_in_sync and "" or branch_not_in_sync_comment
@@ -77,7 +79,7 @@ M.publish_draft = function(tree)
end)
end
--- This function will trigger a popup prompting you to publish all draft notes
+---Trigger a popup prompting the user to publish all draft notes.
M.publish_all_drafts = function()
local branch_in_sync = git.check_current_branch_up_to_date_on_remote(vim.log.levels.ERROR)
local sync_comment = branch_in_sync and "" or branch_not_in_sync_comment
@@ -90,7 +92,7 @@ M.publish_all_drafts = function()
end)
end
----Publishes all draft notes and comments. Re-renders all discussion views.
+---Publish all draft notes and comments and re-render all discussion views.
M.confirm_publish_all_drafts = function()
local body = { publish_all = true }
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
@@ -106,9 +108,8 @@ M.confirm_publish_all_drafts = function()
end)
end
----Publishes the current draft note that is being hovered over in the tree,
----and then makes an API call to refresh the relevant data for that tree
----and re-render it.
+---Publish the current draft note that is being hovered over in the tree, and make an
+---API call to refresh the relevant data for that tree and re-render it.
---@param tree NuiTree
M.confirm_publish_draft = function(tree)
local current_node = tree:get_node()
@@ -133,15 +134,19 @@ M.confirm_publish_draft = function(tree)
end)
end
---- Helper functions
----Tells whether a draft note was left on a particular diff or is an unlinked note
+--
+-- Helper functions
+--
+
+---Return true if a draft note was left on a particular diff, false if it is an unlinked note.
---@param note DraftNote
+---@return boolean
M.has_position = function(note)
return note.position.new_path ~= nil or note.position.old_path ~= nil
end
----Builds a note for the discussion tree for draft notes that are roots
----of their own discussions, e.g. not replies
+---Build a note for the discussion tree for draft notes that are roots of their own
+---discussions, e.g. not replies.
---@param note DraftNote
---@return NuiTree.Node
M.build_root_draft_note = function(note)
@@ -164,8 +169,8 @@ M.build_root_draft_note = function(note)
}, root_text_nodes)
end
----Returns a list of nodes to add to the discussion tree. Can filter and return only unlinked (note) nodes.
----@param unlinked boolean
+---Return a list of nodes to add to the discussion tree.
+---@param unlinked boolean If true, return only unlinked (note) nodes
---@return NuiTree.Node[]
M.add_draft_notes_to_table = function(unlinked)
local draft_notes = List.new(state.DRAFT_NOTES)
diff --git a/lua/gitlab/actions/help.lua b/lua/gitlab/actions/help.lua
index 3fed1556..84ca33de 100644
--- a/lua/gitlab/actions/help.lua
+++ b/lua/gitlab/actions/help.lua
@@ -1,4 +1,3 @@
-local M = {}
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local event = require("nui.utils.autocmd").event
@@ -6,10 +5,13 @@ local state = require("gitlab.state")
local List = require("gitlab.utils.list")
local Popup = require("nui.popup")
+local M = {}
+
---@class HelpPopupOpts
----@field discussion_tree boolean|nil Whether help popup is for the discussion tree
+---@field discussion_tree? boolean Whether help popup is for the discussion tree
---- @param opts HelpPopupOpts|nil Table with options for the help popup
+---Open the help popup.
+---@param opts? HelpPopupOpts Table with options for the help popup
M.open = function(opts)
local help_opts = opts or {}
local bufnr = vim.api.nvim_get_current_buf()
@@ -35,9 +37,16 @@ M.open = function(opts)
)
end
- local longest_line = u.get_longest_string(help_content_lines)
- local popup_opts = { "Help", state.settings.popup.help, longest_line + 3, #help_content_lines, 70 }
- local help_popup = Popup(popup.create_popup_state(unpack(popup_opts)))
+ local max_line_length = u.get_max_length(help_content_lines)
+ ---@type PopupOpts
+ local popup_opts = {
+ title = "Help",
+ user_settings = state.settings.popup.help,
+ width = max_line_length + 3,
+ height = #help_content_lines,
+ zindex = 70,
+ }
+ local help_popup = Popup(popup.create_popup_state(popup_opts))
help_popup:on(event.BufLeave, function()
help_popup:unmount()
diff --git a/lua/gitlab/actions/labels.lua b/lua/gitlab/actions/labels.lua
index e182df73..a5e3a913 100644
--- a/lua/gitlab/actions/labels.lua
+++ b/lua/gitlab/actions/labels.lua
@@ -4,6 +4,7 @@ local u = require("gitlab.utils")
local job = require("gitlab.job")
local state = require("gitlab.state")
local List = require("gitlab.utils.list")
+
local M = {}
M.add_label = function()
@@ -14,16 +15,17 @@ M.delete_label = function()
M.delete_popup("label")
end
+---Update the state with `labels` and refresh the Summary window.
+---@param labels string[]
+---@param message string
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()
- return state.INFO.labels
-end
-
+---Return a list of all label names available in this project.
+---@return List
local get_all_labels = function()
return List.new(state.LABELS):map(function(label)
return label.Name
@@ -32,7 +34,7 @@ end
M.add_popup = function(type)
local all_labels = get_all_labels()
- local current_labels = get_current_labels()
+ local current_labels = state.INFO.labels
local unused_labels = u.difference(all_labels, current_labels)
vim.ui.select(unused_labels, {
prompt = "Choose label to add",
@@ -49,7 +51,7 @@ M.add_popup = function(type)
end
M.delete_popup = function(type)
- local current_labels = get_current_labels()
+ local current_labels = state.INFO.labels
vim.ui.select(current_labels, {
prompt = "Choose label to delete",
}, function(choice)
diff --git a/lua/gitlab/actions/merge.lua b/lua/gitlab/actions/merge.lua
index c8f5e097..55e8a4cb 100644
--- a/lua/gitlab/actions/merge.lua
+++ b/lua/gitlab/actions/merge.lua
@@ -8,7 +8,10 @@ local reviewer = require("gitlab.reviewer")
local M = {}
local function create_squash_message_popup()
- return Popup(popup.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
+ return Popup(popup.create_popup_state({
+ title = "Squash Commit Message",
+ user_settings = state.settings.popup.squash_message,
+ }))
end
---@class MergeOpts
@@ -17,6 +20,9 @@ end
---@field squash boolean?
---@field squash_message string?
+---Merge a mergeable MR into the target branch.
+---Prompt the user for squash message if commits should be squashed but no message is
+---provided.
---@param opts MergeOpts
M.merge = function(opts)
local merge_body = {
@@ -45,6 +51,7 @@ M.merge = function(opts)
end
end
+---Send request to Go server to merge MR.
---@param merge_body MergeOpts
---@param squash_message? string
M.confirm_merge = function(merge_body, squash_message)
diff --git a/lua/gitlab/actions/merge_requests.lua b/lua/gitlab/actions/merge_requests.lua
index d9af5e29..154b5b5b 100644
--- a/lua/gitlab/actions/merge_requests.lua
+++ b/lua/gitlab/actions/merge_requests.lua
@@ -2,6 +2,7 @@ local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local git = require("gitlab.git")
local u = require("gitlab.utils")
+
local M = {}
---@class ChooseMergeRequestOptions
@@ -9,8 +10,8 @@ local M = {}
---@field label? string[]
---@field notlabel? string[]
----Opens up a select menu that lets you choose a different merge request.
----@param opts ChooseMergeRequestOptions|nil
+---Open up a select menu to let user choose a merge request.
+---@param opts? ChooseMergeRequestOptions
M.choose_merge_request = function(opts)
if opts == nil then
opts = state.settings.choose_merge_request
diff --git a/lua/gitlab/actions/miscellaneous.lua b/lua/gitlab/actions/miscellaneous.lua
index 1b0c5550..92bd3d9d 100644
--- a/lua/gitlab/actions/miscellaneous.lua
+++ b/lua/gitlab/actions/miscellaneous.lua
@@ -1,8 +1,10 @@
local state = require("gitlab.state")
local u = require("gitlab.utils")
local job = require("gitlab.job")
+
local M = {}
+---Prompt user to select file, post attachment to Gitlab and paste markdown link in current buffer.
M.attach_file = function()
local attachment_dir = state.settings.attachment_dir
if not attachment_dir or attachment_dir == "" then
diff --git a/lua/gitlab/actions/pipeline.lua b/lua/gitlab/actions/pipeline.lua
index e9817828..7c7e79b5 100644
--- a/lua/gitlab/actions/pipeline.lua
+++ b/lua/gitlab/actions/pipeline.lua
@@ -1,30 +1,32 @@
-- This module is responsible for the MR pipeline
-- This lets the user see the current status of the pipeline
-- and retrigger the pipeline from within the editor
+
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 = {},
latest_pipeline = nil,
pipeline_popup = nil,
}
-local function get_latest_pipelines(count)
- count = count or 1 -- Default to 1 if count is not provided
- local pipelines = {}
-
- if not state.PIPELINE then
+---Return a list of pipeline metadata, if available.
+---@return table[]?
+local function get_latest_pipelines()
+ if state.PIPELINE == nil then
u.notify("Pipeline state is not initialized", vim.log.levels.WARN)
return nil
end
- for i = 1, math.max(count, #state.PIPELINE) do
- local pipeline = state.PIPELINE[i].latest_pipeline
- if type(pipeline) == "table" and u.table_size(pipeline) > 0 then
- table.insert(pipelines, pipeline)
+ local pipelines = {}
+ for _, pipeline in ipairs(state.PIPELINE) do
+ local latest_pipeline = pipeline.latest_pipeline
+ if type(latest_pipeline) == "table" and u.table_size(latest_pipeline) > 0 then
+ table.insert(pipelines, latest_pipeline)
end
end
@@ -35,17 +37,17 @@ local function get_latest_pipelines(count)
return pipelines
end
+---Get the jobs of the idx'th pipeline.
+---@param idx integer
+---@return table
local function get_pipeline_jobs(idx)
return u.reverse(type(state.PIPELINE[idx].jobs) == "table" and state.PIPELINE[idx].jobs or {})
end
--- The function will render the Pipeline state in a popup
+---Render the Pipeline state in a popup.
M.open = function()
- M.latest_pipelines = get_latest_pipelines()
- if not M.latest_pipelines then
- return
- end
- if not M.latest_pipelines or #M.latest_pipelines == 0 then
+ local latest_pipelines = get_latest_pipelines()
+ if not latest_pipelines or #latest_pipelines == 0 then
return
end
@@ -53,7 +55,7 @@ M.open = function()
local total_height = 0
local pipelines_data = {}
- for idx, pipeline in ipairs(M.latest_pipelines) do
+ for idx, pipeline in ipairs(latest_pipelines) do
local width = string.len(pipeline.web_url) + 10
max_width = math.max(max_width, width)
@@ -76,8 +78,13 @@ M.open = function()
})
end
- local pipeline_popup =
- Popup(popup.create_popup_state("Loading Pipelines...", state.settings.popup.pipeline, max_width, total_height, 60))
+ local pipeline_popup = Popup(popup.create_popup_state({
+ title = "Loading Pipelines...",
+ user_settings = state.settings.popup.pipeline,
+ width = max_width,
+ height = total_height,
+ zindex = 60,
+ }))
popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win())
M.pipeline_popup = pipeline_popup
pipeline_popup:mount()
@@ -100,12 +107,12 @@ M.open = function()
table.insert(lines, "")
table.insert(lines, "Jobs:")
- local longest_title = u.get_longest_string(u.map(data.jobs, function(v)
+ local max_title_length = u.get_max_length(u.map(data.jobs, function(v)
return v.name
end))
local function row_offset(name)
- local offset = longest_title - string.len(name)
+ local offset = max_title_length - string.len(name)
local res = string.rep(" ", offset + 5)
return res
end
@@ -157,6 +164,7 @@ M.open = function()
end)
end
+---Re-trigger failed pipelines.
M.retrigger = function()
local pipelines = get_latest_pipelines()
if not pipelines then
@@ -184,10 +192,11 @@ M.retrigger = function()
end
end
+---Close pipeline popup and open the logs from the job under cursor in a new tab.
M.see_logs = 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)
+ local linenr = vim.api.nvim_win_get_cursor(0)[1]
+ local text = u.get_line_content(bufnr, linenr)
local job_name = string.match(text, "(.-)%s%s%s%s%s")
@@ -223,6 +232,7 @@ M.see_logs = function()
return
end
+ -- TODO: make this configurable - allow users to not close the popup when seeing the logs.
M.pipeline_popup:unmount()
vim.cmd.tabnew()
@@ -239,9 +249,14 @@ M.see_logs = function()
end)
end
----Returns the user-defined symbol representing the status
----of the current pipeline. Takes an optional argument to
----colorize the pipeline icon.
+---Return the user-defined symbol representing the status of the current pipeline.
+---Takes an optional argument to colorize the pipeline icon.
+---TODO: This function is only called with `wrap_with_color = false` so
+---`M.latest_pipeline` is never accessed - luckily so, because it is never set either,
+---so accessing `status` on it would throw an error. Pipeline status coloring is now
+---handled differently, so this function can be simplified and probably ultimately
+---inlined at call site.
+---@param idx integer The index of the pipeline within the table of the latest pipelines
---@param wrap_with_color boolean
---@return string
M.get_pipeline_icon = function(idx, wrap_with_color)
@@ -258,9 +273,9 @@ M.get_pipeline_icon = function(idx, wrap_with_color)
return "%#DiagnosticWarn#" .. symbol
end
----Returns the status of the latest pipeline and the symbol
---representing the status of the current pipeline. Takes an optional argument to
----colorize the pipeline icon.
+---Return the status of the latest pipeline and the symbol representing its status.
+---Takes an optional argument to colorize the pipeline icon.
+---@param idx integer The index of the pipeline within the table of the latest pipelines
---@param wrap_with_color boolean
---@return string
M.get_pipeline_status = function(idx, wrap_with_color)
@@ -272,8 +287,15 @@ M.get_pipeline_status = function(idx, wrap_with_color)
)
end
-M.color_status = function(status, bufnr, status_line, linnr)
+---Colorize the pipeline status line.
+---@param status "canceled"|"created"|"failed"|"pending"|"preparing"|"running"|"scheduled"|"skipped"|"success"
+---@param bufnr integer The buffer number of the pipeline popup
+---@param status_line string The content of the given line (used to calculate offset for extmark)
+---@param linenr integer The line number in the pipeline popup to colorize
+M.color_status = function(status, bufnr, status_line, linenr)
local ns_id = vim.api.nvim_create_namespace("GitlabNamespace")
+ -- TODO: The following line is probably dead code. It sets guifg to an icon like "" and
+ -- StatusHighlight is never used.
vim.cmd(string.format("highlight default StatusHighlight guifg=%s", state.settings.pipeline[status]))
local status_to_color_map = {
@@ -291,9 +313,9 @@ M.color_status = function(status, bufnr, status_line, linnr)
vim.api.nvim_buf_set_extmark(
bufnr,
ns_id,
- linnr - 1,
+ linenr - 1,
0,
- { end_row = linnr - 1, end_col = string.len(status_line), hl_group = status_to_color_map[status] }
+ { end_row = linenr - 1, end_col = string.len(status_line), hl_group = status_to_color_map[status] }
)
end
diff --git a/lua/gitlab/actions/rebase.lua b/lua/gitlab/actions/rebase.lua
index 91bb6114..014c88d1 100644
--- a/lua/gitlab/actions/rebase.lua
+++ b/lua/gitlab/actions/rebase.lua
@@ -3,11 +3,13 @@
local M = {}
----@class RebaseOpts
----@field skip_ci boolean? If true, a CI pipeline is not created.
----@field force boolean? If true, MR is rebased even if MR already is rebased.
+---@class RebaseOpts Overrides for the rebase function.
+---@field skip_ci? boolean If true, a CI pipeline is not created
+---@field force? boolean If true, MR is rebased even if MR already is rebased. This is a client-side concept (not sent to Go server)
+---Return true if the rebase is possible, otherwise false.
---@param opts RebaseOpts
+---@return boolean
local can_rebase = function(opts)
local u = require("gitlab.utils")
-- Check if there are local changes (we wouldn't be able to run `git pull` after rebasing)
@@ -51,9 +53,9 @@ local can_rebase = function(opts)
return true
end
----Callback to run after the async `git pull` call exits
----@param result string|nil The stdout from the `git pull` call if any.
----@param err string|nil The stderr from the `git pull` call if any.
+---Callback to run after the async `git pull` call exits.
+---@param result? string The stdout from the `git pull` call if any
+---@param err? string The stderr from the `git pull` call if any
local on_pull_exit = function(result, err)
if result ~= nil then
local reviewer = require("gitlab.reviewer")
@@ -68,6 +70,7 @@ end
---@class RebaseBody
---@field skip_ci boolean? If true, a CI pipeline is not created.
+---Send request to Go server to rebase MR.
---@param rebase_body RebaseBody
local confirm_rebase = function(rebase_body)
local u = require("gitlab.utils")
@@ -85,6 +88,7 @@ local confirm_rebase = function(rebase_body)
end)
end
+---Rebase the feature branch on the server.
---@param opts RebaseOpts
M.rebase = function(opts)
opts = opts or {}
diff --git a/lua/gitlab/actions/summary.lua b/lua/gitlab/actions/summary.lua
index 88e8b122..c494f124 100644
--- a/lua/gitlab/actions/summary.lua
+++ b/lua/gitlab/actions/summary.lua
@@ -1,6 +1,7 @@
-- This module is responsible for the MR description
-- This lets the user open the description in a popup and
-- send edits to the description back to Gitlab
+
local Layout = require("nui.layout")
local Popup = require("nui.popup")
local git = require("gitlab.git")
@@ -11,7 +12,7 @@ local popup = require("gitlab.popup")
local state = require("gitlab.state")
local miscellaneous = require("gitlab.actions.miscellaneous")
--- No-break space used in summary details to make matching different parts of the line more robust
+-- No-break space used in summary details to make matching different parts of the line more robust.
local nbsp = " "
local M = {
@@ -22,9 +23,10 @@ local M = {
description_bufnr = nil,
}
--- The function will render a popup containing the MR title and MR description, and optionally,
--- any additional metadata that the user wants. The title and description are editable and
--- can be changed via the local action keybinding, which also closes the popup
+---Render a popup with the MR title and description, and, optionally, additional
+---metadata configured in `settings.info.fields`.
+---The title and description are editable, and changes can be confirmed via the local
+---action keybinding, which also closes the popup.
M.summary = function()
if M.layout_visible then
M.layout:unmount()
@@ -94,6 +96,7 @@ M.summary = function()
git.check_mr_in_good_condition()
end
+---Update the contents and layout of the summary popup.
M.update_summary_details = function()
if not M.info_popup or not M.info_popup.bufnr then
return
@@ -104,6 +107,9 @@ M.update_summary_details = function()
M.update_details_popup(M.info_popup.bufnr, details_lines)
end
+---Update the contents of the Details buffer.
+---@param bufnr integer Buffer number of the details window
+---@param info_lines string[] The new Detail contents to set in the buffer
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)
@@ -111,7 +117,7 @@ M.update_details_popup = function(bufnr, info_lines)
M.color_details(bufnr) -- Color values in details popup
end
----Return the mergeability checks statuses and descriptions
+---Return the mergeability checks statuses and descriptions.
---@return string[]
local make_mergeability_checks = function()
local lines = {}
@@ -133,8 +139,11 @@ local make_mergeability_checks = function()
return lines
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.
+---Build a list of metadata about the current MR.
+---Only builds the lines that users include in their state.settings.info.fields list.
+---TODO: Consider splitting this into simpler functions and calculating `options` lazily
+---only when necessary.
+---@return string[]
M.build_info_lines = function()
local info = state.INFO
local options = {
@@ -184,8 +193,12 @@ M.build_info_lines = function()
end
end
- local function row_offset(row)
- local offset = vim.fn.strcharlen(longest_used) - vim.fn.strcharlen(row)
+ ---Return the padding (no-break spaces) between the row title and content to make the contents on
+ ---individual rows aligned.
+ ---@param title string The title of the current row or multi-line section
+ ---@return string
+ local function get_padding(title)
+ local offset = vim.fn.strcharlen(longest_used) - vim.fn.strcharlen(title)
return string.rep(nbsp, offset + 3)
end
@@ -195,11 +208,11 @@ M.build_info_lines = function()
v = "detailed_merge_status"
end
local row = options[v]
- local title_prefix = "* " .. row.title .. row_offset(row.title)
+ local title_prefix = "* " .. row.title .. get_padding(row.title)
local content = type(row.content) == "function" and row.content() or row.content
if type(content) == "table" then
-- Multi-line content
- local padding = string.rep(nbsp, vim.fn.strcharlen(title_prefix)) -- no-break space
+ local padding = string.rep(nbsp, vim.fn.strcharlen(title_prefix))
for i, line in ipairs(#content > 0 and content or { "" }) do
table.insert(result, (i == 1 and title_prefix or padding) .. line)
end
@@ -211,7 +224,7 @@ M.build_info_lines = function()
return result
end
--- This function will PUT the new description to the Go server
+---Send the new MR description to the Go server.
M.edit_summary = function()
local description = u.get_buffer_text(M.description_bufnr)
local title = u.get_buffer_text(M.title_bufnr):gsub("\n", " ")
@@ -224,12 +237,13 @@ M.edit_summary = function()
end
---Create the Summary layout and individual popups that make up the Layout.
+---@param info_lines string[] Table of strings that make up the details content
---@return NuiLayout, NuiPopup, NuiPopup, NuiPopup
M.create_layout = function(info_lines)
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(popup.create_popup_state("Description", settings))
+ local description_popup = Popup(popup.create_popup_state({ title = "Description", user_settings = settings }))
M.description_bufnr = description_popup.bufnr
local details_popup
if state.settings.info.enabled then
@@ -255,11 +269,11 @@ M.create_internal_layout = function(info_lines, title_popup, description_popup,
local internal_layout
if state.settings.info.enabled then
if state.settings.info.horizontal then
- local longest_line = u.get_longest_string(info_lines)
+ local max_line_length = u.get_max_length(info_lines)
internal_layout = Layout.Box({
Layout.Box(title_popup, { size = 3 }),
Layout.Box({
- Layout.Box(details_popup, { size = longest_line + 3 }),
+ Layout.Box(details_popup, { size = max_line_length + 3 }),
Layout.Box(description_popup, { grow = 1 }),
}, { dir = "row", size = "95%" }),
}, { dir = "col" })
@@ -279,7 +293,7 @@ M.create_internal_layout = function(info_lines, title_popup, description_popup,
return internal_layout
end
----Create the config for the outer Layout of the Summary
+---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 {})
@@ -293,9 +307,10 @@ M.get_outer_layout_config = function()
}
end
----Return the highlight definition map. Use a light background color (the foreground color of Normal
----highlight) when vim.o.background and the `color` are dark.
----@param color string
+---Return the highlight definition map.
+---Use a light background color (the foreground color of Normal highlight) when
+---vim.o.background and the `color` are dark.
+---@param color string Color definition, e.g., "#dc143c"
---@return vim.api.keyset.highlight
local function label_hl(color)
local r, g, b = color:match("%#(%x%x)(%x%x)(%x%x)")
@@ -306,6 +321,8 @@ local function label_hl(color)
return { fg = color, bg = bg }
end
+---Colorize the details buffer
+---@param bufnr integer The buffer number of the Details popup
M.color_details = function(bufnr)
local details_namespace = vim.api.nvim_create_namespace("Details")
for i, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) do
diff --git a/lua/gitlab/annotations.lua b/lua/gitlab/annotations.lua
index ffedc146..7aee76cf 100644
--- a/lua/gitlab/annotations.lua
+++ b/lua/gitlab/annotations.lua
@@ -1,7 +1,6 @@
---@meta diagnostics
---@alias BorderEnum "rounded" | "single" | "double" | "solid"
----@alias SeverityEnum "ERROR" | "WARN" | "INFO" | "HINT"
---@class Author
---@field id integer
@@ -78,17 +77,17 @@
---@class WinbarTable
---@field view_type string
----@field resolvable_discussions number
----@field resolved_discussions number
----@field non_resolvable_discussions number
----@field inline_draft_notes number
----@field unlinked_draft_notes number
----@field resolvable_notes number
----@field resolved_notes number
----@field non_resolvable_notes number
+---@field resolvable_discussions integer
+---@field resolved_discussions integer
+---@field non_resolvable_discussions integer
+---@field inline_draft_notes integer
+---@field unlinked_draft_notes integer
+---@field resolvable_notes integer
+---@field resolved_notes integer
+---@field non_resolvable_notes integer
---@field help_keymap string
----@field ahead number|nil -- Number of commits local is ahead of remote
----@field behind number|nil -- Number of commits local is behind remote
+---@field ahead? integer Number of commits local is ahead of remote
+---@field behind? integer Number of commits local is behind remote
---@field updated string
---
---@class SignTable
@@ -98,14 +97,6 @@
---@field id number
---@field lnum number
---@field buffer number?
----
----@class DiagnosticTable
----@field message string
----@field col number
----@field severity number
----@field user_data table
----@field source string
----@field code string?
---@class LineRange
---@field start_line integer
@@ -123,143 +114,158 @@
---@field new_sha_focused boolean
---@field current_win_id integer
----@class LocationData
----@field old_line integer | nil
----@field new_line integer | nil
----@field line_range ReviewerRangeInfo|nil
-
---@class DraftNote
---@field note string
---@field id integer
---@field author_id integer
---@field merge_request_id integer
---@field resolve_discussion boolean
----@field discussion_id string -- This will always be ""
----@field commit_id string -- This will always be ""
+---@field discussion_id string This will always be "" in a draft note
+---@field commit_id string This will always be "" in a draft note
---@field line_code string
---@field position NotePosition
----
----
---- Plugin Settings
----
----@class Settings
----@field server ServerSettings
----@field remote_branch "origin" | string -- The remote, "origin" by default
----@field log_path? string -- Log path for the Go server
----@field string? any -- Custom path for `.gitlab.nvim` file, please read the "Connecting to Gitlab" section
----@field debug? DebugSettings -- Which values to log
----@field attachment_dir? string, -- The local directory for files (see the "summary" section)
----@field reviewer_settings? ReviewerSettings -- Settings for the reviewer view
----@field connection_settings? ConnectionSettings -- Settings for the connection to Gitlab
----@field keymaps? Keymaps -- Keymaps for the plugin
----@field popup? PopupSettings -- Settings for the popup windows
----@field discussion_tree? DiscussionSettings -- Settings for the popup windows
----@field emojis? EmojisSettings -- Settings for emojis
----@field choose_merge_request? ChooseMergeRequestSettings -- Default settings when choosing a merge request
----@field info? InfoSettings -- Settings for the "info" or "summary" view
----@field mergeability_checks? MergeabilityChecksSettings -- Settings for the mergeability checks in the "summary" view
----@field discussion_signs? DiscussionSigns -- The settings for discussion signs/diagnostics
----@field pipeline? PipelineSettings -- The settings for the pipeline popup
----@field create_mr? CreateMrSettings -- The settings when creating an MR
----@field rebase_mr? RebaseMrSettings -- The settings when rebasing an MR
----@field colors? ColorSettings --- Colors settings for the plugin
+
+---Plugin Settings
+---@class GitlabSettings
+---@field config_path? string Absolute path to the directory that holds your `.gitlab.nvim` file (see "Connecting to Gitlab")
+---@field auth_provider? GitlabAuthProvider
+---@field server? ServerSettings
+---@field log_path? string Log path for the Go server
+---@field debug? DebugSettings Which values to log
+---@field attachment_dir? string The local directory for files (see the "summary" section)
+---@field reviewer_settings? ReviewerSettings Settings for the reviewer view
+---@field connection_settings? ConnectionSettings Settings for the connection to Gitlab
+---@field keymaps? Keymaps Keymaps for the plugin
+---@field popup? PopupSettings Settings for the popup windows
+---@field discussion_tree? DiscussionSettings Settings for the popup windows
+---@field emojis? EmojisSettings Settings for emojis
+---@field choose_merge_request? ChooseMergeRequestSettings Default settings when choosing a merge request
+---@field info? InfoSettings Settings for the "info" or "summary" view
+---@field mergeability_checks? MergeabilityChecksSettings Settings for the mergeability checks in the "summary" view
+---@field discussion_signs? DiscussionSigns The settings for discussion signs/diagnostics
+---@field pipeline? PipelineSettings The settings for the pipeline popup
+---@field create_mr? CreateMrSettings The settings when creating an MR
+---@field rebase_mr? RebaseMrSettings The settings when rebasing an MR
+---@field colors? GitlabColorSettings Colors settings for the plugin
+---The following are set by the plugin
+---@field root_path? string The root path of the plugin
+---@field auth_token? string The personal access token for Gitlab
+---@field gitlab_url? string The URL of the Gitlab server
+
+---Function that returns the `token`, `gitlab_url`, and error
+---@alias GitlabAuthProvider fun():string?,string?,string?
---@class ServerSettings
----@field port? number -- The port of the Go server, which runs in the background, if omitted or `nil` the port will be chosen automatically
----@field binary? string -- The path to the server binary. If omitted or nil, the server will be built
+---@field port? number The port of the Go server, which runs in the background, if omitted or `nil` the port will be chosen automatically
+---@field binary? string The path to the server binary. If omitted or nil, the server will be built
+---The following is set by the plugin
+---@field binary_provided? boolean
---@class DiscussionSigns: table
----@field enabled? boolean -- Show diagnostics for gitlab comments in the reviewer
----@field skip_resolved_discussion? boolean -- Show diagnostics for resolved discussions
----@field severity? SeverityEnum
----@field virtual_text? boolean -- Whether to show the comment text inline as floating virtual text
----@field use_diagnostic_signs? boolean -- Show diagnostic sign (depending on the `severity` setting) along with the comment icon
----@field priority? number -- Higher will override LSP warnings, etc
----@field icons? IconsOpts -- Customize the icons shown with comments or notes
-
----@class ColorSettings: table
----@field discussion_tree? DiscussionTreeColors -- Colors for elements in the discussion tree
-
----@class DiscussionTreeColors
---- @field username? string
---- @field mention? string
---- @field date? string
---- @field expander? string
---- @field directory? string
---- @field directory_icon? string
---- @field file_name? string
---- @field resolved? string
---- @field unresolved? string
---- @field draft? string
+---@field enabled? boolean Show diagnostics for gitlab comments in the reviewer
+---@field skip_resolved_discussion? boolean Show diagnostics for resolved discussions
+---@field severity? vim.diagnostic.Severity
+---@field virtual_text? boolean Whether to show the comment text inline as floating virtual text
+---@field use_diagnostic_signs? boolean Show diagnostic sign (depending on the `severity` setting) along with the comment icon
+---@field priority? number Higher will override LSP warnings, etc
+---@field icons? IconsOpts Customize the icons shown with comments or notes
+---@field skip_old_revision_discussion? boolean Don't show diagnostics for discussions that were created for earlier MR revisions
---@class CreateMrSettings: table
----@field target? string -- Default branch to target when creating an MR
----@field template_file? string -- Default MR template in .gitlab/merge_request_templates
----@field delete_branch? boolean -- Whether the source branch will be marked for deletion
----@field squash? boolean -- Whether the commits will be marked for squashing
+---@field target? string Default branch to target when creating an MR
+---@field template_file? string Default MR template in .gitlab/merge_request_templates
+---@field delete_branch? boolean Whether the source branch will be marked for deletion
+---@field squash? boolean Whether the commits will be marked for squashing
---@field title_input? TitleInputSettings
---@field fork? ForkSettings
---@class RebaseMrSettings: table
----@field skip_ci? boolean -- If true, a CI pipeline is not created.
----@field force? boolean -- If true, MR is rebased even if MR already is rebased.
+---@field skip_ci? boolean If true, a CI pipeline is not created.
+---@field force? boolean If true, MR is rebased even if MR already is rebased.
+
+---@class GitlabColorSettings: table The color definitions for elements in the plugin's UI, e.g., "Comment", "WarningMsg", etc
+---@field discussion_tree DiscussionTreeColors Colors for elements in the discussion tree
+
+---@class DiscussionTreeColors
+---@field username? string
+---@field mention? string
+---@field date? string
+---@field unlinked? string
+---@field expander? string
+---@field directory? string
+---@field directory_icon? string
+---@field file_name? string
+---@field resolved? string
+---@field unresolved? string
+---@field draft? string
+---@field draft_mode? string
+---@field live_mode? string
+---@field sort_method? string
---@class ForkSettings: table
----@field enabled? boolean -- If making an MR from a fork
----@field forked_project_id? number -- The Gitlab ID of the project you are merging into. If nil, will be prompted.
+---@field enabled? boolean If making an MR from a fork
+---@field forked_project_id? number The Gitlab ID of the project you are merging into. If nil, will be prompted.
---@class TitleInputSettings: table
---@field width? number
---@field border? BorderEnum
---@class PipelineSettings: table
----@field created? string -- What to show for this pipeline status, by default "",
----@field pending? string -- What to show for this pipeline status, by default "",
----@field preparing? string -- What to show for this pipeline status, by default "",
----@field scheduled? string -- What to show for this pipeline status, by default "",
----@field running? string -- What to show for this pipeline status, by default "",
----@field canceled? string -- What to show for this pipeline status, by default "↪",
----@field skipped? string -- What to show for this pipeline status, by default "↪",
----@field success? string -- What to show for this pipeline status, by default "✓",
----@field failed? string -- What to show for this pipeline status, by default "",
+---@field created? string What to show for this pipeline status, by default "",
+---@field pending? string What to show for this pipeline status, by default "",
+---@field preparing? string What to show for this pipeline status, by default "",
+---@field scheduled? string What to show for this pipeline status, by default "",
+---@field running? string What to show for this pipeline status, by default "",
+---@field canceled? string What to show for this pipeline status, by default "↪",
+---@field skipped? string What to show for this pipeline status, by default "↪",
+---@field success? string What to show for this pipeline status, by default "✓",
+---@field failed? string What to show for this pipeline status, by default "",
---@class IconsOpts: table
----@field comment? string -- The icon for comments, by default "→|",
----@field range? string -- The icon for lines in ranged comments, by default " |"
+---@field comment? string The icon for comments, by default "→|",
+---@field range? string The icon for lines in ranged comments, by default " |"
---@class ReviewerSettings: table
----@field diffview? SettingsDiffview -- Settings for diffview (the dependency)
+---@field diffview? SettingsDiffview Settings for diffview (the dependency)
+---@field jump_with_no_diagnostics? boolean Jump to last position in discussion tree if true, otherwise stay in reviewer and show warning.
---@class SettingsDiffview: table
----@field imply_local? boolean -- If true, will attempt to use --imply_local option when calling |:DiffviewOpen|
+---@field imply_local? boolean If true, will attempt to use --imply_local option when calling |:DiffviewOpen|
---@class ConnectionSettings: table
----@field insecure? boolean -- Like curl's --insecure option, ignore bad x509 certificates on connection
+---@field proxy? string Proxy URL to use when connecting to GitLab. Supports URL schemes: http, https, socks5
+---@field insecure? boolean Like curl's --insecure option, ignore bad x509 certificates on connection
+---@field remote string The remote, "origin" by default
---@class DebugSettings: table
----@field go_request? boolean -- Log the requests to Gitlab sent by the Go server
----@field go_response? boolean -- Log the responses received from Gitlab to the Go server
----@field request? boolean -- Log the requests to the Go server
----@field response? boolean -- Log the responses from the Go server
+---@field go_request? boolean Log the requests to Gitlab sent by the Go server
+---@field go_response? boolean Log the responses received from Gitlab to the Go server
+---@field request? boolean Log the requests to the Go server
+---@field response? boolean Log the responses from the Go server
---@class PopupSettings: table
----@field width? string -- The width of the popup, by default "40%"
+---@field width? string The width of the popup, by default "40%"
---@field height? string The width of the popup, by default "60%"
+---@field position? string|table The position of the popup (by default "50%"), or a table specifying horizontal and vertical position separately { row = "90%", col = "100%" }
---@field border? BorderEnum
----@field opacity? number -- From 0.0 (fully transparent) to 1.0 (fully opaque)
----@field comment? table -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 },
----@field edit? table -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
----@field note? table -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
----@field pipeline? table -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
----@field reply? table -- Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
+---@field opacity? number From 0.0 (fully transparent) to 1.0 (fully opaque)
+---@field comment? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 },
+---@field edit? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
+---@field note? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
+---@field help? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
+---@field pipeline? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
+---@field reply? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
---@field squash_message? string The default message when squashing a commit
----@field temp_registers? string[] -- List of registers for backing up popup content (see `:h gitlab.nvim.temp-registers`)
+---@field create_mr? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
+---@field summary? table Individual popup overrides, e.g. { width = "60%", height = "80%", border = "single", opacity = 0.85 }
+---@field temp_registers? string[] List of registers for backing up popup content (see `:h gitlab.nvim.temp-registers`)
---@class ChooseMergeRequestSettings
----@field open_reviewer? boolean -- Open the reviewer window automatically after switching merge requests
+---@field open_reviewer? boolean Open the reviewer window automatically after switching merge requests
---@class InfoSettings
----@field horizontal? boolean -- Display metadata to the left of the summary rather than underneath
+---@field enabled? boolean
+---@field horizontal? boolean Display metadata to the left of the summary rather than underneath
---@field fields? ("author" | "created_at" | "updated_at" | "merge_status" | "draft" | "conflicts" | "assignees" | "reviewers" | "pipeline" | "branch" | "target_branch" | "auto_merge" | "delete_branch" | "squash" | "labels" | "web_url" | "mergeability_checks")[]
---@class MergeabilityChecksSettings
@@ -294,112 +300,153 @@
---@field TITLE_REGEX string|false
---@class DiscussionSettings: table
----@field expanders? ExpanderOpts -- Customize the expander icons in the discussion tree
----@field auto_open? boolean -- Automatically open when the reviewer is opened
----@field focus_on_open? boolean -- Automatically focus the discussion tree when it is opened
+---@field expanders? ExpanderOpts Customize the expander icons in the discussion tree
+---@field spinner_chars? string[] Characters for the refresh animation
+---@field auto_open? boolean Automatically open when the reviewer is opened
+---@field focus_on_open? boolean Automatically focus the discussion tree when it is opened
---@field default_view? string - Show "discussions" or "notes" by default
----@field blacklist? table -- List of usernames to remove from tree (bots, CI, etc)
----@field keep_current_open? boolean -- If true, current discussion stays open even if it should otherwise be closed when toggling
----@field position? "top" | "right" | "bottom" | "left"
----@field size? string -- Size of split, default to "20%"
----@field relative? "editor" | "window" -- Relative position of tree split
----@field resolved? string -- Symbol to show next to resolved discussions
----@field unresolved? '-', -- Symbol to show next to unresolved discussions
----@field tree_type? string -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
----@field draft_mode? boolean -- Whether comments are posted as drafts as part of a review
----@field winbar? function -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
+---@field blacklist? table List of usernames to remove from tree (bots, CI, etc)
+---@field sort_by? "latest_reply"|"original_comment" How to sort discussion tree
+---@field keep_current_open? boolean If true, current discussion stays open even if it should otherwise be closed when toggling
+---@field position? "top"|"right"|"bottom"|"left"
+---@field size? string Size of split, default to "20%"
+---@field relative? "editor"|"window" Relative position of tree split
+---@field resolved? string Symbol to show next to resolved discussions
+---@field unresolved? string Symbol to show next to unresolved discussions
+---@field unlinked? string Symbol to show next to unlinked comments (i.e., not threads)
+---@field draft? string Symbol to show next to draft comments/notes
+---@field tree_type? "simple"|"by_file_name" Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
+---@field draft_mode? boolean Whether comments are posted as drafts as part of a review
+---@field relative_date? boolean Whether to show relative time like "5 days ago" or absolute time like "03/01/2025 at 01:43"
+---@field winopts? GitlabDiscussionsWinopts Window-local options for the discussion tree split
+---@field winbar? function Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
---@class EmojisSettings: table
----@field formatter? function -- Custom function to modify how emojis are displayed in the picker.
----@field version? string|fun(gitlab_url: string):string -- The (function that returns the) emoji version used by the Gitlab instance: https://{GITLAB_URL}/-/emojis/{VERSION}/emojis.json.
+---@field formatter? function Custom function to modify how emojis are displayed in the picker.
+---@field version? string|fun(gitlab_url: string):string The (function that returns the) emoji version used by the Gitlab instance: https://{GITLAB_URL}/-/emojis/{VERSION}/emojis.json.
---@class ExpanderOpts: table
----@field expanded? string -- Icon for expanded discussion thread
----@field collapsed? string -- Icon for collapsed discussion thread
----@field indentation? string -- Indentation Icon
+---@field expanded? string Icon for expanded discussion thread
+---@field collapsed? string Icon for collapsed discussion thread
+---@field indentation? string Indentation Icon
+
+---@class GitlabDiscussionsWinopts
+---@field number? boolean Show line numbers
+---@field relativenumber? boolean Show relative line numbers
+---@field breakindent? boolean Every wrapped line will continue visually indented
+---@field showbreak? string String to put at the start of lines that have been wrapped
---@class Keymaps
----@field help? string -- Open a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc)
----@field global? KeymapsGlobal -- Global keybindings which will apply everywhere in Neovim
----@field popup? KeymapsPopup -- Keymaps for the popups (creating a comment, reading the summary, etc)
----@field discussion_tree? KeymapsDiscussionTree -- Keymaps for the discussion tree pane
----@field reviewer? KeymapsReviewer -- Keymaps for the reviewer view
+---@field disable_all boolean Disable all mappings created by the plugin
+---@field help? string Open a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc)
+---@field global? KeymapsGlobal Global keybindings which will apply everywhere in Neovim
+---@field popup? KeymapsPopup Keymaps for the popups (creating a comment, reading the summary, etc)
+---@field discussion_tree? KeymapsDiscussionTree Keymaps for the discussion tree pane
+---@field reviewer? KeymapsReviewer Keymaps for the reviewer view
---@class KeymapTable: table>
----@field disable_all? boolean -- Disable all built-in keymaps
+---@field disable_all? boolean Disable all built-in keymaps
---@class KeymapsPopup: KeymapTable
----@field next_field? string -- Cycle to the next field. Accepts |count|.
----@field prev_field? string -- Cycle to the previous field. Accepts |count|.
----@field perform_action? string -- Once in normal mode, does action (like saving comment or applying description edit, etc)
----@field perform_linewise_action? string -- Once in normal mode, does the linewise action (see logs for this job, etc)
----@field discard_changes? string -- Quit the popup discarding changes, the popup content isnot? saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
+---@field next_field? string Cycle to the next field. Accepts |count|.
+---@field prev_field? string Cycle to the previous field. Accepts |count|.
+---@field perform_action? string Once in normal mode, does action (like saving comment or applying description edit, etc)
+---@field perform_linewise_action? string Once in normal mode, does the linewise action (see logs for this job, etc)
+---@field discard_changes? string Quit the popup discarding changes, the popup content is not saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
---
---@class KeymapsDiscussionTree: KeymapTable
----@field add_emoji? string -- Add an emoji to the note/comment
----@field delete_emoji? string -- Remove an emoji from a note/comment
----@field delete_comment? string -- Delete comment
----@field edit_comment? string -- Edit comment
----@field reply? string -- Reply to comment
----@field toggle_resolved? string -- Toggle the resolved? status of the whole discussion
----@field jump_to_file? string -- Jump to comment location in file
----@field jump_to_reviewer? string -- Jump to the comment location in the reviewer window
----@field open_in_browser? string -- Jump to the URL of the current note/discussion
----@field copy_node_url? string -- Copy the URL of the current node to clipboard
----@field switch_view? string -- Toggle between the notes and discussions views
----@field toggle_tree_type? string or "by_file_name"
----@field publish_draft? string -- Publish the currently focused note/comment
----@field toggle_draft_mode? string -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
----@field toggle_node? string -- Open or close the discussion
----@field toggle_all_discussions? string -- Open or close? separately both resolved and unresolved discussions
----@field toggle_resolved_discussions? string -- Open or close all resolved discussions
----@field toggle_unresolved_discussions? string -- Open or close all unresolved discussions
----@field refresh_data? string -- Refresh the data in the view by hitting Gitlab's APIs again
----@field print_node? string -- Print the current node (for debugging)
+---@field add_emoji? string Add an emoji to the note/comment
+---@field delete_emoji? string Remove an emoji from a note/comment
+---@field delete_comment? string Delete comment
+---@field edit_comment? string Edit comment
+---@field reply? string Reply to comment
+---@field toggle_resolved? string Toggle the resolved status of the whole discussion
+---@field jump_to_file? string Jump to comment location in file
+---@field jump_to_reviewer? string Jump to the comment location in the reviewer window
+---@field open_in_browser? string Jump to the URL of the current note/discussion
+---@field copy_node_url? string Copy the URL of the current node to clipboard
+---@field switch_view? string Toggle between the notes and discussions views
+---@field toggle_tree_type? string Toggle type of discussion tree - "simple", or "by_file_name"
+---@field publish_draft? string Publish the currently focused note/comment
+---@field toggle_date_format? string Toggle between date formats: relative (e.g., "5 days ago", "just now", "October 13, 2024" for dates more than a month ago) and absolute (e.g., "03/01/2024 at 11:43")
+---@field toggle_draft_mode? string Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
+---@field toggle_sort_method? string Toggle whether discussions are sorted by the "latest_reply", or by "original_comment", see `:h gitlab.nvim.toggle_sort_method`
+---@field toggle_node? string Open or close the discussion
+---@field toggle_all_discussions? string Open or close separately both resolved and unresolved discussions
+---@field toggle_resolved_discussions? string Open or close all resolved discussions
+---@field toggle_unresolved_discussions? string Open or close all unresolved discussions
+---@field refresh_data? string Refresh the data in the view by hitting Gitlab's APIs again
+---@field print_node? string Print the current node (for debugging)
---
---@class KeymapsReviewer: KeymapTable
----@field create_comment? string -- Create a comment for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
----@field create_suggestion? string -- Creates suggestion for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
----@field move_to_discussion_tree? string -- Jump to the comment in the discussion tree
+---@field create_comment? string Create a comment for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
+---@field create_suggestion? string Creates suggestion for the lines that the following {motion} moves over. Repeat the key(s) for creating comment for the current line
+---@field move_to_discussion_tree? string Jump to the comment in the discussion tree
---
---@class KeymapsGlobal: KeymapTable
----@field add_assignee? string -- Add an assignee to the merge request
----@field delete_assignee? string -- Delete an assignee from the merge request
----@field add_label? string -- Add a label from the merge request
----@field delete_label? string -- Remove a label from the merge request
----@field add_reviewer? string -- Add a reviewer to the merge request
----@field delete_reviewer? string -- Delete a reviewer from the merge request
----@field approve? string -- Approve MR
----@field revoke? string -- Revoke MR approval
----@field merge? string -- Merge the feature branch to the target branch and close MR
----@field create_mr? string -- Create a new MR for currently checked-out feature branch
----@field choose_merge_request? string -- Chose MR for review (if necessary check out the feature branch)
----@field start_review? string -- Start review for the currently checked-out branch
----@field reload_review? string -- Load new MR state from Gitlab and apply new diff refs to the diff view
----@field summary? string -- Show the editable summary of the MR
----@field copy_mr_url? string -- Copy the URL of the MR to the system clipboard
----@field open_in_browser? string -- Openthe URL of the MR in the default Internet browser
----@field create_note? string -- Create a note (comment not linked toa specific line)
----@field pipeline? string -- Show the pipeline status
----@field toggle_discussions? string -- Toggle the discussions window
----@field toggle_draft_mode? string -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
----@field publish_all_drafts? string -- Publish all draft comments/notes
-
----@class Settings: KeymapTable
----@field next_field? string -- Cycle to the next field. Accepts |count|.
----@field prev_field? string -- Cycle to the previous field. Accepts |count|.
----@field perform_action? string -- Once in normal mode, does action (like saving comment or applying description edit, etc)
----@field perform_linewise_action? string -- Once in normal mode, does the linewise action (see logs for this job, etc)
----@field discard_changes? string -- Quit the popup discarding changes, the popup content is not? saved to the `temp_registers` (see `:h gitlab.nvim.temp-registers`)
+---@field disable_all? boolean Disable all built-in keymaps
+---@field add_assignee? string Add an assignee to the merge request
+---@field delete_assignee? string Delete an assignee from the merge request
+---@field add_label? string Add a label from the merge request
+---@field delete_label? string Remove a label from the merge request
+---@field add_reviewer? string Add a reviewer to the merge request
+---@field delete_reviewer? string Delete a reviewer from the merge request
+---@field approve? string Approve MR
+---@field revoke? string Revoke MR approval
+---@field merge? string Merge the feature branch to the target branch and close MR
+---@field set_auto_merge? string Set MR to merge automatically when the pipeline succeeds
+---@field rebase? string Rebase the feature branch of the MR on the server and pull the new state
+---@field rebase_skip_ci? string Same as `rebase`, but skip the CI pipeline
+---@field rebase_force? string Same as `rebase`, but rebase even if MR already is rebased
+---@field create_mr? string Create a new MR for currently checked-out feature branch
+---@field choose_merge_request? string Chose MR for review (if necessary check out the feature branch)
+---@field start_review? string Start review for the currently checked-out branch
+---@field reload_review? string Load new MR state from Gitlab and apply new diff refs to the diff view
+---@field summary? string Show the editable summary of the MR
+---@field copy_mr_url? string Copy the URL of the MR to the system clipboard
+---@field open_in_browser? string Open the URL of the MR in the default Internet browser
+---@field create_note? string Create a note (comment not linked to a specific line)
+---@field pipeline? string Show the pipeline status
+---@field toggle_discussions? string Toggle the discussions window
+---@field toggle_draft_mode? string Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
+---@field publish_all_drafts? string Publish all draft comments/notes
---@class List The base class for all list objects
----@field new function -- Creates a new List from a table
----@field map function -- Mutates a given list
----@field filter function -- Filters a given list
----@field partition function -- Partitions a given list into two lists
----@field reduce function -- Applies a function to reduce the list to a single value
----@field sort function -- Sorts the list in place based on a comparator function
----@field find function -- Returns the first element that satisfies the callback
----@field slice function -- Returns a portion of the list between start and end indices
----@field includes function -- Returns true if any of the elements can satisfy the callback
----@field values function -- Returns an iterator over the list's values
+---@field new function Creates a new List from a table
+---@field map function Mutates a given list
+---@field filter function Filters a given list
+---@field partition function Partitions a given list into two lists
+---@field reduce function Applies a function to reduce the list to a single value
+---@field sort function Sorts the list in place based on a comparator function
+---@field find function Returns the first element that satisfies the callback
+---@field slice function Returns a portion of the list between start and end indices
+---@field includes function Returns true if any of the elements can satisfy the callback
+---@field values function Returns an iterator over the list's values
+
+---@class PopupOpts The options for customizing popup windows
+---@field title string The string to appear on top of the popup
+---@field user_settings? table User-defined popup settings
+---@field width? number Override default width
+---@field height? number Override default height
+---@field zindex? number Override default zindex
+
+---@class GitlabDependency Specifications for fetching data from the Go server
+---@field endpoint string The Go server endpoint, e.g., "/merge_requests"
+---@field key? string The key under which the data is stored in the Go server's response, e.g., "merge_requests"
+---@field state string The name under which the data is stored in the plugin state, e.g., "MERGE_REQUESTS"
+---@field refresh boolean If true, forces re-fetching the data, if false, only fetches the data if not in state already.
+---@field method? string The request method to send to the Go server (default: "GET")
+---@field body? fun(opts?):table Function that returns a table that will be passed as the request body to the Go server.
+
+---@class GitlabDependencies
+---@field user GitlabDependency
+---@field info GitlabDependency
+---@field mergeability GitlabDependency
+---@field latest_pipeline GitlabDependency
+---@field labels GitlabDependency
+---@field revisions GitlabDependency
+---@field draft_notes GitlabDependency
+---@field project_members GitlabDependency
+---@field merge_requests GitlabDependency
+---@field merge_requests_by_username GitlabDependency
+---@field discussion_data GitlabDependency
diff --git a/lua/gitlab/async.lua b/lua/gitlab/async.lua
index 1335efc8..a182b49d 100644
--- a/lua/gitlab/async.lua
+++ b/lua/gitlab/async.lua
@@ -1,15 +1,21 @@
-- This module is responsible for calling APIs in sequence. It provides
-- an abstraction around the APIs that lets us ensure state.
+
local server = require("gitlab.server")
local job = require("gitlab.job")
local state = require("gitlab.state")
local M = {}
+---@class Async
+---@field cb fun(args: table)?
local async = {
cb = nil,
}
+---Create a new Async instance, using `o` as the backing table if provided.
+---@param o? table
+---@return Async
function async:new(o)
o = o or {}
setmetatable(o, self)
@@ -17,13 +23,20 @@ function async:new(o)
return o
end
+---Set the callback to invoke once all dependencies have been fetched.
+---@param cb fun(args: table)
function async:init(cb)
self.cb = cb
end
-function async:fetch(dependencies, i, argTable)
+---Make request to the Go server if necessary, set state, and call next dependency.
+---If this is the last dependency, execute self.cb with the args as the parameter.
+---@param dependencies GitlabDependency[]
+---@param i integer The index of the dependency to call in the dependencies table
+---@param args table The args to pass to the self.cb function
+function async:fetch(dependencies, i, args)
if i > #dependencies then
- self.cb(argTable)
+ self.cb(args)
return
end
@@ -31,21 +44,38 @@ function async:fetch(dependencies, i, argTable)
-- If we have data already and refresh is not required, skip this API call
if state[dependency.state] ~= nil and not dependency.refresh then
- self:fetch(dependencies, i + 1, argTable)
+ self:fetch(dependencies, i + 1, args)
return
end
-- Call the API, set the data, and then call the next API
- local body = dependency.body and dependency.body(argTable) or nil
+ -- TODO: Add a on_error_callback that will call choose_merge_request for the user:
+ -- function(data)
+ -- if data.details and data.details:match("call gitlab.choose_merge_request") then
+ -- require("gitlab").choose_merge_request(OPTS)
+ -- end
+ -- end
+ -- Find a way to pass the right OPTS.open_reviewer option - don't open the reviewer if
+ -- the user just wanted to see the summary, add a reviewer, or similar.
+ local body = dependency.body and dependency.body(args) or nil
job.run_job(dependency.endpoint, dependency.method or "GET", body, function(data)
state[dependency.state] = dependency.key and data[dependency.key] or data
- self:fetch(dependencies, i + 1, argTable)
+ -- TODO: Consider if this cannot be called outside of the run_job callback to fetch
+ -- the dependencies in parallel rather than in sequence and run self.cb in this
+ -- callback instead of self:fetch when the last dependency has been fetched.
+ self:fetch(dependencies, i + 1, args)
end)
end
--- Will call APIs in sequence and set global state
+---Return a function that will start fetching in sequence the dependencies and will
+---execute `cb` with the args the function is called with.
+---Sets plugin configuration and builds and starts the server if necessary.
+---@generic T
+---@param dependencies GitlabDependency[]
+---@param cb fun(argrs: T)
+---@return fun(argrs: T)
M.sequence = function(dependencies, cb)
- return function(argTable)
+ return function(args)
local handler = async:new()
handler:init(cb)
@@ -58,13 +88,13 @@ M.sequence = function(dependencies, cb)
-- If go server is already running, then start fetching the values in sequence
if state.go_server_running then
- handler:fetch(dependencies, 1, argTable)
+ handler:fetch(dependencies, 1, args)
return
end
-- Otherwise, start the go server and start fetching the values
server.build_and_start(function()
- handler:fetch(dependencies, 1, argTable)
+ handler:fetch(dependencies, 1, args)
end)
end
end
diff --git a/lua/gitlab/colors.lua b/lua/gitlab/colors.lua
index 2d8bfc8d..bf29dec3 100644
--- a/lua/gitlab/colors.lua
+++ b/lua/gitlab/colors.lua
@@ -1,8 +1,9 @@
local state = require("gitlab.state")
-local colors = state.settings.colors
-
-- Set icons into global vim variables for syntax matching
+-- TODO: This could be simplified to assigning the discussion_tree table to
+-- vim.g.gitlab_discussion_tree and accessing it as a vim dictionary in
+-- after/syntax/gitlab.vim.
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
@@ -11,7 +12,7 @@ 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
+local discussion_colors = state.settings.colors.discussion_tree
local function get_colors_for_group(group)
local normal_fg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(group)), "fg")
@@ -20,19 +21,19 @@ local function get_colors_for_group(group)
end
vim.api.nvim_create_autocmd({ "VimEnter", "ColorScheme" }, {
callback = function()
- vim.api.nvim_set_hl(0, "GitlabUsername", get_colors_for_group(discussion.username))
- vim.api.nvim_set_hl(0, "GitlabMention", get_colors_for_group(discussion.mention))
- vim.api.nvim_set_hl(0, "GitlabDate", get_colors_for_group(discussion.date))
- vim.api.nvim_set_hl(0, "GitlabExpander", get_colors_for_group(discussion.expander))
- vim.api.nvim_set_hl(0, "GitlabDirectory", get_colors_for_group(discussion.directory))
- vim.api.nvim_set_hl(0, "GitlabDirectoryIcon", get_colors_for_group(discussion.directory_icon))
- 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))
+ vim.api.nvim_set_hl(0, "GitlabUsername", get_colors_for_group(discussion_colors.username))
+ vim.api.nvim_set_hl(0, "GitlabMention", get_colors_for_group(discussion_colors.mention))
+ vim.api.nvim_set_hl(0, "GitlabDate", get_colors_for_group(discussion_colors.date))
+ vim.api.nvim_set_hl(0, "GitlabExpander", get_colors_for_group(discussion_colors.expander))
+ vim.api.nvim_set_hl(0, "GitlabDirectory", get_colors_for_group(discussion_colors.directory))
+ vim.api.nvim_set_hl(0, "GitlabDirectoryIcon", get_colors_for_group(discussion_colors.directory_icon))
+ vim.api.nvim_set_hl(0, "GitlabFileName", get_colors_for_group(discussion_colors.file_name))
+ vim.api.nvim_set_hl(0, "GitlabResolved", get_colors_for_group(discussion_colors.resolved))
+ vim.api.nvim_set_hl(0, "GitlabUnresolved", get_colors_for_group(discussion_colors.unresolved))
+ vim.api.nvim_set_hl(0, "GitlabUnlinked", get_colors_for_group(discussion_colors.unlinked))
+ vim.api.nvim_set_hl(0, "GitlabDraft", get_colors_for_group(discussion_colors.draft))
+ vim.api.nvim_set_hl(0, "GitlabDraftMode", get_colors_for_group(discussion_colors.draft_mode))
+ vim.api.nvim_set_hl(0, "GitlabLiveMode", get_colors_for_group(discussion_colors.live_mode))
+ vim.api.nvim_set_hl(0, "GitlabSortMethod", get_colors_for_group(discussion_colors.sort_method))
end,
})
diff --git a/lua/gitlab/emoji.lua b/lua/gitlab/emoji.lua
index 871cb7e2..9d8e5daa 100644
--- a/lua/gitlab/emoji.lua
+++ b/lua/gitlab/emoji.lua
@@ -2,7 +2,9 @@ local u = require("gitlab.utils")
local common = require("gitlab.actions.common")
-- Basic emoji aliases that are missing in Gitlab's list.
+---@type Emoji
local thumbsup = { name = "+1", shortname = ":+1:", moji = "👍", category = "People & Body" }
+---@type Emoji
local thumbsdown = { name = "-1", shortname = ":-1:", moji = "👎", category = "People & Body" }
local M = {
@@ -54,7 +56,9 @@ M.popup_opts = {
border = "single",
}
-M.show_popup = function(char)
+---Show the popup with user names.
+---@param names string The names of the users who awarded this emoji as a comma-separated string
+M.show_popup = function(names)
-- Close existing popup if it's open
if M.popup_win_id and vim.api.nvim_win_is_valid(M.popup_win_id) then
vim.api.nvim_win_close(M.popup_win_id, true)
@@ -64,12 +68,13 @@ M.show_popup = function(char)
local buf = vim.api.nvim_create_buf(false, true)
-- Set the content of the popup buffer to the character
- vim.api.nvim_buf_set_lines(buf, 0, -1, false, { char })
+ vim.api.nvim_buf_set_lines(buf, 0, -1, false, { names })
-- Open the popup window and store its ID
M.popup_win_id = vim.api.nvim_open_win(buf, false, M.popup_opts)
end
+---Close the popup and clear the state.
M.close_popup = function()
if M.popup_win_id and vim.api.nvim_win_is_valid(M.popup_win_id) then
vim.api.nvim_win_close(M.popup_win_id, true)
@@ -77,6 +82,13 @@ M.close_popup = function()
end
end
+---Set up autocommand to show popup with emoji awarder name.
+---TODO: This autocommand iterates over the full emoji map on each CursorHold.
+---This could be more efficiency (and possibly in a more user-friendly way) be
+---implemented as diagnostics: Emojis would only be recalculated on tree changes and
+---we'd get navigation to emojis and showing a popup for free (]d, [d, d).
+---@param tree NuiTree
+---@param bufnr integer The number of the buffer that holds the discussion tree
M.init_popup = function(tree, bufnr)
vim.api.nvim_create_autocmd({ "CursorHold" }, {
callback = function()
@@ -87,6 +99,11 @@ M.init_popup = function(tree, bufnr)
local note_node = common.get_note_node(tree, node)
local root_node = common.get_root_node(tree, node)
+ if note_node == nil or root_node == nil then
+ u.notify("Could not get note or root node of comment", vim.log.levels.ERROR)
+ return
+ end
+
local note_id_str = tostring(note_node.is_root and root_node.root_note_id or note_node.id)
local emojis = require("gitlab.state").DISCUSSION_DATA.emojis
@@ -126,21 +143,35 @@ M.init_popup = function(tree, bufnr)
})
end
----@param name string
+---@class NoteEmoji
+---@field awardable_id integer ID of the note for which emoji was awarded, e.g., 3244783717
+---@field awardable_type string E.g., "Note"
+---@field created_at string Time on which the emoji was created, e.g., "2026-07-06T16:28:14.039Z"
+---@field id integer ID of the emoji, e.g., 50933088
+---@field name string The short emoji name "thumbsup"
+---@field updated_at string Time on which the emoji was updated, e.g., "2026-07-06T16:28:14.039Z"
+---@field user Author The user who awarded the emoji
+
+---Return the names of the users who awarded this emoji as a comma-separated string.
+---@param emoji_name string
+---@param note_emojis NoteEmoji[]
---@return string
-M.get_users_who_reacted_with_emoji = function(name, note_emojis)
+M.get_users_who_reacted_with_emoji = function(emoji_name, note_emojis)
local result = ""
for _, v in pairs(note_emojis) do
- if v.name == name then
+ if v.name == emoji_name then
result = result .. v.user.name .. ", "
end
end
return string.len(result) > 3 and result:sub(1, -3) or result
end
-M.pick_emoji = function(options, cb)
+---Prompt user to pick an emoji from a list and run callback on the selection.
+---@param emojis Emoji[] The list of emojis to pick from
+---@param cb fun(name: string) The callback that will run after selecting an emoji. It is passed the emoji's shortname without the surrounding colons as an argument, e.g., ("thumbsup")
+M.pick_emoji = function(emojis, cb)
local settings = require("gitlab.state").settings
- vim.ui.select(options, {
+ vim.ui.select(emojis, {
prompt = "Choose emoji",
format_item = function(val)
if type(settings.emojis.formatter) == "function" then
@@ -153,7 +184,7 @@ M.pick_emoji = function(options, cb)
return
end
local name = choice.shortname:sub(2, -2)
- cb(name, choice)
+ cb(name)
end)
end
diff --git a/lua/gitlab/git.lua b/lua/gitlab/git.lua
index 9604bbb1..d3a5bb42 100644
--- a/lua/gitlab/git.lua
+++ b/lua/gitlab/git.lua
@@ -2,9 +2,11 @@ local List = require("gitlab.utils.list")
local M = {}
----Runs a system command, captures the output (if it exists) and handles errors
----@param command table
----@return string|nil, string|nil
+---Run a system command, capture the output (if it exists) and handle errors.
+---TODO: Rewrite using vim.system instead of vim.fn.system.
+---@param command string[]
+---@return string? result The result of the command as a string. Nil if the command failed
+---@return string? error The error the command failed with. Nil if the command succeeded
local run_system = function(command)
local result = vim.fn.trim(vim.fn.system(command))
if vim.v.shell_error ~= 0 then
@@ -14,44 +16,46 @@ local run_system = function(command)
return result, nil
end
----Returns all branches for the current repository
----@param args table|nil extra arguments for `git branch`
----@return string|nil, string|nil
+---Return all branches for the current repository.
+---@param args? string[] Extra arguments for `git branch`
+---@return string?, string?
M.branches = function(args)
- -- Load here to prevent loop
local u = require("gitlab.utils")
return run_system(u.combine({ "git", "branch" }, args or {}))
end
----Returns true if the working tree hasn't got any changes that haven't been committed
----@return boolean, string|nil
+---Return true if the working tree hasn't got any changes that haven't been committed.
+---@return boolean, string?
M.has_clean_tree = function()
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
+---Return true if the `file` has got any uncommitted changes.
---@param file string File to check for changes
----@return boolean, string|nil
+---@return boolean, string?
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
----@return string|nil, string|nil
+---Return the base directory of the current project and possible error from git.
+---@return string?, string?
M.base_dir = function()
return run_system({ "git", "rev-parse", "--show-toplevel" })
end
----Switches the current project to the given branch
----@return string|nil, string|nil
+---Switch the current project to the given branch.
+---@param branch string The branch to switch to
+---@return string?, string?
M.switch_branch = function(branch)
return run_system({ "git", "checkout", "-q", branch })
end
----Returns the name of the remote-tracking branch for the current branch or nil if it can't be found
----@return string|nil
+---Return the name of the upstream branch for the current branch or nil if it can't be found.
+---TODO: This should really be called "get_upstream_branch", and the use of "remote
+---branch" should be revised throughout this module.
+---@return string?
M.get_remote_branch = function()
local remote_branch, err = run_system({ "git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}" })
if err or remote_branch == "" then
@@ -61,9 +65,9 @@ M.get_remote_branch = function()
return remote_branch
end
----Fetch the remote branch
+---Fetch the remote branch.
---@param remote_branch string The name of the repo and branch to fetch (e.g., "origin/some_branch")
----@return boolean fetch_successful False if an error occurred while fetching, true otherwise.
+---@return boolean fetch_successful False if an error occurred while fetching, true otherwise
M.fetch_remote_branch = function(remote_branch)
local remote, branch = string.match(remote_branch, "([^/]+)/(.+)")
if not remote or not branch then
@@ -78,11 +82,12 @@ M.fetch_remote_branch = function(remote_branch)
return true
end
----Determines whether the tracking branch is ahead of or behind the current branch and returns the
----number of ahead and behind commits or nil values in case of errors.
----@param current_branch string|nil
----@param remote_branch string|nil
----@return integer|nil ahead, integer|nil behind
+---Return the number of commits the local branch is ahead of and behind the upstream or
+---nil values in case of errors.
+---@param current_branch? string The name of the local branch
+---@param remote_branch? string The name of the remote branch
+---@return integer? ahead The number of commits local is ahead of upstream
+---@return integer? behind The number of commits local is behind upstream
M.get_ahead_behind = function(current_branch, remote_branch)
if current_branch == nil or remote_branch == nil then
return nil, nil
@@ -109,7 +114,7 @@ M.get_ahead_behind = function(current_branch, remote_branch)
end
---Return the name of the current branch, or nil (detached HEAD or git failure).
----@return string|nil
+---@return string?
M.get_current_branch = function()
local current_branch, err = run_system({ "git", "branch", "--show-current" })
if err then
@@ -124,7 +129,7 @@ M.get_current_branch = function()
end
---Return the list of possible merge targets.
----@return table|nil
+---@return table?
M.get_all_merge_targets = function()
local current_branch = M.get_current_branch()
if current_branch == nil then
@@ -136,7 +141,8 @@ M.get_all_merge_targets = function()
end
---Return the list of names of all remote-tracking branches or an empty list.
----@return table, string|nil
+---@return string[] result List of branch names
+---@return string? error The error when listing branches fails
M.get_all_remote_branches = function()
local state = require("gitlab.state")
local all_branches, err = M.branches({ "--remotes" })
@@ -151,7 +157,7 @@ M.get_all_remote_branches = function()
local lines = u.lines_into_table(all_branches)
return List.new(lines)
:map(function(line)
- -- Trim the remote branch
+ -- Trim the remote name from the branch name
return line:match(state.settings.connection_settings.remote .. "/(%S+)")
end)
:filter(function(branch)
@@ -161,17 +167,18 @@ M.get_all_remote_branches = function()
end
---Return whether something
+---TODO: Remove - unused.
---@param current_branch string
----@return string|nil, string|nil
+---@return string?, string?
M.contains_branch = function(current_branch)
return run_system({ "git", "branch", "-r", "--contains", current_branch })
end
---- Returns true if `branch` is up-to-date on remote, otherwise false and warns user
----@param ahead integer|nil The number of commits the current branch is ahead of remote
----@param behind integer|nil The number of commits the current branch is behind remote
----@param remote_branch string|nil The remote branch, e.g., origin/feature-branch
----@param log_level number
+---Return true if local is up-to-date with remote, otherwise false and notify user.
+---@param ahead? integer The number of commits the current branch is ahead of remote
+---@param behind? integer The number of commits the current branch is behind remote
+---@param remote_branch? string The remote branch, e.g., origin/feature-branch
+---@param log_level vim.log.levels
---@return boolean
M.evaluate_ahead_behind = function(ahead, behind, remote_branch, log_level)
if ahead == nil or behind == nil then
@@ -200,13 +207,13 @@ M.evaluate_ahead_behind = function(ahead, behind, remote_branch, log_level)
return false
end
- return true -- Checks passed, branch is up-to-date
+ return true -- Checks passed, local is up-to-date
end
---- Returns true if `branch` is up-to-date on remote, otherwise false and notifies user.
---- This is a blocking function. For a non-blocking version use
---- gitlab.git_async.check_current_branch_up_to_date_on_remote.
----@param log_level number The log level with which user will be notified
+---Return true if local is up-to-date with remote, otherwise false and notify user.
+---This is a blocking function. For a non-blocking version use
+---gitlab.git_async.check_current_branch_up_to_date_on_remote.
+---@param log_level vim.log.levels The log level with which user will be notified
---@return boolean
M.check_current_branch_up_to_date_on_remote = function(log_level)
local current_branch = M.get_current_branch()
@@ -216,7 +223,7 @@ M.check_current_branch_up_to_date_on_remote = function(log_level)
return M.evaluate_ahead_behind(ahead, behind, remote_branch, log_level)
end
----Warns user if the current MR is in a bad state (closed, has conflicts, merged)
+---Warn user if the current MR is in a bad state (closed, has conflicts, merged).
M.check_mr_in_good_condition = function()
local state = require("gitlab.state")
local u = require("gitlab.utils")
@@ -234,12 +241,12 @@ M.check_mr_in_good_condition = function()
end
end
----Returns the full diff between the local working tree relative to the named `base_sha`, for the
----given file(s).
----@param base_sha string Base sha to diff against
----@param old_path string? Old file name
----@param new_path string? New file name - relevant for renamed files, ignored if same as old_path
----@return string|nil diff, string|nil err
+---Return the full diff between the local working tree relative to the named `base_sha`,
+---for the given file(s).
+---@param base_sha string Base SHA to diff against
+---@param old_path? string Old file name
+---@param new_path? string New file name - relevant for renamed files, ignored if same as old_path
+---@return string? diff, string? err
M.diff_files = function(base_sha, old_path, new_path)
return run_system({
"git",
diff --git a/lua/gitlab/git_async.lua b/lua/gitlab/git_async.lua
index 096ad9f0..2ce04ff4 100644
--- a/lua/gitlab/git_async.lua
+++ b/lua/gitlab/git_async.lua
@@ -1,13 +1,11 @@
local M = {}
----Function to run when an async system call finishes. Receives the command's stdout as result when
----successful, or the command's stderr as err when unsuccessful.
----@alias OnExitCallback fun(result:string|nil, err:string|nil)
+---Function to run when an async system call finishes.
+---Receives the command's stdout as result when successful, or the command's stderr as
+---err when unsuccessful.
+---@alias OnExitCallback fun(result:string?, err:string?)
----Function to run on the result of getting the ahead and behind in the get_ahead_behind function.
----@alias GetAheadBehindCallback fun(ahead:integer|nil, behind:integer|nil, remote_branch:string|nil)
-
----Runs a system command asynchronously
+---Run a system command asynchronously.
---@param command string[]
---@param on_exit OnExitCallback
local run_system_async = function(command, on_exit)
@@ -24,10 +22,10 @@ local run_system_async = function(command, on_exit)
end
---Pull a branch asynchronously from a remote and execute callback on exit.
----@param remote string The remote from which to pull.
----@param branch string The branch to pull.
----@param on_exit OnExitCallback The callback to execute when the command finishes.
----@param args? string[] Extra arguments passed to the `git pull` command.
+---@param remote string The remote to pull from
+---@param branch string The branch to pull
+---@param on_exit OnExitCallback The callback to execute when the command finishes
+---@param args? string[] Extra arguments passed to the `git pull` command
M.pull = function(remote, branch, on_exit, args)
local current_branch = require("gitlab.git").get_current_branch()
if not current_branch then
@@ -43,7 +41,7 @@ M.pull = function(remote, branch, on_exit, args)
run_system_async(cmd, on_exit)
end
----Fetch the remote branch
+---Fetch the remote branch.
---@param remote_branch string The name of the repo and branch to fetch (e.g., "origin/some_branch")
---@param on_exit OnExitCallback
M.fetch_remote_branch = function(remote_branch, on_exit)
@@ -55,10 +53,14 @@ M.fetch_remote_branch = function(remote_branch, on_exit)
run_system_async({ "git", "fetch", remote, branch }, on_exit)
end
---- Determines the number of commits the current branch is ahead of or behind the remote branch and
---- runs on_exit callback with the values.
----@param current_branch string|nil
----@param remote_branch string|nil
+---Function to run on the result of getting the ahead and behind in the get_ahead_behind
+---function.
+---@alias GetAheadBehindCallback fun(ahead:integer?, behind:integer?, remote_branch:string?)
+
+---Determines the number of commits the current branch is ahead of or behind the remote
+---branch and runs on_exit callback with the values.
+---@param current_branch? string
+---@param remote_branch? string
---@param on_exit GetAheadBehindCallback
M.get_ahead_behind = function(current_branch, remote_branch, on_exit)
if current_branch == nil or remote_branch == nil then
@@ -66,17 +68,17 @@ M.get_ahead_behind = function(current_branch, remote_branch, on_exit)
return
end
- ---Callback to run after the async `git fetch` call exits
- ---@param result string|nil The stdout from the `git fetch` call if any.
- ---@param err string|nil The stderr from the `git fetch` call if any.
+ ---Callback to run after the async `git fetch` call exits.
+ ---@param result? string The stdout from the `git fetch` call if any
+ ---@param err? string The stderr from the `git fetch` call if any
local fetch_remote_branch_callback = function(result, err)
if result ~= nil then
local u = require("gitlab.utils")
run_system_async(
{ "git", "rev-list", "--left-right", "--count", current_branch .. "..." .. remote_branch },
- --- The function to run after the async `git rev-list` call exits
- ---@param r string|nil The stdout from the `git rev-list` call if any.
- ---@param e string|nil The stderr from the `git rev-list` call if any.
+ --- The function to run after the async `git rev-list` call exits.
+ ---@param r? string The stdout from the `git rev-list` call if any
+ ---@param e? string The stderr from the `git rev-list` call if any
function(r, e)
if e ~= nil or r == nil then
u.notify("Could not determine if branch is up-to-date: " .. (e or "unknown error"), vim.log.levels.ERROR)
@@ -101,18 +103,20 @@ M.get_ahead_behind = function(current_branch, remote_branch, on_exit)
M.fetch_remote_branch(remote_branch, fetch_remote_branch_callback)
end
----Callback to run on the result of getting the ahead and behind in the get_ahead_behind function.
----@param ahead integer|nil The number of commits the current branch is ahead of remote
----@param behind integer|nil The number of commits the current branch is behind remote
----@param remote_branch string|nil The remote from which to pull.
+---Callback to run on the result of getting the ahead and behind in
+---check_current_branch_up_to_date_on_remote.
+---@param ahead? integer The number of commits the current branch is ahead of remote
+---@param behind? integer The number of commits the current branch is behind remote
+---@param remote_branch? string The remote from which to pull
local check_up_to_date_callback = function(ahead, behind, remote_branch)
require("gitlab.state").ahead_behind = { ahead, behind }
require("gitlab.git").evaluate_ahead_behind(ahead, behind, remote_branch, vim.log.levels.WARN)
end
---- Evaluates if `branch` is up-to-date on remote and warns user.
---- This is a non-blocking function. For a blocking version that can be used to abort further
---- execution if branch is not up-to-date use gitlab.git.check_current_branch_up_to_date_on_remote.
+---Evaluate if local is up-to-date with remote and notify user.
+---This is a non-blocking function. For a blocking version that can be used to abort
+---further execution if branch is not up-to-date use
+---gitlab.git.check_current_branch_up_to_date_on_remote.
M.check_current_branch_up_to_date_on_remote = function()
local git = require("gitlab.git")
local current_branch = git.get_current_branch()
diff --git a/lua/gitlab/health.lua b/lua/gitlab/health.lua
index 5831806d..ad153ec9 100644
--- a/lua/gitlab/health.lua
+++ b/lua/gitlab/health.lua
@@ -2,10 +2,12 @@ local state = require("gitlab.state")
local List = require("gitlab.utils.list")
local version = require("gitlab.version")
local u = require("gitlab.utils")
+
local M = {}
----Checks the health of the plugin
----@param return_results boolean
+---Check the health of the plugin.
+---@param return_results boolean If false (e.g., when invoked by `:checkhealth gitlab`), the result is not returned and a health report is started instead
+---@return boolean? success When return_results is false returns nil, otherwise returns true if the health ckeck doesn't find any issues, and false if there are errors
M.check = function(return_results)
local warnings = List.new({})
local errors = List.new({})
@@ -14,6 +16,7 @@ M.check = function(return_results)
vim.health.start("gitlab.nvim")
end
+ -- TODO: Remove state.settings.reviewer and add to `removed_settings_fields` - this warning has been here for two years...
if state.settings.reviewer == "delta" then
table.insert(
warnings,
diff --git a/lua/gitlab/hover.lua b/lua/gitlab/hover.lua
deleted file mode 100644
index 57c2edfd..00000000
--- a/lua/gitlab/hover.lua
+++ /dev/null
@@ -1,78 +0,0 @@
-local M = {}
-
-M.show_popup = function(char)
- -- Close existing popup if it's open
- if M.popup_win_id and vim.api.nvim_win_is_valid(M.popup_win_id) then
- vim.api.nvim_win_close(M.popup_win_id, true)
- end
-
- -- Create a buffer for the popup window
- local buf = vim.api.nvim_create_buf(false, true)
-
- -- Set the content of the popup buffer to the character
- vim.api.nvim_buf_set_lines(buf, 0, -1, false, { char })
-
- -- Define the popup window options
- local opts = {
- relative = "cursor",
- row = -2,
- col = 0,
- width = 2, -- Width set to 2 to accommodate the border
- height = 1,
- style = "minimal",
- border = "single",
- }
-
- -- Open the popup window and store its ID
- M.popup_win_id = vim.api.nvim_open_win(buf, false, opts)
-end
-
-M.close_popup = function()
- if M.popup_win_id and vim.api.nvim_win_is_valid(M.popup_win_id) then
- vim.api.nvim_win_close(M.popup_win_id, true)
- M.popup_win_id = nil -- Reset the window ID
- end
-end
-
-M.init = function()
- -- Set up autocommands
- vim.api.nvim_create_autocmd({ "CursorHold" }, {
- callback = function()
- -- Get the current cursor position
- local row, col = unpack(vim.api.nvim_win_get_cursor(0))
- row = row - 1 -- Adjust row because Lua is 1-indexed
- col = col - 2 -- Adjust to account for > at front of line
-
- if col < 1 then
- return
- end
-
- -- Get the text of the current line
- local line = vim.api.nvim_buf_get_lines(0, row, row + 1, false)[1]
-
- -- Correctly handle multi-byte characters, such as emojis
- local byteIndexStart = vim.str_byteindex(line, col)
- local byteIndexEnd = vim.str_byteindex(line, col + 1)
-
- -- Extract the character (or emoji) under the cursor
- local char = line:sub(byteIndexStart + 1, byteIndexEnd)
-
- -- Proceed only if char is not empty (to avoid empty popups)
- if char == "" then
- return
- end
-
- M.show_popup(char)
- end,
- buffer = vim.api.nvim_get_current_buf(),
- })
-
- vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
- callback = function()
- M.close_popup()
- end,
- buffer = vim.api.nvim_get_current_buf(),
- })
-end
-
-return M
diff --git a/lua/gitlab/hunks.lua b/lua/gitlab/hunks.lua
index 0b4b4d6a..01df7160 100644
--- a/lua/gitlab/hunks.lua
+++ b/lua/gitlab/hunks.lua
@@ -1,6 +1,7 @@
local List = require("gitlab.utils.list")
local u = require("gitlab.utils")
local state = require("gitlab.state")
+
local M = {}
---@class Hunk
@@ -10,12 +11,12 @@ local M = {}
---@field new_range integer
---@class HunksAndDiff
----@field hunks Hunk[] list of hunks
----@field all_diff_output table The data from the git diff command
+---@field hunks Hunk[] List of hunks
+---@field all_diff_output string[] The data from the git diff command
----Turn hunk line into Lua table
+---Parse hunk header line into a Lua table. Return nil, if line is not a hunk header.
---@param line string
----@return Hunk|nil
+---@return Hunk?
M.parse_possible_hunk_headers = function(line)
if line:sub(1, 2) == "@@" then
-- match:
@@ -32,11 +33,13 @@ M.parse_possible_hunk_headers = function(line)
}
end
end
----@param linnr number
----@param hunk Hunk
+
+---Return true if given line was removed in the MR.
+---@param linenr integer Line number in the old version of the file
+---@param hunk Hunk A hunk candidate from the file's diff
---@param all_diff_output string[]
---@return boolean
-local line_was_removed = function(linnr, hunk, all_diff_output)
+local line_was_removed = function(linenr, hunk, all_diff_output)
for matching_line_index, line in ipairs(all_diff_output) do
local found_hunk = M.parse_possible_hunk_headers(line)
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
@@ -45,7 +48,7 @@ local line_was_removed = function(linnr, hunk, all_diff_output)
-- to see if that line is deleted or not.
for hunk_line_index = found_hunk.old_line, hunk.old_line + hunk.old_range, 1 do
local line_content = all_diff_output[matching_line_index + 1]
- if hunk_line_index == linnr then
+ if hunk_line_index == linenr then
if string.match(line_content, "^%-") then
return true
end
@@ -56,11 +59,12 @@ local line_was_removed = function(linnr, hunk, all_diff_output)
return false
end
----@param linnr number
----@param hunk Hunk
+---Return true if given line was added in the MR.
+---@param linenr integer Line number in the new version of the file
+---@param hunk Hunk A hunk candidate from the file's diff
---@param all_diff_output string[]
---@return boolean
-local line_was_added = function(linnr, hunk, all_diff_output)
+local line_was_added = function(linenr, hunk, all_diff_output)
for matching_line_index, line in ipairs(all_diff_output) do
local found_hunk = M.parse_possible_hunk_headers(line)
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
@@ -80,7 +84,7 @@ local line_was_added = function(linnr, hunk, all_diff_output)
-- matches the index of a line that was added
local starting_index = found_hunk.new_line - 1 -- The "+j" will add one
for j, _ in ipairs(hunk_lines) do
- if (starting_index + j) == linnr then
+ if (starting_index + j) == linenr then
return true
end
end
@@ -89,9 +93,9 @@ local line_was_added = function(linnr, hunk, all_diff_output)
return false
end
----Parse git diff hunks.
----@param base_sha string Git base SHA of merge request.
----@return HunksAndDiff
+---Parse the diff of the current file against the base SHA of the MR.
+---@param base_sha string Git base SHA of the merge request
+---@return HunksAndDiff hunks_and_diff The hunk headers and full diff of the file
local parse_hunks_and_diff = function(base_sha)
local hunks = {}
local all_diff_output = {}
@@ -113,11 +117,11 @@ local parse_hunks_and_diff = function(base_sha)
return { hunks = hunks, all_diff_output = all_diff_output }
end
--- Parses the lines from a diff and returns the
--- index of the next hunk, when provided an initial index
+---Parse the lines from a diff and return the index of the next hunk, when provided an
+---initial index.
---@param lines string[]
---@param i integer
----@return integer|nil
+---@return integer?
local next_hunk_index = function(lines, i)
for j, line in ipairs(lines) do
local hunk = M.parse_possible_hunk_headers(line)
@@ -128,25 +132,25 @@ local next_hunk_index = function(lines, i)
return nil
end
---- Processes the number of changes until the target is reached. This returns
---- a negative or positive number indicating the number of lines in the hunk
---- that have been added or removed prior to the target line
----@param line_number number
+---Process the number of changes until the target is reached.
+---This returns a negative or positive number indicating the number of lines in
+---the hunk that have been added or removed prior to the target line.
+---@param linenr integer
---@param hunk Hunk
----@param lines table
+---@param lines string[]
---@return integer
-local net_changed_in_hunk_before_line = function(line_number, hunk, lines)
+local net_changed_in_hunk_before_line = function(linenr, hunk, lines)
local net_lines = 0
local current_line_old = hunk.old_line
for _, line in ipairs(lines) do
if line:sub(1, 1) == "-" then
- if current_line_old < line_number then
+ if current_line_old < linenr then
net_lines = net_lines - 1
end
current_line_old = current_line_old + 1
elseif line:sub(1, 1) == "+" then
- if current_line_old < line_number then
+ if current_line_old < linenr then
net_lines = net_lines + 1
end
else
@@ -157,8 +161,9 @@ local net_changed_in_hunk_before_line = function(line_number, hunk, lines)
return net_lines
end
----Counts the total number of changes in a set of lines, can be positive if added lines or negative if removed lines
----@param lines table
+---Count the total number of changes in a set of lines, positive if added lines and
+---negative if removed lines.
+---@param lines string[]
---@return integer
local count_changes = function(lines)
local total = 0
@@ -172,10 +177,11 @@ local count_changes = function(lines)
return total
end
----@param new_line number|nil
+---Return the modification type for the selected line.
+---@param new_line? integer The starting or ending line of the current selection in the new version
---@param hunks Hunk[]
----@param all_diff_output table
----@return string|nil
+---@param all_diff_output string[]
+---@return ("added"|"bad_file_unmodified")?
local function get_modification_type_from_new_sha(new_line, hunks, all_diff_output)
if new_line == nil then
return nil
@@ -188,11 +194,12 @@ local function get_modification_type_from_new_sha(new_line, hunks, all_diff_outp
end) and "added" or "bad_file_unmodified"
end
----@param old_line number|nil
----@param new_line number|nil
+---Return the modification type for the selected line.
+---@param old_line? integer The starting or ending line of the current selection in the old version
+---@param new_line? integer The starting or ending line of the current selection in the new version
---@param hunks Hunk[]
----@param all_diff_output table
----@return string|nil
+---@param all_diff_output string[]
+---@return ("deleted"|"unmodified")?
local function get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output)
if old_line == nil then
return nil
@@ -207,12 +214,19 @@ local function get_modification_type_from_old_sha(old_line, new_line, hunks, all
end) and "deleted" or "unmodified"
end
----Returns whether the comment is on a deleted line, added line, or unmodified line.
----This is in order to build the payload for Gitlab correctly by setting the old line and new line.
----@param old_line number|nil
----@param new_line number|nil
+---Return the modification type of the line for which the comment is created.
+---This is in order to build the payload for Gitlab correctly by setting the old line
+---and new line.
+---FIXME: This misses the fact that Gitlab also uses the type "expanded" (when
+---commenting on lines that are more than 3 lines away from any change, thus are on
+---folded lines that the user expanded manually).
+---FIXME: This function is called three times when creating a ranged comment - this
+---means three `git diff` calls, six times parsing the same diff output. This should
+---only be done once!
+---@param old_line? integer
+---@param new_line? integer
---@param new_sha_focused boolean
----@return string|nil
+---@return ("added"|"bad_file_unmodified"|"deleted"|"unmodified")?
function M.get_modification_type(old_line, new_line, new_sha_focused)
local hunk_and_diff_data = parse_hunks_and_diff(state.INFO.diff_refs.base_sha)
if hunk_and_diff_data.hunks == nil then
@@ -225,14 +239,15 @@ function M.get_modification_type(old_line, new_line, new_sha_focused)
or get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output)
end
----Returns the matching line number of a line in the new/old version of the file compared to the current SHA.
----@param old_sha string
----@param new_sha string
----@param file_path string
----@param old_file_path string
----@param line_number number
----@return number|nil
-M.calculate_matching_line_new = function(old_sha, new_sha, file_path, old_file_path, line_number)
+---Return the matching line number of a line in the new/old version of the file compared
+---to the currently selected version.
+---@param old_sha string The base SHA of the MR when getting matching line in the old version, otherwise the head SHA when getting matching line in the new version
+---@param new_sha string The head SHA of the MR when getting matching line in the old version, otherwise the base SHA when getting matching line in the new version
+---@param file_path string The file name after change
+---@param old_file_path string The file name before change (different from file_path for renamed/moved files)
+---@param linenr integer The starting or ending line of the current selection
+---@return integer?
+M.calculate_matching_line_new = function(old_sha, new_sha, file_path, old_file_path, linenr)
local net_change = 0
local diff_cmd = string.format(
"git diff --minimal --unified=0 --no-color %s %s -- %s %s",
@@ -256,17 +271,17 @@ M.calculate_matching_line_new = function(old_sha, new_sha, file_path, old_file_p
for i, line in ipairs(all_lines) do
local hunk = M.parse_possible_hunk_headers(line)
if hunk ~= nil then
- if line_number <= hunk.old_line then
+ if linenr <= hunk.old_line then
-- We have reached a hunk which starts after our target, return the changed total lines
- return line_number + net_change
+ return linenr + net_change
end
local n = next_hunk_index(all_lines, i) or #all_lines
local diff_lines = all_lines:slice(i + 1, n - 1)
-- If the line is IN the hunk, process the hunk and return the change until that line
- if line_number >= hunk.old_line and line_number < hunk.old_line + hunk.old_range then
- net_change = line_number + net_change + net_changed_in_hunk_before_line(line_number, hunk, diff_lines)
+ if linenr >= hunk.old_line and linenr < hunk.old_line + hunk.old_range then
+ net_change = linenr + net_change + net_changed_in_hunk_before_line(linenr, hunk, diff_lines)
return net_change
end
@@ -276,7 +291,7 @@ M.calculate_matching_line_new = function(old_sha, new_sha, file_path, old_file_p
end
-- TODO: Possibly handle lines that are out of range in the new files
- return line_number + net_change + 1
+ return linenr + net_change + 1
end
return M
diff --git a/lua/gitlab/indicators/common.lua b/lua/gitlab/indicators/common.lua
index 04f68acc..649b13d1 100644
--- a/lua/gitlab/indicators/common.lua
+++ b/lua/gitlab/indicators/common.lua
@@ -6,9 +6,9 @@ local M = {}
---@class NoteWithValues
---@field position NotePosition
----@field resolvable boolean|nil
----@field resolved boolean|nil
----@field created_at string|nil
+---@field resolvable? boolean
+---@field resolved? boolean
+---@field created_at? string
---Return true if discussion has a placeable diagnostic, false otherwise.
---@param note NoteWithValues
@@ -28,7 +28,7 @@ local filter_discussions_and_notes = function(note)
end
---Filter all discussions and drafts which have placeable signs and diagnostics.
----@return Discussion|DraftNote[]
+---@return (Discussion|DraftNote)[]
M.filter_placeable_discussions = function()
local discussions = u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {})
if type(discussions) ~= "table" then
@@ -52,12 +52,19 @@ M.filter_placeable_discussions = function()
return u.join(filtered_discussions, filtered_draft_notes)
end
+---Parse old and new line from a line code like "3f454a98e586d1aa0d322e19afd5e67e08f2d3c8_10_44".
+---@param line_code string A SHA hash of the file name and line numbers before and after change
+---@return integer The line number before the change
+---@return integer The line number after the change
M.parse_line_code = function(line_code)
local line_code_regex = "%w+_(%d+)_(%d+)"
local old_line, new_line = line_code:match(line_code_regex)
- return tonumber(old_line), tonumber(new_line)
+ old_line = tonumber(old_line) --[[@as integer]]
+ new_line = tonumber(new_line) --[[@as integer]]
+ return old_line, new_line
end
+---Return true if discussion/draft belongs to the old file, otherwise false.
---@param d_or_n Discussion|DraftNote
---@return boolean
M.is_old_sha = function(d_or_n)
@@ -66,12 +73,14 @@ M.is_old_sha = function(d_or_n)
return position.old_line ~= nil and old_start_line ~= 0
end
+---Return true if discussion/draft belongs to the new file, otherwise false.
---@param discussion Discussion|DraftNote
---@return boolean
M.is_new_sha = function(discussion)
return not M.is_old_sha(discussion)
end
+---Return true if the discussion/draft doesn't have a line range.
---@param d_or_n Discussion|DraftNote
---@return boolean
M.is_single_line = function(d_or_n)
@@ -80,12 +89,7 @@ M.is_single_line = function(d_or_n)
return line_range == nil
end
----@param discussion Discussion
----@return boolean
-M.is_multi_line = function(discussion)
- return not M.is_single_line(discussion)
-end
-
+---Return the first note from a Discussion thread or the DraftNote.
---@param d_or_n Discussion|DraftNote
---@return Note|DraftNote
M.get_first_note = function(d_or_n)
diff --git a/lua/gitlab/indicators/diagnostics.lua b/lua/gitlab/indicators/diagnostics.lua
index 91f7a01b..d4d6b810 100644
--- a/lua/gitlab/indicators/diagnostics.lua
+++ b/lua/gitlab/indicators/diagnostics.lua
@@ -3,17 +3,19 @@ local indicators_common = require("gitlab.indicators.common")
local actions_common = require("gitlab.actions.common")
local List = require("gitlab.utils.list")
local state = require("gitlab.state")
-local discussion_sign_name = "gitlab_discussion"
local M = {}
-local diagnostics_namespace = vim.api.nvim_create_namespace(discussion_sign_name)
-M.diagnostics_namespace = diagnostics_namespace
-M.discussion_sign_name = discussion_sign_name
+
+M.discussion_sign_name = "gitlab_discussion"
+M.diagnostics_namespace = vim.api.nvim_create_namespace(M.discussion_sign_name)
+
+---Clear the Gitlab Discussion diagnostic namespace.
M.clear_diagnostics = function()
- vim.diagnostic.reset(diagnostics_namespace)
+ vim.diagnostic.reset(M.diagnostics_namespace)
end
--- Display options for the diagnostic
+---Return display options for the diagnostics.
+---@return vim.diagnostic.Opts
local create_display_opts = function()
return {
virtual_text = state.settings.discussion_signs.virtual_text,
@@ -23,11 +25,14 @@ local create_display_opts = function()
}
end
----Takes some range information and data about a discussion
----and creates a diagnostic to be placed in the reviewer
----@param range_info table
----@param d_or_n Discussion|DraftNote
----@return Diagnostic
+---@class RangeInfo
+---@field lnum integer The starting line of the diagnostic (0-indexed)
+---@field end_lnum? integer The final line of the diagnostic (0-indexed)
+
+---Return a diagnostic to be placed in the reviewer.
+---@param range_info RangeInfo Information about diagnostic position
+---@param d_or_n Discussion|DraftNote The data to build the diagnostic content from
+---@return vim.Diagnostic.Set
local function create_diagnostic(range_info, d_or_n)
local first_note = indicators_common.get_first_note(d_or_n)
local header = actions_common.build_note_header(first_note)
@@ -51,9 +56,9 @@ local function create_diagnostic(range_info, d_or_n)
return vim.tbl_deep_extend("force", diagnostic, range_info)
end
----Creates a single line diagnostic
+---Create a single line diagnostic.
---@param d_or_n Discussion|DraftNote
----@return Diagnostic
+---@return vim.Diagnostic.Set
local create_single_line_diagnostic = function(d_or_n)
local linnr = actions_common.get_line_number(d_or_n.id)
return create_diagnostic({
@@ -61,9 +66,9 @@ local create_single_line_diagnostic = function(d_or_n)
}, d_or_n)
end
----Creates a mutli-line line diagnostic
+---Create a mutli-line line diagnostic.
---@param d_or_n Discussion|DraftNote
----@return Diagnostic
+---@return vim.Diagnostic.Set
local create_multiline_diagnostic = function(d_or_n)
local first_note = indicators_common.get_first_note(d_or_n)
local line_range = first_note.position.line_range
@@ -85,17 +90,16 @@ local create_multiline_diagnostic = function(d_or_n)
end
---Set diagnostics in the given buffer.
----@param namespace number namespace for diagnostics
----@param bufnr number the bufnr for placing the diagnostics
----@param diagnostics table see :h vim.diagnostic.set
----@param opts? table see :h vim.diagnostic.set
+---@param namespace integer Namespace ID for diagnostics
+---@param bufnr integer The bufnr for placing the diagnostics
+---@param diagnostics vim.Diagnostic.Set[] A list of diagnostics definitions
+---@param opts? vim.diagnostic.Opts see :h vim.diagnostic.set
local set_diagnostics = function(namespace, bufnr, diagnostics, opts)
vim.diagnostic.set(namespace, bufnr, diagnostics, opts)
require("gitlab.indicators.signs").set_signs(diagnostics, bufnr)
end
----Refresh the diagnostics for all the reviewed files, and place diagnostics for the currently
----visible buffers.
+---Refresh diagnostics for all files, and place diagnostics for the currently visible buffers.
M.refresh_diagnostics = function()
require("gitlab.indicators.signs").clear_signs()
M.clear_diagnostics()
@@ -111,7 +115,7 @@ M.refresh_diagnostics = function()
end
---Filter and place the diagnostics for the given buffer.
----@param bufnr number The number of the buffer for placing diagnostics.
+---@param bufnr integer The bufnr for placing the diagnostics
M.place_diagnostics = function(bufnr)
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
return
@@ -119,6 +123,7 @@ M.place_diagnostics = function(bufnr)
if not state.settings.discussion_signs.enabled then
return
end
+ -- TODO: Use cur_layout = require("gitlab.reviewer").diffview_layout instead.
local view = require("gitlab.reviewer").diffview
if view == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
@@ -139,12 +144,12 @@ M.place_diagnostics = function(bufnr)
return
end
- local new_diagnostics, old_diagnostics = List.new(file_discussions):partition(indicators_common.is_new_sha)
+ local new_discussions, old_discussions = List.new(file_discussions):partition(indicators_common.is_new_sha)
if bufnr == view.cur_layout.a.file.bufnr then
- set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(old_diagnostics), create_display_opts())
+ set_diagnostics(M.diagnostics_namespace, bufnr, M.parse_diagnostics(old_discussions), create_display_opts())
elseif bufnr == view.cur_layout.b.file.bufnr then
- set_diagnostics(diagnostics_namespace, bufnr, M.parse_diagnostics(new_diagnostics), create_display_opts())
+ set_diagnostics(M.diagnostics_namespace, bufnr, M.parse_diagnostics(new_discussions), create_display_opts())
end
end)
@@ -153,10 +158,9 @@ M.place_diagnostics = function(bufnr)
end
end
----Iterates over each discussion and returns a list of tables with sign
----data, for instance group, priority, line number etc
----@param discussions List
----@return DiagnosticTable[]
+---Return a list of diagnostics definitions parsed from discussions.
+---@param discussions List
+---@return vim.Diagnostic.Set[]
M.parse_diagnostics = function(discussions)
local single_line, multi_line = discussions:partition(indicators_common.is_single_line)
single_line = single_line:map(create_single_line_diagnostic)
diff --git a/lua/gitlab/indicators/signs.lua b/lua/gitlab/indicators/signs.lua
index abf56b97..167ad248 100644
--- a/lua/gitlab/indicators/signs.lua
+++ b/lua/gitlab/indicators/signs.lua
@@ -4,6 +4,7 @@ local List = require("gitlab.utils.list")
local discussion_sign_name = require("gitlab.indicators.diagnostics").discussion_sign_name
local M = {}
+
M.clear_signs = function()
vim.fn.sign_unplace(discussion_sign_name)
end
@@ -18,10 +19,11 @@ local severity_map = {
"Hint",
}
----Refresh the discussion signs for currently loaded file in reviewer For convinience we use same
----string for sign name and sign group ( currently there is only one sign needed)
----@param diagnostics Diagnostic[]
----@param bufnr number
+---Refresh discussion signs for the given buffer.
+---For convinience, we use the same string for the sign name and sign group (currently
+---there is only one sign needed).
+---@param diagnostics vim.Diagnostic.Set[]
+---@param bufnr integer
M.set_signs = function(diagnostics, bufnr)
if not state.settings.discussion_signs.enabled then
return
@@ -33,6 +35,7 @@ M.set_signs = function(diagnostics, bufnr)
local existing_signs =
vim.fn.sign_getplaced(vim.api.nvim_get_current_buf(), { group = discussion_sign_name })[1].signs
+ -- Set gitlab_range signs for lines 2 and upwards of multiline diagnostics
if diagnostic.end_lnum then
local linenr = diagnostic.lnum + 1
while linenr <= diagnostic.end_lnum do
@@ -40,6 +43,8 @@ M.set_signs = function(diagnostics, bufnr)
local conflicting_comment_sign = List.new(existing_signs):find(function(sign)
return u.ends_with(sign.name, gitlab_comment) and sign.lnum == linenr
end)
+ -- Don't place gitlab_range sign if there's a gitlab_comment (which shows start of commnet)
+ -- sign on the same line
if conflicting_comment_sign == nil then
vim.fn.sign_place(
linenr,
@@ -52,6 +57,7 @@ M.set_signs = function(diagnostics, bufnr)
end
end
+ -- Set gitlab_comment signs marking the beginning of a comment
vim.fn.sign_place(
diagnostic.lnum + 1,
discussion_sign_name,
@@ -64,7 +70,7 @@ M.set_signs = function(diagnostics, bufnr)
end
end
----Define signs for discussions
+---Define signs for discussions.
M.setup_signs = function()
local discussion_sign_settings = state.settings.discussion_signs
local comment_icon = discussion_sign_settings.icons.comment
diff --git a/lua/gitlab/init.lua b/lua/gitlab/init.lua
index 37bcbf1c..a6412a21 100644
--- a/lua/gitlab/init.lua
+++ b/lua/gitlab/init.lua
@@ -29,19 +29,18 @@ local revisions = state.dependencies.revisions
local merge_requests_dep = state.dependencies.merge_requests
local merge_requests_by_username_dep = state.dependencies.merge_requests_by_username
----@param args Settings | {} | nil
+---@param args? GitlabSettings
---@return nil
local function setup(args)
if args == nil then
args = {}
end
- state.merge_settings(args) -- Merges user settings with default settings
- server.build() -- Builds the Go binary if it doesn't exist
- state.set_global_keymaps() -- Sets keymaps that are not bound to a specific buffer
+ state.merge_settings(args)
+ server.build()
+ state.set_global_keymaps()
require("gitlab.colors") -- Sets colors
- reviewer.init()
- discussions.initialize_discussions() -- place signs / diagnostics for discussions in reviewer
+ discussions.initialize_discussions()
local is_healthy = health.check(true)
if not is_healthy then
diff --git a/lua/gitlab/job.lua b/lua/gitlab/job.lua
index 4a0989f0..aaa23267 100644
--- a/lua/gitlab/job.lua
+++ b/lua/gitlab/job.lua
@@ -1,13 +1,14 @@
-- This module is responsible for making API calls to the Go server and
-- running the callbacks associated with those jobs when the JSON is returned
local u = require("gitlab.utils")
+
local M = {}
----Send a request to the Go server
----@param endpoint string The endpoint path on the server.
----@param method string The HTTP rquest method.
----@param callback fun(data: table) The function to run on the decoded JSON response data if the response contains no error details.
----@param on_error_callback? fun(data: table) The function to run on the decoded JSON response data in case the response contains error details.
+---Send a request to the Go server.
+---@param endpoint string The endpoint path on the server
+---@param method string The HTTP rquest method
+---@param callback fun(data: table) The function to run on the decoded JSON response data if the response contains no error details
+---@param on_error_callback? fun(data: table) The function to run on the decoded JSON response data in case the response contains error details
M.run_job = function(endpoint, method, body, callback, on_error_callback)
local state = require("gitlab.state")
local port = state.settings.server and state.settings.server.port
diff --git a/lua/gitlab/popup.lua b/lua/gitlab/popup.lua
index 1f95bf38..ce986be4 100644
--- a/lua/gitlab/popup.lua
+++ b/lua/gitlab/popup.lua
@@ -3,14 +3,10 @@ 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
+---@param opts PopupOpts Options for customizing the popup
---@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 {})
+M.create_popup_state = function(opts)
+ local settings = u.merge(require("gitlab.state").settings.popup, opts.user_settings or {})
local view_opts = {
buf_options = {
filetype = "markdown",
@@ -18,17 +14,17 @@ M.create_popup_state = function(title, user_settings, width, height, zindex)
relative = "editor",
enter = true,
focusable = true,
- zindex = zindex or 50,
+ zindex = opts.zindex or 50,
border = {
style = settings.border,
text = {
- top = title,
+ top = opts.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,
+ width = opts.width and math.min(opts.width, vim.o.columns - 2) or settings.width,
+ height = opts.height and math.min(opts.height, vim.o.lines - 3) or settings.height,
},
opacity = settings.opacity,
}
@@ -37,7 +33,7 @@ M.create_popup_state = function(title, user_settings, width, height, zindex)
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 title? string 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
@@ -147,18 +143,18 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts)
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
+---Set up autocommands for the popup.
+---@param popup NuiPopup
+---@param layout? NuiLayout
+---@param previous_window? integer Number of window active before the popup was opened
+---@param opts? PopupOpts 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)))
+ popup:update_layout(opts and M.create_popup_state(opts))
end
end)
@@ -239,11 +235,11 @@ M.set_cycle_popups_keymaps = function(popups)
end
---Create the title for the comment popup.
----@param title string The main title, e.g., "Comment".
----@param file_name string Name of file for which comment is created.
----@param start_line integer Start of the line range.
----@param end_line integer End of the line range.
----@return string title The full title of the popup.
+---@param title string The main title, e.g., "Comment"
+---@param file_name string Name of file for which comment is created
+---@param start_line integer Start of the line range
+---@param end_line integer End of the line range
+---@return string title The full title of the popup
M.create_title = function(title, file_name, start_line, end_line)
local range = start_line < end_line and string.format("-%s", end_line) or ""
local position = string.format("%s%s", start_line, range)
diff --git a/lua/gitlab/reviewer/init.lua b/lua/gitlab/reviewer/init.lua
index 3b276a38..0336bf4b 100644
--- a/lua/gitlab/reviewer/init.lua
+++ b/lua/gitlab/reviewer/init.lua
@@ -17,17 +17,7 @@ local M = {
buf_winids = {},
}
--- Checks for legacy installations, only Diffview is supported.
-M.init = function()
- if state.settings.reviewer ~= "diffview" then
- vim.notify(
- string.format("gitlab.nvim could not find reviewer %s, only diffview is supported", state.settings.reviewer),
- vim.log.levels.ERROR
- )
- end
-end
-
--- Opens the reviewer windows.
+-- Open the reviewer windows.
M.open = function()
require("gitlab.emoji").init() -- Read in emojis for lookup purposes
local diff_refs = state.INFO.diff_refs
@@ -62,12 +52,12 @@ M.open = function()
M.is_open = true
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
- M.diffview = require("diffview.lib").get_current_view()
+ M.diffview = require("diffview.lib").get_current_view() --[[@as DiffView?]]
if M.diffview == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
return
end
- M.diffview_layout = M.diffview.cur_layout
+ M.diffview_layout = M.diffview.cur_layout --[[@as Diff4]]
M.tabid = vim.api.nvim_get_current_tabpage()
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
@@ -100,17 +90,20 @@ M.open = function()
git.check_mr_in_good_condition()
end
--- Closes the reviewer and cleans up
+---Close the reviewer and clean up.
M.close = function()
if M.tabid ~= nil and vim.api.nvim_tabpage_is_valid(M.tabid) then
+ -- FIXME: This fails if there is only one tabpage. Find a way to use DiffviewClose
+ -- that was originally here, but use it for the correct tabpage when there are
+ -- multiple Diffviews open.
vim.cmd.tabclose(vim.api.nvim_tabpage_get_number(M.tabid))
end
local discussions = require("gitlab.actions.discussions")
discussions.close()
end
----Loads new INFO state from Gitlab, then if diffview.api is available applies the new diff refs to
----the existing diffview, otherwise closes and re-opens the reviewer.
+---Load new INFO state from Gitlab. Then, if diffview.api is available, apply the new
+---diff refs to the existing diffview, otherwise close and re-open the reviewer.
M.reload = function()
state.load_new_state("info", function()
state.load_new_state("revisions", function()
@@ -128,12 +121,12 @@ M.reload = function()
end)
end
---- Jumps to the location provided in the reviewer window
----@param file_name string The file name after change.
----@param old_file_name string The file name before change (different from file_name for renamed/moved files).
----@param line_number number Line number from the discussion node.
----@param new_buffer boolean If true, jump to the NEW SHA.
-M.jump = function(file_name, old_file_name, line_number, new_buffer)
+---Jump to the location provided in the reviewer window.
+---@param file_name string The file name after change
+---@param old_file_name string The file name before change (different from file_name for renamed/moved files)
+---@param linenr integer Line number from the discussion node
+---@param new_buffer boolean If true, jump to the NEW SHA
+M.jump = function(file_name, old_file_name, linenr, new_buffer)
-- Draft comments don't have `old_file_name` set
old_file_name = old_file_name or file_name
@@ -162,35 +155,34 @@ M.jump = function(file_name, old_file_name, line_number, new_buffer)
end
async.await(M.diffview:set_file(file))
- local layout = M.diffview.cur_layout
local number_of_lines
if new_buffer then
- layout.b:focus()
- number_of_lines = u.get_buffer_length(layout.b.file.bufnr)
+ M.diffview_layout.b:focus()
+ number_of_lines = u.get_buffer_length(M.diffview_layout.b.file.bufnr)
else
- layout.a:focus()
- number_of_lines = u.get_buffer_length(layout.a.file.bufnr)
+ M.diffview_layout.a:focus()
+ number_of_lines = u.get_buffer_length(M.diffview_layout.a.file.bufnr)
end
- if line_number > number_of_lines then
+ if linenr > number_of_lines then
u.notify("Diagnostic position outside buffer. Jumping to last line instead.", vim.log.levels.WARN)
- line_number = number_of_lines
+ linenr = number_of_lines
end
- vim.api.nvim_win_set_cursor(0, { line_number, 0 })
+ vim.api.nvim_win_set_cursor(0, { linenr, 0 })
u.open_fold_under_cursor()
vim.cmd("normal! zz")
end
----Get the data from diffview, such as line information and file name. May be used by
----other modules such as the comment module to create line codes or set diagnostics
+---Get the data from diffview, such as line information and file name.
+---To be used by other modules such as the comment module to create line codes or set
+---diagnostics.
---@param current_win integer The ID of the currently focused window
----@return DiffviewInfo | nil
+---@return DiffviewInfo?
M.get_reviewer_data = function(current_win)
if M.diffview == nil then
return
end
- local layout = M.diffview.cur_layout
- local old_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
- local new_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
+ local old_win = u.get_window_id_by_buffer_id(M.diffview_layout.a.file.bufnr)
+ local new_win = u.get_window_id_by_buffer_id(M.diffview_layout.b.file.bufnr)
if old_win == nil or new_win == nil then
u.notify("Error getting window IDs for current files", vim.log.levels.ERROR)
@@ -214,16 +206,18 @@ M.get_reviewer_data = function(current_win)
return
end
+ -- FIXME: This causes false positive warnings when the selection range spans a
+ -- modified line but the new_line itself is on an unmodified line.
if modification_type == "bad_file_unmodified" then
u.notify("Comments on unmodified lines will be placed in the old file", vim.log.levels.WARN)
end
- local current_bufnr = new_sha_focused and layout.b.file.bufnr or layout.a.file.bufnr
- local opposite_bufnr = new_sha_focused and layout.a.file.bufnr or layout.b.file.bufnr
+ local current_bufnr = new_sha_focused and M.diffview_layout.b.file.bufnr or M.diffview_layout.a.file.bufnr
+ local opposite_bufnr = new_sha_focused and M.diffview_layout.a.file.bufnr or M.diffview_layout.b.file.bufnr
return {
- old_file_name = M.is_file_renamed() and layout.a.file.path or "",
- file_name = layout.b.file.path,
+ old_file_name = M.is_file_renamed() and M.diffview_layout.a.file.path or "",
+ file_name = M.diffview_layout.b.file.path,
old_line_from_buf = old_line,
new_line_from_buf = new_line,
modification_type = modification_type,
@@ -234,13 +228,12 @@ M.get_reviewer_data = function(current_win)
}
end
----Return whether user is focused on the new version of the file
+---Return true if user is focused on the new version of the file, otherwise false.
---@param current_win integer The ID of the currently focused window
---@return boolean
M.is_new_sha_focused = function(current_win)
- local layout = M.diffview.cur_layout
- local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
- local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
+ local b_win = u.get_window_id_by_buffer_id(M.diffview_layout.b.file.bufnr)
+ local a_win = u.get_window_id_by_buffer_id(M.diffview_layout.a.file.bufnr)
if a_win ~= current_win and b_win ~= current_win then
current_win = M.stored_win
M.stored_win = nil
@@ -248,41 +241,42 @@ M.is_new_sha_focused = function(current_win)
return current_win == b_win
end
----Get currently shown file data
+---Get data of currently shown file.
+---@return FileEntry?
M.get_current_file_data = function()
return M.diffview and M.diffview.panel and M.diffview.panel.cur_file
end
----Get currently shown file path
----@return string|nil
+---Get path of currently shown file.
+---@return string?
M.get_current_file_path = function()
local file_data = M.get_current_file_data()
return file_data and file_data.path
end
----Get currently shown file's old path
----@return string|nil
+---Get old path of currently shown file.
+---@return string?
M.get_current_file_oldpath = function()
local file_data = M.get_current_file_data()
- return file_data and file_data.oldpath or file_data.path
+ return file_data and (file_data.oldpath or file_data.path)
end
----Tell whether current file is renamed or not
----@return boolean|nil
+---Return true if current file is renamed, otherwise false.
+---@return boolean?
M.is_file_renamed = function()
local file_data = M.get_current_file_data()
return file_data and file_data.status == "R"
end
----Tell whether current file has changes or not
----@return boolean|nil
+---Return true if current file has changes, otherwise false.
+---@return boolean?
M.does_file_have_changes = function()
local file_data = M.get_current_file_data()
- return file_data.stats.additions > 0 or file_data.stats.deletions > 0
+ return file_data and (file_data.stats.additions > 0 or file_data.stats.deletions > 0)
end
---Run callback every time the buffer in one of the two reviewer windows changes.
----@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
+---@param callback fun(opts: table) For more information about opts see `callback` in :h nvim_create_autocmd
M.set_callback_for_file_changed = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.file_changed", {})
vim.api.nvim_create_autocmd("User", {
@@ -297,7 +291,7 @@ M.set_callback_for_file_changed = function(callback)
end
---Run callback the first time a new diff buffer is created and loaded into a window.
----@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
+---@param callback fun(opts: table) For more information about opts see `callback` in :h nvim_create_autocmd
M.set_callback_for_buf_read = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.buf_read", {})
vim.api.nvim_create_autocmd("User", {
@@ -306,7 +300,7 @@ M.set_callback_for_buf_read = function(callback)
callback = function(...)
-- Only run the callback when we're in the MR's tabpage or when the tabpage has
-- not yet been set (tabid = nil) in a freshly started review (is_open = true).
- -- TODO: This is a hacky workaround for cases when an added file is diffeed
+ -- FIXME: This is a hacky workaround for cases when an added file is diffed
-- against diffview://null and this autocommand fires before tabid is set.
if vim.api.nvim_get_current_tabpage() == M.tabid or (M.is_open and M.tabid == nil) then
callback(...)
@@ -316,7 +310,7 @@ M.set_callback_for_buf_read = function(callback)
end
---Run callback when the reviewer is closed or the user switches to another tab.
----@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
+---@param callback fun(opts: table) For more information about opts see `callback` in :h nvim_create_autocmd
M.set_callback_for_reviewer_leave = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.leave", {})
vim.api.nvim_create_autocmd("User", {
@@ -330,9 +324,9 @@ M.set_callback_for_reviewer_leave = function(callback)
})
end
----Run callback when the reviewer is opened for the first time or the view is entered from another
----tab page.
----@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
+---Run callback when the reviewer is opened for the first time or the view is entered
+---from another tab page.
+---@param callback fun(opts: table) For more information about opts see `callback` in :h nvim_create_autocmd
M.set_callback_for_reviewer_enter = function(callback)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.enter", {})
vim.api.nvim_create_autocmd("User", {
@@ -346,16 +340,19 @@ M.set_callback_for_reviewer_enter = function(callback)
})
end
----Create the line-wise visual selection in the range of the motion (or on the [count] number of
----lines) and execute the gitlab.nvim API function. After that, restore the cursor position and the
----original operatorfunc.
----@param callback string Name of the gitlab.nvim API function to call
-M.execute_callback = function(callback)
+-- TODO: Add callback for view_post_layout to update M.diffview and M.diffview_layout
+-- after switching layout.
+
+---Create the line-wise visual selection in the range of the motion (or on the [count]
+---number of lines) and execute the callback. After that, restore the cursor position
+---and the original operatorfunc.
+---@param cb string Name of the gitlab.nvim API function to call
+M.execute_callback = function(cb)
return function()
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { "'[V']" } }, {})
local _, err = pcall(
vim.api.nvim_cmd,
- { cmd = "lua", args = { ("require'gitlab'.%s()"):format(callback) }, mods = { lockmarks = true } },
+ { cmd = "lua", args = { ("require'gitlab'.%s()"):format(cb) }, mods = { lockmarks = true } },
{}
)
vim.api.nvim_win_set_cursor(M.old_winnr, M.old_cursor_position)
@@ -366,9 +363,9 @@ M.execute_callback = function(callback)
end
end
----Set the operatorfunc that will work on the lines defined by the motion that follows after the
----operator mapping, and enter the operator-pending mode.
----@param cb string Name of the gitlab.nvim API function to call, e.g., "create_multiline_comment".
+---Set the operatorfunc that will work on the lines defined by the motion that follows
+---after the operator mapping, and enter the operator-pending mode.
+---@param cb string Name of the gitlab.nvim API function to call, e.g., "create_multiline_comment"
local function execute_operatorfunc(cb)
M.old_opfunc = vim.opt.operatorfunc
M.old_winnr = vim.api.nvim_get_current_win()
@@ -380,7 +377,7 @@ local function execute_operatorfunc(cb)
end
---Set keymaps for creating comments, suggestions and for jumping to discussion tree.
----@param bufnr integer Number of the buffer for which the keybindings will be created.
+---@param bufnr integer Number of the buffer for which the keybindings will be created
M.set_keymaps = function(bufnr)
if bufnr == nil or not vim.api.nvim_buf_is_loaded(bufnr) then
return
@@ -394,6 +391,9 @@ M.set_keymaps = function(bufnr)
-- Set mappings for creating comments
if keymaps.reviewer.create_comment ~= false then
-- Set keymap for repeated operator keybinding
+ -- FIXME: Replace this by explicitly mapping repeated keymap in normal mode. The way it is now,
+ -- pressing `keymaps.reviewer.create_comment` in *any* operator-pending mode (e.g., after
+ -- pressing `d` for delete), triggers comment creation!
vim.keymap.set("o", keymaps.reviewer.create_comment, function()
-- 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) } }, {})
@@ -425,6 +425,7 @@ M.set_keymaps = function(bufnr)
-- Set mappings for creating suggestions
if keymaps.reviewer.create_suggestion ~= false then
-- Set keymap for repeated operator keybinding
+ -- FIXME: Fix the same problem as for keymaps.reviewer.create_comment.
vim.keymap.set("o", keymaps.reviewer.create_suggestion, function()
-- 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) } }, {})
@@ -464,7 +465,7 @@ M.set_keymaps = function(bufnr)
end
---Delete keymaps from reviewer buffers.
----@param bufnr integer Number of the buffer from which the keybindings will be removed.
+---@param bufnr integer Number of the buffer from which the keybindings will be removed
local del_keymaps = function(bufnr)
if bufnr == nil or not vim.api.nvim_buf_is_loaded(bufnr) then
return
@@ -486,7 +487,7 @@ local del_keymaps = function(bufnr)
end
end
---- Set up autocaommands that will take care of setting and unsetting buffer-local options and keymaps
+---Set up autocommands to set and unset buffer-local options and keymaps.
M.set_reviewer_autocommands = function(bufnr)
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.win_enter." .. bufnr, {})
vim.api.nvim_create_autocmd({ "WinEnter", "BufWinEnter" }, {
@@ -495,11 +496,12 @@ M.set_reviewer_autocommands = function(bufnr)
callback = function()
if vim.api.nvim_get_current_win() == M.buf_winids[bufnr] then
M.stored_win = vim.api.nvim_get_current_win()
- vim.api.nvim_buf_set_option(bufnr, "modifiable", false)
+ u.switch_can_edit_buf(bufnr, false)
M.set_keymaps(bufnr)
else
+ -- Only make the local file modifiable, not the diffview buffer for the old revision
if M.diffview_layout.b.id == M.buf_winids[bufnr] then
- vim.api.nvim_buf_set_option(bufnr, "modifiable", true)
+ u.switch_can_edit_buf(bufnr, true)
end
del_keymaps(bufnr)
end
@@ -507,10 +509,11 @@ M.set_reviewer_autocommands = function(bufnr)
})
end
---- Update the stored winid for a given reviewer buffer. This is necessary for the
---- M.set_reviewer_autocommands function to work correctly in cases like when the user closes one of
---- the original reviewer windows and Diffview automatically creates a new pair
---- of reviewer windows or the user wipes out a buffer and Diffview reloads it with a different ID.
+---Update the stored winid for a given reviewer buffer.
+---This is necessary for the M.set_reviewer_autocommands function to work correctly in
+---cases like when the user closes one of the original reviewer windows and Diffview
+---automatically creates a new pair of reviewer windows or the user wipes out a buffer
+---and Diffview reloads it with a different ID.
M.update_winid_for_buffer = function(bufnr)
M.buf_winids[bufnr] = vim.fn.bufwinid(bufnr)
end
diff --git a/lua/gitlab/reviewer/location.lua b/lua/gitlab/reviewer/location.lua
index a66f2058..87d65172 100755
--- a/lua/gitlab/reviewer/location.lua
+++ b/lua/gitlab/reviewer/location.lua
@@ -2,27 +2,33 @@ local u = require("gitlab.utils")
local hunks = require("gitlab.hunks")
local state = require("gitlab.state")
----@class Location
----@field location_data LocationData
----@field reviewer_data DiffviewInfo
----@field run function
----@field build_location_data function
----@field visual_range table
-
---@class ReviewerLineInfo
----@field old_line integer|nil
----@field new_line integer|nil
+---@field old_line? integer
+---@field new_line? integer
---@field type "new"|"old"
---@class ReviewerRangeInfo
---@field start ReviewerLineInfo
---@field end ReviewerLineInfo
+---@class LocationData
+---@field old_line? integer
+---@field new_line? integer
+---@field line_range? ReviewerRangeInfo
+
+---@class Location
+---@field location_data LocationData
+---@field reviewer_data DiffviewInfo
+---@field run function
+---@field build_location_data function
+---@field visual_range table
+
local Location = {}
Location.__index = Location
----The new() function returns nil when the location cannot be created due to missing
----reviewer data.
----@return Location | nil
+
+---Return information about the selection in the reviewer.
+---Return nil when the location cannot be created due to missing reviewer data.
+---@return Location?
function Location.new()
local current_win = vim.api.nvim_get_current_win()
local reviewer_data = require("gitlab.reviewer").get_reviewer_data(current_win)
@@ -38,8 +44,8 @@ function Location.new()
return instance
end
----Takes in information about the current changes, such as the file name, modification type of the diff, and the line numbers
----and builds the appropriate payload when creating a comment.
+---Build the payload for creating a comment based on the file name, modification type of
+---the diff, and line numbers.
function Location:build_location_data()
---@type DiffviewInfo
local reviewer_data = self.reviewer_data
@@ -71,6 +77,7 @@ function Location:build_location_data()
self.location_data.new_line = reviewer_data.new_line_from_buf
end
+ -- TODO: Don't skip line_range for single-line comments (Gitlab doesn't skip them either).
if end_line > start_line then
self.location_data.line_range = {
start = {},
@@ -80,8 +87,8 @@ function Location:build_location_data()
return
end
- self:set_start_range()
- self:set_end_range()
+ self:set_range_start()
+ self:set_range_end()
-- Ranged comments should always use the end of the range.
-- Otherwise they will not highlight the full comment in Gitlab.
@@ -91,14 +98,14 @@ end
-- Helper methods 🤝
--- Returns the matching line from the new SHA.
--- For instance, line 12 in the new SHA may be scroll-linked
--- to line 10 in the old SHA.
----@param line number
----@return number|nil
-function Location:get_line_number_from_new_sha(line)
+---Return the matching line number from the new version of the file.
+---For instance, line 12 in the new version may be scroll-linked to line 10 in the old
+---version.
+---@param linenr integer The starting or ending line of the current selection
+---@return integer?
+function Location:get_line_number_from_new_sha(linenr)
if self.reviewer_data.new_sha_focused then
- return line
+ return linenr
end
-- Otherwise we want to get the matching line in the opposite buffer
return hunks.calculate_matching_line_new(
@@ -106,18 +113,18 @@ function Location:get_line_number_from_new_sha(line)
self.head_sha,
self.reviewer_data.file_name,
self.reviewer_data.old_file_name,
- line
+ linenr
)
end
--- Returns the matching line from the old SHA.
--- For instance, line 12 in the new SHA may be scroll-linked
--- to line 10 in the old SHA.
----@param line number
----@return number|nil
-function Location:get_line_number_from_old_sha(line)
+---Return the matching line number from the old version of the file.
+---For instance, line 12 in the new version may be scroll-linked to line 10 in the old
+---version.
+---@param linenr integer The starting or ending line of the current selection
+---@return integer?
+function Location:get_line_number_from_old_sha(linenr)
if not self.reviewer_data.new_sha_focused then
- return line
+ return linenr
end
-- Otherwise we want to get the matching line in the opposite buffer
@@ -126,13 +133,13 @@ function Location:get_line_number_from_old_sha(line)
self.base_sha,
self.reviewer_data.file_name,
self.reviewer_data.old_file_name,
- line
+ linenr
)
end
--- Returns the current line number from whatever SHA (new or old)
--- the reviewer is focused in.
----@return number|nil
+---Return the current line number from whatever version (new or old) the reviewer is
+---focused in.
+---@return integer?
function Location:get_current_line()
if self.reviewer_data.current_win_id == nil then
return
@@ -142,9 +149,9 @@ function Location:get_current_line()
return current_line
end
--- Given a modification type, a visual selection range, and the hunk data, sets the start range
--- information to the location_data for the Gitlab payload
-function Location:set_start_range()
+---Set the range start to the location_data for the Gitlab payload based on the
+---modification type, visual selection range, and the hunk data.
+function Location:set_range_start()
local current_file = require("gitlab.reviewer").get_current_file_path()
if current_file == nil then
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
@@ -152,13 +159,13 @@ function Location:set_start_range()
end
if self.reviewer_data.current_win_id == nil then
- u.notify("Error getting window number of SHA for start range", vim.log.levels.ERROR)
+ u.notify("Error getting window number of SHA for start of range", vim.log.levels.ERROR)
return
end
local current_line = self:get_current_line()
if current_line == nil then
- u.notify("Error getting current line for start range", vim.log.levels.ERROR)
+ u.notify("Error getting current line for start of range", vim.log.levels.ERROR)
return
end
@@ -168,7 +175,7 @@ function Location:set_start_range()
(new_line == nil and self.reviewer_data.modification_type ~= "deleted")
or (old_line == nil and self.reviewer_data.modification_type ~= "added")
then
- u.notify("Error getting new or old line for start range", vim.log.levels.ERROR)
+ u.notify("Error getting new or old line for start of range", vim.log.levels.ERROR)
return
end
@@ -181,22 +188,31 @@ function Location:set_start_range()
self.location_data.line_range.start = {
new_line = modification_type ~= "deleted" and new_line or nil,
old_line = modification_type ~= "added" and old_line or nil,
+ -- FIXME: The type should only be "old" explicitly for comments on deleted lines.
+ -- For unchanged lines this should be empty. Apart from that, the modification type
+ -- can also be "expanded" (when commenting on lines that are more than 3 lines away
+ -- from any change, thus are on folded lines that the user expanded manually.
type = modification_type == "added" and "new" or "old",
}
end
--- Given a modification type, a visual selection range, and the hunk data, sets the end range
--- information to the location_data for the Gitlab payload
-function Location:set_end_range()
+---Set the range end to the location_data for the Gitlab payload based on the
+---modification type, visual selection range, and the hunk data.
+function Location:set_range_end()
local current_file = require("gitlab.reviewer").get_current_file_path()
if current_file == nil then
u.notify("Error getting current file from Diffview", vim.log.levels.ERROR)
return
end
+ if self.reviewer_data.current_win_id == nil then
+ u.notify("Error getting window number of SHA for end of range", vim.log.levels.ERROR)
+ return
+ end
+
local current_line = self:get_current_line()
if current_line == nil then
- u.notify("Error getting current line for end range", vim.log.levels.ERROR)
+ u.notify("Error getting current line for end of range", vim.log.levels.ERROR)
return
end
@@ -207,7 +223,7 @@ function Location:set_end_range()
(new_line == nil and self.reviewer_data.modification_type ~= "deleted")
or (old_line == nil and self.reviewer_data.modification_type ~= "added")
then
- u.notify("Error getting new or old line for end range", vim.log.levels.ERROR)
+ u.notify("Error getting new or old line for end of range", vim.log.levels.ERROR)
return
end
diff --git a/lua/gitlab/server.lua b/lua/gitlab/server.lua
index c26407d3..813cbc61 100644
--- a/lua/gitlab/server.lua
+++ b/lua/gitlab/server.lua
@@ -5,6 +5,7 @@ local state = require("gitlab.state")
local u = require("gitlab.utils")
local job = require("gitlab.job")
local version = require("gitlab.version")
+
local M = {}
-- Builds the binary if it doesn't exist, and starts the server. If the pre-existing binary has an older
@@ -38,7 +39,7 @@ M.build_and_start = function(callback)
end)
end
--- Starts the Go server and call the callback provided
+---Starts the Go server and call the callback provided
M.start = function(callback)
if state.settings.port ~= nil and state.settings.server.port == nil then
state.settings.server.port = state.settings.port
diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua
index 77437d8e..f264abb9 100644
--- a/lua/gitlab/state.lua
+++ b/lua/gitlab/state.lua
@@ -5,12 +5,14 @@
local u = require("gitlab.utils")
local List = require("gitlab.utils.list")
+
local M = {
ahead_behind = { nil, nil },
}
----Returns a gitlab token, and a gitlab URL. Used to connect to gitlab.
----@return string|nil, string|nil, string|nil
+---Return a gitlab token and a gitlab URL required to connect to Gitlab.
+---TODO: Remove the third return value - it is always nil.
+---@return string?, string?, string?
M.default_auth_provider = function()
local git = require("gitlab.git")
local base_path, err = M.settings.config_path, nil
@@ -41,9 +43,10 @@ M.default_auth_provider = function()
return auth_token, gitlab_url, err
end
---- These are the default settings for the plugin
+---The default settings for the plugin.
M.settings = {
auth_provider = M.default_auth_provider,
+ -- TODO: Remove file_separator, replace with u.path_separator in code.
file_separator = u.path_separator,
server = {
binary = nil,
@@ -197,6 +200,7 @@ M.settings = {
template_file = nil,
delete_branch = false,
squash = false,
+ -- TODO: The "fork" settings could be a config in .gitlab.nvim - they seem to be project-local
fork = {
enabled = false,
forked_project_id = nil,
@@ -289,8 +293,6 @@ M.settings = {
success = "✓",
failed = "",
},
- go_server_running = false,
- is_gitlab_project = false,
colors = {
discussion_tree = {
username = "Keyword",
@@ -312,6 +314,7 @@ M.settings = {
}
-- These are the initial states of the discussion trees
+-- TODO: Move to M definition.
M.discussion_tree = {
resolved_expanded = false,
unresolved_expanded = false,
@@ -322,9 +325,11 @@ M.unlinked_discussion_tree = {
}
-- Used to set a specific MR when choosing a merge request
+-- TODO: Move to M definition.
M.chosen_mr_iid = 0
--- These keymaps are set globally when the plugin is initialized
+---Set global keymaps.
+---To be used when the plugin is initialized.
M.set_global_keymaps = function()
local keymaps = M.settings.keymaps
@@ -332,6 +337,7 @@ M.set_global_keymaps = function()
return
end
+ -- TODO: Refactor: simplify all the `function() somefunc() end` calls to `somefunc`.
if keymaps.global.start_review then
vim.keymap.set("n", keymaps.global.start_review, function()
require("gitlab").review()
@@ -483,9 +489,9 @@ M.set_global_keymaps = function()
end
end
--- Merges user settings into the default settings, overriding them
----@param args Settings
----@return Settings
+---Merge user settings into the default settings, overriding the defaults.
+---@param args GitlabSettings
+---@return GitlabSettings
M.merge_settings = function(args)
if args.server and args.server.binary ~= nil then
M.settings.server.binary_provided = true
@@ -498,10 +504,9 @@ M.print_settings = function()
vim.print(M.settings)
end
--- First reads environment variables into the settings module,
--- then attemps to read a `.gitlab.nvim` configuration file.
--- If after doing this, any variables are missing, alerts the user.
--- The `.gitlab.nvim` configuration file takes precedence.
+---Load Gitlab auth token and URL into the settings and notify user if auth token is
+---missing.
+---@return boolean success True if plugin is already initialized or when initialization succeeds, otherwise false (if auth token is missing)
M.set_plugin_configuration = function()
if M.initialized then
return true
@@ -509,7 +514,7 @@ M.set_plugin_configuration = function()
local token, url, err = M.settings.auth_provider()
if err ~= nil then
- return
+ return false
end
M.settings.auth_token = token
@@ -527,11 +532,10 @@ M.set_plugin_configuration = function()
return true
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
--- for each of the actions to occur. This is necessary because some Gitlab behaviors (like
--- adding a reviewer) requires some initial state.
+-- GitlabDependency definitions to be passed to the async.sequence function, which calls
+-- them in sequence before performing an action. They are used to set global state
+-- that's required for each of the actions to occur.
+---@type GitlabDependencies
M.dependencies = {
user = {
endpoint = "/users/me",
@@ -630,6 +634,9 @@ M.dependencies = {
},
}
+---Load new state for a dependency and execute callback with the data it returns.
+---@param dep string The dependency name to re-load
+---@param cb fun(data) The function to call with the dependency data
M.load_new_state = function(dep, cb)
local job = require("gitlab.job")
local dependency = M.dependencies[dep]
@@ -648,10 +655,12 @@ M.load_new_state = function(dep, cb)
)
end
--- This function clears out all of the previously fetched data. It's used
--- to reset the plugin state when the Go server is restarted
+---Clear out all of the previously fetched data.
+---Used to reset plugin state when the Go server is restarted.
M.clear_data = function()
M.INFO = nil
+ -- FIXME: The following loop should use pairs instead of ipairs to actually clear the
+ -- state.
for _, dep in ipairs(M.dependencies) do
M[dep.state] = nil
end
diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua
index fcc4775e..366a097c 100644
--- a/lua/gitlab/utils/init.lua
+++ b/lua/gitlab/utils/init.lua
@@ -1,12 +1,15 @@
+-- This has become a huge garbage can for helper functions.
+-- TODO: Consider splitting this into meaningful modules.
+
local git = require("gitlab.git")
local List = require("gitlab.utils.list")
-local has_devicons, devicons = pcall(require, "nvim-web-devicons")
+
local M = {}
----Pulls out a list of values matching a given key from an array of tables
+---Pull out a list of values matching a given key from an array of tables.
---@param t table List of tables to search
---@param key string Value to search for in the list
----@return table List List of values that were extracted
+---@return table
M.extract = function(t, key)
local resultTable = {}
for _, value in ipairs(t) do
@@ -17,22 +20,12 @@ M.extract = function(t, key)
return resultTable
end
----Get the last word in a sentence
----@param sentence string The string to get the last word from
----@param divider string The regex to split the sentence by, defaults to whitespace
----@return string
-M.get_last_word = function(sentence, divider)
- local words = {}
- local pattern = string.format("([^%s]+)", divider or " ")
- for word in sentence:gmatch(pattern) do
- table.insert(words, word)
- end
- return words[#words] or ""
-end
-
----Return the first non-nil value in the input table, or nil
----@param values table The list of input values
----@return any
+---Return the first value in the input table or nil if there are no values in the table.
+---This is useful for cases where we want to get the first non-nil boolean value, but
+---`b ~= nil and b or c` would evaluate to `c` if `b` was `false`.
+---Note: Lua removes `nil` values from a table automatically.
+---@param values boolean[] The list of input values
+---@return boolean
M.get_first_non_nil_value = function(values)
for _, val in pairs(values) do
if val ~= nil then
@@ -41,25 +34,30 @@ M.get_first_non_nil_value = function(values)
end
end
----Returns whether a string ends with a substring
+---Return whether a string ends with a suffix (true if suffix is empty).
---@param str string
----@param ending string
+---@param suffix string
---@return boolean
-M.ends_with = function(str, ending)
- return ending == "" or str:sub(-#ending) == ending
+M.ends_with = function(str, suffix)
+ return suffix == "" or str:sub(-#suffix) == suffix
end
+---Return a copy of `input_table` with `value_to_remove` removed.
+---@generic T
+---@param input_table T[]
+---@param value_to_remove T
+---@return T[]
M.filter = function(input_table, value_to_remove)
- local resultTable = {}
+ local result = {}
for _, v in ipairs(input_table) do
if v ~= value_to_remove then
- table.insert(resultTable, v)
+ table.insert(result, v)
end
end
- return resultTable
+ return result
end
----Merges two deeply nested tables together, overriding values from the first with conflicts
+---Merge two deeply nested tables overriding values from the first in case of conflicts.
---@param defaults table The first table
---@param overrides table The second table
---@return table
@@ -70,12 +68,11 @@ M.merge = function(defaults, overrides)
return vim.tbl_deep_extend("force", defaults, overrides)
end
----Combines two list-like (non associative) tables, keeping values from both
----@param t1 table The first table
----@param ... table[] The first table
+---Combine list-like (non associative) tables in input order, keeping values from all.
+---@param ... table The tables to combine
---@return table
-M.combine = function(t1, ...)
- local result = t1
+M.combine = function(...)
+ local result = {}
local tables = { ... }
for _, t in ipairs(tables) do
for _, v in ipairs(t) do
@@ -85,7 +82,8 @@ M.combine = function(t1, ...)
return result
end
----Pluralizes the input word, e.g. "3 cows"
+---Pluralize the input word if necessary, e.g. "3 minutes", but "1 minute".
+---TODO: Fix "-1" which produces "-1 minutes".
---@param num integer The count of the item/word
---@param word string The word to pluralize
---@return string
@@ -93,8 +91,13 @@ M.pluralize = function(num, word)
return num .. string.format(" %s", word) .. ((num > 1 or num <= 0) and "s" or "")
end
---- Provides a human readable time since a given ISO date string
----@param date_string string -- The ISO time stamp to compare with the current time
+---Provide a human readable time since a given ISO date string.
+---TODO: Verify that time zone is handled correctly by this function. Current date is
+---calculated as Coordinated Universal Time, but the reference `date_string` seems to be
+---taken at face value. This should handle date_string formats like
+---"2026-06-23T15:31:08.521+02:00", and "2026-05-24T14:50:46.096Z"
+---@param date_string string The ISO time stamp to compare with the current time
+---@param current_date_table? osdate Only used in tests. Table with YYYY, MM, DD, HH, MM, SS, weekday (Sunday is 1), day of the year, and boolean daylight saving flag
---@return string
M.time_since = function(date_string, current_date_table)
local dt = current_date_table or os.date("!*t")
@@ -126,7 +129,8 @@ M.time_since = function(date_string, current_date_table)
end
end
----Spreads all the values from t2 into t1
+---Spread all the values from t2 into t1.
+---TODO: Replace with M.combine.
---@param t1 table The first table (gets the values)
---@param t2 table The second table
---@return table
@@ -138,7 +142,8 @@ M.spread = function(t1, t2)
return t1
end
----Returns the number of keys or values in a table
+---Return the number of keys or values in a table.
+---TODO: Replace with next().
---@param t table The table to count
---@return integer
M.table_size = function(t)
@@ -149,7 +154,7 @@ M.table_size = function(t)
return count
end
----Returns whether a given value is in a list or not
+---Return whether a given value is in a list or not.
---@param list table The list to search
---@return boolean
M.contains = function(list, search_value)
@@ -161,9 +166,10 @@ M.contains = function(list, search_value)
return false
end
----Splits a string by new lines and returns an iterator
+---Split a string by new lines and return an iterator.
+-- TODO: Replace newline hack with s:gmatch("[^\r\n]+"), add tests.
---@param s string The string to split
----@return table: An iterator object
+---@return fun():string new_lines The iterator object
M.split_by_new_lines = function(s)
if s:sub(-1) ~= "\n" then
s = s .. "\n"
@@ -171,7 +177,7 @@ M.split_by_new_lines = function(s)
return s:gmatch("(.-)\n") -- Match 0 or more (as few as possible) characters followed by a new line.
end
----Takes a string of lines and returns a table of lines
+---Take a string of newline-separated lines and return a table of lines.
---@param s string The string to parse
---@return table
M.lines_into_table = function(s)
@@ -182,9 +188,10 @@ M.lines_into_table = function(s)
return lines
end
--- Reverses the order of elements in a list
----@param list table The list to reverse
----@return table
+---Return a new list which is a copy of `list` with the order reversed.
+---@generic T
+---@param list T[] The list to reverse
+---@return T[]
M.reverse = function(list)
if #list == 0 then
return list
@@ -196,9 +203,9 @@ M.reverse = function(list)
return rev
end
----Returns the difference between a time offset and UTC time, in seconds
----@param offset string The offset to compare, e.g. -0500 for EST
----@return number
+---Return the value in seconds of a time offset.
+---@param offset string The offset to compare, e.g. "-0500" for EST
+---@return integer
M.offset_to_seconds = function(offset)
local sign, hours, minutes = offset:match("([%+%-])(%d%d)(%d%d)")
local offset_in_seconds = tonumber(hours) * 3600 + tonumber(minutes) * 60
@@ -208,7 +215,12 @@ M.offset_to_seconds = function(offset)
return offset_in_seconds
end
----Converts a UTC timestamp and offset to a human readable datestring
+---Convert a UTC timestamp and offset to a human readable datestring.
+---TODO: 1. Always called with vim.fn.strftime("%z") as `offset` outside of tests,
+--- consider using it as a "default" value
+--- 2. Simplify the triple date_string:match
+--- 3. Simplify the time zone offset calculation
+--- 4. Use YYYY-MM-DD instead of MM/DD/YYYY in return statement (https://xkcd.com/1179/)
---@param date_string string The time stamp
---@param offset string The offset of the user's local time zone, e.g. -0500 for EST
---@return string
@@ -256,7 +268,7 @@ M.format_to_local = function(date_string, offset)
return tostring(os.date("%m/%d/%Y at %H:%M", localTimestamp))
end
--- Returns a comma separated (human readable) list of values from a list of associative tables
+---Return a comma separated (human readable) list of values from a list of associative tables.
---@param list_of_tables table The list to traverse
---@param key string The key of the values to pull from the tables
---@return string
@@ -271,12 +283,12 @@ M.make_readable_list = function(list_of_tables, key)
return res
end
--- Returns the length of the longest string in a list of strings
----@param list table The list of strings
----@return number
-M.get_longest_string = function(list)
+---Return the length of the longest string in a list of strings.
+---@param strings string[]
+---@return integer
+M.get_max_length = function(strings)
local longest = 0
- for _, v in pairs(list) do
+ for _, v in pairs(strings) do
if vim.fn.strcharlen(v) > longest then
longest = vim.fn.strcharlen(v)
end
@@ -284,6 +296,12 @@ M.get_longest_string = function(list)
return longest
end
+---Return table `tbl` with function `f` applied to each value in the table.
+---TODO: The only use of this function can be replaced by M.extract(t, "name").
+---@generic K, V, U
+---@param tbl table
+---@param f fun(value: V): U
+---@return table
M.map = function(tbl, f)
local t = {}
for k, v in pairs(tbl) do
@@ -292,15 +310,24 @@ M.map = function(tbl, f)
return t
end
+---Notify user with a message with a prepended plugin identifier.
+---@param msg string
+---@param lvl vim.log.levels
M.notify = function(msg, lvl)
vim.notify("gitlab.nvim: " .. msg, lvl)
end
--- Re-raise Vimscript error message after removing existing message prefixes
+---Re-raise Vimscript error message after removing existing message prefixes.
+---This is used instead of plain M.notify to suppress double use of the gitlab.nvim
+---prefix in vimscript errors that called gitlab.nvim's API.
+---@param msg string
+---@param lvl vim.log.levels
M.notify_vim_error = function(msg, lvl)
M.notify(msg:gsub("^Vim:", ""):gsub("^gitlab.nvim: ", ""), lvl)
end
+---Return true when running on Windows.
+---@return boolean
M.is_windows = function()
if vim.fn.has("win32") == 1 or vim.fn.has("win32unix") == 1 then
return true
@@ -323,6 +350,9 @@ M.split_path = function(path)
return path_parts
end
+---Return the contents of a buffer as a string with lines separated by the '\n' character.
+---@param bufnr integer
+---@return string
M.get_buffer_text = function(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) then
return ""
@@ -332,12 +362,14 @@ M.get_buffer_text = function(bufnr)
return text
end
----Returns the number of lines in the buffer. Returns 1 even for empty buffers.
+---Return the number of lines in the buffer. Returns 1 even for empty buffers.
+---@param bufnr integer
+---@return integer
M.get_buffer_length = function(bufnr)
return #vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
end
----Convert string to corresponding boolean
+---Convert string to corresponding Boolean.
---@param str string
---@return boolean
M.string_to_bool = function(str)
@@ -350,7 +382,7 @@ M.string_to_bool = function(str)
return false
end
----Convert boolean to corresponding string
+---Convert Boolean to corresponding string.
---@param bool boolean
---@return string
M.bool_to_string = function(bool)
@@ -360,7 +392,7 @@ M.bool_to_string = function(bool)
return "false"
end
----Toggle boolean value
+---Toggle Boolean value.
---@param bool string
---@return string
M.toggle_string_bool = function(bool)
@@ -372,6 +404,7 @@ M.toggle_string_bool = function(bool)
["False"] = "True",
["FALSE"] = "TRUE",
}
+ -- TODO: Just do one replacement with "%s+".
bool = bool:gsub("^%s+", ""):gsub("%s+$", "")
local toggled = string_bools[bool]
if toggled == nil then
@@ -381,6 +414,7 @@ M.toggle_string_bool = function(bool)
return toggled
end
+---Simulate the user pressing the key in order to get into normal mode.
M.press_escape = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", false, true, true), "nx", false)
end
@@ -392,6 +426,12 @@ M.from_iso_format_date_to_timestamp = function(date_string)
local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
return os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
end
+
+---Return a copy of `a` without the items that are also in `b`.
+---@generic T
+---@param a T[]
+---@param b T[]
+---@return T[]
M.difference = function(a, b)
local set_b = {}
for _, val in ipairs(b) do
@@ -408,6 +448,13 @@ M.difference = function(a, b)
return not_included
end
+---@class ReadFileOpts
+---@field remove_newlines? boolean
+
+---Return the contents of a file as a string.
+---@param file_path string
+---@param opts? ReadFileOpts
+---@return string?
M.read_file = function(file_path, opts)
local file = io.open(file_path, "r")
if file == nil then
@@ -423,18 +470,26 @@ M.read_file = function(file_path, opts)
return file_contents
end
--- Returns the root path of the plugin (four levels up from this file: lua/gitlab/utils/init.lua)
+---Return the root path of the plugin (four levels up from this file: lua/gitlab/utils/init.lua)
+---@return string
M.get_root_path = function()
local path = debug.getinfo(1, "S").source:sub(2)
return vim.fn.fnamemodify(path, ":p:h:h:h:h")
end
-M.get_line_content = function(bufnr, start)
+---Return the specified line of the given (or current) buffer.
+---@param bufnr? integer The buffer to get the line from. If nil, the current buffer is used
+---@param linenr integer The 1-indexed line number to return
+---@return string
+M.get_line_content = function(bufnr, linenr)
local current_buffer = vim.api.nvim_get_current_buf()
- local lines = vim.api.nvim_buf_get_lines(bufnr ~= nil and bufnr or current_buffer, start - 1, start, false)
+ local lines = vim.api.nvim_buf_get_lines(bufnr ~= nil and bufnr or current_buffer, linenr - 1, linenr, false)
return lines[1]
end
+---Switch if buffer can be modified.
+---@param buf integer Buffer number
+---@param bool boolean The value to set
M.switch_can_edit_buf = function(buf, bool)
if not vim.api.nvim_buf_is_valid(buf) then
return
@@ -443,9 +498,9 @@ M.switch_can_edit_buf = function(buf, bool)
vim.api.nvim_set_option_value("readonly", not bool, { buf = buf })
end
--- Gets the window holding a buffer in the current tab page
----@param buffer_id number Id of a buffer
----@return integer|nil
+---Return the window holding a buffer in the current tab page.
+---@param buffer_id integer Id of a buffer
+---@return integer?
M.get_window_id_by_buffer_id = function(buffer_id)
local tabpage = vim.api.nvim_get_current_tabpage()
local windows = vim.api.nvim_tabpage_list_wins(tabpage)
@@ -456,6 +511,10 @@ M.get_window_id_by_buffer_id = function(buffer_id)
end)
end
+---Return the list of file and directory names in the given directory, sorted by the
+---last modification time from newest to oldest.
+---@param folder_path string
+---@return string[]? files
M.list_files_in_folder = function(folder_path)
if vim.fn.isdirectory(folder_path) == 0 then
return nil
@@ -486,8 +545,9 @@ M.list_files_in_folder = function(folder_path)
return files
end
----Check if current mode is visual mode
----@return boolean is_visual true if current mode is visual mode
+---Return true if current mode is visual mode, otherwise false.
+---TODO: Move to lua/gitlab/actions/comment.lua where is its only call site.
+---@return boolean
M.check_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode
if mode ~= "v" and mode ~= "V" then
@@ -498,7 +558,9 @@ M.check_visual_mode = function()
end
---Return start line and end line of visual selection.
----@return integer start,integer end Start line and end line
+---TODO: Move to `lua/gitlab/reviewer/location.lua`
+---@return integer
+---@return integer
M.get_visual_selection_boundaries = function()
local start_line = vim.fn.line("v")
local end_line = vim.fn.line(".")
@@ -508,10 +570,12 @@ M.get_visual_selection_boundaries = function()
return start_line, end_line
end
----Get icon for filename if nvim-web-devicons plugin is available otherwise return empty string
+---Get icon for filename if nvim-web-devicons plugin is available, otherwise return
+---empty string.
---@return string?
---@return string?
M.get_icon = function(filename)
+ local has_devicons, devicons = pcall(require, "nvim-web-devicons")
if has_devicons then
local extension = vim.fn.fnamemodify(filename, ":e")
local icon, icon_hl = devicons.get_icon(filename, extension, { default = true })
@@ -525,7 +589,8 @@ M.get_icon = function(filename)
end
end
----Return content between start_line and end_line
+---Return content between start_line and end_line.
+---TODO: Consider removing this thin wrapper.
---@param start_line integer
---@param end_line integer
---@return string[]
@@ -533,7 +598,7 @@ M.get_lines = function(start_line, end_line)
return vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
end
----Select a git branch and perform callback with the branch as an argument
+---Select a git branch and perform callback with the branch as an argument.
---@param cb function The callback to perform with the selected branch
M.select_target_branch = function(cb)
local all_branch_names = git.get_all_merge_targets()
@@ -549,6 +614,8 @@ M.select_target_branch = function(cb)
end)
end
+---Return the project's Gitlab URL.
+---@return string?
M.get_web_url = function()
local web_url = require("gitlab.state").INFO.web_url
if web_url ~= nil then
@@ -557,6 +624,7 @@ M.get_web_url = function()
M.notify("Could not get Gitlab URL", vim.log.levels.ERROR)
end
+---Open the `url` based on OS.
---@param url? string
M.open_in_browser = function(url)
if vim.fn.has("mac") == 1 then
@@ -570,7 +638,8 @@ M.open_in_browser = function(url)
end
end
----Combines two tables
+---Combine two tables.
+---TODO: Replace with M.combine.
---@param t1 table
---@param t2 table
---@return table
@@ -584,13 +653,17 @@ M.join = function(t1, t2)
end
return res
end
----Trims the trailing slash from a URL
+
+---Trim the trailing slash from a URL
---@param s string
---@return string
M.trim_slash = function(s)
return (s:gsub("/+$", ""))
end
+---Return an empty table if `data` is nil, otherwise return `data` unchanged.
+---@param data? table|vim.NIL
+---@return table
M.ensure_table = function(data)
if data == vim.NIL or data == nil then
return {}
@@ -598,9 +671,15 @@ M.ensure_table = function(data)
return data
end
-M.get_nested_field = function(table, field)
+---Return the value of a `field` from `tbl`.
+---Recurses into subtables, if the field name contains "." characters, e.g.,
+---get_nested_field({subtable = {field = 1}}, "subtable.field") will return `1`
+---@param tbl table
+---@param field string The field to return
+---@return any
+M.get_nested_field = function(tbl, field)
local subfield = string.match(field, "[^.]+")
- local subtable = table[subfield]
+ local subtable = tbl[subfield]
if subtable ~= nil then
local new_field = string.gsub(field, "^" .. subfield .. ".?", "")
if new_field ~= "" then
@@ -611,6 +690,7 @@ M.get_nested_field = function(table, field)
end
end
+---Open one fold level if there are closed folds under the cursor.
M.open_fold_under_cursor = function()
if vim.fn.foldclosed(vim.fn.line(".")) > -1 then
vim.cmd("normal! zo")
diff --git a/lua/gitlab/utils/list.lua b/lua/gitlab/utils/list.lua
index 775522ff..a7784e45 100644
--- a/lua/gitlab/utils/list.lua
+++ b/lua/gitlab/utils/list.lua
@@ -1,16 +1,23 @@
+---@class List
local List = {}
List.__index = List
+---Create a new List from a provided table or from an empty table.
+---@generic T
+---@param t? T[]
+---@return List
function List.new(t)
local list = t or {}
setmetatable(list, List)
return list
end
----Mutates a given list
+---Return a new list of elements mutated by func.
---@generic T
----@param func fun(v: T):T
----@return List @Returns a new list of elements mutated by func
+---@generic U
+---@param self List
+---@param func fun(v: T):U
+---@return List
function List:map(func)
local result = List.new()
for _, v in ipairs(self) do
@@ -19,10 +26,11 @@ function List:map(func)
return result
end
----Filters a given list
+---Return a new list of elements for which func returns true.
---@generic T
+---@param self List
---@param func fun(v: T, i: integer):boolean
----@return List @Returns a new list of elements for which func returns true
+---@return List
function List:filter(func)
local result = List.new()
for i, v in ipairs(self) do
@@ -33,10 +41,11 @@ function List:filter(func)
return result
end
----Partitions a given list into two lists
+---Return two lists: With elements for which func returns true, or false.
---@generic T
+---@param self List
---@param func fun(v: T, i: integer):boolean
----@return List, List @Returns two lists: the 1st with elements for which func returns true, the 2nd with elements for which it returns false
+---@return List, List
function List:partition(func)
local result_true = List.new()
local result_false = List.new()
@@ -50,6 +59,12 @@ function List:partition(func)
return result_true, result_false
end
+---Apply func to list elements to reduce the list to a single value.
+---@generic T, U
+---@param self List
+---@param func fun(agg: U, v: T, i: integer):U
+---@param agg U
+---@return U
function List:reduce(func, agg)
for i, v in ipairs(self) do
agg = func(agg, v, i)
@@ -57,12 +72,22 @@ function List:reduce(func, agg)
return agg
end
+---Sort the list in place based on a comparator func.
+---@generic T
+---@param self List
+---@param func fun(a: T, b: T):boolean
+---@return List
function List:sort(func)
local result = List.new(self)
table.sort(result, func)
return result
end
+---Return the first element in the list that satisfies the callback func, or nil.
+---@generic T
+---@param self List
+---@param func fun(a: T):boolean
+---@return T?
function List:find(func)
for _, v in ipairs(self) do
if func(v) == true then
@@ -72,6 +97,13 @@ function List:find(func)
return nil
end
+---Return a portion of the list between start and end indices.
+---@generic T
+---@param self List
+---@param first integer The index of the first item to include
+---@param last integer The index of the last item to include
+---@param step? integer The step to increment the index (default: 1)
+---@return List
function List:slice(first, last, step)
local sliced = List.new()
for i = first or 1, last or #self, step or 1 do
@@ -80,10 +112,11 @@ function List:slice(first, last, step)
return sliced
end
----Returns true if any of the elements can satisfy the callback
+---Return true if any of the elements can satisfy the callback func, otherwise false.
---@generic T
+---@param self List
---@param func fun(v: T, i: integer):boolean
----@return List @Returns a boolean
+---@return boolean
function List:includes(func)
for i, v in ipairs(self) do
if func(v, i) == true then
@@ -93,6 +126,10 @@ function List:includes(func)
return false
end
+---Return an iterator over the list's values.
+---@generic T
+---@param self List
+---@return T[]
function List:values()
local result = {}
for _, v in ipairs(self) do
diff --git a/lua/gitlab/version.lua b/lua/gitlab/version.lua
index 3f348847..1a9ab3d8 100644
--- a/lua/gitlab/version.lua
+++ b/lua/gitlab/version.lua
@@ -2,6 +2,8 @@ local M = {}
local minimum_go_version = "1.25.1"
+---Return true if Go is available and its version is supported by the plugin.
+---@return boolean
M.is_go_valid = function()
local has_go, go = pcall(vim.system, { "go", "version" })
if not has_go then
@@ -12,6 +14,8 @@ M.is_go_valid = function()
return go_version ~= nil and vim.version.ge(go_version, minimum_go_version)
end
+---Return error if the Go version is not valid.
+---@return string?
M.check_go_version = function()
local has_version = M.is_go_valid()
if not has_version then
diff --git a/tests/spec/util_spec.lua b/tests/spec/util_spec.lua
index 8ef8c8a1..abcd84d8 100644
--- a/tests/spec/util_spec.lua
+++ b/tests/spec/util_spec.lua
@@ -20,32 +20,6 @@ describe("utils/init.lua", function()
end)
end)
- describe("get_last_word", function()
- it("Returns the last word in a sentence", function()
- local sentence = "Hello world!"
- local got = u.get_last_word(sentence)
- local want = "world!"
- assert.are.same(want, got)
- end)
- it("Returns an empty string without text", function()
- local sentence = ""
- local got = u.get_last_word(sentence)
- local want = ""
- assert.are.same(want, got)
- end)
- it("Returns whole string w/out divider", function()
- local sentence = "Thisdoesnothavebreaks"
- local got = u.get_last_word(sentence)
- assert.are.same(sentence, got)
- end)
- it("Returns correct word w/ different divider", function()
- local sentence = "this|uses|a|different|divider"
- local got = u.get_last_word(sentence, "|")
- local want = "divider"
- assert.are.same(want, got)
- end)
- end)
-
describe("format_date", function()
local current_date = {
day = 19,