feat(picker): added git_branches picker. Closes #614

This commit is contained in:
Folke Lemaitre 2025-01-19 16:24:35 +01:00
parent 903431903b
commit 8563dfce68
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
8 changed files with 196 additions and 7 deletions

View file

@ -144,6 +144,21 @@ function M.git_stage(picker)
end
end
function M.git_checkout(picker, item)
picker:close()
if item then
local what = item.branch or item.commit
if not what then
Snacks.notify.warn("No branch or commit found", { title = "Snacks Picker" })
return
end
local cmd = { "git", "checkout", what }
Snacks.picker.util.cmd(cmd, function()
Snacks.notify("Checkout " .. what, { title = "Snacks Picker" })
end, { cwd = item.cwd })
end
end
---@param items snacks.picker.Item[]
---@param opts? {win?:number}
local function setqflist(items, opts)

View file

@ -44,6 +44,8 @@ Snacks.util.set_hl({
KeymapRhs = "NonText",
GitCommit = "@variable.builtin",
GitBreaking = "Error",
GitBranch = "Title",
GitBranchCurrent = "Number",
GitDate = "Special",
GitIssue = "Number",
GitType = "Title", -- conventional commit type

View file

@ -126,6 +126,22 @@ M.files = {
supports_live = true,
}
M.git_branches = {
finder = "git_branches",
format = "git_branch",
preview = "git_log",
confirm = "git_checkout",
on_show = function(picker)
for i, item in ipairs(picker:items()) do
if item.current then
picker.list:view(i)
Snacks.picker.actions.list_scroll_center(picker)
break
end
end
end,
}
-- Find git files
---@class snacks.picker.git.files.Config: snacks.picker.Config
---@field untracked? boolean show untracked files

View file

@ -35,6 +35,7 @@ function M.new(opts, main)
colorcolumn = "",
number = true,
relativenumber = true,
list = false,
},
},
opts.win.preview,
@ -93,6 +94,9 @@ end
---@param picker snacks.Picker
function M:show(picker)
local item, prev = picker:current(), self.item
if self.item == item then
return
end
self.item = item
if item then
local buf = self.win.buf
@ -160,6 +164,7 @@ function M:reset()
vim.o.eventignore = "all"
vim.bo[self.win.buf].filetype = "snacks_picker_preview"
vim.bo[self.win.buf].syntax = ""
vim.bo[self.win.buf].buftype = "nofile"
vim.o.eventignore = ei
self:wo({ cursorline = false })
self:wo(self.win.opts.wo)
@ -174,7 +179,7 @@ function M:scratch()
vim.bo[buf].filetype = "snacks_picker_preview"
vim.o.eventignore = ei
vim.api.nvim_win_set_buf(self.win.win, buf)
self:wo({ number = false, relativenumber = false })
self:wo({ number = false, relativenumber = false, signcolumn = "no" })
return buf
end
@ -232,6 +237,10 @@ end
---@param level? "info" | "warn" | "error"
---@param opts? {item?:boolean}
function M:notify(msg, level, opts)
if not self.win:buf_valid() then
Snacks.notify(msg, { level = level })
return
end
level = level or "info"
local lines = vim.split(level .. ": " .. msg, "\n", { plain = true })
local msg_len = #lines

View file

