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
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,16 @@ A session holds one stack: the popups of the current window (or of the window th
belongs to), oldest first, keeping the newest `persist.max_items`. Only locations, titles, pins,
buffer modes and parent links are saved; edits made in copy-mode popups are not.
Restoring a session adds its popups to the current window's stack and keeps the popups already there.
Each popup comes back in the buffer mode it was opened with, whatever `ui.popup.buffer_mode` is set to.

> [!WARNING]
> 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.
> The storage is resolved when a save, delete, or rename is requested, so changing directory while
> it is still being written does not move it to another repository.
> it is still being written does not move it to another repository. Session commands resolve it, and
> the window whose stack they act on, when the command runs: changing directory or window while a
> name prompt, session picker, or delete confirmation is open does not redirect them, and a save or
> restore is dropped if that window has been closed by then.

Each git repository (each worktree, found by looking upward from the current window's working
directory, so `:lcd` / `:tcd` count) has its own storage file, and session names are scoped to it.
Expand All @@ -383,6 +387,11 @@ directory, so `:lcd` / `:tcd` count) has its own storage file, and session names
- **Unreadable storage**: if the storage file cannot be read (an I/O error, invalid JSON, or a version
this release does not know), peekstack warns and refuses to save, delete, or rename sessions in it
instead of replacing it with an empty store. Fix or move the file to recover.
- **Several Neovim instances**: instances sharing a storage file take turns through a `<file>.lock`
file held for each read and write, so saving different sessions at the same time keeps both. Saving
the same name keeps the later save. If another instance holds the lock for more than a second, the
operation fails with a warning instead of writing. A lock is removed only once the instance that
created it has exited.

> [!IMPORTANT]
> Sessions are written as plain JSON under `vim.fn.stdpath("state") .. "/peekstack/"`. Each entry
Expand All @@ -398,11 +407,13 @@ When `persist.auto.enabled = true`, peekstack can automatically restore and save

- **Restore** on `VimEnter` / `DirChanged` (only when the stack is empty if `restore_if_empty = true`)
- **Save** on `PeekstackPush` / `PeekstackClose` / `PeekstackRestorePopup` with a debounce
- **Save on leave** on `VimLeavePre` if `save_on_leave = true`
- **Save on leave** on `VimLeavePre` if `save_on_leave = true` (works on its own: with `save = false`
the stack is written only when leaving)

The auto session holds a single stack: the one that changed most recently, saved to the repository
that was current when it changed. Closing every popup of that stack saves it empty, so nothing is
restored next time.
restored next time. Quick peek popups never join a stack, so opening or closing one does not touch
the auto session.

Leaving Neovim saves that stack synchronously, even if the cursor is in another window. It skips the
save if no stack changed during the session, and keeps the stored session as-is once that stack's
Expand Down
23 changes: 19 additions & 4 deletions doc/peekstack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,8 @@ window the stack view belongs to), oldest first, keeping the newest
`persist.max_items`. Only locations, titles, pins, buffer modes and parent
links are saved; edits made in copy-mode popups are not. Restoring a
session adds its popups to the current window's stack and keeps the popups
already there.
already there. Each popup comes back in the buffer mode it was opened
with, whatever `ui.popup.buffer_mode` is set to.

Persistence uses repository storage when the current working directory is
inside a git repository. Outside a git repository, it falls back to
Expand All @@ -494,7 +495,11 @@ upward from the current window's working directory, so |:lcd| and |:tcd|
count) has its own storage file, and session names are scoped to it. The
storage is resolved when a save, delete, or rename is requested, so
changing directory while it is still being written does not move it to
another repository.
another repository. Session commands resolve it, and the window whose
stack they act on, when the command runs: changing directory or window
while a name prompt, session picker, or delete confirmation is open does
not redirect them, and a save or restore is dropped if that window has
been closed by then.

Empty stacks:
Saving an empty stack replaces the session with an empty one; restoring
Expand All @@ -513,6 +518,14 @@ Unreadable storage:
save, delete, or rename sessions in it instead of replacing it with an
empty store. Fix or move the file to recover.

Several Neovim instances:
Instances sharing a storage file take turns through a `<file>.lock`
file held for each read and write, so saving different sessions at
the same time keeps both. Saving the same name keeps the later save.
If another instance holds the lock for more than a second, the
operation fails with a warning instead of writing. A lock is removed
only once the instance that created it has exited.

`persist.session.default_name` is used when a name is omitted, unless
`persist.session.prompt_if_missing` is true.

Expand All @@ -523,12 +536,14 @@ Auto persistence (`persist.auto`) requires `persist.enabled = true`:
restore_if_empty only restore when the stack is empty
save save on PeekstackPush/Close/RestorePopup (debounced)
debounce_ms debounce time for auto save
save_on_leave save on VimLeavePre
save_on_leave save on VimLeavePre (also with `save = false`, in
which case the stack is written only when leaving)

The auto session holds a single stack: the one that changed most
recently, saved to the repository that was current when it changed.
Closing every popup of that stack saves it empty, so nothing is restored
next time.
next time. Quick peek popups never join a stack, so opening or closing one
does not touch the auto session.

