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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ if you do not pass a name. If `persist.session.prompt_if_missing = true`, you'll
for a name instead of using the default.

> [!WARNING]
> Persistence target is fixed to the current git repository.
> Persistence uses repository storage when the current working directory is inside a git repository.
> Outside a git repository, sessions fall back to cwd-based storage.

### Auto persist (optional)

Expand All @@ -338,6 +339,14 @@ When `persist.auto.enabled = true`, peekstack can automatically restore and save
Auto persist only runs inside a git repository and always uses the repository session storage. Make sure
`persist.enabled = true` as well.

## 🔁 Re-running setup

Calling `require("peekstack").setup()` again replaces config, re-registers providers, commands,
autocmds, picker backends, and auto-persist hooks.

It does not migrate existing popup windows, stack entries, or history in place. Updated settings apply
to future actions, and to existing stacks only after those popups are reopened, restored, or recreated.

## 🪟 Popup buffer modes

`ui.popup.buffer_mode` controls how popups are backed:
Expand Down
14 changes: 13 additions & 1 deletion doc/peekstack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,9 @@ PERSIST *peekstack-persist*

Persistence is disabled by default. Enable with `persist.enabled = true`.

Persistence target is fixed to the current git repository.
Persistence uses repository storage when the current working directory is
inside a git repository. Outside a git repository, it falls back to
cwd-based storage.

`persist.session.default_name` is used when a name is omitted, unless
`persist.session.prompt_if_missing` is true.
Expand All @@ -447,6 +449,16 @@ Auto persistence (`persist.auto`) requires `persist.enabled = true`:
Auto persistence runs only inside a git repository and always uses
the repository session storage.

==============================================================================
SETUP RELOAD *peekstack-setup-reload*

Calling `require("peekstack").setup()` again replaces config and re-registers
providers, commands, autocmds, picker backends, and auto-persist hooks.

It does not migrate existing popup windows, stack entries, or history in
place. Updated settings apply to future actions, and to existing stacks only
after those popups are reopened, restored, or recreated.

==============================================================================
BUFFER MODES *peekstack-buffer-modes*

Expand Down
4 changes: 2 additions & 2 deletions lua/peekstack/core/events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ local function ensure_popup_cursor_tracking(group, bufnr)
})
end

---Close all ephemeral popups across all stacks and reflow
---Close ephemeral popups that belong to the current root window.
local function close_ephemeral_popups()
stack.close_ephemerals()
stack.close_ephemerals(vim.api.nvim_get_current_win())
end

---Check if a window is a floating popup
Expand Down
89 changes: 75 additions & 14 deletions lua/peekstack/core/stack/events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,47 @@ local function deps()
end
end

---@param stack PeekstackStackModel
---@param idx integer
---@param item PeekstackPopupModel
---@param opts? { close_window?: boolean, highlight_origin?: boolean }
local function remove_stack_popup(stack, idx, item, opts)
opts = opts or {}
if stack.zoomed_id == item.id then
stack.zoomed_id = nil
end
if opts.highlight_origin ~= false then
feedback.highlight_origin(item.origin)
end
common.emit_popup_event("PeekstackClose", item, stack.root_winid)
history.push_entry(stack, history.build_entry(item, idx))
user_events.emit("PeekstackHistoryPush", {
popup_id = item.id,
location = item.location,
root_winid = stack.root_winid,
})
state.unindex_popup(item)
table.remove(stack.popups, idx)
if opts.close_window ~= false and item.winid and vim.api.nvim_win_is_valid(item.winid) then
popup.close(item)
end
end

---@param id integer
---@param item PeekstackPopupModel
---@param opts? { close_window?: boolean }
local function remove_ephemeral(id, item, opts)
opts = opts or {}
if opts.close_window ~= false and item.winid and vim.api.nvim_win_is_valid(item.winid) then
popup.close(item)
end
state.unregister_ephemeral(id)
user_events.emit(
"PeekstackClose",
user_events.build_popup_data(item, item.origin and item.origin.winid or 0, { ephemeral = true })
)
end

