Files
gitea.nvim/lua/gitea/commands.lua
2024-12-23 21:34:50 +01:00

476 lines
16 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
------------------------------------------------------------------------------
-- Well 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
------------------------------------------------------------------------------
-- Well store "metadata" in a table like octo.nvims 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", "<CR>", select_issue)
map("n", "<CR>", 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 <number>", 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 <object> <action> [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