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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,25 @@ Candidate labels are shown in a readable unified format:

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

## 🔌 Extensions (push from external pickers)

Push results from external pickers (telescope / fzf-lua / snacks.nvim) directly
onto the peekstack stack. Each extension provides `push_file`, `push_grep`,
`push_lsp_references`, and a generic `actions.push` for custom configurations.

```lua
-- snacks.nvim
vim.keymap.set("n", "<leader>pf", require("peekstack.extensions.snacks").push_file)

-- fzf-lua
vim.keymap.set("n", "<leader>pf", require("peekstack.extensions.fzf_lua").push_file)

-- telescope
vim.keymap.set("n", "<leader>pf", "<cmd>Telescope peekstack push_file<cr>")
```

See `:help peekstack-extensions` for the full API and custom action examples.

## 💾 Persist sessions

When `persist.enabled = true`, `PeekstackSaveSession` uses `persist.session.default_name`
Expand Down
91 changes: 91 additions & 0 deletions doc/peekstack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,97 @@ or, when text is empty:

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

==============================================================================
EXTENSIONS *peekstack-extensions*

Extensions push results from external picker plugins onto the peekstack stack.
Unlike picker backends (which peekstack opens internally), extensions hook into
your picker workflow so selected items become peekstack popups.

Supported pickers: telescope, fzf-lua, snacks.nvim.

Each extension provides:
`push_file(opts)` Open file picker → push to stack
`push_grep(opts)` Open grep picker → push to stack
`push_lsp_references(opts)` Open LSP references picker → push to stack
`actions.push` Generic action for custom picker configs

SNACKS.NVIM ~
*peekstack-extensions-snacks*
>lua
local snacks = require("peekstack.extensions.snacks")
vim.keymap.set("n", "<leader>pf", snacks.push_file)
vim.keymap.set("n", "<leader>pg", snacks.push_grep)
vim.keymap.set("n", "<leader>pr", snacks.push_lsp_references)
<

Custom action:
>lua
require("snacks.picker").grep({
confirm = function(picker, item)
require("peekstack.extensions.snacks").actions.push(picker, item, {
provider = "my_grep",
})
end,
})
<

FZF-LUA ~
*peekstack-extensions-fzf-lua*
>lua
local fzf = require("peekstack.extensions.fzf_lua")
vim.keymap.set("n", "<leader>pf", fzf.push_file)
vim.keymap.set("n", "<leader>pg", fzf.push_grep)
vim.keymap.set("n", "<leader>pr", fzf.push_lsp_references)
<

Custom action:
>lua
require("fzf-lua").live_grep({
actions = {
["default"] = function(selected)
require("peekstack.extensions.fzf_lua").actions.push(selected, {
provider = "my_grep",
})
end,
},
})
<

TELESCOPE ~
*peekstack-extensions-telescope*

Telescope uses the standard extension mechanism:
>lua
vim.keymap.set("n", "<leader>pf", "<cmd>Telescope peekstack push_file<cr>")
vim.keymap.set("n", "<leader>pg", "<cmd>Telescope peekstack push_grep<cr>")
vim.keymap.set("n", "<leader>pr", "<cmd>Telescope peekstack push_lsp_references<cr>")
<

Custom action:
>lua
require("telescope.builtin").live_grep({
attach_mappings = function(_, map)
local push = require("telescope").extensions.peekstack.actions.push
map("i", "<CR>", function(bufnr) push(bufnr, { provider = "my_grep" }) end)
map("n", "<CR>", function(bufnr) push(bufnr, { provider = "my_grep" }) end)
return true
end,
})
<

GENERIC PUSH ~
*peekstack-extensions-push-entry*

For picker-agnostic integration:
>lua
require("peekstack").extensions.push_entry({
filename = "/path/to/file.lua",
lnum = 42, -- 1-based
col = 5, -- 1-based
}, { provider = "my_source" })
<

==============================================================================
COMMANDS *peekstack-cmds*

Expand Down
66 changes: 66 additions & 0 deletions lua/peekstack/extensions/fzf_lua.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
local ext = require("peekstack.extensions")

local M = {}

---@param selected string[]
---@param opts? table
local function push_action(selected, opts)
if not selected or not selected[1] then
return
end
local ok, fzf = pcall(require, "fzf-lua")
if not ok then
return
end
local entry = fzf.path.entry_to_file(selected[1])
if entry then
ext.push_entry({
filename = entry.path,
lnum = entry.line,
col = entry.col,
}, opts)
end
end

---@param fzf_picker string
---@param provider string
---@param opts? table
local function open_picker(fzf_picker, provider, opts)
opts = opts or {}
local ok, fzf = pcall(require, "fzf-lua")
if not ok then
vim.notify("fzf-lua not available", vim.log.levels.WARN)
return
end
local fn = fzf[fzf_picker]
if not fn then
vim.notify("fzf-lua." .. fzf_picker .. " not found", vim.log.levels.WARN)
return
end

local push_opts = { provider = provider, mode = opts.mode }

fn(vim.tbl_extend("force", opts, {
actions = {
["default"] = function(selected)
push_action(selected, push_opts)
end,
},
}))
end

