local M = {} local config = require("gitea.config") local api = require("gitea.api") local auth = require("gitea.auth") -- We'll keep detect_owner_repo() from before 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 -------------------------------------------------------------------------------- --- Data Structures to track what's in the ephemeral buffer -------------------------------------------------------------------------------- -- We'll store some metadata for each "segment" in the buffer: -- - The issue's title line range -- - The issue's body line range -- - Each existing comment (with start/end line range) -- - Possibly space for a new comment -- -- On save (:w), we compare what's changed and do the appropriate API calls. -------------------------------------------------------------------------------- local function create_issue_buffer_metadata(issue) -- track lines for the title, the body, each comment local meta = { issue_number = issue.number, -- will fill in fields once we place them in the buffer title_start = nil, title_end = nil, body_start = nil, body_end = nil, comments = {}, -- array of { id, start, end } new_comment_start = nil, -- where user can type a new comment } return meta end -------------------------------------------------------------------------------- --- RENDER ISSUE INTO BUFFER -------------------------------------------------------------------------------- local function render_issue_into_buf(bufnr, issue, comments) -- We assume the buffer is empty and ephemeral. local 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, "# Edit the title below, then the body, then any comments.") table.insert(lines, "# On save (:w), changes to the title/body will be updated; changes to comment lines will be updated, and new text at the bottom becomes a new comment.") table.insert(lines, "") -- store line index references for metadata local meta = create_issue_buffer_metadata(issue) -- place the "title line" meta.title_start = #lines+1 table.insert(lines, issue.title or "") meta.title_end = #lines -- a blank line table.insert(lines, "") -- place the "body lines" meta.body_start = #lines+1 if issue.body then local body_lines = vim.split(issue.body, "\n", true) if #body_lines == 0 then table.insert(lines, "") else for _, l in ipairs(body_lines) do table.insert(lines, l) end end else table.insert(lines, "No issue description.") end meta.body_end = #lines -- spacing table.insert(lines, "") table.insert(lines, "# --- Comments ---") local line_count = #lines local comment_meta = {} for _, c in ipairs(comments) do table.insert(lines, "") local start_line = #lines+1 -- We'll place a small header: e.g. "Comment (ID=12345) by username" local author = c.user and c.user.login or c.user and c.user.username or c.user or "?" table.insert(lines, string.format("COMMENT #%d by %s", c.id, author)) -- Then the actual comment body lines 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, start_line = start_line, end_line = end_line, }) end -- place a blank line for user to add a new comment table.insert(lines, "") table.insert(lines, "# --- Add a new comment below this line. If you leave it blank, no new comment is created. ---") local new_comment_start = #lines+1 table.insert(lines, "") -- an empty line to start from vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) -- store metadata local b_meta = { issue_number = issue.number, title_start = meta.title_start, title_end = meta.title_end, body_start = meta.body_start, body_end = meta.body_end, comments = comment_meta, new_comment_start = new_comment_start, } return b_meta end -------------------------------------------------------------------------------- --- Parse buffer changes and do API updates on save -------------------------------------------------------------------------------- local function on_issue_buf_write(bufnr, metadata, owner, repo) -- read all buffer lines local new_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) ----------------- Title update check ----------------- local new_title = table.concat(vim.list_slice(new_lines, metadata.title_start, metadata.title_end), "\n") -- remove trailing newlines just in case new_title = new_title:gsub("%s+$", "") -- we do a get_issue call to see the current title 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 to verify updates", vim.log.levels.ERROR) return end local changed_title = false if (current_issue_data.title or "") ~= new_title then changed_title = true end ----------------- Body update check ------------------ local new_body_lines = vim.list_slice(new_lines, metadata.body_start, metadata.body_end) local new_body = table.concat(new_body_lines, "\n") local changed_body = false if (current_issue_data.body or "") ~= new_body then changed_body = true end ----------------- Comments update check ------------- local changed_comments = {} for _, cmeta in ipairs(metadata.comments) do local c_lines = vim.list_slice(new_lines, cmeta.start_line, cmeta.end_line) -- The first line is "COMMENT # by " -- The rest is the actual body -- e.g. `COMMENT #112 by alice` local first_line = c_lines[1] or "" local c_body = {} for i=2,#c_lines do table.insert(c_body, c_lines[i]) end local new_comment_body = table.concat(c_body, "\n") -- We fetch the current comment from the server to check if changed -- But Gitea's issue comment API does not necessarily let us get a single comment by ID easily. -- Instead, we do "issue_data, with comments" or store old body in memory. -- For simplicity, let's store the old body in an extmark or in a table. But let's just do a naive approach: -- We'll assume if the line is changed, we want to patch it. We have no big reference of old body, so let's do it. table.insert(changed_comments, { id = cmeta.id, new_body = new_comment_body }) end ----------------- New comment check ------------------ local new_comment_body_lines = {} for i=metadata.new_comment_start, #new_lines do table.insert(new_comment_body_lines, new_lines[i]) end local new_comment_body = table.concat(new_comment_body_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "") local create_new_comment = (#new_comment_body > 0) -------------- Perform the updates -------------- 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 -- for each changed comment, attempt to patch -- We do not do a diff, so we always do a "PATCH" if we suspect changes -- => If the user changed nothing, Gitea might respond 200 anyway, no big deal for _, cupd in ipairs(changed_comments) do local updated_comment, st = api.edit_issue_comment(owner, repo, metadata.issue_number, cupd.id, cupd.new_body) if not updated_comment then vim.notify(string.format("[gitea.nvim] Failed to update comment %d. (HTTP %s)", cupd.id, st or "?"), vim.log.levels.ERROR) else vim.notify(string.format("[gitea.nvim] Comment #%d updated.", cupd.id)) end end 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 new comment (HTTP %s).", st or "?"), vim.log.levels.ERROR) end end end -------------------------------------------------------------------------------- --- Open a single ephemeral buffer for the entire Issue with comments -------------------------------------------------------------------------------- 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 -- get all existing comments from the Gitea API -- for Gitea, we can do: GET /repos/{owner}/{repo}/issues/{index}/comments -- The plugin's `api` might have a function for that, or we can add it quickly 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", "markdown") vim.api.nvim_buf_set_name(buf, string.format("gitea_issue_full_%d", number)) local metadata = render_issue_into_buf(buf, issue_data, all_comments) -- We create an autocmd on BufWriteCmd to parse changes and 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(args) 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 -- use the same telescope approach as before: local has_telescope, tele = 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, status = api.list_issues(owner, repo, {}) if not issues then vim.notify(string.format("[gitea.nvim] Error fetching issues (HTTP %s).", status 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 -- Instead of just editing, we now open the full timeline buffer 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) -- Alternative: show a single issue by number 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, -- The rest of your commands can remain or be removed as needed -- but presumably we can just use "show" for everything -- or keep an existing minimal create command, etc. } subcommands.pr = { -- We'll leave your existing PR commands as is or remove for brevity } -------------------------------------------------------------------------------- --- 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) -- remove object table.remove(args, 1) -- remove action fn(args) end, { nargs = "*", complete = function(arg_lead, cmd_line, cursor_pos) 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