feat(picker): flexible filename format (#2294)

## Description

This PR builds on top of #743 to add flexible filename formatting in the
picker.

### Changes:
- **Flexible path truncation**: Added support for different truncation
strategies (`left`, `center`, `right`) instead of just a fixed number
- `left`: truncates the beginning of the path (e.g.,
`…/path/to/file.lua`)
- `center`: truncates the middle of the path (default behavior, e.g.,
`~/pro…/file.lua`)
  - `right`: truncates the end of the path (e.g., `~/projects/long…`)
- **Dynamic width calculation**: The filename formatter now adapts to
available window width using a resolve function
- **Enhanced truncate utility**: Updated `M.truncate()` to support
left-side truncation

### Implementation:
The filename formatter now uses a `resolve` function that receives
context including the picker, item, current offset, and maximum
available width. This allows the formatter to make intelligent decisions
about how to display the path based on actual available space.

## Related Issue(s)
- Based on #743
- Addresses dynamic path formatting needs

## Technical Details
The implementation introduces:
1. `snacks.picker.format.ctx` - context passed to resolve functions
2. `snacks.picker.format.resolve` - callback type for dynamic formatting
3. `Snacks.picker.highlight.resolve()` - resolves flex text elements in
highlight arrays

---------

Co-authored-by: qw457812 <37494864+qw457812@users.noreply.github.com>
This commit is contained in:
Folke Lemaitre 2025-10-20 14:02:52 -07:00 committed by GitHub
parent 475fb69947
commit 9ad5d5374a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 101 additions and 15 deletions

View file

@ -1,7 +1,14 @@
local M = {}
---@class snacks.picker.format.ctx
---@field picker snacks.Picker
---@field item snacks.picker.Item
---@field offset number
---@field max_width number
---@alias snacks.picker.format.resolve fun(ctx:snacks.picker.format.ctx):snacks.picker.Highlight[]
---@alias snacks.picker.Extmark vim.api.keyset.set_extmark|{col:number, row?:number, field?:string}
---@alias snacks.picker.Text {[1]:string, [2]:string?, virtual?:boolean, field?:string}
---@alias snacks.picker.Text {[1]:string, [2]:string?, virtual?:boolean, field?:string, resolve?:snacks.picker.format.resolve}
---@alias snacks.picker.Highlight snacks.picker.Text|snacks.picker.Extmark
---@alias snacks.picker.format fun(item:snacks.picker.Item, picker:snacks.Picker):snacks.picker.Highlight[]
---@alias snacks.picker.preview fun(ctx: snacks.picker.preview.ctx):boolean?
@ -146,7 +153,11 @@ local defaults = {
},
file = {
filename_first = false, -- display filename before the file path
truncate = 40, -- truncate the file path to (roughly) this length
--- * left: truncate the beginning of the path
--- * center: truncate the middle of the path
--- * right: truncate the end of the path
---@type "left"|"center"|"right"
truncate = "center",
filename_only = false, -- only show the filename
icon_width = 2, -- width of the icon (in characters)
git_status_hl = true, -- use the git status highlight group for the filename

View file

@ -475,6 +475,16 @@ function M:format(item)
-- Add the formatted item
local line = self.picker.format(item, self.picker)
---@type snacks.picker.format.ctx
local ctx = {
picker = self.picker,
item = item,
offset = vim.api.nvim_strwidth(text),
max_width = vim.api.nvim_win_get_width(self.win.win) - 5,
}
line = Snacks.picker.highlight.resolve(line, ctx)
while #line > 0 and type(line[#line][1]) == "string" and line[#line][1]:find("^%s*$") do
table.remove(line)
end

View file

@ -41,13 +41,26 @@ end
---@param item snacks.picker.Item
function M.filename(item, picker)
---@type snacks.picker.Text[]
return {
{
"",
resolve = function(ctx)
return M._filename(ctx)
end,
},
}
end
---@type snacks.picker.format.resolve
function M._filename(ctx)
local picker, item = ctx.picker, ctx.item
---@type snacks.picker.Highlight[]
local ret = {}
if not item.file then
return ret
end
local path = Snacks.picker.util.path(item) or item.file
path = Snacks.picker.util.truncpath(path, picker.opts.formatters.file.truncate or 40, { cwd = picker:cwd() })
local name, cat = path, "file"
if item.buf and vim.api.nvim_buf_is_loaded(item.buf) then
name = vim.bo[item.buf].filetype
@ -67,6 +80,9 @@ function M.filename(item, picker)
ret[#ret + 1] = { icon, hl, virtual = true }
end
local truncate = picker.opts.formatters.file.truncate
path = Snacks.picker.util.truncpath(path, ctx.max_width, { cwd = picker:cwd(), kind = truncate })
local base_hl = item.dir and "SnacksPickerDirectory" or "SnacksPickerFile"
local function is(prop)
local it = item

View file

@ -202,6 +202,42 @@ function M.winhl(prefix, links)
return table.concat(ret, ",")
end
--- Resolves the first flex text in the line.
---@param line snacks.picker.Highlight[]
---@param ctx snacks.picker.format.ctx
function M.resolve(line, ctx)
local ret = {} ---@type snacks.picker.Highlight[]
local offset = 0
local width = 0
local resolve ---@type number?
for t, text in ipairs(line) do
local w = M.offset({ text }, { char_idx = true })
if not resolve and type(text) == "table" and text.resolve then
---@cast text snacks.picker.Text
resolve = t
elseif resolve then
width = width + w
else
width = width + w
offset = offset + w
end
end
if resolve then
ctx.offset = offset
ctx.max_width = ctx.max_width - width
vim.list_extend(ret, line, 1, resolve - 1)
offset = M.offset(ret)
vim.list_extend(ret, line[resolve].resolve(ctx))
local diff = M.offset(ret) - offset
vim.list_extend(ret, line, resolve + 1)
M.fix_offset(ret, diff, resolve + 1)
end
return ret
end
---@param line snacks.picker.Highlight[]
---@param opts? {offset?:number}
function M.to_text(line, opts)
@ -248,13 +284,16 @@ function M.to_text(line, opts)
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
---@param start_idx? number
function M.fix_offset(hl, offset, start_idx)
for i, t in ipairs(hl) do
if start_idx == nil or i >= start_idx then
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
end

View file

@ -15,9 +15,10 @@ function M.path(item)
end
---@param path string
---@param len? number
---@param opts? {cwd?: string}
---@param len number
---@param opts? {cwd?: string, kind?: "left" | "center" | "right"}
function M.truncpath(path, len, opts)
opts = opts or {}
local cwd = svim.fs.normalize(opts and opts.cwd or vim.fn.getcwd(), { _fast = true, expand_env = false })
local home = svim.fs.normalize("~")
path = svim.fs.normalize(path, { _fast = true, expand_env = false })
@ -35,6 +36,12 @@ function M.truncpath(path, len, opts)
end
path = path:gsub("/$", "")
if opts.kind == "left" then
return M.truncate(path, len, true)
elseif opts.kind == "right" then
return M.truncate(path, len, false)
end
if vim.api.nvim_strwidth(path) <= len then
return path
end
@ -135,9 +142,12 @@ end
---@param text string
---@param width number
function M.truncate(text, width)
if vim.api.nvim_strwidth(text) > width then
return vim.fn.strcharpart(text, 0, width - 1) .. ""
---@param left? boolean
function M.truncate(text, width, left)
local tw = vim.api.nvim_strwidth(text)
if tw > width then
return left and "" .. vim.fn.strcharpart(text, tw - width + 1, width - 1)
or vim.fn.strcharpart(text, 0, width - 1) .. ""
end
return text
end