-- 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") local job = require("gitlab.job") local common = require("gitlab.actions.common") local u = require("gitlab.utils") 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 local nbsp = " " local M = { layout_visible = false, layout = nil, layout_buf = nil, title_bufnr = nil, 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 M.summary = function() if M.layout_visible then M.layout:unmount() M.layout_visible = false return end local title = state.INFO.title local description_lines = common.build_content(state.INFO.description) 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, info_popup, } 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 local function exit() layout:unmount() M.layout_visible = false end vim.schedule(function() vim.api.nvim_buf_set_lines(description_popup.bufnr, 0, -1, false, description_lines) vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { title }) if info_popup then M.update_details_popup(info_popup.bufnr, info_lines) end 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 } ) popup.set_popup_keymaps( title_popup, M.edit_summary, nil, { cb = exit, action_before_close = true, action_before_exit = true } ) popup.set_popup_keymaps( info_popup, M.edit_summary, nil, { cb = exit, action_before_close = true, action_before_exit = true } ) popup.set_cycle_popups_keymaps(popups) vim.api.nvim_set_current_buf(description_popup.bufnr) end) git.check_current_branch_up_to_date_on_remote(vim.log.levels.WARN) 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 ---Return the mergeability checks statuses and descriptions ---@return string[] local make_mergeability_checks = function() local lines = {} for _, check in ipairs(state.MERGEABILITY.mergeability_checks) do local status = state.settings.mergeability_checks.statuses[check.status] if status == nil then u.notify(string.format("Unknown mergeability check status: %s", check.status), vim.log.levels.ERROR) end if status then local description = state.settings.mergeability_checks.checks[check.identifier] if description == nil then u.notify(string.format("Unknown mergeability check identifier: %s", check.identifier), vim.log.levels.ERROR) end if description then table.insert(lines, status .. " " .. description) end end end 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. M.build_info_lines = function() local info = state.INFO local options = { author = { title = "Author", content = "@" .. info.author.username .. " (" .. info.author.name .. ")" }, created_at = { title = "Created", content = u.format_to_local(info.created_at, vim.fn.strftime("%z")) }, updated_at = { title = "Updated", content = u.time_since(info.updated_at) }, detailed_merge_status = { title = "Status", content = info.detailed_merge_status }, draft = { title = "Draft", content = (info.draft and "Yes" or "No") }, conflicts = { title = "Merge Conflicts", content = (info.has_conflicts and "Yes" or "No") }, assignees = { title = "Assignees", content = u.make_readable_list(info.assignees, "name") }, reviewers = { title = "Reviewers", content = u.make_readable_list(info.reviewers, "name") }, branch = { title = "Branch", content = info.source_branch }, labels = { title = "Labels", content = table.concat(info.labels, ", ") }, target_branch = { title = "Target Branch", content = info.target_branch }, delete_branch = { title = "Delete Source Branch", content = (info.force_remove_source_branch and "Yes" or "No"), }, squash = { title = "Squash Commits", content = (info.squash and "Yes" or "No") }, pipeline = { title = "Pipeline Status", content = function() local pipeline = state.INFO.pipeline if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then return "" end return pipeline.status end, }, web_url = { title = "MR URL", content = info.web_url }, mergeability_checks = { title = "Mergeability checks", content = make_mergeability_checks }, } local longest_used = "" for _, v in ipairs(state.settings.info.fields) do if v == "merge_status" then v = "detailed_merge_status" end -- merge_status was deprecated, see https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204 if options[v] == nil then u.notify(string.format("Invalid field in settings.info.fields: '%s'", v), vim.log.levels.ERROR) else local title = options[v].title if vim.fn.strcharlen(title) > vim.fn.strcharlen(longest_used) then longest_used = title end end end local function row_offset(row) local offset = vim.fn.strcharlen(longest_used) - vim.fn.strcharlen(row) return string.rep(nbsp, offset + 3) end local result = {} for _, v in ipairs(state.settings.info.fields) do if v == "merge_status" then v = "detailed_merge_status" end local row = options[v] local title_prefix = "* " .. row.title .. row_offset(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 for i, line in ipairs(#content > 0 and content or { "" }) do table.insert(result, (i == 1 and title_prefix or padding) .. line) end else -- Single-line content table.insert(result, title_prefix .. (content or "")) end end return result end -- This function will PUT the new 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", " ") local body = { title = title, description = description } job.run_job("/mr/summary", "PUT", body, function(data) u.notify(data.message, vim.log.levels.INFO) state.INFO.description = data.mr.description state.INFO.title = data.mr.title 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 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)) 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 if state.settings.info.horizontal then local longest_line = u.get_longest_string(info_lines) internal_layout = Layout.Box({ Layout.Box(title_popup, { size = 3 }), Layout.Box({ Layout.Box(details_popup, { size = longest_line + 3 }), Layout.Box(description_popup, { grow = 1 }), }, { dir = "row", size = "95%" }), }, { dir = "col" }) else internal_layout = Layout.Box({ Layout.Box(title_popup, { size = 3 }), Layout.Box(description_popup, { grow = 1 }), Layout.Box(details_popup, { size = #info_lines + 3 }), }, { dir = "col" }) end else internal_layout = Layout.Box({ Layout.Box(title_popup, { size = 3 }), Layout.Box(description_popup, { grow = 1 }), }, { dir = "col" }) end return internal_layout end ---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 = settings.width, height = settings.height, }, } end 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 if line:match("^* Labels") then for j, label in ipairs(state.LABELS) do local start_idx, end_idx = line:find(label.Name, 1, true) if start_idx ~= nil and end_idx ~= nil then vim.cmd("highlight " .. "label" .. j .. " guifg=white") vim.api.nvim_set_hl(0, ("label" .. j), { fg = label.Color }) vim.hl.range(bufnr, details_namespace, ("label" .. j), { i - 1, start_idx - 1 }, { i - 1, end_idx }) end end elseif line:match("^* Status") then local status = line:match("[^" .. nbsp .. "]-$") local hl = ({ blocked_status = "DiagnosticError", broken_status = "DiagnosticError", checking = "DiagnosticInfo", ci_must_pass = "DiagnosticWarn", ci_still_running = "DiagnosticInfo", discussions_not_resolved = "DiagnosticWarn", draft_status = "Comment", external_status_checks = "DiagnosticHint", mergeable = "DiagnosticOK", not_approved = "DiagnosticWarn", not_open = "NonText", policies_denied = "DiagnosticError", unchecked = "NonText", })[status] or "Normal" local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$") vim.hl.range(bufnr, details_namespace, hl, { i - 1, start_idx - 1 }, { i - 1, end_idx }) elseif line:match("^* Branch") or line:match("^* Target Branch") then local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$") vim.hl.range(bufnr, details_namespace, "Title", { i - 1, start_idx - 1 }, { i - 1, end_idx }) elseif line:match("^* Pipeline") then local status = line:match("[^" .. nbsp .. "]-$") local hl = ({ canceled = "DiagnosticWarn", created = "DiagnosticInfo", failed = "DiagnosticError", manual = "DiagnosticHint", pending = "DiagnosticWarn", running = "DiagnosticInfo", skipped = "Comment", success = "DiagnosticOK", unknown = "NonText", })[status] or "Normal" local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$") vim.hl.range(bufnr, details_namespace, hl, { i - 1, start_idx - 1 }, { i - 1, end_idx }) elseif line:match(nbsp .. "No$") or line:match(nbsp .. "Yes$") then local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$") vim.api.nvim_set_hl(0, "boolean", { link = "Constant" }) vim.hl.range(bufnr, details_namespace, "boolean", { i - 1, start_idx - 1 }, { i - 1, end_idx }) end end end return M