function M.push_file(opts)
open_picker("files", "extension.file", opts)
end

function M.push_grep(opts)
open_picker("live_grep", "extension.grep", opts)
end

function M.push_lsp_references(opts)
open_picker("lsp_references", "extension.lsp_references", opts)
end

M.actions = { push = push_action }

return M
22 changes: 22 additions & 0 deletions lua/peekstack/extensions/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
local M = {}

--- Convert picker entry to PeekstackLocation and push it.
---@param entry { filename: string, lnum?: integer, col?: integer }
---@param opts? { provider?: string, mode?: string }
function M.push_entry(entry, opts)
if not entry or not entry.filename then
return
end
local location = require("peekstack.core.location")
local peekstack = require("peekstack")
local loc = location.normalize({
filename = entry.filename,
lnum = entry.lnum or 1,
col = entry.col or 1,
}, opts and opts.provider or "extension")
if loc then
peekstack.peek_location(loc, opts)
end
end

return M
73 changes: 73 additions & 0 deletions lua/peekstack/extensions/snacks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
local ext = require("peekstack.extensions")

local M = {}

---@param item table
---@param opts? table
local function push_from_snacks(item, opts)
if not item then
return
end
local filename = item.file
if filename and item.cwd and vim.fn.fnamemodify(filename, ":p") ~= filename then
filename = item.cwd .. "/" .. filename
end
-- snacks pos is {1-based line, 0-based col}; push_entry expects 1-based col
local col = item.pos and (item.pos[2] + 1) or nil
ext.push_entry({
filename = filename,
lnum = item.pos and item.pos[1],
col = col,
}, opts)
end

---@param snacks_picker string
---@param provider string
---@param opts? table
local function open_picker(snacks_picker, provider, opts)
opts = opts or {}
local ok, snacks = pcall(require, "snacks.picker")
if not ok then
vim.notify("snacks.nvim not available", vim.log.levels.WARN)
return
end
local fn = snacks[snacks_picker]
if not fn then
vim.notify("snacks.picker." .. snacks_picker .. " not found", vim.log.levels.WARN)
return
end

local push_opts = { provider = provider, mode = opts.mode }

fn(vim.tbl_extend("force", opts, {
confirm = function(picker, item)
picker:close()
push_from_snacks(item, push_opts)
end,
}))
end

function M.push_file(opts)
open_picker("files", "extension.file", opts)
end

function M.push_grep(opts)
open_picker("grep", "extension.grep", opts)
end

function M.push_lsp_references(opts)
open_picker("lsp_references", "extension.lsp_references", opts)
end

--- Generic action for use in custom snacks picker configurations.
---@param picker table
---@param item table
---@param opts? { provider?: string, mode?: string }
local function push_action(picker, item, opts)
picker:close()
push_from_snacks(item, opts)
end

M.actions = { push = push_action }

return M
8 changes: 8 additions & 0 deletions lua/peekstack/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,12 @@ M.persist = setmetatable({}, {
end,
})

---Proxy table for `peekstack.extensions`.
---@type table
M.extensions = setmetatable({}, {
__index = function(_, k)
return require("peekstack.extensions")[k]
end,
})

return M
72 changes: 72 additions & 0 deletions lua/telescope/_extensions/peekstack.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
local ext = require("peekstack.extensions")

---@param entry table
---@param opts? table
local function push_from_telescope(entry, opts)
if not entry then
return
end
ext.push_entry({
filename = entry.path or entry.filename or entry.value,
lnum = entry.lnum,
col = entry.col,
}, opts)
end

---@param builtin_name string
---@param provider string
---@param opts? table
local function open_builtin(builtin_name, provider, opts)
opts = opts or {}
local builtin = require("telescope.builtin")
local fn = builtin[builtin_name]
if not fn then
vim.notify("telescope.builtin." .. builtin_name .. " not found", vim.log.levels.WARN)
return
end

local push_opts = { provider = provider, mode = opts.mode }

fn(vim.tbl_extend("force", opts, {
attach_mappings = function(_, map)
local actions = require("telescope.actions")
local state = require("telescope.actions.state")
local function on_select(prompt_bufnr)
local entry = state.get_selected_entry()
actions.close(prompt_bufnr)
push_from_telescope(entry, push_opts)
end
map("i", "<CR>", on_select)
map("n", "<CR>", on_select)
return true
end,
}))
end

--- Generic action for use in custom telescope mappings.
---@param prompt_bufnr integer
---@param opts? { provider?: string, mode?: string }
local function push_action(prompt_bufnr, opts)
local actions = require("telescope.actions")
local state = require("telescope.actions.state")
local entry = state.get_selected_entry()
actions.close(prompt_bufnr)
push_from_telescope(entry, opts)
end

return require("telescope").register_extension({
exports = {
push_file = function(opts)
open_builtin("find_files", "extension.file", opts)
end,
push_grep = function(opts)
open_builtin("live_grep", "extension.grep", opts)
end,
push_lsp_references = function(opts)
open_builtin("lsp_references", "extension.lsp_references", opts)
end,
actions = {
push = push_action,
},
},
})
Loading