local M = {} local config = require("gitea.config") local api = require("gitea.api") local auth = require("gitea.auth") local highlights = require("gitea.highlights") ------------------------------------------------------------------------------ -- Debug toggle ------------------------------------------------------------------------------ local DEBUG = true ------------------------------------------------------------------------------ -- We’ll create a namespace for our extmarks ------------------------------------------------------------------------------ local GITEA_NS = vim.api.nvim_create_namespace("gitea_nvim") ------------------------------------------------------------------------------ -- Utility: get all lines from the region defined by an extmark ------------------------------------------------------------------------------ local function get_extmark_region(bufnr, extmark_id) -- extmark lookup with "details = true" returns: { row, col, details={ end_row=..., end_col=... } } local markinfo = vim.api.nvim_buf_get_extmark_by_id(bufnr, GITEA_NS, extmark_id, { details = true }) if not markinfo or #markinfo == 0 then return {}, 0, 0 -- no lines end local start_line = markinfo[1] local end_line = markinfo[3].end_row local lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line + 1, false) return lines, start_line, end_line end ------------------------------------------------------------------------------ -- Utility: set an extmark covering a region from start_line..end_line ------------------------------------------------------------------------------ local function set_extmark_range(bufnr, start_line, end_line) -- We store the extmark covering these lines from start_line..end_line -- We do end_col=0 to anchor at column 0, but the end_row is inclusive -- so we do { end_line, -1 } or end_col=0 with end_row=that line + 1 local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, GITEA_NS, start_line, 0, { end_line = end_line, end_col = 0, }) return extmark_id end ------------------------------------------------------------------------------ -- DETECT OWNER/REPO ------------------------------------------------------------------------------ local function detect_owner_repo() local cmd = "git config remote.origin.url" local url = vim.fn.systemlist(cmd)[1] if vim.v.shell_error ~= 0 or not url or url == "" then return nil, nil end url = url:gsub("%.git$", "") local _, after = url:match("^(https?://)(.*)$") if after then local path = after:match("[^/]+/(.*)$") if not path then return nil, nil end local owner, repo = path:match("^(.-)/(.*)$") return owner, repo end local user_host, path = url:match("^(.-):(.*)$") if user_host and path then local owner, repo = path:match("^(.-)/(.*)$") return owner, repo end return nil, nil end ------------------------------------------------------------------------------ -- We’ll store "metadata" in a table like octo.nvim’s approach: -- metadata.title_extmark -> ID -- metadata.body_extmark -> ID -- metadata.comments = { { id=..., extmark_id=... }, ... } ------------------------------------------------------------------------------ local function create_issue_buffer_metadata(issue) return { issue_number = issue.number, title_extmark = nil, body_extmark = nil, comments = {}, new_comment_extmark = nil, -- region for new comment } end ------------------------------------------------------------------------------ -- APPLY CUSTOM HIGHLIGHTS ------------------------------------------------------------------------------ local function apply_issue_highlights(bufnr) -- We can still highlight lines #0 and #1, or do it differently vim.api.nvim_buf_add_highlight(bufnr, 0, "GiteaIssueMeta", 0, 0, -1) vim.api.nvim_buf_add_highlight(bufnr, 0, "GiteaIssueMeta", 1, 0, -1) end ------------------------------------------------------------------------------ -- RENDER ISSUE INTO BUFFER (octo.nvim style) ------------------------------------------------------------------------------ local function render_issue_into_buf(bufnr, issue, comments) -- Clear buffer, extmarks, etc. vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {}) vim.api.nvim_buf_clear_namespace(bufnr, GITEA_NS, 0, -1) local meta = create_issue_buffer_metadata(issue) local lines = {} -- Some short header lines table.insert(lines, string.format("# Gitea Issue #%d: %s", issue.number, issue.title)) table.insert(lines, "# STATE: " .. (issue.state or "unknown")) table.insert(lines, "# You can edit below. On save, the changes are applied.") table.insert(lines, "") -- Add placeholder for title local title_start = #lines table.insert(lines, issue.title or "") local title_end = #lines -- blank line table.insert(lines, "") -- Add placeholder for body local body_start = #lines local body_text = issue.body or "" if body_text == "" then body_text = "No issue description." end local body_lines = vim.split(body_text, "\n", true) if #body_lines == 0 then table.insert(lines, "") else for _, line in ipairs(body_lines) do table.insert(lines, line) end end local body_end = #lines -- blank line table.insert(lines, "") table.insert(lines, "# --- Comments ---") local comment_meta = {} for _, c in ipairs(comments or {}) do table.insert(lines, "") local start_line = #lines local author = c.user and (c.user.login or c.user.username) or "?" table.insert(lines, string.format("COMMENT #%d by %s", c.id, author)) local c_body_lines = vim.split(c.body or "", "\n", true) if #c_body_lines == 0 then table.insert(lines, "") else for _, cb in ipairs(c_body_lines) do table.insert(lines, cb) end end local end_line = #lines table.insert(comment_meta, { id = c.id, extmark_id = nil, start_line = start_line, end_line = end_line }) end -- final blank line for new comment table.insert(lines, "") table.insert(lines, "# --- Add a new comment below (multiline ok). ---") local new_comment_start = #lines + 1 table.insert(lines, "") -- at least one blank line -- Actually write out vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) -- Now create extmark for the title -- NOTE: extmark is zero-based, so set_extmark_range expects 0-based line indexes meta.title_extmark = set_extmark_range(bufnr, title_start - 1, title_end - 1) -- extmark for body meta.body_extmark = set_extmark_range(bufnr, body_start - 1, body_end - 1) -- extmarks for comments for _, cmeta in ipairs(comment_meta) do local mark_id = set_extmark_range(bufnr, cmeta.start_line - 1, cmeta.end_line - 1) cmeta.extmark_id = mark_id end meta.comments = comment_meta -- extmark for new comment region meta.new_comment_extmark = set_extmark_range(bufnr, new_comment_start - 1, #lines - 1) apply_issue_highlights(bufnr) return meta end ------------------------------------------------------------------------------ -- ON SAVE => PARSE & UPDATE (octo.nvim style) ------------------------------------------------------------------------------ local function on_issue_buf_write(bufnr, metadata, owner, repo) local current_issue_data = api.get_issue(owner, repo, metadata.issue_number) if not current_issue_data then vim.notify("[gitea.nvim] Could not re-fetch the issue", vim.log.levels.ERROR) return end -- 1) Gather new title local title_lines = get_extmark_region(bufnr, metadata.title_extmark) local new_title = table.concat(title_lines, "\n") -- Trim whitespace from the title (since GitHub disallows multi-line titles) new_title = new_title:gsub("^%s+", ""):gsub("%s+$", ""):gsub("[\n\r]+", " ") -- 2) Gather new body local body_lines = get_extmark_region(bufnr, metadata.body_extmark) local new_body = table.concat(body_lines, "\n") -- multiline is fine, do not trim if DEBUG then print("DEBUG: new_title = ", new_title) print("DEBUG: new_body (lines) = ", vim.inspect(body_lines)) end local changed_title = (current_issue_data.title or "") ~= new_title local changed_body = (current_issue_data.body or "") ~= new_body -- 3) Gather updates for existing comments local changed_comments = {} for _, cinfo in ipairs(metadata.comments) do local comment_lines = get_extmark_region(bufnr, cinfo.extmark_id) -- The first line might be "COMMENT #ID by Author" -- So let's separate that from the actual text local c_header = comment_lines[1] or "" local c_body_lines = {} for i = 2, #comment_lines do table.insert(c_body_lines, comment_lines[i]) end local new_comment_body = table.concat(c_body_lines, "\n") table.insert(changed_comments, { id = cinfo.id, new_body = new_comment_body, }) end -- 4) Possibly new comment local extra_comment_lines = get_extmark_region(bufnr, metadata.new_comment_extmark) -- remove lines that are just comment-hint if you want, but we can accept them all local new_comment_body = table.concat(extra_comment_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "") local create_new_comment = #new_comment_body > 0 if DEBUG then print("DEBUG: new_comment_body = ", new_comment_body) end -- Validate title if changed_title and new_title == "" then vim.notify("[gitea.nvim] Title cannot be empty. Skipping.", vim.log.levels.ERROR) if not changed_body and #changed_comments == 0 and not create_new_comment then return end changed_title = false end -- Edit issue if needed if changed_title or changed_body then local updated, st = api.edit_issue(owner, repo, metadata.issue_number, { title = changed_title and new_title or current_issue_data.title, body = changed_body and new_body or current_issue_data.body, }) if updated then vim.notify("[gitea.nvim] Issue updated.") else vim.notify(string.format("[gitea.nvim] Failed to update issue (HTTP %s).", st or "?"), vim.log.levels.ERROR) end end -- Update each existing comment for _, c in ipairs(changed_comments) do if (c.new_body or "") ~= "" then local updated_comment, st = api.edit_issue_comment(owner, repo, metadata.issue_number, c.id, c.new_body) if not updated_comment then vim.notify(string.format("[gitea.nvim] Failed to update comment %d", c.id), vim.log.levels.ERROR) else vim.notify(string.format("[gitea.nvim] Comment #%d updated.", c.id)) end end end -- Possibly create new comment if create_new_comment then local created, st = api.comment_issue(owner, repo, metadata.issue_number, new_comment_body) if created then vim.notify("[gitea.nvim] New comment posted.") else vim.notify(string.format("[gitea.nvim] Failed to create comment (HTTP %s).", st or "?"), vim.log.levels.ERROR) end end end ------------------------------------------------------------------------------ -- OPEN ISSUE BUFFER ------------------------------------------------------------------------------ local function open_full_issue_buffer(owner, repo, number) local issue_data, status = api.get_issue(owner, repo, number) if not issue_data then vim.notify(string.format("[gitea.nvim] Error retrieving issue #%d (HTTP %s).", number, status or "?"), vim.log.levels.ERROR) return end local all_comments, cstatus = api.get_issue_comments(owner, repo, number) if not all_comments then vim.notify(string.format("[gitea.nvim] Error retrieving issue #%d comments (HTTP %s).", number, cstatus or "?"), vim.log.levels.ERROR) return end local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_option(buf, "buftype", "acwrite") vim.api.nvim_buf_set_option(buf, "filetype", "gitea") vim.api.nvim_buf_set_name(buf, string.format("gitea_issue_full_%d", number)) highlights.setup() -- Render lines & create extmarks local metadata = render_issue_into_buf(buf, issue_data, all_comments) -- Hook saving => parse extmarks & do updates vim.api.nvim_create_autocmd("BufWriteCmd", { buffer = buf, callback = function() on_issue_buf_write(buf, metadata, owner, repo) end }) vim.api.nvim_set_current_buf(buf) end ------------------------------------------------------------------------------ -- SUBCOMMANDS ------------------------------------------------------------------------------ local subcommands = {} subcommands.issue = { list = function(_) local owner, repo = detect_owner_repo() if not owner or not repo then vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR) return end local has_telescope, _ = pcall(require, "telescope") if not has_telescope then vim.notify("[gitea.nvim] telescope.nvim is not installed", vim.log.levels.ERROR) return end local pickers = require("telescope.pickers") local finders = require("telescope.finders") local conf = require("telescope.config").values local actions = require("telescope.actions") local action_state = require("telescope.actions.state") local issues, st = api.list_issues(owner, repo, {}) if not issues then vim.notify(string.format("[gitea.nvim] Error fetching issues (HTTP %s).", st or "?"), vim.log.levels.ERROR) return end pickers.new({}, { prompt_title = string.format("Issues: %s/%s", owner, repo), finder = finders.new_table { results = issues, entry_maker = function(issue) return { value = issue, display = string.format("#%d %s", issue.number, issue.title), ordinal = issue.number .. " " .. issue.title } end }, sorter = conf.generic_sorter({}), attach_mappings = function(prompt_bufnr, map) local function select_issue() local selection = action_state.get_selected_entry() actions.close(prompt_bufnr) if not selection or not selection.value then return end open_full_issue_buffer(owner, repo, selection.value.number) end map("i", "", select_issue) map("n", "", select_issue) return true end, }):find() end, show = function(args) local number = tonumber(args[1]) if not number then vim.notify("[gitea.nvim] Usage: :Gitea issue show ", vim.log.levels.ERROR) return end local owner, repo = detect_owner_repo() if not owner or not repo then vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR) return end open_full_issue_buffer(owner, repo, number) end, } subcommands.pr = { -- If you have PR subcommands, add them here } ------------------------------------------------------------------------------ -- REGISTER ------------------------------------------------------------------------------ function M.register() vim.api.nvim_create_user_command("Gitea", function(cmd_opts) auth.ensure_token() local args = {} for w in string.gmatch(cmd_opts.args, "%S+") do table.insert(args, w) end local object = args[1] local action = args[2] if not object then print("Usage: :Gitea [args]") print("Examples:") print(" :Gitea issue list") print(" :Gitea issue show 123") return end local sc = subcommands[object] if not sc then vim.notify("[gitea.nvim] Unknown object: " .. object, vim.log.levels.ERROR) return end if not action then vim.notify("[gitea.nvim] Please specify an action for " .. object, vim.log.levels.ERROR) return end local fn = sc[action] if not fn then vim.notify(string.format("[gitea.nvim] Unknown action '%s' for object '%s'", action, object), vim.log.levels.ERROR) return end table.remove(args, 1) table.remove(args, 1) fn(args) end, { nargs = "*", complete = function(arg_lead, cmd_line, _) local parts = vim.split(cmd_line, "%s+") if #parts == 2 then local objs = {} for k, _ in pairs(subcommands) do if k:match("^" .. arg_lead) then table.insert(objs, k) end end return objs elseif #parts == 3 then local object = parts[2] local acts = {} if subcommands[object] then for k, _ in pairs(subcommands[object]) do if k:match("^" .. arg_lead) then table.insert(acts, k) end end end return acts else return {} end end, }) end return M