diff --git a/lua/snacks/gh/actions.lua b/lua/snacks/gh/actions.lua index 74f1471c..f0569dcc 100644 --- a/lua/snacks/gh/actions.lua +++ b/lua/snacks/gh/actions.lua @@ -28,6 +28,24 @@ local M = {} ---@field type? "pr" | "issue" ---@field enabled? fun(item: snacks.picker.gh.Item): boolean +---@param item snacks.picker.gh.Item +---@param ctx snacks.gh.action.ctx +local function update_main(item, ctx) + local gh = { repo = item.repo, number = item.number, type = item.type } + if ctx.main and vim.api.nvim_win_is_valid(ctx.main) then + local buf = vim.api.nvim_win_get_buf(ctx.main) + if vim.deep_equal(vim.b[buf].snacks_gh or {}, gh) then + return ctx.main, buf + end + end + local win = vim.api.nvim_get_current_win() + local buf = vim.api.nvim_win_get_buf(win) + if vim.deep_equal(vim.b[buf].snacks_gh or {}, gh) then + ctx.main = win + return ctx.main, buf + end +end + ---@class snacks.gh.actions: {[string]:snacks.gh.Action} M.actions = setmetatable({}, { __index = function(_, key) @@ -80,7 +98,7 @@ M.actions.gh_actions = { if ctx.action and ctx.action.cmd then return Snacks.picker.actions.jump(ctx.picker, item, ctx.action) end - ctx.main = ctx.main or ctx.picker and ctx.picker.main or nil + update_main(item, ctx) local actions = M.get_actions(item) actions.gh_actions = nil -- remove this action actions.gh_perform_action = nil -- remove this action @@ -103,9 +121,10 @@ M.actions.gh_actions = { end ctx.action = action if ctx.picker then + ctx.picker.visual = ctx.picker.visual or picker.visual or nil ctx.picker:focus() end - ctx.main = ctx.main or picker and picker.main or nil + update_main(item, ctx) it.action.action(item, ctx) picker:close() end, @@ -242,25 +261,55 @@ M.actions.gh_comment = { title = "Comment on {type} #{number}", icon = " ", action = function(item, ctx) - item = item.gh_item or item -- unwrap from - local current_win = vim.api.nvim_get_current_win() - local win = vim.w[current_win].snacks_picker_preview and current_win or ctx.main or current_win - local buf = vim.api.nvim_win_get_buf(win) - local action = vim.deepcopy(M.cli_actions.gh_comment) - if vim.b[buf].snacks_meta then - local lino = vim.api.nvim_win_get_cursor(win)[1] - local meta = vim.b[buf].snacks_meta or {} - for _, c in ipairs(meta) do - if c.line == lino and c.comment_id then + local win, buf = update_main(item, ctx) + if win and buf then + local meta = Snacks.picker.highlight.meta(buf) + if meta then + ---@type {comment_id?: number, diff?: snacks.diff.Meta}? + local m = meta[vim.api.nvim_win_get_cursor(win)[1]] + if m and m.comment_id then action.title = "Reply to comment on {type} #{number}" + action.api = { + endpoint = "/repos/{repo}/pulls/{number}/comments", + input = { in_reply_to = m.comment_id }, + } + elseif m and m.diff then + local visual = ctx.picker and ctx.picker.visual or Snacks.picker.util.visual() + visual = visual and visual.buf == buf and visual or nil + local line = m.diff.line ---@type number + local start_line ---@type number? + if visual then + local line_diff = vim.tbl_get(meta, visual.end_pos[1], "diff") or m.diff --[[@as {file: string, line: number, side: string}]] + local start_diff = vim.tbl_get(meta, visual.pos[1], "diff") or m.diff --[[@as {file: string, line: number, side: string}]] + if line_diff.file ~= start_diff.file then + Snacks.notify.error("Cannot add comment: visual selection spans multiple files") + return + end + line, start_line = line_diff.line, start_diff.line + start_line, line = math.min(start_line or line, line), math.max(start_line or line, line) + end + start_line = start_line ~= line and start_line or nil + if start_line then + action.title = ("Comment on lines %s%d to %s%d"):format( + m.diff.side:sub(1, 1):upper(), + start_line or line, + m.diff.side:sub(1, 1):upper(), + line + ) + else + action.title = ("Comment on line %s%d"):format(m.diff.side:sub(1, 1):upper(), line) + end action.api = { endpoint = "/repos/{repo}/pulls/{number}/comments", input = { - in_reply_to = c.comment_id, + commit_id = item.headRefOid, -- or item.headCommit.oid depending on your data structure + path = m.diff.file, + side = m.diff.side:upper(), -- "RIGHT" or "LEFT" (uppercase) + line = line, + start_line = start_line, }, } - break end end end diff --git a/lua/snacks/gh/api.lua b/lua/snacks/gh/api.lua index e44758e4..5c277ee2 100644 --- a/lua/snacks/gh/api.lua +++ b/lua/snacks/gh/api.lua @@ -56,6 +56,7 @@ local config = { "baseRefName", "deletions", "headRefName", + "headRefOid", "mergedAt", "statusCheckRollup", "reviews", diff --git a/lua/snacks/gh/buf.lua b/lua/snacks/gh/buf.lua index e92f467d..e6617f32 100644 --- a/lua/snacks/gh/buf.lua +++ b/lua/snacks/gh/buf.lua @@ -32,7 +32,7 @@ function M.new(buf, item) vim.b[buf].snacks_gh = { repo = item.repo, type = item.type, - number = item.number, + number = tonumber(item.number) or item.number, } self:bo() self:wo() diff --git a/lua/snacks/gh/types.lua b/lua/snacks/gh/types.lua index 0b76592c..3c886914 100644 --- a/lua/snacks/gh/types.lua +++ b/lua/snacks/gh/types.lua @@ -133,6 +133,7 @@ ---@field statusCheckRollup? snacks.gh.Check[] ---@field baseRefName? string ---@field headRefName? string +---@field headRefOid? string ---@field isDraft? boolean ---@field reviews? snacks.gh.Review[] ---@field reviewThreads? snacks.gh.review.Thread[] diff --git a/lua/snacks/picker/config/defaults.lua b/lua/snacks/picker/config/defaults.lua index cea49600..77f9e3c9 100644 --- a/lua/snacks/picker/config/defaults.lua +++ b/lua/snacks/picker/config/defaults.lua @@ -2,7 +2,7 @@ local M = {} ---@alias snacks.picker.format.resolve fun(max_width:number):snacks.picker.Highlight[] ---@alias snacks.picker.Extmark vim.api.keyset.set_extmark|{col:number, row?:number, field?:string} ----@alias snacks.picker.Meta {line?:number, [string]:any} +---@alias snacks.picker.Meta {[string]:any} ---@alias snacks.picker.Text {[1]:string, [2]:(string|string[])?, virtual?:boolean, field?:string, resolve?:snacks.picker.format.resolve, inline?:boolean} ---@alias snacks.picker.Highlight snacks.picker.Text|snacks.picker.Extmark|{meta?:snacks.picker.Meta} ---@alias snacks.picker.format fun(item:snacks.picker.Item, picker:snacks.Picker):snacks.picker.Highlight[] diff --git a/lua/snacks/picker/config/sources.lua b/lua/snacks/picker/config/sources.lua index d3d4ad49..92d1dedf 100644 --- a/lua/snacks/picker/config/sources.lua +++ b/lua/snacks/picker/config/sources.lua @@ -282,11 +282,12 @@ M.gh_diff = { group = true, finder = "gh_diff", format = "git_status", - preview = "diff", + preview = "gh_preview_diff", win = { preview = { keys = { ["a"] = { "gh_comment", mode = { "n", "x" } }, + [""] = { "gh_actions", mode = { "n", "x" } }, }, }, }, diff --git a/lua/snacks/picker/core/actions.lua b/lua/snacks/picker/core/actions.lua index f2631de8..d6848c7f 100644 --- a/lua/snacks/picker/core/actions.lua +++ b/lua/snacks/picker/core/actions.lua @@ -32,7 +32,6 @@ end ---@param action snacks.picker.Action.spec ---@param ref snacks.Picker.ref ---@param name? string ----@return snacks.win.Action? function M.wrap(action, ref, name) local picker = ref() if not picker then @@ -40,6 +39,7 @@ function M.wrap(action, ref, name) end action = M.resolve(action, picker, name) action.name = name + ---@type snacks.win.Action return { name = name, action = function() diff --git a/lua/snacks/picker/preview.lua b/lua/snacks/picker/preview.lua index 822343b1..74cecb94 100644 --- a/lua/snacks/picker/preview.lua +++ b/lua/snacks/picker/preview.lua @@ -188,7 +188,6 @@ local function fancy_diff(diff, ft, ctx) local buf = ctx.preview:scratch() ctx.preview.win:map() require("snacks.picker.util.diff").render(buf, ns, diff, { - ft = ft, annotations = ctx.item.annotations or ctx.picker.opts.annotations, }) Snacks.util.wo(ctx.win, ctx.picker.opts.previewers.diff.wo or {}) diff --git a/lua/snacks/picker/source/gh.lua b/lua/snacks/picker/source/gh.lua index 7737bdbd..fad4f64c 100644 --- a/lua/snacks/picker/source/gh.lua +++ b/lua/snacks/picker/source/gh.lua @@ -15,10 +15,15 @@ M.actions = setmetatable({}, { local action = { desc = Actions.actions[k].desc, action = function(picker, item, action) + local items = picker:selected({ fallback = true }) + if item.gh_item then + item = item.gh_item + items = { item } + end ---@diagnostic disable-next-line: param-type-mismatch return Actions.actions[k].action(item, { picker = picker, - items = picker:selected({ fallback = true }), + items = items, action = action, }) end, @@ -307,6 +312,19 @@ function M.format(item, picker) return ret end +---@param ctx snacks.picker.preview.ctx +function M.preview_diff(ctx) + Snacks.picker.preview.diff(ctx) + local item = ctx.item.gh_item ---@type snacks.picker.gh.Item? + if item then + vim.b[ctx.buf].snacks_gh = { + repo = item.repo, + type = item.type, + number = item.number, + } + end +end + ---@param ctx snacks.picker.preview.ctx function M.preview(ctx) local config = require("snacks.gh").config() diff --git a/lua/snacks/picker/util/diff.lua b/lua/snacks/picker/util/diff.lua index 6168a519..742c2757 100644 --- a/lua/snacks/picker/util/diff.lua +++ b/lua/snacks/picker/util/diff.lua @@ -3,7 +3,6 @@ local M = {} ---@class snacks.diff.Config ---@field max_hunk_lines? number only show last N lines of each hunk (used by GitHub PRs) ---@field hunk_header? boolean whether to show hunk header (default: true) ----@field ft? "diff" | "git" ---@field annotations? snacks.diff.Annotation[] ---@class snacks.diff.Annotation @@ -14,6 +13,11 @@ local M = {} ---@field line number ---@field text snacks.picker.Highlight[][] +---@class snacks.diff.Meta +---@field side "left" | "right" +---@field file string +---@field line number + ---@class snacks.diff.ctx ---@field diff snacks.picker.Diff ---@field opts snacks.diff.Config diff --git a/lua/snacks/picker/util/highlight.lua b/lua/snacks/picker/util/highlight.lua index 6c2acaef..bb9e5064 100644 --- a/lua/snacks/picker/util/highlight.lua +++ b/lua/snacks/picker/util/highlight.lua @@ -2,7 +2,7 @@ local M = {} ---@class (private) vim.var_accessor ----@field snacks_meta? snacks.picker.Meta[] +---@field snacks_meta? table M.langs = {} ---@type table M._scratch = {} ---@type table @@ -471,6 +471,19 @@ function M.fix_offset(hl, offset, start_idx) return hl end +--- tables with number as keys are stored in vim.b as an array, +--- so we need to filter out vim.NIL +---@param buf number +function M.meta(buf) + local ret = {} ---@type table + for k, v in pairs(vim.b[buf].snacks_meta or {}) do + if v ~= vim.NIL then + ret[k] = v + end + end + return not vim.tbl_isempty(ret) and ret or nil +end + ---@param dst snacks.picker.Highlight[] ---@param src snacks.picker.Highlight[] function M.extend(dst, src) @@ -492,7 +505,7 @@ function M.render(buf, ns, lines, opts) vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) end - local meta = {} ---@type snacks.picker.Meta[] + local meta = {} ---@type table local changed = #lines ~= #old_lines local offset = opts.append and vim.api.nvim_buf_line_count(buf) or 0 @@ -504,8 +517,7 @@ function M.render(buf, ns, lines, opts) changed = true end if line_meta then - line_meta.line = offset + l - meta[#meta + 1] = line_meta + meta[offset + l] = line_meta end for _, extmark in ipairs(extmarks) do local e = vim.deepcopy(extmark) diff --git a/lua/snacks/picker/util/init.lua b/lua/snacks/picker/util/init.lua index 050ef240..c80c5f24 100644 --- a/lua/snacks/picker/util/init.lua +++ b/lua/snacks/picker/util/init.lua @@ -196,6 +196,7 @@ function M.visual() local text = table.concat(lines, "\n") ---@class snacks.picker.Visual local ret = { + buf = vim.api.nvim_get_current_buf(), pos = pos, end_pos = end_pos, text = text,