mirror of
https://github.com/folke/snacks.nvim
synced 2025-12-23 08:47:57 +00:00
feat(gh): create review comments in GitHub PR diff, on diff lines. Closes #2446
This commit is contained in:
parent
42374e9a6d
commit
85bf3f0123
12 changed files with 111 additions and 25 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ local config = {
|
|||
"baseRefName",
|
||||
"deletions",
|
||||
"headRefName",
|
||||
"headRefOid",
|
||||
"mergedAt",
|
||||
"statusCheckRollup",
|
||||
"reviews",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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" } },
|
||||
["<cr>"] = { "gh_actions", mode = { "n", "x" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
local M = {}
|
||||
|
||||
---@class (private) vim.var_accessor
|
||||
---@field snacks_meta? snacks.picker.Meta[]
|
||||
---@field snacks_meta? table<number,snacks.picker.Meta>
|
||||
|
||||
M.langs = {} ---@type table<string, boolean>
|
||||
M._scratch = {} ---@type table<string, number>
|
||||
|
|
@ -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<number, snacks.picker.Meta>
|
||||
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<number, snacks.picker.Meta>
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue