Skip to content

Commit 580692c

Browse files
authored
Merge pull request #9 from mhiro2/feat/picker-readable-labels
feat(picker): Unify readable labels and backend-native highlights
2 parents acafa3b + f3c290e commit 580692c

13 files changed

Lines changed: 328 additions & 22 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ default backend is `builtin`. To use an external picker, install the plugin and
272272
`picker.backend` to one of: `telescope`, `fzf-lua`, `snacks`.
273273
When using these external backends, the picker preview window shows the selected file
274274
content around the target location.
275+
Candidate labels are shown in a readable unified format:
276+
`<text> - <path>:<line>:<col>` (or `<path>:<line>:<col>` when text is empty).
275277

276278
```lua
277279
{

doc/peekstack.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ Configure the backend with `picker.backend`:
155155

156156
For external backends (`telescope`, `fzf-lua`, `snacks`), picker preview windows
157157
show file content around the selected location.
158+
Picker labels use a unified readable format:
159+
<text> - <path>:<line>:<col>
160+
or, when text is empty:
161+
<path>:<line>:<col>
158162

159163
If the chosen plugin is not installed, a warning is shown and the picker will not open.
160164

lua/peekstack/picker/fzf_lua.lua

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@ function M.pick(locations, opts, cb)
1818
item.index = idx
1919
end
2020

21+
local user_opts = opts or {}
22+
local fzf_opts = vim.tbl_extend("force", user_opts.fzf_opts or {}, {
23+
["--delimiter"] = "\t",
24+
["--with-nth"] = "2",
25+
["--nth"] = "2",
26+
})
27+
2128
---@type table
22-
local exec_opts = vim.tbl_extend("force", opts or {}, {
29+
local exec_opts = vim.tbl_deep_extend("force", user_opts, {
2330
prompt = "Peekstack> ",
2431
previewer = "builtin",
32+
fzf_opts = fzf_opts,
2533
actions = {
2634
["default"] = function(selected)
2735
if not selected or not selected[1] then
@@ -41,7 +49,8 @@ function M.pick(locations, opts, cb)
4149
for _, item in ipairs(items) do
4250
local file = item.file or ""
4351
local label = item.label:gsub("[%r\n\t]", " ")
44-
table.insert(lines, string.format("%s:%d:%d:%s\t%d", file, item.lnum, item.col, label, item.index))
52+
local loc = string.format("%s:%d:%d", file, item.lnum, item.col)
53+
table.insert(lines, string.format("%s\t%s\t%d", loc, label, item.index))
4554
end
4655
return lines
4756
end, exec_opts)

lua/peekstack/picker/snacks.lua

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@ local picker_util = require("peekstack.util.picker")
22

33
local M = {}
44

5+
---@param item table
6+
---@return table
7+
local function format_item(item)
8+
local chunks = {}
9+
local symbol = item.symbol
10+
if type(symbol) == "string" and symbol ~= "" then
11+
chunks[#chunks + 1] = { symbol, "SnacksPickerLabel" }
12+
chunks[#chunks + 1] = { " - ", "SnacksPickerDelim" }
13+
end
14+
15+
local path = item.path
16+
if type(path) ~= "string" or path == "" then
17+
path = item.text or ""
18+
end
19+
picker_util.append_path_chunks(chunks, path, "SnacksPickerDir", "SnacksPickerFile")
20+
21+
if type(item.display_lnum) == "number" and item.display_lnum > 0 then
22+
chunks[#chunks + 1] = { ":", "SnacksPickerDelim" }
23+
chunks[#chunks + 1] = { tostring(item.display_lnum), "SnacksPickerRow" }
24+
end
25+
26+
if type(item.display_col) == "number" and item.display_col > 0 then
27+
chunks[#chunks + 1] = { ":", "SnacksPickerDelim" }
28+
chunks[#chunks + 1] = { tostring(item.display_col), "SnacksPickerCol" }
29+
end
30+
31+
return chunks
32+
end
33+
534
---Pick a location using snacks.nvim picker
635
---@param locations PeekstackLocation[]
736
---@param opts? table
@@ -20,6 +49,10 @@ function M.pick(locations, opts, cb)
2049
local start = loc.range and loc.range.start or {}
2150
table.insert(items, {
2251
text = item.label,
52+
symbol = item.symbol,
53+
path = item.path,
54+
display_lnum = item.display_lnum,
55+
display_col = item.display_col,
2356
file = item.file or loc.uri,
2457
pos = { item.lnum, start.character or 0 },
2558
peekstack_loc = loc,
@@ -29,7 +62,7 @@ function M.pick(locations, opts, cb)
2962
local picker_opts = vim.tbl_extend("force", opts or {}, {
3063
title = "Peekstack",
3164
items = items,
32-
format = "file",
65+
format = format_item,
3366
confirm = function(picker, item)
3467
if item and item.peekstack_loc then
3568
picker:close()

lua/peekstack/picker/telescope.lua

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,47 @@ local picker_util = require("peekstack.util.picker")
22

33
local M = {}
44

5+
---@param primary string
6+
---@param fallback string
7+
---@return string
8+
local function hl(primary, fallback)
9+
if vim.fn.hlexists(primary) == 1 then
10+
return primary
11+
end
12+
return fallback
13+
end
14+
15+
---@param displayer fun(chunks: table): string
16+
---@param item PeekstackPickerExternalItem
17+
---@return string
18+
local function display_entry(displayer, item)
19+
local chunks = {}
20+
if type(item.symbol) == "string" and item.symbol ~= "" then
21+
chunks[#chunks + 1] = { item.symbol, hl("TelescopeResultsIdentifier", "Function") }
22+
chunks[#chunks + 1] = { " - ", hl("TelescopeResultsComment", "Comment") }
23+
end
24+
25+
local path = item.path or item.label
26+
picker_util.append_path_chunks(
27+
chunks,
28+
path,
29+
hl("TelescopeResultsComment", "Comment"),
30+
hl("TelescopeResultsIdentifier", "Directory")
31+
)
32+
33+
if type(item.display_lnum) == "number" and item.display_lnum > 0 then
34+
chunks[#chunks + 1] = { ":", hl("TelescopeResultsComment", "Comment") }
35+
chunks[#chunks + 1] = { tostring(item.display_lnum), hl("TelescopeResultsNumber", "Number") }
36+
end
37+
38+
if type(item.display_col) == "number" and item.display_col > 0 then
39+
chunks[#chunks + 1] = { ":", hl("TelescopeResultsComment", "Comment") }
40+
chunks[#chunks + 1] = { tostring(item.display_col), hl("TelescopeResultsNumber", "Number") }
41+
end
42+
43+
return displayer(chunks)
44+
end
45+
546
---Pick a location using Telescope
647
---@param locations PeekstackLocation[]
748
---@param opts? table
@@ -13,16 +54,25 @@ function M.pick(locations, opts, cb)
1354
return
1455
end
1556
local finders = require("telescope.finders")
57+
local entry_display = require("telescope.pickers.entry_display")
1658
local conf = require("telescope.config").values
1759
local telescope_opts = opts or {}
60+
local displayer = entry_display.create({
61+
separator = "",
62+
items = {
63+
{ remaining = true },
64+
},
65+
})
1866

1967
local items = picker_util.build_external_items(locations, 1)
2068
local entries = {}
2169
for _, item in ipairs(items) do
2270
table.insert(entries, {
2371
value = item.value,
24-
display = item.label,
25-
ordinal = item.label,
72+
display = function()
73+
return display_entry(displayer, item)
74+
end,
75+
ordinal = string.format("%s %s", item.label, item.file or ""),
2676
filename = item.file,
2777
lnum = item.lnum,
2878
col = item.col,

lua/peekstack/providers/lsp.lua

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,11 @@ local function append_document_symbol(symbol, uri, provider, out)
1717
local start_pos = range and range.start
1818
local end_pos = range and range["end"]
1919
if start_pos and end_pos then
20-
local text = symbol.name
21-
if type(text) ~= "string" then
22-
text = nil
23-
end
24-
if type(symbol.detail) == "string" and symbol.detail ~= "" then
25-
if text and text ~= "" then
26-
text = string.format("%s - %s", text, symbol.detail)
27-
else
28-
text = symbol.detail
29-
end
20+
local text
21+
if type(symbol.name) == "string" and symbol.name ~= "" then
22+
text = symbol.name
23+
elseif type(symbol.detail) == "string" and symbol.detail ~= "" then
24+
text = symbol.detail
3025
end
3126

3227
table.insert(out, {

lua/peekstack/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@
118118
---@field file? string
119119
---@field lnum integer
120120
---@field col integer
121+
---@field symbol? string
122+
---@field path? string
123+
---@field display_lnum? integer
124+
---@field display_col? integer
121125

122126
---@class PeekstackHistoryEntry
123127
---@field location PeekstackLocation

lua/peekstack/util/picker.lua

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,71 @@ local fs = require("peekstack.util.fs")
44

55
local M = {}
66

7+
---@param chunks table
8+
---@param path string
9+
---@param dir_hl? string
10+
---@param file_hl? string
11+
function M.append_path_chunks(chunks, path, dir_hl, file_hl)
12+
local file_group = file_hl or "Directory"
13+
local dir, base = path:match("^(.*[/\\])(.+)$")
14+
if dir and base then
15+
if type(dir_hl) == "string" and dir_hl ~= "" then
16+
chunks[#chunks + 1] = { dir, dir_hl }
17+
else
18+
chunks[#chunks + 1] = { dir, file_group }
19+
end
20+
chunks[#chunks + 1] = { base, file_group }
21+
return
22+
end
23+
chunks[#chunks + 1] = { path, file_group }
24+
end
25+
26+
---@param text? string
27+
---@return string
28+
local function normalize_label_text(text)
29+
if type(text) ~= "string" then
30+
return ""
31+
end
32+
local normalized = text:gsub("[\r\n\t]+", " "):gsub("%s+", " ")
33+
return vim.trim(normalized)
34+
end
35+
36+
---@param suffix string
37+
---@return string, integer, integer
38+
local function parse_suffix_location(suffix)
39+
local path, line, col = suffix:match("^(.*):(%d+):(%d+)$")
40+
if not path then
41+
return suffix, 0, 0
42+
end
43+
return path, tonumber(line) or 0, tonumber(col) or 0
44+
end
45+
46+
---@param loc PeekstackLocation
47+
---@param preview_lines integer
48+
---@param opts PeekstackDisplayTextOpts
49+
---@return { label: string, symbol: string, path: string, display_lnum: integer, display_col: integer }
50+
local function build_location_label_payload(loc, preview_lines, opts)
51+
local suffix = location.display_text(loc, 0, opts)
52+
local path, display_lnum, display_col = parse_suffix_location(suffix)
53+
local symbol = preview_lines > 0 and normalize_label_text(loc.text) or ""
54+
if symbol == "" then
55+
return {
56+
label = suffix,
57+
symbol = "",
58+
path = path,
59+
display_lnum = display_lnum,
60+
display_col = display_col,
61+
}
62+
end
63+
return {
64+
label = string.format("%s - %s", symbol, suffix),
65+
symbol = symbol,
66+
path = path,
67+
display_lnum = display_lnum,
68+
display_col = display_col,
69+
}
70+
end
71+
772
---@return PeekstackDisplayTextOpts
873
local function display_text_opts()
974
local ui_path = config.get().ui.path or {}
@@ -24,8 +89,9 @@ function M.build_items(locations, preview_lines)
2489
local opts = display_text_opts()
2590
local items = {}
2691
for _, loc in ipairs(locations) do
92+
local payload = build_location_label_payload(loc, preview_lines, opts)
2793
table.insert(items, {
28-
label = location.display_text(loc, preview_lines, opts),
94+
label = payload.label,
2995
value = loc,
3096
})
3197
end
@@ -40,8 +106,13 @@ function M.build_external_items(locations, preview_lines)
40106
local items = {}
41107
for _, loc in ipairs(locations) do
42108
local start = loc.range and loc.range.start or {}
109+
local payload = build_location_label_payload(loc, preview_lines, opts)
43110
table.insert(items, {
44-
label = location.display_text(loc, preview_lines, opts),
111+
label = payload.label,
112+
symbol = payload.symbol,
113+
path = payload.path,
114+
display_lnum = payload.display_lnum,
115+
display_col = payload.display_col,
45116
value = loc,
46117
file = fs.uri_to_fname(loc.uri),
47118
lnum = (start.line or 0) + 1,

tests/lsp_provider_spec.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe("peekstack.providers.lsp", function()
8787
assert.equals(vim.uri_from_bufnr(ctx.bufnr), received[1].uri)
8888
assert.equals(1, received[1].range.start.line)
8989
assert.equals(2, received[1].range.start.character)
90-
assert.equals("Parent - class", received[1].text)
90+
assert.equals("Parent", received[1].text)
9191
assert.equals(5, received[1].kind)
9292
assert.equals("lsp.symbols_document", received[1].provider)
9393

tests/picker_fzf_lua_spec.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ describe("peekstack.picker.fzf_lua", function()
1010
fzf_exec = function(source, opts)
1111
captured_opts = opts
1212
local lines = source()
13-
assert.is_true(lines[1]:match("^/tmp/same.lua:1:1:") ~= nil)
14-
assert.is_true(lines[2]:match("^/tmp/same.lua:1:1:") ~= nil)
13+
assert.is_true(lines[1]:match("^/tmp/same.lua:1:1\t") ~= nil)
14+
assert.is_true(lines[2]:match("^/tmp/same.lua:1:1\t") ~= nil)
1515
assert.is_true(lines[1]:match("\t1$") ~= nil)
1616
assert.is_true(lines[2]:match("\t2$") ~= nil)
1717
opts.actions["default"]({ lines[2] })
@@ -36,6 +36,9 @@ describe("peekstack.picker.fzf_lua", function()
3636
assert.are.same(loc2, picked)
3737
assert.equals("builtin", captured_opts.previewer)
3838
assert.equals("Peekstack> ", captured_opts.prompt)
39+
assert.equals("\t", captured_opts.fzf_opts["--delimiter"])
40+
assert.equals("2", captured_opts.fzf_opts["--with-nth"])
41+
assert.equals("2", captured_opts.fzf_opts["--nth"])
3942

4043
package.loaded["fzf-lua"] = original
4144
end)

0 commit comments

Comments
 (0)