---@param winid integer
function M.handle_win_closed(winid)
deps()
Expand Down Expand Up @@ -83,21 +124,32 @@ function M.handle_buf_wipeout(bufnr)
end
for id, item in pairs(state.ephemerals) do
if item.bufnr == bufnr then
state.unregister_ephemeral(id)
remove_ephemeral(id, item, { close_window = false })
end
end
for _, stack in pairs(state.stacks) do
local removed = false
local focused_removed = false
for idx = #stack.popups, 1, -1 do
local item = stack.popups[idx]
if item.bufnr == bufnr then
if stack.zoomed_id == item.id then
stack.zoomed_id = nil
if stack.focused_id == item.id then
focused_removed = true
end
state.unindex_popup(item)
table.remove(stack.popups, idx)
remove_stack_popup(stack, idx, item, { close_window = false })
removed = true
end
end
layout.reflow(stack)
if removed then
if focused_removed then
if #stack.popups > 0 then
stack.focused_id = stack.popups[#stack.popups].id
else
stack.focused_id = nil
end
end
layout.reflow(stack)
end
end
end

Expand All @@ -121,23 +173,32 @@ function M.handle_origin_wipeout(bufnr)
end
for id, item in pairs(state.ephemerals) do
if should_close_for_origin(item) then
popup.close(item)
state.unregister_ephemeral(id)
remove_ephemeral(id, item)
end
end
for _, stack in pairs(state.stacks) do
local removed = false
local focused_removed = false
for idx = #stack.popups, 1, -1 do
local item = stack.popups[idx]
if should_close_for_origin(item) then
if stack.zoomed_id == item.id then
stack.zoomed_id = nil
if stack.focused_id == item.id then
focused_removed = true
end
remove_stack_popup(stack, idx, item)
removed = true
end
end
if removed then
if focused_removed then
if #stack.popups > 0 then
stack.focused_id = stack.popups[#stack.popups].id
else
stack.focused_id = nil
end
popup.close(item)
state.unindex_popup(item)
table.remove(stack.popups, idx)
end
layout.reflow(stack)
end
layout.reflow(stack)
end
end

Expand Down
44 changes: 30 additions & 14 deletions lua/peekstack/core/stack/operations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -432,27 +432,43 @@ function M.close_stale(now_ms, opts)
end
end

function M.close_ephemerals()
---@param winid? integer
function M.close_ephemerals(winid)
deps()
local target_root_winid = nil
if winid ~= nil or vim.api.nvim_get_current_win() ~= nil then
target_root_winid = state.get_root_winid(winid)
end

for _, stack in pairs(state.stacks) do
local removed = false
for idx = #stack.popups, 1, -1 do
local item = stack.popups[idx]
if item.ephemeral then
popup.close(item)
state.unindex_popup(item)
table.remove(stack.popups, idx)
removed = true
if target_root_winid == nil or stack.root_winid == target_root_winid then
local removed = false
for idx = #stack.popups, 1, -1 do
local item = stack.popups[idx]
if item.ephemeral then
popup.close(item)
state.unindex_popup(item)
table.remove(stack.popups, idx)
removed = true
end
end
if removed then
layout.reflow(stack)
end
end
if removed then
layout.reflow(stack)
end
end

for id, item in pairs(state.ephemerals) do
popup.close(item)
state.unregister_ephemeral(id)
local entry = state.lookup_by_id(item.id)
local root_winid = entry and entry.root_winid or nil
if target_root_winid == nil or root_winid == target_root_winid then
popup.close(item)
state.unregister_ephemeral(id)
user_events.emit(
"PeekstackClose",
user_events.build_popup_data(item, item.origin and item.origin.winid or 0, { ephemeral = true })
)
end
end
end

Expand Down
33 changes: 32 additions & 1 deletion lua/peekstack/providers/grep.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ local notify = require("peekstack.util.notify")

local M = {}

---@param text string?
---@return string
local function compact_message(text)
local message = (text or ""):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
if message == "" then
return "unknown error"
end
return message
end

---@param stderr string?
---@return boolean
local function is_ignore_file_error(stderr)
local message = compact_message(stderr):lower()
return message:find(".gitignore", 1, true) ~= nil
or message:find(".ignore", 1, true) ~= nil
or message:find(".rgignore", 1, true) ~= nil
or (message:find("ignore", 1, true) ~= nil and message:find("glob", 1, true) ~= nil)
end

---@param stderr string?
---@return string
local function format_failure_message(stderr)
local message = compact_message(stderr)
if is_ignore_file_error(stderr) then
return "rg failed; check .gitignore/.ignore patterns or encoding: " .. message
end
return "rg failed: " .. message
end

---@param line string
---@return string?, integer?, integer?, string?
local function parse_rg_line(line)
Expand Down Expand Up @@ -56,7 +86,7 @@ function M.search(_, cb)
vim.system({ "rg", "--vimgrep", "--max-count=1000", "--", query }, { text = true }, function(result)
vim.schedule(function()
if result.code ~= 0 and result.code ~= 1 then
notify.warn("rg failed: " .. (result.stderr or "unknown error"))
notify.warn(format_failure_message(result.stderr))
cb({})
return
end
Expand All @@ -68,5 +98,6 @@ end

---Expose parser for tests.
M._parse_output = parse_rg_output
M._format_failure_message = format_failure_message

return M
3 changes: 2 additions & 1 deletion lua/telescope/_extensions/peekstack.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local ext = require("peekstack.extensions")
local notify = require("peekstack.util.notify")

---@param entry table
---@param opts? table
Expand All @@ -21,7 +22,7 @@ local function open_builtin(builtin_name, provider, opts)
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)
notify.warn("telescope.builtin." .. builtin_name .. " not found")
return
end

Expand Down
37 changes: 37 additions & 0 deletions tests/events_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,41 @@ describe("peekstack.core.events", function()
assert.equals(1, #buf_leave)
assert.equals(2, #win_leave)
end)

it("closes quick peek popups only for the current root window", function()
local location = {
uri = vim.uri_from_bufnr(0),
range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 10 } },
provider = "test",
}

config.setup({
ui = {
quick_peek = { close_events = { "CursorMoved" } },
popup = { auto_close = { enabled = false } },
},
})
events.setup()

local left_win = vim.api.nvim_get_current_win()
vim.api.nvim_cmd({ cmd = "vsplit" }, {})
local right_win = vim.api.nvim_get_current_win()

vim.api.nvim_set_current_win(left_win)
local left_popup = stack.push(location, { stack = false })
assert.is_not_nil(left_popup)

vim.api.nvim_set_current_win(right_win)
local right_popup = stack.push(location, { stack = false })
assert.is_not_nil(right_popup)

vim.api.nvim_set_current_win(left_win)
vim.api.nvim_exec_autocmds("CursorMoved", { modeline = false })

assert.is_nil(stack._ephemerals()[left_popup.id])
assert.is_not_nil(stack._ephemerals()[right_popup.id])

stack.close(right_popup.id)
vim.api.nvim_cmd({ cmd = "only" }, {})
end)
end)
Loading
Loading