Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ default backend is `builtin`. To use an external picker, install the plugin and
`picker.backend` to one of: `telescope`, `fzf-lua`, `snacks`.
When using these external backends, the picker preview window shows the selected file
content around the target location.
Candidate labels are shown in a readable unified format:
`<text> - <path>:<line>:<col>` (or `<path>:<line>:<col>` when text is empty).

```lua
{
Expand Down
4 changes: 4 additions & 0 deletions doc/peekstack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ Configure the backend with `picker.backend`:

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

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

Expand Down
13 changes: 11 additions & 2 deletions lua/peekstack/picker/fzf_lua.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ function M.pick(locations, opts, cb)
item.index = idx
end

local user_opts = opts or {}
local fzf_opts = vim.tbl_extend("force", user_opts.fzf_opts or {}, {
["--delimiter"] = "\t",
["--with-nth"] = "2",
["--nth"] = "2",
})

---@type table
local exec_opts = vim.tbl_extend("force", opts or {}, {
local exec_opts = vim.tbl_deep_extend("force", user_opts, {
prompt = "Peekstack> ",
previewer = "builtin",
fzf_opts = fzf_opts,
actions = {
["default"] = function(selected)
if not selected or not selected[1] then
Expand All @@ -41,7 +49,8 @@ function M.pick(locations, opts, cb)
for _, item in ipairs(items) do
local file = item.file or ""
local label = item.label:gsub("[%r\n\t]", " ")
table.insert(lines, string.format("%s:%d:%d:%s\t%d", file, item.lnum, item.col, label, item.index))
local loc = string.format("%s:%d:%d", file, item.lnum, item.col)
table.insert(lines, string.format("%s\t%s\t%d", loc, label, item.index))
end
return lines
end, exec_opts)
Expand Down
35 changes: 34 additions & 1 deletion lua/peekstack/picker/snacks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@ local picker_util = require("peekstack.util.picker")

local M = {}

---@param item table
---@return table
local function format_item(item)
local chunks = {}
local symbol = item.symbol
if type(symbol) == "string" and symbol ~= "" then
chunks[#chunks + 1] = { symbol, "SnacksPickerLabel" }
chunks[#chunks + 1] = { " - ", "SnacksPickerDelim" }
end

local path = item.path
if type(path) ~= "string" or path == "" then
path = item.text or ""
end
picker_util.append_path_chunks(chunks, path, "SnacksPickerDir", "SnacksPickerFile")

if type(item.display_lnum) == "number" and item.display_lnum > 0 then
chunks[#chunks + 1] = { ":", "SnacksPickerDelim" }
chunks[#chunks + 1] = { tostring(item.display_lnum), "SnacksPickerRow" }
end

if type(item.display_col) == "number" and item.display_col > 0 then
chunks[#chunks + 1] = { ":", "SnacksPickerDelim" }
chunks[#chunks + 1] = { tostring(item.display_col), "SnacksPickerCol" }
end

return chunks
end

---Pick a location using snacks.nvim picker
---@param locations PeekstackLocation[]
---@param opts? table
Expand All @@ -20,6 +49,10 @@ function M.pick(locations, opts, cb)
local start = loc.range and loc.range.start or {}
table.insert(items, {
text = item.label,
symbol = item.symbol,
path = item.path,
display_lnum = item.display_lnum,
display_col = item.display_col,
file = item.file or loc.uri,
pos = { item.lnum, start.character or 0 },
peekstack_loc = loc,
Expand All @@ -29,7 +62,7 @@ function M.pick(locations, opts, cb)
local picker_opts = vim.tbl_extend("force", opts or {}, {
title = "Peekstack",
items = items,
format = "file",
format = format_item,
confirm = function(picker, item)
if item and item.peekstack_loc then
picker:close()
Expand Down
54 changes: 52 additions & 2 deletions lua/peekstack/picker/telescope.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@ local picker_util = require("peekstack.util.picker")

local M = {}

---@param primary string
---@param fallback string
---@return string
local function hl(primary, fallback)
if vim.fn.hlexists(primary) == 1 then
return primary
end
return fallback
end

---@param displayer fun(chunks: table): string
---@param item PeekstackPickerExternalItem
---@return string
local function display_entry(displayer, item)
local chunks = {}
if type(item.symbol) == "string" and item.symbol ~= "" then
chunks[#chunks + 1] = { item.symbol, hl("TelescopeResultsIdentifier", "Function") }
chunks[#chunks + 1] = { " - ", hl("TelescopeResultsComment", "Comment") }
end

local path = item.path or item.label
picker_util.append_path_chunks(
chunks,
path,
hl("TelescopeResultsComment", "Comment"),
hl("TelescopeResultsIdentifier", "Directory")
)

if type(item.display_lnum) == "number" and item.display_lnum > 0 then
chunks[#chunks + 1] = { ":", hl("TelescopeResultsComment", "Comment") }
chunks[#chunks + 1] = { tostring(item.display_lnum), hl("TelescopeResultsNumber", "Number") }
end

if type(item.display_col) == "number" and item.display_col > 0 then
chunks[#chunks + 1] = { ":", hl("TelescopeResultsComment", "Comment") }
chunks[#chunks + 1] = { tostring(item.display_col), hl("TelescopeResultsNumber", "Number") }
end

return displayer(chunks)
end

---Pick a location using Telescope
---@param locations PeekstackLocation[]
---@param opts? table
Expand All @@ -13,16 +54,25 @@ function M.pick(locations, opts, cb)
return
end
local finders = require("telescope.finders")
local entry_display = require("telescope.pickers.entry_display")
local conf = require("telescope.config").values
local telescope_opts = opts or {}
local displayer = entry_display.create({
separator = "",
items = {
{ remaining = true },
},
})

local items = picker_util.build_external_items(locations, 1)
local entries = {}
for _, item in ipairs(items) do
table.insert(entries, {
value = item.value,
display = item.label,
ordinal = item.label,
display = function()
return display_entry(displayer, item)
end,
ordinal = string.format("%s %s", item.label, item.file or ""),
filename = item.file,
lnum = item.lnum,
col = item.col,
Expand Down
15 changes: 5 additions & 10 deletions lua/peekstack/providers/lsp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,11 @@ local function append_document_symbol(symbol, uri, provider, out)
local start_pos = range and range.start
local end_pos = range and range["end"]
if start_pos and end_pos then
local text = symbol.name
if type(text) ~= "string" then
text = nil
end
if type(symbol.detail) == "string" and symbol.detail ~= "" then
if text and text ~= "" then
text = string.format("%s - %s", text, symbol.detail)
else
text = symbol.detail
end
local text
if type(symbol.name) == "string" and symbol.name ~= "" then
text = symbol.name
elseif type(symbol.detail) == "string" and symbol.detail ~= "" then
text = symbol.detail
end

table.insert(out, {
Expand Down
4 changes: 4 additions & 0 deletions lua/peekstack/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
---@field file? string
---@field lnum integer
---@field col integer
---@field symbol? string
---@field path? string
---@field display_lnum? integer
---@field display_col? integer

---@class PeekstackHistoryEntry
---@field location PeekstackLocation
Expand Down
75 changes: 73 additions & 2 deletions lua/peekstack/util/picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,71 @@ local fs = require("peekstack.util.fs")

local M = {}

---@param chunks table
---@param path string
---@param dir_hl? string
---@param file_hl? string
function M.append_path_chunks(chunks, path, dir_hl, file_hl)
local file_group = file_hl or "Directory"
local dir, base = path:match("^(.*[/\\])(.+)$")
if dir and base then
if type(dir_hl) == "string" and dir_hl ~= "" then
chunks[#chunks + 1] = { dir, dir_hl }
else
chunks[#chunks + 1] = { dir, file_group }
end
chunks[#chunks + 1] = { base, file_group }
return
end
chunks[#chunks + 1] = { path, file_group }
end

---@param text? string
---@return string
local function normalize_label_text(text)
if type(text) ~= "string" then
return ""
end
local normalized = text:gsub("[\r\n\t]+", " "):gsub("%s+", " ")
return vim.trim(normalized)
end

---@param suffix string
---@return string, integer, integer
local function parse_suffix_location(suffix)
local path, line, col = suffix:match("^(.*):(%d+):(%d+)$")
if not path then
return suffix, 0, 0
end
return path, tonumber(line) or 0, tonumber(col) or 0
end

---@param loc PeekstackLocation
---@param preview_lines integer
---@param opts PeekstackDisplayTextOpts
---@return { label: string, symbol: string, path: string, display_lnum: integer, display_col: integer }
local function build_location_label_payload(loc, preview_lines, opts)
local suffix = location.display_text(loc, 0, opts)
local path, display_lnum, display_col = parse_suffix_location(suffix)
local symbol = preview_lines > 0 and normalize_label_text(loc.text) or ""
if symbol == "" then
return {
label = suffix,
symbol = "",
path = path,
display_lnum = display_lnum,
display_col = display_col,
}
end
return {
label = string.format("%s - %s", symbol, suffix),
symbol = symbol,
path = path,
display_lnum = display_lnum,
display_col = display_col,
}
end

---@return PeekstackDisplayTextOpts
local function display_text_opts()
local ui_path = config.get().ui.path or {}
Expand All @@ -24,8 +89,9 @@ function M.build_items(locations, preview_lines)
local opts = display_text_opts()
local items = {}
for _, loc in ipairs(locations) do
local payload = build_location_label_payload(loc, preview_lines, opts)
table.insert(items, {
label = location.display_text(loc, preview_lines, opts),
label = payload.label,
value = loc,
})
end
Expand All @@ -40,8 +106,13 @@ function M.build_external_items(locations, preview_lines)
local items = {}
for _, loc in ipairs(locations) do
local start = loc.range and loc.range.start or {}
local payload = build_location_label_payload(loc, preview_lines, opts)
table.insert(items, {
label = location.display_text(loc, preview_lines, opts),
label = payload.label,
symbol = payload.symbol,
path = payload.path,
display_lnum = payload.display_lnum,
display_col = payload.display_col,
value = loc,
file = fs.uri_to_fname(loc.uri),
lnum = (start.line or 0) + 1,
Expand Down
2 changes: 1 addition & 1 deletion tests/lsp_provider_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe("peekstack.providers.lsp", function()
assert.equals(vim.uri_from_bufnr(ctx.bufnr), received[1].uri)
assert.equals(1, received[1].range.start.line)
assert.equals(2, received[1].range.start.character)
assert.equals("Parent - class", received[1].text)
assert.equals("Parent", received[1].text)
assert.equals(5, received[1].kind)
assert.equals("lsp.symbols_document", received[1].provider)

Expand Down
7 changes: 5 additions & 2 deletions tests/picker_fzf_lua_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ describe("peekstack.picker.fzf_lua", function()
fzf_exec = function(source, opts)
captured_opts = opts
local lines = source()
assert.is_true(lines[1]:match("^/tmp/same.lua:1:1:") ~= nil)
assert.is_true(lines[2]:match("^/tmp/same.lua:1:1:") ~= nil)
assert.is_true(lines[1]:match("^/tmp/same.lua:1:1\t") ~= nil)
assert.is_true(lines[2]:match("^/tmp/same.lua:1:1\t") ~= nil)
assert.is_true(lines[1]:match("\t1$") ~= nil)
assert.is_true(lines[2]:match("\t2$") ~= nil)
opts.actions["default"]({ lines[2] })
Expand All @@ -36,6 +36,9 @@ describe("peekstack.picker.fzf_lua", function()
assert.are.same(loc2, picked)
assert.equals("builtin", captured_opts.previewer)
assert.equals("Peekstack> ", captured_opts.prompt)
assert.equals("\t", captured_opts.fzf_opts["--delimiter"])
assert.equals("2", captured_opts.fzf_opts["--with-nth"])
assert.equals("2", captured_opts.fzf_opts["--nth"])

package.loaded["fzf-lua"] = original
end)
Expand Down
20 changes: 18 additions & 2 deletions tests/picker_snacks_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("peekstack.picker.snacks", function()
assert.is_true(warned)
end)

it("passes file format items and confirms selected location", function()
it("passes text format items and confirms selected location", function()
local picked = nil
local closed = false
local captured = nil
Expand All @@ -45,6 +45,7 @@ describe("peekstack.picker.snacks", function()
local loc1 = {
uri = "file:///tmp/a.lua",
range = { start = { line = 3, character = 4 }, ["end"] = { line = 3, character = 4 } },
text = "Alpha",
provider = "test",
}
local loc2 = {
Expand All @@ -58,9 +59,24 @@ describe("peekstack.picker.snacks", function()
end)

assert.equals("Peekstack", captured.title)
assert.equals("file", captured.format)
assert.equals("function", type(captured.format))
assert.equals("/tmp/a.lua", captured.items[1].file)
assert.same({ 4, 4 }, captured.items[1].pos)
assert.equals("Alpha - /tmp/a.lua:4:5", captured.items[1].text)
assert.equals("Alpha", captured.items[1].symbol)
assert.equals("/tmp/a.lua", captured.items[1].path)
assert.equals(4, captured.items[1].display_lnum)
assert.equals(5, captured.items[1].display_col)
assert.same({
{ "Alpha", "SnacksPickerLabel" },
{ " - ", "SnacksPickerDelim" },
{ "/tmp/", "SnacksPickerDir" },
{ "a.lua", "SnacksPickerFile" },
{ ":", "SnacksPickerDelim" },
{ "4", "SnacksPickerRow" },
{ ":", "SnacksPickerDelim" },
{ "5", "SnacksPickerCol" },
}, captured.format(captured.items[1]))
assert.is_nil(captured.items[1].loc)
assert.are.same(loc1, captured.items[1].peekstack_loc)
assert.are.same(loc2, picked)
Expand Down
Loading