@ -98,8 +98,11 @@ function M.git_log(item, picker)
local ret = {} ---@type snacks.picker.Highlight[]
ret[#ret + 1] = { picker.opts.icons.git.commit, "SnacksPickerGitCommit" }
ret[#ret + 1] = { item.commit, "SnacksPickerGitCommit" }
ret[#ret + 1] = { " " }
ret[#ret + 1] = { a(item.date, 16), "SnacksPickerGitDate" }
if item.date then
ret[#ret + 1] = { a(item.date, 16), "SnacksPickerGitDate" }
end
local msg = item.msg ---@type string
local type, scope, breaking, body = msg:match("^(%S+)(%(.-%))(!?):%s*(.*)$")
@ -130,6 +133,24 @@ function M.git_log(item, picker)
return ret
end
function M.git_branch(item, picker)
local a = Snacks.picker.util.align
local ret = {} ---@type snacks.picker.Highlight[]
if item.current then
ret[#ret + 1] = { a("", 2), "SnacksPickerGitBranchCurrent" }
ret[#ret + 1] = { a(item.branch, 30, { truncate = true }), "SnacksPickerGitBranch" }
else
ret[#ret + 1] = { a("", 2) }
ret[#ret + 1] = { a(item.branch, 30, { truncate = true }), "SnacksPickerGitBranch" }
end
ret[#ret + 1] = { " " }
local offset = Snacks.picker.highlight.offset(ret)
local log = M.git_log(item, picker)
Snacks.picker.highlight.fix_offset(log, offset)
vim.list_extend(ret, log)
return ret
end
function M.lsp_symbol(item, picker)
local ret = {} ---@type snacks.picker.Highlight[]
if item.hierarchy then

View file

@ -115,6 +115,7 @@ function M.file(ctx)
file:close()
vim.bo[ctx.buf].buftype = ""
vim.api.nvim_buf_set_lines(ctx.buf, 0, -1, false, lines)
vim.bo[ctx.buf].modifiable = false
ctx.preview:highlight({ file = path, ft = ctx.picker.opts.previewers.file.ft, buf = ctx.buf })
@ -125,14 +126,29 @@ end
---@param cmd string[]
---@param ctx snacks.picker.preview.ctx
---@param opts? {env?:table<string, string>, pty?:boolean, ft?:string}
---@param opts? {add?:fun(text:string, row:number), env?:table<string, string>, pty?:boolean, ft?:string}
function M.cmd(cmd, ctx, opts)
opts = opts or {}
local buf = ctx.preview:scratch()
vim.bo[buf].buftype = "nofile"
local pty = opts.pty ~= false and not opts.ft
local killed = false
local chan = pty and vim.api.nvim_open_term(buf, {}) or nil
local output = {} ---@type string[]
local line ---@type string?
local l = 0
---@param text string
local function add_line(text)
l = l + 1
vim.bo[buf].modifiable = true
if opts.add then
opts.add(text, l)
else
vim.api.nvim_buf_set_lines(buf, -1, -1, false, { text })
end
vim.bo[buf].modifiable = false
end
---@param data string
local function add(data)
@ -144,10 +160,12 @@ function M.cmd(cmd, ctx, opts)
end)
end
else
vim.bo[buf].modifiable = true
local lines = vim.split(table.concat(output, "\n"), "\n")
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
line = (line or "") .. data
local lines = vim.split(line, "\r?\n")
line = table.remove(lines)
for _, text in ipairs(lines) do
add_line(text)
end
end
end
@ -167,6 +185,9 @@ function M.cmd(cmd, ctx, opts)
add(table.concat(data, "\n"))
end,
on_exit = function(_, code)
if not killed and line and line ~= "" and vim.api.nvim_buf_is_valid(buf) then
add_line(line)
end
if not killed and code ~= 0 then
Snacks.notify.error(
("Terminal **cmd** `%s` failed with code `%d`:\n- `vim.o.shell = %q`\n\nOutput:\n%s"):format(
@ -217,6 +238,48 @@ function M.git_show(ctx)
M.cmd(cmd, ctx, { ft = not native and "git" or nil })
end
---@param ctx snacks.picker.preview.ctx
function M.git_log(ctx)
local native = ctx.picker.opts.previewers.git.native
local cmd = {
"git",
"-c",
"delta." .. vim.o.background .. "=true",
"log",
"--pretty=format:%h %s (%ch)",
"--abbrev-commit",
"--decorate",
"--date=short",
"--color=never",
"--no-show-signature",
"--no-patch",
ctx.item.branch,
}
if not native then
table.insert(cmd, 2, "--no-pager")
end
local row = 0
M.cmd(cmd, ctx, {
ft = not native and "git" or nil,
---@param text string
add = not native and function(text)
local commit, msg, date = text:match("^(%S+) (.*) %((.*)%)$")
if commit then
row = row + 1
local hl = Snacks.picker.format.git_log({
idx = 1,
score = 0,
text = "",
commit = commit,
msg = msg,
date = date,
}, ctx.picker)
Snacks.picker.highlight.set(ctx.buf, ns, row, hl)
end
end or nil,
})
end
---@param ctx snacks.picker.preview.ctx
function M.git_diff(ctx)
local native = ctx.picker.opts.previewers.git.native

View file

@ -86,6 +86,7 @@ end
---@type snacks.picker.finder
function M.status(opts)
local args = {
"--no-pager",
"status",
"-uall",
"--porcelain=v1",
@ -154,4 +155,26 @@ function M.diff(opts)
end
end
---@param opts snacks.picker.Config
---@type snacks.picker.finder
function M.branches(opts)
local args = { "--no-pager", "branch", "--no-color", "-vvl" }
local cwd = vim.fs.normalize(opts and opts.cwd or uv.cwd() or ".") or nil
cwd = Snacks.git.get_root(cwd)
return require("snacks.picker.source.proc").proc(vim.tbl_deep_extend("force", {
cwd = cwd,
cmd = "git",
args = args,
---@param item snacks.picker.finder.Item
transform = function(item)
local status, branch, commit, msg = item.text:match("^(.)%s(%S+)%s+([a-zA-Z0-9]+)%s*(.*)$")
item.cwd = cwd
item.current = status == "*"
item.branch = branch
item.commit = commit
item.msg = msg
end,
}, opts or {}))
end
return M

View file

@ -224,4 +224,44 @@ function M.to_text(line, opts)
return table.concat(parts), ret
end
---@param hl snacks.picker.Highlight[]
function M.fix_offset(hl, offset)
for _, t in ipairs(hl) do
if t.col then
t.col = t.col + offset
end
if t.end_col then
t.end_col = t.end_col + offset
end
end
end
---@param buf number
---@param ns number
---@param row number
---@param hl snacks.picker.Highlight[]
function M.set(buf, ns, row, hl)
while #hl > 0 and type(hl[#hl][1]) == "string" and hl[#hl][1]:find("^%s*$") do
table.remove(hl)
end
local line_text, extmarks = Snacks.picker.highlight.to_text(hl)
vim.api.nvim_buf_set_lines(buf, row - 1, row, false, { line_text })
for _, extmark in ipairs(extmarks) do
local col = extmark.col
extmark.col = nil
extmark.row = nil
extmark.field = nil
local ok, err = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row - 1, col, extmark)
if not ok then
Snacks.notify.error(
"Failed to set extmark. This should not happen. Please report.\n"
.. err
.. "\n```lua\n"
.. vim.inspect(extmark)
.. "\n```"
)
end
end
end
return M