feat(picker.scratch): add scratch picker with grep, new and delete keybinds (#1019)

## Description


The scratch module uses `vim.ui.select` which misses the nice things
about the picker.
This implementation adds scratch picker with ability to create, grep and
delete scratch buffers.

Couldn't figure out how to prettify the scratch buffer's name so any
help would be appreciated.
## Related Issue(s)

<!--
  If this PR fixes any issues, please link to the issue here.
  - Fixes #<issue_number>
-->

## Screenshots

<!-- Add screenshots of the changes if applicable. -->

---------

Co-authored-by: Folke Lemaitre <folke.lemaitre@gmail.com>
This commit is contained in:
maskudo 2025-10-23 14:38:02 +05:45 committed by GitHub
parent 4e1070867a
commit ca0f8b2c09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 94 additions and 35 deletions

View file

@ -300,6 +300,19 @@ function M.finder(finder)
return M.field(finder) or nop
end
---@param picker snacks.Picker
---@param action string
function M.action(picker, action)
local ret = (picker.opts.actions or {})[action] or require("snacks.picker.actions")[action]
if ret then
return ret
end
local source = action:match("^(.-)_")
if source then -- source specific action
return (M.field(("%s_actions"):format(source)) or {})[action]
end
end
--- Resolves a module field
---@param spec string
function M.field(spec)

View file

@ -807,6 +807,21 @@ M.registers = {
-- Special picker that resumes the last picker
M.resume = {}
-- Open or create scratch buffers
M.scratch = {
finder = "scratch",
format = "scratch_format",
confirm = "scratch_open",
win = {
input = {
keys = {
["<c-x>"] = { "scratch_delete", mode = { "n", "i" } },
["<c-n>"] = { "scratch_new", mode = { "n", "i" } },
},
},
},
}
-- Neovim search history
---@type snacks.picker.history.Config
M.search_history = {

View file

@ -81,16 +81,8 @@ function M.resolve(action, picker, name, stack)
end
end
stack[#stack + 1] = action
return M.resolve(
(picker.opts.actions or {})[action]
or require("snacks.picker.actions")[action]
or require("snacks.explorer.actions").actions[action],
picker,
action,
stack
)
return M.resolve(Snacks.picker.config.action(picker, action), picker, action, stack)
elseif type(action) == "table" and svim.islist(action) then
---@type snacks.picker.Action[]
local actions = vim.tbl_map(function(a)
return M.resolve(a, picker, nil, stack)
end, action)

View file

@ -4,6 +4,8 @@ local Tree = require("snacks.explorer.tree")
local M = {}
M.actions = Actions.actions
---@type table<snacks.Picker, snacks.picker.explorer.State>
M._state = setmetatable({}, { __mode = "k" })
local uv = vim.uv or vim.loop

View file

@ -0,0 +1,62 @@
local M = {}
---@class snacks.scratch.actions
---@field [string] snacks.picker.Action.spec
M.actions = {
scratch_open = function(picker, item)
picker:close()
if not item then
return
end
Snacks.scratch.open({ icon = item.item.icon, file = item.item.file, name = item.item.name, ft = item.item.ft })
end,
scratch_delete = function(picker, item)
local current = item.file
os.remove(current)
picker.list:set_selected()
picker.list:set_target()
picker:find()
end,
scratch_new = function(picker)
picker:close()
Snacks.scratch.open()
end,
}
---@param opts snacks.picker.proc.Config
---@type snacks.picker.finder
function M.scratch(opts)
local list = Snacks.scratch.list()
local items = {} ---@type snacks.picker.finder.Item[]
for _, item in ipairs(list) do
items[#items + 1] = {
file = item.file,
item = item,
title = item.name,
text = Snacks.picker.util.text(item, { "name", "branch", "ft" }),
branch = item.branch and ("branch:%s"):format(item.branch) or "",
}
end
return items
end
---@type snacks.picker.format
function M.format(item, picker)
local file = item.item
local ret = {} ---@type snacks.picker.Highlight[]
local a = Snacks.picker.util.align
local icon, icon_hl = file.icon, nil
if not icon then
icon, icon_hl = Snacks.util.icon(file.ft, "filetype")
end
ret[#ret + 1] = { a(icon, 3), icon_hl }
ret[#ret + 1] = { a(file.name, 20, { truncate = true }) }
ret[#ret + 1] = { " " }
ret[#ret + 1] = { a(item.branch, 20, { truncate = true }), "Number" }
ret[#ret + 1] = { " " }
---@diagnostic disable-next-line: missing-fields
vim.list_extend(ret, Snacks.picker.format.filename({ text = "", dir = true, file = file.cwd }, picker))
return ret
end
return M

View file

@ -119,32 +119,7 @@ end
--- Select a scratch buffer from a list of scratch buffers.
function M.select()
local widths = { 0, 0, 0, 0 }
local items = M.list()
for _, item in ipairs(items) do
item.icon = item.icon or Snacks.util.icon(item.ft, "filetype")
item.branch = item.branch and ("branch:%s"):format(item.branch) or ""
item.cwd = item.cwd and vim.fn.fnamemodify(item.cwd, ":p:~") or ""
widths[1] = math.max(widths[1], vim.api.nvim_strwidth(item.cwd))
widths[2] = math.max(widths[2], vim.api.nvim_strwidth(item.icon))
widths[3] = math.max(widths[3], vim.api.nvim_strwidth(item.name))
widths[4] = math.max(widths[4], vim.api.nvim_strwidth(item.branch))
end
vim.ui.select(items, {
prompt = "Select Scratch Buffer",
---@param item snacks.scratch.File
format_item = function(item)
local parts = { item.cwd, item.icon, item.name, item.branch }
for i, part in ipairs(parts) do
parts[i] = part .. string.rep(" ", widths[i] - vim.api.nvim_strwidth(part))
end
return table.concat(parts, " ")
end,
}, function(selected)
if selected then
M.open({ icon = selected.icon, file = selected.file, name = selected.name, ft = selected.ft })
end
end)
return Snacks.picker.scratch()
end
--- Open a scratch buffer with the given options.