Leaving Neovim saves that stack synchronously, even if the cursor is in
another window. It skips the save if no stack changed during the session,
Expand Down
40 changes: 34 additions & 6 deletions lua/peekstack/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,32 @@ local function complete_session_names(arg_lead)
return filter_by_prefix(list_session_names(), arg_lead)
end

---Storage and stack a session command acts on.
---Prompts and pickers answer asynchronously, and the user may change
---directory or window before they do, so both are fixed when the command
---runs and handed to every later step.
---@class PeekstackSessionCommandTarget
---@field store_path string
---@field root_winid integer

---@return PeekstackSessionCommandTarget
local function capture_session_target()
return {
store_path = require("peekstack.persist.orchestrator").store_path(),
-- The resolved stack root, so closing the popup or stack view the command
-- ran from does not lose the stack; saves and restores are dropped once
-- the root window itself is gone.
root_winid = require("peekstack.persist.sessions").resolve_root_winid() --[[@as integer]],
}
end

---Confirm and delete a named session.
---@param name string
local function confirm_delete_session(name)
---@param target PeekstackSessionCommandTarget
local function confirm_delete_session(name, target)
vim.ui.select({ "Yes", "No" }, { prompt = "Delete session '" .. name .. "'?" }, function(choice)
if choice == "Yes" then
require("peekstack.persist").delete_session(name)
require("peekstack.persist").delete_session(name, { store_path = target.store_path })
end
end)
end
Expand All @@ -92,11 +112,15 @@ function M.setup()
local cfg = require("peekstack.config").get()

if not name and cfg.persist.session and cfg.persist.session.prompt_if_missing then
local target = capture_session_target()
vim.ui.input(
{ prompt = "Session name: ", default = cfg.persist.session.default_name or "default" },
function(input)
if input and input ~= "" then
require("peekstack.persist").save_current(input)
require("peekstack.persist").save_current(input, {
store_path = target.store_path,
root_winid = target.root_winid,
})
end
end
)
Expand All @@ -116,7 +140,9 @@ function M.setup()

vim.api.nvim_create_user_command("PeekstackListSessions", function()
local persist = require("peekstack.persist")
local target = capture_session_target()
persist.list_sessions({
store_path = target.store_path,
on_done = function(sessions)
local names = vim.tbl_keys(sessions)
table.sort(names)
Expand All @@ -137,7 +163,7 @@ function M.setup()
)
vim.ui.select({ "Restore", "Info only" }, { prompt = info }, function(action)
if action == "Restore" then
persist.restore(selected)
persist.restore(selected, { store_path = target.store_path, root_winid = target.root_winid })
end
end)
end)
Expand All @@ -147,12 +173,14 @@ function M.setup()

vim.api.nvim_create_user_command("PeekstackDeleteSession", function(opts)
local name = opts.args and opts.args ~= "" and opts.args or nil
local target = capture_session_target()
if name then
confirm_delete_session(name)
confirm_delete_session(name, target)
return
end

require("peekstack.persist").list_sessions({
store_path = target.store_path,
on_done = function(sessions)
local names = vim.tbl_keys(sessions)
table.sort(names)
Expand All @@ -164,7 +192,7 @@ function M.setup()
if not selected then
return
end
confirm_delete_session(selected)
confirm_delete_session(selected, target)
end)
end,
})
Expand Down
29 changes: 20 additions & 9 deletions lua/peekstack/persist/auto.lua
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,24 @@ function M.maybe_restore()
return true
end

---Record a stack change and debounce a save of it when `auto.save` is on.
---The change is tracked either way, so the leave-time save still knows
---which stack to write when only `auto.save_on_leave` is enabled.
---@param opts? { root_winid?: integer }
---@return boolean
---@return boolean scheduled whether a debounced save was scheduled
function M.schedule_save(opts)
if not is_enabled() then
return false
end

local cfg = config.get()
if not cfg.persist.auto.save then
local target = capture_target(opts and opts.root_winid or nil)
if not target then
return false
end
tracked_target = target

local target = capture_target(opts and opts.root_winid or nil)
if not target then
local cfg = config.get()
if not cfg.persist.auto.save then
return false
end

Expand All @@ -172,7 +176,6 @@ function M.schedule_save(opts)
flush_pending()
end
pending_target = target
tracked_target = target

local debounce_ms = tonumber(cfg.persist.auto.debounce_ms) or 1000
if save_timer then
Expand Down Expand Up @@ -255,13 +258,21 @@ function M.setup()
})
end

if cfg.persist.auto.save then
-- Changes are observed for the leave-time save too, which needs to know
-- the stack to write even when debounced saves are off.
if cfg.persist.auto.save or cfg.persist.auto.save_on_leave then
vim.api.nvim_create_autocmd("User", {
group = group,
pattern = { "PeekstackPush", "PeekstackClose", "PeekstackRestorePopup" },
callback = function(args)
local root_winid = args.data and args.data.root_winid or nil
M.schedule_save({ root_winid = root_winid })
local data = args.data or {}
-- Quick peek popups never join a stack, so opening or closing one
-- leaves nothing to save; treating it as a change would overwrite the
-- session with the (usually empty) stack of the window it opened in.
if data.ephemeral then
return
end
M.schedule_save({ root_winid = data.root_winid })
end,
})
end
Expand Down
Loading
Loading