Skip to content

Commit d108fee

Browse files
authored
Merge pull request #3 from mhiro2/feat/picker-external-preview
feat(picker): Add preview window support for telescope/fzf-lua/snacks
2 parents 89d29d7 + 74040f5 commit d108fee

10 files changed

Lines changed: 239 additions & 67 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ Configure via `require("peekstack").setup({ ... })`.
264264
peekstack uses a picker when multiple locations are returned (e.g. references). The
265265
default backend is `builtin`. To use an external picker, install the plugin and set
266266
`picker.backend` to one of: `telescope`, `fzf-lua`, `snacks`.
267+
When using these external backends, the picker preview window shows the selected file
268+
content around the target location.
267269

268270
```lua
269271
{

doc/peekstack.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ Configure the backend with `picker.backend`:
150150
})
151151
<
152152

153+
For external backends (`telescope`, `fzf-lua`, `snacks`), picker preview windows
154+
show file content around the selected location.
155+
153156
If the chosen plugin is not installed, a warning is shown and the picker will not open.
154157

155158
==============================================================================

lua/peekstack/picker/fzf_lua.lua

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,45 @@ local M = {}
66
---@param locations PeekstackLocation[]
77
---@param opts? table
88
---@param cb fun(location: PeekstackLocation)
9-
function M.pick(locations, _opts, cb)
9+
function M.pick(locations, opts, cb)
1010
local ok, fzf = pcall(require, "fzf-lua")
1111
if not ok then
1212
vim.notify("fzf-lua not available", vim.log.levels.WARN)
1313
return
1414
end
1515

16-
local items = picker_util.build_items(locations, 1)
16+
local items = picker_util.build_external_items(locations, 1)
1717
for idx, item in ipairs(items) do
1818
item.index = idx
1919
end
2020

21-
fzf.fzf_exec(function()
22-
local lines = {}
23-
for _, item in ipairs(items) do
24-
table.insert(lines, string.format("%d\t%s", item.index, item.label))
25-
end
26-
return lines
27-
end, {
21+
---@type table
22+
local exec_opts = vim.tbl_extend("force", opts or {}, {
2823
prompt = "Peekstack> ",
24+
previewer = "builtin",
2925
actions = {
3026
["default"] = function(selected)
3127
if not selected or not selected[1] then
3228
return
3329
end
34-
local idx = tonumber(selected[1]:match("^(%d+)"))
30+
local idx = tonumber(selected[1]:match("\t(%d+)$"))
3531
local item = idx and items[idx]
3632
if item then
3733
cb(item.value)
3834
end
3935
end,
4036
},
4137
})
38+
39+
fzf.fzf_exec(function()
40+
local lines = {}
41+
for _, item in ipairs(items) do
42+
local file = item.file or ""
43+
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))
45+
end
46+
return lines
47+
end, exec_opts)
4248
end
4349

4450
return M

lua/peekstack/picker/snacks.lua

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,27 @@ function M.pick(locations, opts, cb)
1313
return
1414
end
1515

16-
local raw_items = picker_util.build_items(locations, 1)
16+
local raw_items = picker_util.build_external_items(locations, 1)
1717
local items = {}
1818
for _, item in ipairs(raw_items) do
1919
local loc = item.value
20+
local start = loc.range and loc.range.start or {}
2021
table.insert(items, {
2122
text = item.label,
22-
file = loc.uri,
23-
row = (loc.range.start.line or 0) + 1,
24-
col = loc.range.start.character or 0,
25-
loc = loc,
23+
file = item.file or loc.uri,
24+
pos = { item.lnum, start.character or 0 },
25+
peekstack_loc = loc,
2626
})
2727
end
2828

2929
local picker_opts = vim.tbl_extend("force", opts or {}, {
3030
title = "Peekstack",
3131
items = items,
32-
format = "text",
32+
format = "file",
3333
confirm = function(picker, item)
34-
if item and item.loc then
34+
if item and item.peekstack_loc then
3535
picker:close()
36-
cb(item.loc)
36+
cb(item.peekstack_loc)
3737
end
3838
end,
3939
})

lua/peekstack/picker/telescope.lua

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,33 @@ function M.pick(locations, opts, cb)
1313
return
1414
end
1515
local finders = require("telescope.finders")
16-
local sorters = require("telescope.config").values
16+
local conf = require("telescope.config").values
17+
local telescope_opts = opts or {}
1718

18-
local items = picker_util.build_items(locations, 1)
19+
local items = picker_util.build_external_items(locations, 1)
1920
local entries = {}
2021
for _, item in ipairs(items) do
2122
table.insert(entries, {
2223
value = item.value,
2324
display = item.label,
2425
ordinal = item.label,
26+
filename = item.file,
27+
lnum = item.lnum,
28+
col = item.col,
2529
})
2630
end
2731

2832
telescope
29-
.new(opts or {}, {
33+
.new(telescope_opts, {
3034
prompt_title = "Peekstack",
3135
finder = finders.new_table({
3236
results = entries,
3337
entry_maker = function(entry)
3438
return entry
3539
end,
3640
}),
37-
sorter = sorters.generic_sorter(opts or {}),
41+
sorter = conf.generic_sorter(telescope_opts),
42+
previewer = conf.grep_previewer(telescope_opts),
3843
attach_mappings = function(_, map)
3944
map("i", "<CR>", function(bufnr)
4045
local selection = require("telescope.actions.state").get_selected_entry()

lua/peekstack/types.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@
109109
---@class PeekstackPicker
110110
---@field pick fun(locations: PeekstackLocation[], opts?: table, cb: fun(location: PeekstackLocation))
111111

112+
---@class PeekstackPickerItem
113+
---@field label string
114+
---@field value PeekstackLocation
115+
116+
---@class PeekstackPickerExternalItem: PeekstackPickerItem
117+
---@field file? string
118+
---@field lnum integer
119+
---@field col integer
120+
112121
---@class PeekstackHistoryEntry
113122
---@field location PeekstackLocation
114123
---@field title? string

lua/peekstack/util/picker.lua

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
11
local config = require("peekstack.config")
22
local location = require("peekstack.core.location")
3+
local fs = require("peekstack.util.fs")
34

45
local M = {}
56

6-
---@param locations PeekstackLocation[]
7-
---@param preview_lines integer
8-
---@return table[]
9-
function M.build_items(locations, preview_lines)
7+
---@return PeekstackDisplayTextOpts
8+
local function display_text_opts()
109
local ui_path = config.get().ui.path or {}
1110
local max_width = ui_path.max_width or 0
1211
if max_width == 0 then
1312
max_width = math.floor(vim.o.columns * 0.7)
1413
end
15-
local opts = {
14+
return {
1615
path_base = ui_path.base,
1716
max_width = max_width,
1817
}
18+
end
19+
20+
---@param locations PeekstackLocation[]
21+
---@param preview_lines integer
22+
---@return PeekstackPickerItem[]
23+
function M.build_items(locations, preview_lines)
24+
local opts = display_text_opts()
25+
local items = {}
26+
for _, loc in ipairs(locations) do
27+
table.insert(items, {
28+
label = location.display_text(loc, preview_lines, opts),
29+
value = loc,
30+
})
31+
end
32+
return items
33+
end
34+
35+
---@param locations PeekstackLocation[]
36+
---@param preview_lines integer
37+
---@return PeekstackPickerExternalItem[]
38+
function M.build_external_items(locations, preview_lines)
39+
local opts = display_text_opts()
1940
local items = {}
2041
for _, loc in ipairs(locations) do
42+
local start = loc.range and loc.range.start or {}
2143
table.insert(items, {
2244
label = location.display_text(loc, preview_lines, opts),
2345
value = loc,
46+
file = fs.uri_to_fname(loc.uri),
47+
lnum = (start.line or 0) + 1,
48+
col = (start.character or 0) + 1,
2449
})
2550
end
2651
return items

tests/picker_fzf_lua_spec.lua

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ describe("peekstack.picker.fzf_lua", function()
44
it("should select the correct item when labels collide", function()
55
local original = package.loaded["fzf-lua"]
66
local picked = nil
7+
local captured_opts = nil
78

89
package.loaded["fzf-lua"] = {
910
fzf_exec = function(source, opts)
11+
captured_opts = opts
1012
local lines = source()
11-
assert.is_true(lines[1]:match("^1\t") ~= nil)
12-
assert.is_true(lines[2]:match("^2\t") ~= nil)
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)
15+
assert.is_true(lines[1]:match("\t1$") ~= nil)
16+
assert.is_true(lines[2]:match("\t2$") ~= nil)
1317
opts.actions["default"]({ lines[2] })
1418
end,
1519
}
@@ -30,6 +34,8 @@ describe("peekstack.picker.fzf_lua", function()
3034
end)
3135

3236
assert.are.same(loc2, picked)
37+
assert.equals("builtin", captured_opts.previewer)
38+
assert.equals("Peekstack> ", captured_opts.prompt)
3339

3440
package.loaded["fzf-lua"] = original
3541
end)

tests/picker_snacks_spec.lua

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,69 @@
11
describe("peekstack.picker.snacks", function()
2-
local config = require("peekstack.config")
2+
local picker = require("peekstack.picker.snacks")
3+
local original_snacks = nil
4+
local original_notify = nil
35

46
before_each(function()
5-
config.setup({
6-
picker = { backend = "snacks" },
7-
})
7+
original_snacks = package.loaded["snacks.picker"]
8+
original_notify = vim.notify
89
end)
910

10-
it("should warn when snacks.nvim is not available", function()
11-
-- Test by checking the module behavior when snacks is not installed
12-
-- The pick function should handle the missing dependency gracefully
13-
local location = require("peekstack.core.location")
11+
after_each(function()
12+
package.loaded["snacks.picker"] = original_snacks
13+
vim.notify = original_notify
14+
end)
1415

15-
local test_loc = {
16-
uri = "file:///path/to/test.lua",
17-
range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 10 } },
18-
provider = "test_provider",
19-
}
16+
it("warns when snacks.nvim is not available", function()
17+
package.loaded["snacks.picker"] = nil
18+
local warned = false
19+
vim.notify = function(msg)
20+
if msg == "snacks.nvim not available" then
21+
warned = true
22+
end
23+
end
2024

21-
-- Just verify the location module works correctly
22-
local display_text = location.display_text(test_loc, 1)
23-
assert.is_not_nil(display_text)
24-
assert.is_true(type(display_text) == "string")
25+
picker.pick({}, nil, function() end)
2526

26-
-- The snacks picker itself will warn when snacks is not available
27-
-- but we can't easily test that in isolation without mocking
27+
assert.is_true(warned)
2828
end)
2929

30-
it("should format locations correctly", function()
31-
local location = require("peekstack.core.location")
32-
33-
local test_loc = {
34-
uri = "file:///path/to/test.lua",
35-
range = { start = { line = 5, character = 10 }, ["end"] = { line = 5, character = 20 } },
36-
provider = "test_provider",
30+
it("passes file format items and confirms selected location", function()
31+
local picked = nil
32+
local closed = false
33+
local captured = nil
34+
package.loaded["snacks.picker"] = {
35+
pick = function(opts)
36+
captured = opts
37+
opts.confirm({
38+
close = function()
39+
closed = true
40+
end,
41+
}, opts.items[2])
42+
end,
3743
}
3844

39-
local display_text = location.display_text(test_loc, 1)
40-
assert.is_not_nil(display_text)
41-
assert.is_true(type(display_text) == "string")
42-
end)
43-
44-
it("should be registered as a known backend", function()
45-
local known_backends = {
46-
"builtin",
47-
"telescope",
48-
"fzf-lua",
49-
"snacks",
45+
local loc1 = {
46+
uri = "file:///tmp/a.lua",
47+
range = { start = { line = 3, character = 4 }, ["end"] = { line = 3, character = 4 } },
48+
provider = "test",
49+
}
50+
local loc2 = {
51+
uri = "file:///tmp/b.lua",
52+
range = { start = { line = 7, character = 2 }, ["end"] = { line = 7, character = 2 } },
53+
provider = "test",
5054
}
5155

52-
assert.is_true(vim.list_contains(known_backends, "snacks"))
56+
picker.pick({ loc1, loc2 }, nil, function(choice)
57+
picked = choice
58+
end)
59+
60+
assert.equals("Peekstack", captured.title)
61+
assert.equals("file", captured.format)
62+
assert.equals("/tmp/a.lua", captured.items[1].file)
63+
assert.same({ 4, 4 }, captured.items[1].pos)
64+
assert.is_nil(captured.items[1].loc)
65+
assert.are.same(loc1, captured.items[1].peekstack_loc)
66+
assert.are.same(loc2, picked)
67+
assert.is_true(closed)
5368
end)
5469
end)

0 commit comments

Comments
 (0)