Files
gitlab.nvim/lua/gitlab/state.lua
Harrison (Harry) Cramer 9fc47bd3bc feat!: Implements default keybindings for all actions.
This is a breaking change to the way the plugin is configured. If users are using the old configuration the plugin will warn them which fields have been removed in their configuration. Old keybindings can be found here: https://github.com/harrisoncramer/gitlab.nvim/pull/340#issuecomment-2282756924 (#331)

feat: Customize discussion tree chevrons (#339)
2024-08-11 22:29:08 -04:00

663 lines
19 KiB
Lua

-- This module is responsible for holding and setting shared state between
-- modules, such as keybinding data and other settings and configuration.
-- This module is also responsible for ensuring that the state of the plugin
-- is valid via dependencies
local git = require("gitlab.git")
local u = require("gitlab.utils")
local M = {}
M.emoji_map = nil
---Returns a gitlab token, and a gitlab URL. Used to connect to gitlab.
---@return string|nil, string|nil, string|nil
M.default_auth_provider = function()
local base_path, err = M.settings.config_path, nil
if base_path == nil then
base_path, err = git.base_dir()
end
if err ~= nil then
return "", ""
end
local config_file_path = base_path .. M.settings.file_separator .. ".gitlab.nvim"
local config_file_content = u.read_file(config_file_path, { remove_newlines = true })
local file_properties = {}
if config_file_content ~= nil then
local file = assert(io.open(config_file_path, "r"))
for line in file:lines() do
for key, value in string.gmatch(line, "(.-)=(.-)$") do
file_properties[key] = value
end
end
end
local auth_token = file_properties.auth_token or os.getenv("GITLAB_TOKEN")
local gitlab_url = file_properties.gitlab_url or os.getenv("GITLAB_URL")
return auth_token, gitlab_url, err
end
-- These are the default settings for the plugin
M.settings = {
auth_provider = M.default_auth_provider,
port = nil, -- choose random port
debug = {
go_request = false,
go_response = false,
},
log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"),
config_path = nil,
reviewer = "diffview",
reviewer_settings = {
diffview = {
imply_local = false,
},
},
connection_settings = {
insecure = true,
},
attachment_dir = "",
keymaps = {
disable_all = false,
help = "g?",
global = {
disable_all = false,
add_assignee = "glaa",
delete_assignee = "glad",
add_label = "glla",
delete_label = "glld",
add_reviewer = "glra",
delete_reviewer = "glrd",
approve = "glA",
revoke = "glR",
merge = "glM",
create_mr = "glC",
choose_merge_request = "glc",
start_review = "glS",
summary = "gls",
copy_mr_url = "glu",
open_in_browser = "glo",
create_note = "gln",
pipeline = "glp",
toggle_discussions = "gld",
toggle_draft_mode = "glD",
publish_all_drafts = "glP",
},
popup = {
disable_all = false,
next_field = "<Tab>",
prev_field = "<S-Tab>",
perform_action = "ZZ",
perform_linewise_action = "ZA",
discard_changes = "ZQ",
},
discussion_tree = {
disable_all = false,
add_emoji = "Ea",
delete_emoji = "Ed",
delete_comment = "dd",
edit_comment = "e",
reply = "r",
toggle_resolved = "-",
jump_to_file = "o",
jump_to_reviewer = "a",
open_in_browser = "b",
copy_node_url = "u",
switch_view = "c",
toggle_tree_type = "i",
publish_draft = "P",
toggle_draft_mode = "D",
toggle_node = "t",
toggle_all_discussions = "T",
toggle_resolved_discussions = "R",
toggle_unresolved_discussions = "U",
refresh_data = "<C-R>",
print_node = "<leader>p",
},
reviewer = {
disable_all = false,
create_comment = "c",
create_suggestion = "s",
move_to_discussion_tree = "a",
},
},
popup = {
width = "40%",
height = "60%",
border = "rounded",
opacity = 1.0,
edit = nil,
comment = nil,
note = nil,
help = nil,
pipeline = nil,
reply = nil,
squash_message = nil,
temp_registers = {},
},
discussion_tree = {
expanders = {
expanded = "",
collapsed = "",
indentation = " ",
},
auto_open = true,
default_view = "discussions",
blacklist = {},
keep_current_open = false,
position = "left",
size = "20%",
relative = "editor",
resolved = "",
unresolved = "-",
tree_type = "simple",
draft_mode = false,
},
create_mr = {
target = nil,
template_file = nil,
delete_branch = false,
squash = false,
fork = {
enabled = false,
forked_project_id = nil,
},
title_input = {
width = 40,
border = "rounded",
},
},
choose_merge_request = {
open_reviewer = true,
},
info = {
enabled = true,
horizontal = false,
fields = {
"author",
"created_at",
"updated_at",
"merge_status",
"draft",
"conflicts",
"assignees",
"reviewers",
"pipeline",
"branch",
"target_branch",
"delete_branch",
"squash",
"labels",
},
},
discussion_signs = {
enabled = true,
skip_resolved_discussion = false,
severity = vim.diagnostic.severity.INFO,
virtual_text = false,
use_diagnostic_signs = true,
priority = 100,
icons = {
comment = "→|",
range = " |",
},
skip_old_revision_discussion = false,
},
pipeline = {
created = "",
pending = "",
preparing = "",
scheduled = "",
running = "",
canceled = "",
skipped = "",
success = "",
failed = "",
},
go_server_running = false,
is_gitlab_project = false,
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",
},
},
}
-- These are the initial states of the discussion trees
M.discussion_tree = {
resolved_expanded = false,
unresolved_expanded = false,
}
M.unlinked_discussion_tree = {
resolved_expanded = false,
unresolved_expanded = false,
}
-- These keymaps are set globally when the plugin is initialized
M.set_global_keymaps = function()
local keymaps = M.settings.keymaps
if keymaps.disable_all or keymaps.global.disable_all then
return
end
if keymaps.global.start_review then
vim.keymap.set("n", keymaps.global.start_review, function()
require("gitlab").review()
end, { desc = "Start Gitlab review", nowait = keymaps.global.start_review_nowait })
end
if keymaps.global.choose_merge_request then
vim.keymap.set("n", keymaps.global.choose_merge_request, function()
require("gitlab").choose_merge_request()
end, { desc = "Choose MR for review", nowait = keymaps.global.choose_merge_request_nowait })
end
if keymaps.global.summary then
vim.keymap.set("n", keymaps.global.summary, function()
require("gitlab").summary()
end, { desc = "Show MR summary", nowait = keymaps.global.summary_nowait })
end
if keymaps.global.approve then
vim.keymap.set("n", keymaps.global.approve, function()
require("gitlab").approve()
end, { desc = "Approve MR", nowait = keymaps.global.approve_nowait })
end
if keymaps.global.revoke then
vim.keymap.set("n", keymaps.global.revoke, function()
require("gitlab").revoke()
end, { desc = "Revoke approval", nowait = keymaps.global.revoke_nowait })
end
if keymaps.global.create_mr then
vim.keymap.set("n", keymaps.global.create_mr, function()
require("gitlab").create_mr()
end, { desc = "Create MR", nowait = keymaps.global.create_mr_nowait })
end
if keymaps.global.create_note then
vim.keymap.set("n", keymaps.global.create_note, function()
require("gitlab").create_note()
end, { desc = "Create MR note", nowait = keymaps.global.create_note_nowait })
end
if keymaps.global.toggle_discussions then
vim.keymap.set("n", keymaps.global.toggle_discussions, function()
require("gitlab").toggle_discussions()
end, { desc = "Toggle MR discussions", nowait = keymaps.global.toggle_discussions_nowait })
end
if keymaps.global.add_assignee then
vim.keymap.set("n", keymaps.global.add_assignee, function()
require("gitlab").add_assignee()
end, { desc = "Add MR assignee", nowait = keymaps.global.add_assignee_nowait })
end
if keymaps.global.delete_assignee then
vim.keymap.set("n", keymaps.global.delete_assignee, function()
require("gitlab").delete_assignee()
end, { desc = "Delete MR assignee", nowait = keymaps.global.delete_assignee_nowait })
end
if keymaps.global.add_label then
vim.keymap.set("n", keymaps.global.add_label, function()
require("gitlab").add_label()
end, { desc = "Add MR label", nowait = keymaps.global.add_label_nowait })
end
if keymaps.global.delete_label then
vim.keymap.set("n", keymaps.global.delete_label, function()
require("gitlab").delete_label()
end, { desc = "Delete MR label", nowait = keymaps.global.delete_label_nowait })
end
if keymaps.global.add_reviewer then
vim.keymap.set("n", keymaps.global.add_reviewer, function()
require("gitlab").add_reviewer()
end, { desc = "Add MR reviewer", nowait = keymaps.global.add_reviewer_nowait })
end
if keymaps.global.delete_reviewer then
vim.keymap.set("n", keymaps.global.delete_reviewer, function()
require("gitlab").delete_reviewer()
end, { desc = "Delete MR reviewer", nowait = keymaps.global.delete_reviewer_nowait })
end
if keymaps.global.pipeline then
vim.keymap.set("n", keymaps.global.pipeline, function()
require("gitlab").pipeline()
end, { desc = "Show MR pipeline status", nowait = keymaps.global.pipeline_nowait })
end
if keymaps.global.open_in_browser then
vim.keymap.set("n", keymaps.global.open_in_browser, function()
require("gitlab").open_in_browser()
end, { desc = "Open MR in browser", nowait = keymaps.global.open_in_browser_nowait })
end
if keymaps.global.merge then
vim.keymap.set("n", keymaps.global.merge, function()
require("gitlab").merge()
end, { desc = "Merge MR", nowait = keymaps.global.merge_nowait })
end
if keymaps.global.copy_mr_url then
vim.keymap.set("n", keymaps.global.copy_mr_url, function()
require("gitlab").copy_mr_url()
end, { desc = "Copy MR url", nowait = keymaps.global.copy_mr_url_nowait })
end
if keymaps.global.publish_all_drafts then
vim.keymap.set("n", keymaps.global.publish_all_drafts, function()
require("gitlab").publish_all_drafts()
end, { desc = "Publish all MR comment drafts", nowait = keymaps.global.publish_all_drafts_nowait })
end
if keymaps.global.toggle_draft_mode then
vim.keymap.set("n", keymaps.global.toggle_draft_mode, function()
require("gitlab").toggle_draft_mode()
end, { desc = "Toggle MR comment draft mode", nowait = keymaps.global.toggle_draft_mode_nowait })
end
end
-- Merges user settings into the default settings, overriding them
M.merge_settings = function(args)
M.settings = u.merge(M.settings, args)
-- Check deprecated settings and alert users!
if M.settings.dialogue ~= nil then
u.notify("The dialogue field has been deprecated, please remove it from your setup function", vim.log.levels.WARN)
end
if M.settings.reviewer == "delta" then
u.notify(
"Delta is no longer a supported reviewer, please use diffview and update your setup function",
vim.log.levels.ERROR
)
return false
end
local diffview_ok, _ = pcall(require, "diffview")
if not diffview_ok then
u.notify("Please install diffview, it is required")
return false
end
local removed_fields_in_user_config = {}
local removed_settings_fields = {
"discussion_tree.add_emoji",
"discussion_tree.copy_node_url",
"discussion_tree.delete_comment",
"discussion_tree.delete_emoji",
"discussion_tree.edit_comment",
"discussion_tree.jump_to_file",
"discussion_tree.jump_to_reviewer",
"discussion_tree.open_in_browser",
"discussion_tree.publish_draft",
"discussion_tree.refresh_data",
"discussion_tree.reply",
"discussion_tree.switch_view",
"discussion_tree.toggle_all_discussions",
"discussion_tree.toggle_draft_mode",
"discussion_tree.toggle_node",
"discussion_tree.toggle_resolved",
"discussion_tree.toggle_resolved_discussions",
"discussion_tree.toggle_tree_type",
"discussion_tree.toggle_unresolved_discussions",
"help",
"popup.keymaps.next_field",
"popup.keymaps.prev_field",
"popup.perform_action",
"popup.perform_linewise_action",
"review_pane", -- Only relevant for the Delta reviewer
}
for _, field in ipairs(removed_settings_fields) do
if u.get_nested_field(M.settings, field) ~= nil then
table.insert(removed_fields_in_user_config, field)
end
end
if #removed_fields_in_user_config ~= 0 then
u.notify(
"The following settings fields have been removed:\n" .. table.concat(removed_fields_in_user_config, "\n"),
vim.log.levels.WARN
)
end
M.settings.file_separator = (u.is_windows() and "\\" or "/")
return true
end
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.
M.setPluginConfiguration = function()
if M.initialized then
return true
end
local token, url, err = M.settings.auth_provider()
if err ~= nil then
return
end
M.settings.auth_token = token
M.settings.gitlab_url = u.trim_slash(url or "https://gitlab.com")
if M.settings.auth_token == nil then
vim.notify(
"Missing authentication token for Gitlab, please provide it as an environment variable or in the .gitlab.nvim file",
vim.log.levels.ERROR
)
return false
end
M.initialized = true
return true
end
local function exit(popup, opts)
if opts.action_before_exit and opts.cb ~= nil then
opts.cb()
popup:unmount()
else
popup:unmount()
if opts.cb ~= nil then
opts.cb()
end
end
end
-- These keymaps are buffer specific and are set dynamically when popups mount
M.set_popup_keymaps = function(popup, action, linewise_action, opts)
if M.settings.keymaps.disable_all or M.settings.keymaps.popup.disable_all then
return
end
if opts == nil then
opts = {}
end
if action ~= "Help" and M.settings.keymaps.help then -- Don't show help on the help popup
vim.keymap.set("n", M.settings.keymaps.help, function()
local help = require("gitlab.actions.help")
help.open()
end, { buffer = popup.bufnr, desc = "Open help", nowait = M.settings.keymaps.help_nowait })
end
if action ~= nil and M.settings.keymaps.popup.perform_action then
vim.keymap.set("n", M.settings.keymaps.popup.perform_action, function()
local text = u.get_buffer_text(popup.bufnr)
if opts.action_before_close then
action(text, popup.bufnr)
exit(popup, opts)
else
exit(popup, opts)
action(text, popup.bufnr)
end
end, { buffer = popup.bufnr, desc = "Perform action", nowait = M.settings.keymaps.popup.perform_action_nowait })
end
if linewise_action ~= nil and M.settings.keymaps.popup.perform_action then
vim.keymap.set("n", M.settings.keymaps.popup.perform_linewise_action, function()
local bufnr = vim.api.nvim_get_current_buf()
local linnr = vim.api.nvim_win_get_cursor(0)[1]
local text = u.get_line_content(bufnr, linnr)
linewise_action(text)
end, {
buffer = popup.bufnr,
desc = "Perform linewise action",
nowait = M.settings.keymaps.popup.perform_linewise_action_nowait,
})
end
if M.settings.keymaps.popup.discard_changes then
vim.keymap.set("n", M.settings.keymaps.popup.discard_changes, function()
local temp_registers = M.settings.popup.temp_registers
M.settings.popup.temp_registers = {}
vim.cmd("quit!")
M.settings.popup.temp_registers = temp_registers
end, {
buffer = popup.bufnr,
desc = "Quit discarding changes",
nowait = M.settings.keymaps.popup.discard_changes_nowait,
})
end
if opts.save_to_temp_register then
vim.api.nvim_create_autocmd("BufWinLeave", {
buffer = popup.bufnr,
callback = function()
local text = u.get_buffer_text(popup.bufnr)
for _, register in ipairs(M.settings.popup.temp_registers) do
vim.fn.setreg(register, text)
end
end,
})
end
if opts.action_before_exit then
vim.api.nvim_create_autocmd("BufWinLeave", {
buffer = popup.bufnr,
callback = function()
exit(popup, opts)
end,
})
end
end
-- Dependencies
-- These tables are passed to the async.sequence function, which calls them in sequence
-- before calling an action. They are used to set global state that's required
-- for each of the actions to occur. This is necessary because some Gitlab behaviors (like
-- adding a reviewer) requires some initial state.
M.dependencies = {
user = {
endpoint = "/users/me",
key = "user",
state = "USER",
refresh = false,
},
info = {
endpoint = "/mr/info",
key = "info",
state = "INFO",
refresh = false,
},
latest_pipeline = {
endpoint = "/pipeline",
key = "latest_pipeline",
state = "PIPELINE",
refresh = true,
},
labels = {
endpoint = "/mr/label",
key = "labels",
state = "LABELS",
refresh = false,
},
revisions = {
endpoint = "/mr/revisions",
key = "Revisions",
state = "MR_REVISIONS",
refresh = false,
},
draft_notes = {
endpoint = "/mr/draft_notes/",
key = "draft_notes",
state = "DRAFT_NOTES",
refresh = false,
},
project_members = {
endpoint = "/project/members",
key = "ProjectMembers",
state = "PROJECT_MEMBERS",
refresh = false,
},
merge_requests = {
endpoint = "/merge_requests",
key = "merge_requests",
state = "MERGE_REQUESTS",
refresh = false,
},
discussion_data = {
-- key is missing here...
endpoint = "/mr/discussions/list",
state = "DISCUSSION_DATA",
refresh = false,
method = "POST",
body = function()
return {
blacklist = M.settings.discussion_tree.blacklist,
}
end,
},
}
M.load_new_state = function(dep, cb)
local job = require("gitlab.job")
local dependency = M.dependencies[dep]
job.run_job(
dependency.endpoint,
dependency.method or "GET",
dependency.body and dependency.body() or nil,
function(data)
if dependency.key then
M[dependency.state] = u.ensure_table(data[dependency.key])
end
if type(cb) == "function" then
cb(data) -- To set data manually...
end
end
)
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
M.clear_data = function()
M.INFO = nil
for _, dep in ipairs(M.dependencies) do
M[dep.state] = nil
end
end
return M