* Feat: Enable sorting discussions by original comment (#422)
* Feat: Improve popup UX (#426)
* Feat: Automatically update MR summary details (#427)
* Feat: Show update progress in winbar (#432)
* Feat: Abbreviate winbar (#439)
* Fix: Note Creation Bug (#441)
* Fix: Checking whether comment can be created (#434)
* Fix: Syntax in discussion tree (#433)
* fix: improve indication of resolved threads and drafts (#442)
* Docs: Various minor improvements (#445)

---------

Co-authored-by: Jakub F. Bortlík <jakub.bortlik@proton.me>
This commit is contained in:
Harrison (Harry) Cramer
2024-12-11 14:21:50 -05:00
committed by GitHub
parent be027331e1
commit 495e64c8bc
32 changed files with 880 additions and 564 deletions

View File

@@ -1,13 +1,26 @@
local job = require("gitlab.job")
local state = require("gitlab.state")
local u = require("gitlab.utils")
local M = {}
local refresh_status_state = function(data)
u.notify(data.message, vim.log.levels.INFO)
state.load_new_state("info", function()
require("gitlab.actions.summary").update_summary_details()
end)
end
M.approve = function()
job.run_job("/mr/approve", "POST")
job.run_job("/mr/approve", "POST", nil, function(data)
refresh_status_state(data)
end)
end
M.revoke = function()
job.run_job("/mr/revoke", "POST")
job.run_job("/mr/revoke", "POST", nil, function(data)
refresh_status_state(data)
end)
end
return M

View File

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

View File

@@ -3,10 +3,10 @@
--- to this module the data required to make the API calls
local Popup = require("nui.popup")
local Layout = require("nui.layout")
local diffview_lib = require("diffview.lib")
local state = require("gitlab.state")
local job = require("gitlab.job")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local git = require("gitlab.git")
local discussions = require("gitlab.actions.discussions")
local draft_notes = require("gitlab.actions.draft_notes")
@@ -46,6 +46,18 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
return
end
-- Creating a draft reply, in response to a discussion ID
if discussion_id ~= nil and is_draft then
local body = { comment = text, discussion_id = discussion_id }
job.run_job("/mr/draft_notes/", "POST", body, function()
u.notify("Draft reply created!", vim.log.levels.INFO)
draft_notes.load_draft_notes(function()
discussions.rebuild_view(unlinked)
end)
end)
return
end
-- Creating a note (unlinked comment)
if unlinked and discussion_id == nil then
local body = { comment = text }
@@ -89,18 +101,6 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion
line_range = location_data.line_range,
}
-- Creating a draft reply, in response to a discussion ID
if discussion_id ~= nil and is_draft then
local body = { comment = text, discussion_id = discussion_id, position = position_data }
job.run_job("/mr/draft_notes/", "POST", body, function()
u.notify("Draft reply created!", vim.log.levels.INFO)
draft_notes.load_draft_notes(function()
discussions.rebuild_view(unlinked)
end)
end)
return
end
-- Creating a new comment (linked to specific changes)
local body = u.merge({ type = "text", comment = text }, position_data)
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
@@ -157,52 +157,26 @@ end
---multi-line comment. It also sets up the basic keybindings for switching between
---window panes, and for the non-primary sections.
---@param opts LayoutOpts
---@return NuiLayout|nil
---@return NuiLayout
M.create_comment_layout = function(opts)
if opts.unlinked ~= true and opts.discussion_id == nil then
-- Check that diffview is initialized
if reviewer.tabnr == nil then
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
return
end
-- Check that Diffview is the current view
local view = diffview_lib.get_current_view()
if view == nil and not opts.reply then
u.notify("Comments should be left in the reviewer pane", vim.log.levels.ERROR)
return
end
-- Check that we are in the diffview tab
local tabnr = vim.api.nvim_get_current_tabpage()
if tabnr ~= reviewer.tabnr then
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
return
end
-- Check that the file has not been renamed
if reviewer.is_file_renamed() and not reviewer.does_file_have_changes() then
u.notify("Commenting on (unchanged) renamed or moved files is not supported", vim.log.levels.WARN)
return
end
-- Check that we are hovering over the code
local filetype = vim.bo[0].filetype
if not opts.reply and (filetype == "DiffviewFiles" or filetype == "gitlab") then
u.notify(
"Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
vim.log.levels.ERROR
)
return
end
local popup_settings = state.settings.popup
local title
local user_settings
if opts.discussion_id ~= nil then
title = "Reply"
user_settings = popup_settings.reply
elseif opts.unlinked then
title = "Note"
user_settings = popup_settings.note
else
title = "Comment"
user_settings = popup_settings.comment
end
local title = opts.discussion_id and "Reply" or "Comment"
local settings = opts.discussion_id ~= nil and state.settings.popup.reply or state.settings.popup.comment
local settings = u.merge(popup_settings, user_settings or {})
M.current_win = vim.api.nvim_get_current_win()
M.comment_popup = Popup(u.create_popup_state(title, settings))
M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
M.comment_popup = Popup(popup.create_popup_state(title, settings))
M.draft_popup = Popup(popup.create_box_popup_state("Draft", false, settings))
M.start_line, M.end_line = u.get_visual_selection_boundaries()
local internal_layout = Layout.Box({
@@ -211,98 +185,69 @@ M.create_comment_layout = function(opts)
}, { dir = "col" })
local layout = Layout({
position = "50%",
position = settings.position,
relative = "editor",
size = {
width = "50%",
height = "55%",
width = settings.width,
height = settings.height,
},
}, internal_layout)
miscellaneous.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
popup.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
popup.set_up_autocommands(M.comment_popup, layout, M.current_win)
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
local unlinked = opts.unlinked or false
---Keybinding for focus on draft section
state.set_popup_keymaps(M.draft_popup, function()
popup.set_popup_keymaps(M.draft_popup, function()
local text = u.get_buffer_text(M.comment_popup.bufnr)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
end, miscellaneous.toggle_bool, miscellaneous.non_editable_popup_opts)
end, miscellaneous.toggle_bool, popup.non_editable_popup_opts)
---Keybinding for focus on text section
state.set_popup_keymaps(M.comment_popup, function(text)
popup.set_popup_keymaps(M.comment_popup, function(text)
confirm_create_comment(text, range, unlinked, opts.discussion_id)
vim.api.nvim_set_current_win(M.current_win)
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
end, miscellaneous.attach_file, popup.editable_popup_opts)
vim.schedule(function()
local draft_mode = state.settings.discussion_tree.draft_mode
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
end)
--Send back to previous window on close
vim.api.nvim_create_autocmd("BufHidden", {
buffer = M.draft_popup.bufnr,
callback = function()
vim.api.nvim_set_current_win(M.current_win)
end,
})
return layout
end
--- This function will open a comment popup in order to create a comment on the changed/updated
--- line in the current MR
M.create_comment = function()
local has_clean_tree, err = git.has_clean_tree()
if err ~= nil then
return
end
local is_modified = vim.bo[0].modified
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
u.notify(
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
vim.log.levels.WARN
)
return
end
if not M.sha_exists() then
if not M.can_create_comment(false) then
return
end
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
if layout ~= nil then
layout:mount()
end
layout:mount()
end
--- This function will open a multi-line comment popup in order to create a multi-line comment
--- on the changed/updated line in the current MR
M.create_multiline_comment = function()
if not u.check_visual_mode() then
return
end
if not M.sha_exists() then
if not M.can_create_comment(true) then
u.press_escape()
return
end
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
if layout ~= nil then
layout:mount()
end
layout:mount()
end
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
--- on the changed/updated line in the current MR
M.create_note = function()
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
if layout ~= nil then
layout:mount()
end
layout:mount()
end
---Given the current visually selected area of text, builds text to fill in the
@@ -347,21 +292,16 @@ end
--- on the changed/updated line in the current MR
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
M.create_comment_suggestion = function()
if not u.check_visual_mode() then
return
end
if not M.sha_exists() then
if not M.can_create_comment(true) then
u.press_escape()
return
end
local suggestion_lines, range_length = build_suggestion()
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
if layout ~= nil then
layout:mount()
else
return -- Failure in creating the comment layout
end
layout:mount()
vim.schedule(function()
if suggestion_lines then
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
@@ -369,6 +309,68 @@ M.create_comment_suggestion = function()
end)
end
---Returns true if it's possible to create an Inline Comment
---@param must_be_visual boolean True if current mode must be visual
---@return boolean
M.can_create_comment = function(must_be_visual)
-- Check that diffview is initialized
if reviewer.tabnr == nil then
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
return false
end
-- Check that we are in the Diffview tab
local tabnr = vim.api.nvim_get_current_tabpage()
if tabnr ~= reviewer.tabnr then
u.notify("Comments can only be left in the reviewer pane", vim.log.levels.ERROR)
return false
end
-- Check that we are hovering over the code
local filetype = vim.bo[0].filetype
if filetype == "DiffviewFiles" or filetype == "gitlab" then
u.notify(
"Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
vim.log.levels.ERROR
)
return false
end
-- Check that the file has not been renamed
if reviewer.is_file_renamed() and not reviewer.does_file_have_changes() then
u.notify("Commenting on (unchanged) renamed or moved files is not supported", vim.log.levels.ERROR)
return false
end
-- Check that we are in a valid buffer
if not M.sha_exists() then
return false
end
-- Check that there aren't saved modifications
local file = reviewer.get_current_file_path()
if file == nil then
return false
end
local has_changes, err = git.has_changes(file)
if err ~= nil then
return false
end
-- Check that there aren't unsaved modifications
local is_modified = vim.bo[0].modified
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or has_changes) then
u.notify("Cannot leave comments on changed files, please stash or commit and push", vim.log.levels.ERROR)
return false
end
-- Check we're in visual mode for code suggestions and multiline comments
if must_be_visual and not u.check_visual_mode() then
return false
end
return true
end
---Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
---buffers as 'null'
---@return boolean

View File

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

View File

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

View File

@@ -7,12 +7,12 @@ local Popup = require("nui.popup")
local NuiTree = require("nui.tree")
local job = require("gitlab.job")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local common = require("gitlab.actions.common")
local List = require("gitlab.utils.list")
local tree_utils = require("gitlab.actions.discussions.tree")
local miscellaneous = require("gitlab.actions.miscellaneous")
local discussions_tree = require("gitlab.actions.discussions.tree")
local draft_notes = require("gitlab.actions.draft_notes")
local diffview_lib = require("diffview.lib")
@@ -48,13 +48,15 @@ M.rebuild_view = function(unlinked, all)
else
M.rebuild_discussion_tree()
end
M.refresh_diagnostics_and_winbar()
state.discussion_tree.last_updated = os.time()
M.refresh_diagnostics()
end)
end
---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil
M.load_discussions = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("discussion_data", function(data)
if not state.DISCUSSION_DATA then
state.DISCUSSION_DATA = {}
@@ -70,9 +72,10 @@ end
---Initialize everything for discussions like setup of signs, callbacks for reviewer, etc.
M.initialize_discussions = function()
state.discussion_tree.last_updated = os.time()
signs.setup_signs()
reviewer.set_callback_for_file_changed(function()
M.refresh_diagnostics_and_winbar()
M.refresh_diagnostics()
M.modifiable(false)
reviewer.set_reviewer_keymaps()
end)
@@ -102,11 +105,10 @@ M.modifiable = function(bool)
end
--- Take existing data and refresh the diagnostics, the winbar, and the signs
M.refresh_diagnostics_and_winbar = function()
M.refresh_diagnostics = function()
if state.settings.discussion_signs.enabled then
diagnostics.refresh_diagnostics()
end
winbar.update_winbar()
common.add_empty_titles()
end
@@ -154,7 +156,7 @@ M.open = function(callback)
end
vim.schedule(function()
M.refresh_diagnostics_and_winbar()
M.refresh_diagnostics()
end)
end
@@ -251,9 +253,7 @@ M.reply = function(tree)
reply = true,
})
if layout then
layout:mount()
end
layout:mount()
end
-- This function (settings.keymaps.discussion_tree.delete_comment) will trigger a popup prompting you to delete the current comment
@@ -284,7 +284,7 @@ end
-- This function (settings.keymaps.discussion_tree.edit_comment) will open the edit popup for the current comment in the discussion tree
M.edit_comment = function(tree, unlinked)
local edit_popup = Popup(u.create_popup_state("Edit Comment", state.settings.popup.edit))
local edit_popup = Popup(popup.create_popup_state("Edit Comment", state.settings.popup.edit))
local current_node = tree:get_node()
local note_node = common.get_note_node(tree, current_node)
local root_node = common.get_root_node(tree, current_node)
@@ -293,6 +293,8 @@ M.edit_comment = function(tree, unlinked)
return
end
popup.set_up_autocommands(edit_popup, nil, vim.api.nvim_get_current_win())
edit_popup:mount()
-- Gather all lines from immediate children that aren't note nodes
@@ -310,19 +312,19 @@ M.edit_comment = function(tree, unlinked)
-- Draft notes module handles edits for draft notes
if M.is_draft_note(tree) then
state.set_popup_keymaps(
popup.set_popup_keymaps(
edit_popup,
draft_notes.confirm_edit_draft_note(note_node.id, unlinked),
nil,
miscellaneous.editable_popup_opts
popup.editable_popup_opts
)
else
local comment = require("gitlab.actions.comment")
state.set_popup_keymaps(
popup.set_popup_keymaps(
edit_popup,
comment.confirm_edit_comment(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked),
nil,
miscellaneous.editable_popup_opts
popup.editable_popup_opts
)
end
end
@@ -585,7 +587,7 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.jump_to_reviewer then
vim.keymap.set("n", keymaps.discussion_tree.jump_to_reviewer, function()
if M.is_current_node_note(tree) then
common.jump_to_reviewer(tree, M.refresh_diagnostics_and_winbar)
common.jump_to_reviewer(tree, M.refresh_diagnostics)
end
end, { buffer = bufnr, desc = "Jump to reviewer", nowait = keymaps.discussion_tree.jump_to_reviewer_nowait })
end
@@ -603,7 +605,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
if keymaps.discussion_tree.refresh_data then
vim.keymap.set("n", keymaps.discussion_tree.refresh_data, function()
u.notify("Refreshing data...", vim.log.levels.INFO)
draft_notes.rebuild_view(unlinked, false)
end, {
buffer = bufnr,
@@ -646,6 +647,16 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
})
end
if keymaps.discussion_tree.toggle_sort_method then
vim.keymap.set("n", keymaps.discussion_tree.toggle_sort_method, function()
M.toggle_sort_method()
end, {
buffer = bufnr,
desc = "Toggle sort method",
nowait = keymaps.discussion_tree.toggle_sort_method_nowait,
})
end
if keymaps.discussion_tree.toggle_resolved then
vim.keymap.set("n", keymaps.discussion_tree.toggle_resolved, function()
if M.is_current_node_note(tree) and not M.is_draft_note(tree) then
@@ -746,16 +757,6 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
})
end
if keymaps.discussion_tree.print_node then
vim.keymap.set("n", keymaps.discussion_tree.print_node, function()
common.print_node(tree)
end, {
buffer = bufnr,
desc = "Print current node (for debugging)",
nowait = keymaps.discussion_tree.print_node_nowait,
})
end
if keymaps.discussion_tree.add_emoji then
vim.keymap.set("n", keymaps.discussion_tree.add_emoji, function()
M.add_emoji_to_note(tree, unlinked)
@@ -792,7 +793,18 @@ end
---Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
M.toggle_draft_mode = function()
state.settings.discussion_tree.draft_mode = not state.settings.discussion_tree.draft_mode
end
---Toggle between sorting by "original comment" (oldest at the top) or "latest reply" (newest at the
---top).
M.toggle_sort_method = function()
if state.settings.discussion_tree.sort_by == "original_comment" then
state.settings.discussion_tree.sort_by = "latest_reply"
else
state.settings.discussion_tree.sort_by = "original_comment"
end
winbar.update_winbar()
M.rebuild_view(false, true)
end
---Indicates whether the node under the cursor is a draft note or not

View File

@@ -277,13 +277,16 @@ local function build_note_body(note, resolve_info)
)
end
local resolve_symbol = ""
local symbol = ""
local is_draft = note.note ~= nil
if resolve_info ~= nil and resolve_info.resolvable then
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
or state.settings.discussion_tree.unresolved
elseif not is_draft and resolve_info and not resolve_info.resolvable then
symbol = state.settings.discussion_tree.unlinked
end
local noteHeader = common.build_note_header(note) .. " " .. resolve_symbol
local noteHeader = common.build_note_header(note) .. " " .. symbol
return noteHeader, text_nodes
end
@@ -454,7 +457,9 @@ M.restore_cursor_position = function(winid, tree, original_node, root_node)
end
end
if line_number ~= nil then
vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
end
end
end

View File

@@ -54,7 +54,19 @@ local get_data = function(nodes)
return total_resolvable, total_resolved, total_non_resolvable
end
local spinner_index = 0
state.discussion_tree.last_updated = nil
local function content()
local updated
if state.discussion_tree.last_updated then
local last_update = tostring(os.date("!%Y-%m-%dT%H:%M:%S", state.discussion_tree.last_updated))
updated = u.time_since(last_update) .. ""
else
spinner_index = (spinner_index % #state.settings.discussion_tree.spinner_chars) + 1
updated = state.settings.discussion_tree.spinner_chars[spinner_index]
end
local resolvable_discussions, resolved_discussions, non_resolvable_discussions =
get_data(state.DISCUSSION_DATA.discussions)
local resolvable_notes, resolved_notes, non_resolvable_notes = get_data(state.DISCUSSION_DATA.unlinked_discussions)
@@ -82,9 +94,10 @@ local function content()
resolved_notes = resolved_notes,
non_resolvable_notes = non_resolvable_notes,
help_keymap = state.settings.keymaps.help,
updated = updated,
}
return M.make_winbar(t)
return state.settings.discussion_tree.winbar and state.settings.discussion_tree.winbar(t) or M.make_winbar(t)
end
---This function updates the winbar
@@ -108,7 +121,7 @@ M.update_winbar = function()
end
local function get_connector(base_title)
return string.match(base_title, "%($") and "" or "; "
return string.match(base_title, "%($") and "" or " "
end
---Builds the title string for both sections, using the count of resolvable and draft nodes
@@ -116,84 +129,128 @@ end
---@param resolvable_count integer
---@param resolved_count integer
---@param drafts_count integer
---@param focused boolean
---@return string
local add_drafts_and_resolvable = function(
base_title,
resolvable_count,
resolved_count,
drafts_count,
non_resolvable_count
non_resolvable_count,
focused
)
if resolvable_count == 0 and drafts_count == 0 and non_resolvable_count == 0 then
return base_title
end
base_title = base_title .. " ("
if non_resolvable_count ~= 0 then
base_title = base_title .. u.pluralize(non_resolvable_count, "comment")
end
if resolvable_count ~= 0 then
base_title = base_title
.. get_connector(base_title)
.. string.format("%d/%s", resolved_count, u.pluralize(resolvable_count, "thread"))
base_title = base_title .. M.get_resolved_text(focused, resolved_count, resolvable_count)
end
if non_resolvable_count ~= 0 then
base_title = base_title .. M.get_nonresolveable_text(base_title, non_resolvable_count, focused)
end
if drafts_count ~= 0 then
base_title = base_title .. get_connector(base_title) .. u.pluralize(drafts_count, "draft")
base_title = base_title .. M.get_drafts_text(base_title, drafts_count, focused)
end
base_title = base_title .. ")"
return base_title
end
---@param t WinbarTable
M.make_winbar = function(t)
local discussion_title = add_drafts_and_resolvable(
"Inline Comments",
local discussions_focused = M.current_view_type == "discussions"
local discussion_text = add_drafts_and_resolvable(
"Inline Comments:",
t.resolvable_discussions,
t.resolved_discussions,
t.inline_draft_notes,
t.non_resolvable_discussions
t.non_resolvable_discussions,
discussions_focused
)
local notes_title = add_drafts_and_resolvable(
"Notes",
local notes_text = add_drafts_and_resolvable(
"Notes:",
t.resolvable_notes,
t.resolved_notes,
t.unlinked_draft_notes,
t.non_resolvable_notes
t.non_resolvable_notes,
not discussions_focused
)
-- Colorize the active tab
if M.current_view_type == "discussions" then
discussion_title = "%#Text#" .. discussion_title
notes_title = "%#Comment#" .. notes_title
elseif M.current_view_type == "notes" then
discussion_title = "%#Comment#" .. discussion_title
notes_title = "%#Text#" .. notes_title
if discussions_focused then
discussion_text = "%#Text#" .. discussion_text
notes_text = "%#Comment#" .. notes_text
else
discussion_text = "%#Comment#" .. discussion_text
notes_text = "%#Text#" .. notes_text
end
local sort_method = M.get_sort_method()
local mode = M.get_mode()
-- Join everything together and return it
local separator = "%#Comment#|"
local end_section = "%="
local updated = "%#Text#" .. t.updated
local help = "%#Comment#Help: " .. (t.help_keymap and t.help_keymap:gsub(" ", "<space>") .. " " or "unmapped")
return string.format(
" %s %s %s %s %s %s %s",
discussion_title,
" %s %s %s %s %s %s %s %s %s %s %s",
discussion_text,
separator,
notes_title,
notes_text,
end_section,
updated,
separator,
sort_method,
separator,
mode,
separator,
help
)
end
---Returns a string for the winbar indicating the sort method
---@return string
M.get_sort_method = function()
local sort_method = state.settings.discussion_tree.sort_by == "original_comment" and "↓ by thread" or "↑ by reply"
return "%#GitlabSortMethod#" .. sort_method .. "%#Comment#"
end
M.get_resolved_text = function(focused, resolved_count, resolvable_count)
local text = focused and ("%#GitlabResolved#" .. state.settings.discussion_tree.resolved .. "%#Text#")
or state.settings.discussion_tree.resolved
return " " .. string.format("%d%s/%d", resolved_count, text, resolvable_count)
end
M.get_drafts_text = function(base_title, drafts_count, focused)
return get_connector(base_title)
.. string.format(
"%d%s",
drafts_count,
(
focused and ("%#GitlabDraft#" .. state.settings.discussion_tree.draft .. "%#Text#")
or state.settings.discussion_tree.draft
)
)
end
M.get_nonresolveable_text = function(base_title, non_resolvable_count, focused)
return get_connector(base_title)
.. string.format(
"%d%s",
non_resolvable_count,
(
focused and ("%#GitlabUnlinked#" .. state.settings.discussion_tree.unlinked .. "%#Text#")
or state.settings.discussion_tree.unlinked
)
)
end
---Returns a string for the winbar indicating the mode type, live or draft
---@return string
M.get_mode = function()
if state.settings.discussion_tree.draft_mode then
return "%#DiagnosticWarn#Draft Mode"
return "%#GitlabDraftMode#Draft"
else
return "%#DiagnosticOK#Live Mode"
return "%#GitlabLiveMode#Live"
end
end
@@ -215,4 +272,8 @@ M.switch_view_type = function(override)
M.update_winbar()
end
-- Set up a timer to update the winbar periodically
local timer = vim.uv.new_timer()
timer:start(0, 100, vim.schedule_wrap(M.update_winbar))
return M

View File

@@ -25,6 +25,7 @@ end
---Makes API call to get the discussion data, stores it in the state, and calls the callback
---@param callback function|nil
M.load_draft_notes = function(callback)
state.discussion_tree.last_updated = nil
state.load_new_state("draft_notes", function()
if callback ~= nil then
callback()

View File

@@ -1,6 +1,6 @@
local M = {}
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local event = require("nui.utils.autocmd").event
local state = require("gitlab.state")
local List = require("gitlab.utils.list")
@@ -16,15 +16,31 @@ M.open = function()
end
return agg
end, {})
table.insert(help_content_lines, "")
table.insert(
help_content_lines,
string.format(
"%s = draft; %s = unlinked comment; %s = resolved",
state.settings.discussion_tree.draft,
state.settings.discussion_tree.unlinked,
state.settings.discussion_tree.resolved
)
)
local longest_line = u.get_longest_string(help_content_lines)
local help_popup =
Popup(u.create_popup_state("Help", state.settings.popup.help, longest_line + 3, #help_content_lines + 3, 60))
local opts = { "Help", state.settings.popup.help, longest_line + 3, #help_content_lines, 70 }
local help_popup = Popup(popup.create_popup_state(unpack(opts)))
help_popup:on(event.BufLeave, function()
help_popup:unmount()
end)
popup.set_up_autocommands(help_popup, nil, vim.api.nvim_get_current_win(), opts)
help_popup:mount()
state.set_popup_keymaps(help_popup, "Help", nil)
popup.set_popup_keymaps(help_popup, "Help", nil)
local currentBuffer = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(currentBuffer, 0, #help_content_lines, false, help_content_lines)
u.switch_can_edit_buf(currentBuffer, false)

View File

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

View File

@@ -1,14 +1,14 @@
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local Popup = require("nui.popup")
local state = require("gitlab.state")
local job = require("gitlab.job")
local reviewer = require("gitlab.reviewer")
local miscellaneous = require("gitlab.actions.miscellaneous")
local M = {}
local function create_squash_message_popup()
return Popup(u.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
return Popup(popup.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
end
---@class MergeOpts
@@ -31,10 +31,11 @@ M.merge = function(opts)
if merge_body.squash then
local squash_message_popup = create_squash_message_popup()
popup.set_up_autocommands(squash_message_popup, nil, vim.api.nvim_get_current_win())
squash_message_popup:mount()
state.set_popup_keymaps(squash_message_popup, function(text)
popup.set_popup_keymaps(squash_message_popup, function(text)
M.confirm_merge(merge_body, text)
end, nil, miscellaneous.editable_popup_opts)
end, nil, popup.editable_popup_opts)
else
M.confirm_merge(merge_body)
end

View File

@@ -12,14 +12,6 @@ local M = {}
---Opens up a select menu that lets you choose a different merge request.
---@param opts ChooseMergeRequestOptions|nil
M.choose_merge_request = function(opts)
local has_clean_tree, clean_tree_err = git.has_clean_tree()
if clean_tree_err ~= nil then
return
elseif has_clean_tree ~= "" then
u.notify("Your local branch has changes, please stash or commit and push", vim.log.levels.ERROR)
return
end
if opts == nil then
opts = state.settings.choose_merge_request
end
@@ -38,6 +30,19 @@ M.choose_merge_request = function(opts)
reviewer.close()
end
if choice.source_branch ~= git.get_current_branch() then
local has_clean_tree, clean_tree_err = git.has_clean_tree()
if clean_tree_err ~= nil then
return
elseif not has_clean_tree then
u.notify(
"Cannot switch branch when working tree has changes, please stash or commit and push",
vim.log.levels.ERROR
)
return
end
end
vim.schedule(function()
local _, branch_switch_err = git.switch_branch(choice.source_branch)
if branch_switch_err ~= nil then

View File

@@ -34,70 +34,6 @@ M.attach_file = function()
end)
end
M.editable_popup_opts = {
action_before_close = true,
action_before_exit = false,
save_to_temp_register = true,
}
M.non_editable_popup_opts = {
action_before_close = true,
action_before_exit = false,
save_to_temp_register = false,
}
-- Get the index of the next popup when cycling forward
local function next_index(i, n, count)
count = count > 0 and count or 1
for _ = 1, count do
if i < n then
i = i + 1
elseif i == n then
i = 1
end
end
return i
end
---Get the index of the previous popup when cycling backward
---@param i integer The current index
---@param n integer The total number of popups
---@param count integer The count used with the keymap (replaced with 1 if no count was given)
local function prev_index(i, n, count)
count = count > 0 and count or 1
for _ = 1, count do
if i > 1 then
i = i - 1
elseif i == 1 then
i = n
end
end
return i
end
---Setup keymaps for cycling popups. The keymap accepts count.
---@param popups table Table of Popups
M.set_cycle_popups_keymaps = function(popups)
local keymaps = require("gitlab.state").settings.keymaps
if keymaps.disable_all or keymaps.popup.disable_all then
return
end
local number_of_popups = #popups
for i, popup in ipairs(popups) do
if keymaps.popup.next_field then
popup:map("n", keymaps.popup.next_field, function()
vim.api.nvim_set_current_win(popups[next_index(i, number_of_popups, vim.v.count)].winid)
end, { desc = "Go to next field (accepts count)", nowait = keymaps.popup.next_field_nowait })
end
if keymaps.popup.prev_field then
popup:map("n", keymaps.popup.prev_field, function()
vim.api.nvim_set_current_win(popups[prev_index(i, number_of_popups, vim.v.count)].winid)
end, { desc = "Go to previous field (accepts count)", nowait = keymaps.popup.prev_field_nowait })
end
end
end
---Toggle the value in a "Boolean buffer"
M.toggle_bool = function()
local bufnr = vim.api.nvim_get_current_buf()

View File

@@ -5,6 +5,7 @@ local Popup = require("nui.popup")
local state = require("gitlab.state")
local job = require("gitlab.job")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local M = {
pipeline_jobs = nil,
latest_pipeline = nil,
@@ -40,7 +41,8 @@ M.open = function()
local height = 6 + #M.pipeline_jobs + 3
local pipeline_popup =
Popup(u.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60))
Popup(popup.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60))
popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win())
M.pipeline_popup = pipeline_popup
pipeline_popup:mount()
@@ -91,7 +93,7 @@ M.open = function()
end
pipeline_popup.border:set_text("top", "Pipeline Status", "center")
state.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
popup.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
u.switch_can_edit_buf(bufnr, false)
end)
end

View File

@@ -7,6 +7,7 @@ local git = require("gitlab.git")
local job = require("gitlab.job")
local common = require("gitlab.actions.common")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local List = require("gitlab.utils.list")
local state = require("gitlab.state")
local miscellaneous = require("gitlab.actions.miscellaneous")
@@ -34,6 +35,9 @@ M.summary = function()
local info_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
layout:mount()
local popups = {
title_popup,
description_popup,
@@ -41,6 +45,9 @@ M.summary = function()
}
M.layout = layout
M.info_popup = info_popup
M.title_popup = title_popup
M.description_popup = description_popup
M.layout_buf = layout.bufnr
M.layout_visible = true
@@ -54,30 +61,28 @@ M.summary = function()
vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { title })
if info_popup then
vim.api.nvim_buf_set_lines(info_popup.bufnr, 0, -1, false, info_lines)
u.switch_can_edit_buf(info_popup.bufnr, false)
M.color_details(info_popup.bufnr) -- Color values in details popup
M.update_details_popup(info_popup.bufnr, info_lines)
end
state.set_popup_keymaps(
popup.set_popup_keymaps(
description_popup,
M.edit_summary,
miscellaneous.attach_file,
{ cb = exit, action_before_close = true, action_before_exit = true, save_to_temp_register = true }
)
state.set_popup_keymaps(
popup.set_popup_keymaps(
title_popup,
M.edit_summary,
nil,
{ cb = exit, action_before_close = true, action_before_exit = true }
)
state.set_popup_keymaps(
popup.set_popup_keymaps(
info_popup,
M.edit_summary,
nil,
{ cb = exit, action_before_close = true, action_before_exit = true }
)
miscellaneous.set_cycle_popups_keymaps(popups)
popup.set_cycle_popups_keymaps(popups)
vim.api.nvim_set_current_buf(description_popup.bufnr)
end)
@@ -86,6 +91,23 @@ M.summary = function()
git.check_mr_in_good_condition()
end
M.update_summary_details = function()
if not M.info_popup or not M.info_popup.bufnr then
return
end
local details_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
local internal_layout = M.create_internal_layout(details_lines, M.title_popup, M.description_popup, M.info_popup)
M.layout:update(M.get_outer_layout_config(), internal_layout)
M.update_details_popup(M.info_popup.bufnr, details_lines)
end
M.update_details_popup = function(bufnr, info_lines)
u.switch_can_edit_buf(bufnr, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, info_lines)
u.switch_can_edit_buf(bufnr, false)
M.color_details(bufnr) -- Color values in details popup
end
-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
-- lines that users include in their state.settings.info.fields list.
M.build_info_lines = function()
@@ -165,16 +187,37 @@ M.edit_summary = function()
end)
end
---Create the Summary layout and individual popups that make up the Layout.
---@return NuiLayout, NuiPopup, NuiPopup, NuiPopup
M.create_layout = function(info_lines)
local title_popup = Popup(u.create_box_popup_state(nil, false))
local settings = u.merge(state.settings.popup, state.settings.popup.summary or {})
local title_popup = Popup(popup.create_box_popup_state(nil, false, settings))
M.title_bufnr = title_popup.bufnr
local description_popup = Popup(u.create_box_popup_state("Description", true))
local description_popup = Popup(popup.create_popup_state("Description", settings))
M.description_bufnr = description_popup.bufnr
local details_popup
if state.settings.info.enabled then
details_popup = Popup(popup.create_box_popup_state("Details", false, settings))
end
local internal_layout = M.create_internal_layout(info_lines, title_popup, description_popup, details_popup)
local layout = Layout(M.get_outer_layout_config(), internal_layout)
popup.set_up_autocommands(description_popup, layout, vim.api.nvim_get_current_win())
return layout, title_popup, description_popup, details_popup
end
---Create the internal layout of the Summary and individual popups that make up the Layout.
---@param info_lines string[] Table of strings that make up the details content
---@param title_popup NuiPopup
---@param description_popup NuiPopup
---@param details_popup NuiPopup
---@return NuiLayout.Box
M.create_internal_layout = function(info_lines, title_popup, description_popup, details_popup)
local internal_layout
if state.settings.info.enabled then
details_popup = Popup(u.create_box_popup_state("Details", false))
if state.settings.info.horizontal then
local longest_line = u.get_longest_string(info_lines)
internal_layout = Layout.Box({
@@ -182,7 +225,7 @@ M.create_layout = function(info_lines)
Layout.Box({
Layout.Box(details_popup, { size = longest_line + 3 }),
Layout.Box(description_popup, { grow = 1 }),
}, { dir = "row", size = "100%" }),
}, { dir = "row", size = "95%" }),
}, { dir = "col" })
else
internal_layout = Layout.Box({
@@ -197,18 +240,21 @@ M.create_layout = function(info_lines)
Layout.Box(description_popup, { grow = 1 }),
}, { dir = "col" })
end
return internal_layout
end
local layout = Layout({
position = "50%",
---Create the config for the outer Layout of the Summary
---@return nui_layout_options
M.get_outer_layout_config = function()
local settings = u.merge(state.settings.popup, state.settings.popup.summary or {})
return {
position = settings.position,
relative = "editor",
size = {
width = "95%",
height = "95%",
width = settings.width,
height = settings.height,
},
}, internal_layout)
layout:mount()
return layout, title_popup, description_popup, details_popup
}
end
M.color_details = function(bufnr)