diff --git a/README.md b/README.md index 4302b58..edba2d6 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,11 @@ Built-in provider names: `diagnostics.under_cursor`, `diagnostics.in_buffer`, `file.under_cursor`, `grep.search`, `marks.buffer`, `marks.global`, `marks.all` (marks require their provider enabled; `grep.search` requires `rg`). +The marks providers show the characters in `providers.marks.include`, letters only by default. +Numbered marks (`0`-`9`) appear only after adding them to `include`. Special marks +(`` ' ` ^ . < > [ ] " ``) need both `include_special = true` and their character in `include`; +`include_special` alone adds nothing. + ## 💻 Commands - `:PeekstackStack` — open the stack view panel @@ -136,7 +141,7 @@ Defaults inside popup windows: Defaults in stack view: -- `` — focus selected popup +- `` — focus selected popup (moves to it, which closes the panel) - `dd` — close selected popup - `u` — undo close (restore last) - `U` — restore all closed popups @@ -146,10 +151,14 @@ Defaults in stack view: - `/` — filter - `gg/G` — jump to first/last stack item - `j/k` — move cursor by stack item (skip header/preview lines) -- `z` — toggle zoom (maximize top popup) +- `z` — toggle zoom of the top popup (the most recently pushed one, not the selected entry) - `?` — help - `q` — close +The stack view is a read-only panel that closes as soon as focus leaves it. Motions, visual selection +and yanking work as usual; editing commands fail because the buffer is not modifiable, and `/` filters +the list instead of searching. + ## ⚙️ Configuration Configure via `require("peekstack").setup({ ... })`. @@ -352,15 +361,28 @@ When `persist.enabled = true`, `PeekstackSaveSession` uses `persist.session.defa if you do not pass a name. If `persist.session.prompt_if_missing = true`, you'll be prompted for a name instead of using the default. +A session holds one stack: the popups of the current window (or of the 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. + > [!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. -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. +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. + +- **Empty stacks**: saving an empty stack replaces the session with an empty one; restoring an empty + session reports that there is no saved session. Use `:PeekstackDeleteSession` to remove one. +- **Skipped items**: entries whose file no longer exists or that are malformed are skipped on restore. + The warning lists the first five skipped files with their reasons, and the `PeekstackRestore` event + data carries every skipped item as `skipped`. +- **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. > [!IMPORTANT] > Sessions are written as plain JSON under `vim.fn.stdpath("state") .. "/peekstack/"`. Each entry @@ -379,19 +401,25 @@ When `persist.auto.enabled = true`, peekstack can automatically restore and save - **Save on leave** on `VimLeavePre` if `save_on_leave = true` The auto session holds a single stack: the one that changed most recently, saved to the repository -that was current when it changed. Leaving Neovim saves that stack even if the cursor is in another -window, skips the save if no stack changed during the session, and keeps the stored session as-is -once that stack's window has been closed. The save on leave waits up to one second for earlier -saves to finish; if they are still writing, it is skipped with a warning rather than racing them. +that was current when it changed. Closing every popup of that stack saves it empty, so nothing is +restored next time. + +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 +window has been closed. The save on leave waits up to one second for earlier saves to finish; if they +are still writing, it is skipped with a warning rather than racing them. 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. Providers and pickers registered with -`register_provider()` / `register_picker()` are kept. +Calling `require("peekstack").setup()` again replaces config, re-registers the built-in providers and +picker backends for the new config, and recreates the autocmds and auto-persist hooks. Providers and +pickers registered with `register_provider()` / `register_picker()` are kept. + +User commands are created by the first `setup()` call and reused afterwards; they read the config +when they run, so they always use the latest settings. 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. @@ -404,6 +432,15 @@ to future actions, and to existing stacks only after those popups are reopened, - **source**: uses the real source buffer; useful for editing, with safety options in `ui.popup.source` (`confirm_on_close`, `prevent_auto_close_if_modified`) +A copy popup is a snapshot taken when it opens (up to 500 lines around the target in large files) +and does not follow later changes to the file. Edits made in it with `ui.popup.editable = true` +stay in the popup: they are never written to the file and are dropped when it closes, so history, +sessions and promote reopen the file itself. + +A source popup edits the real buffer, so `:w` saves the file. Closing the popup keeps the buffer +loaded with any unsaved changes; `confirm_on_close` asks before the close key closes a modified +popup, and `prevent_auto_close_if_modified` keeps auto close away from it. + Switching buffers inside a popup (`:buffer`, `:edit`) keeps the popup and makes it follow the new buffer as a source popup at the cursor, so its title, keymaps, provider requests and history refer to that buffer. diff --git a/doc/peekstack.txt b/doc/peekstack.txt index c0520fb..69e94fc 100644 --- a/doc/peekstack.txt +++ b/doc/peekstack.txt @@ -267,7 +267,8 @@ For picker-agnostic integration: ============================================================================== COMMANDS *peekstack-cmds* -Commands are registered after `setup()` is called. +Commands are created by the first `setup()` call +(see |peekstack-setup-reload|). :PeekstackStack *:PeekstackStack* Open the stack view panel listing all active popups. @@ -355,9 +356,25 @@ Marks (requires providers.marks.enable = true): marks.buffer buffer-local marks marks.global global marks marks.all buffer + global marks - Filter with providers.marks.include / include_special. Scope is selected by the provider name. + `providers.marks.include` lists the mark characters to show; the + default is the letters `a`-`z` and `A`-`Z`. Numbered marks (`0`-`9`) + are shown only after adding them to `include`. Special marks + (`'` `` ` `` `^` `.` `<` `>` `[` `]` `"`) need both + `include_special = true` and their character in `include`; + `include_special` alone adds nothing. +>lua + providers = { + marks = { + enable = true, + include = "abcdefghijklmnopqrstuvwxyz" + .. "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.^", + include_special = true, + }, + } +< + ============================================================================== API *peekstack-api* @@ -463,16 +480,38 @@ PERSIST *peekstack-persist* Persistence is disabled by default. Enable with `persist.enabled = true`. +A session holds one stack: the popups of the current window (or of the +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. + 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. 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. - -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. +cwd-based storage. Each git repository (each worktree, found by looking +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. + +Empty stacks: + Saving an empty stack replaces the session with an empty one; restoring + an empty session reports that there is no saved session. Use + |:PeekstackDeleteSession| to remove one. + +Skipped items: + Entries whose file no longer exists or that are malformed are skipped + on restore. The warning lists the first five skipped files with their + reasons, and the `PeekstackRestore` event data carries every skipped + item as `skipped` (a list of `{ uri?, reason }`). + +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. `persist.session.default_name` is used when a name is omitted, unless `persist.session.prompt_if_missing` is true. @@ -488,11 +527,15 @@ Auto persistence (`persist.auto`) requires `persist.enabled = true`: The auto session holds a single stack: the one that changed most recently, saved to the repository that was current when it changed. -Leaving Neovim saves that stack even if the cursor is in another window, -skips the save if no stack changed during the session, and keeps the -stored session as-is once that stack's window has been closed. The save on -leave waits up to one second for earlier saves to finish; if they are -still writing, it is skipped with a warning rather than racing them. +Closing every popup of that stack saves it empty, so nothing is restored +next time. + +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 window has been +closed. The save on leave waits up to one second for earlier saves to +finish; if they are still writing, it is skipped with a warning rather +than racing them. Auto persistence runs only inside a git repository and always uses the repository session storage. @@ -511,10 +554,13 @@ paths or symbol names are sensitive. ============================================================================== SETUP RELOAD *peekstack-setup-reload* -Calling `require("peekstack").setup()` again replaces config and re-registers -providers, commands, autocmds, picker backends, and auto-persist hooks. -Providers and pickers registered with `register_provider()` / -`register_picker()` are kept. +Calling `require("peekstack").setup()` again replaces config, re-registers +the built-in providers and picker backends for the new config, and recreates +the autocmds and auto-persist hooks. Providers and pickers registered with +`register_provider()` / `register_picker()` are kept. + +User commands are created by the first `setup()` call and reused afterwards; +they read the config when they run, so they always use the latest settings. It does not migrate existing popup windows, stack entries, or history in place. Updated settings apply to future actions, and to existing stacks only @@ -532,6 +578,17 @@ BUFFER MODES *peekstack-buffer-modes* `ui.popup.source.confirm_on_close` prompts on close if modified `ui.popup.source.prevent_auto_close_if_modified` blocks auto-close +A copy popup is a snapshot taken when it opens (up to 500 lines around the +target in large files) and does not follow later changes to the file. +Edits made in it with `ui.popup.editable = true` stay in the popup: they are +never written to the file and are dropped when it closes, so history, +sessions and promote reopen the file itself. + +A source popup edits the real buffer, so `:w` saves the file. Closing the +popup keeps the buffer loaded with any unsaved changes; `confirm_on_close` +asks before the close key (`ui.keys.close`) closes a modified popup, and +`prevent_auto_close_if_modified` keeps auto close away from it. + Switching buffers inside a popup (`:buffer`, `:edit`) keeps the popup and makes it follow the new buffer as a "source" popup at the cursor: its title, keymaps, provider requests and history entry refer to that buffer. @@ -545,6 +602,14 @@ STACK VIEW *peekstack-stack-view* "left" left-side panel "bottom" bottom panel across full editor width +The panel is read-only and closes as soon as focus leaves it, so ``, +which moves to the selected popup, also closes it. Motions, visual +selection and yanking work as usual; editing commands fail because the +buffer is not modifiable, and `/` filters the list instead of searching. + +`z` toggles zoom of the top popup (the most recently pushed one), not of +the selected entry, the same as |:PeekstackZoom|. + ============================================================================== EVENTS *peekstack-events* @@ -566,7 +631,7 @@ Persist events: Event data keys (vary per event): event, popup_id, winid, bufnr, root_winid, provider, location - session, item_count, from, to, action, ephemeral + session, item_count, skipped, from, to, action, ephemeral ============================================================================== KEYMAPS *peekstack-keymaps* @@ -585,7 +650,7 @@ Default keymaps inside popup windows: Stack view keymaps: - `` Focus the selected popup + `` Focus the selected popup (closes the panel) `dd` Close the selected popup `u` Undo close (restore last closed popup) `U` Restore all closed popups @@ -595,7 +660,7 @@ Stack view keymaps: `/` Filter the list `gg/G` Jump to first/last stack item `j/k` Move cursor by stack item (skip header/preview lines) - `z` Toggle zoom (maximize top popup) + `z` Toggle zoom of the top popup (not the selected entry) `?` Toggle help `q` Close the stack view diff --git a/lua/peekstack/persist/service.lua b/lua/peekstack/persist/service.lua index 18cee2a..c5c17cc 100644 --- a/lua/peekstack/persist/service.lua +++ b/lua/peekstack/persist/service.lua @@ -21,11 +21,15 @@ end ---@param item PeekstackSessionItem ---@param id_remap table ---@param root_winid integer stack root frozen when the restore was requested ----@return boolean restored whether a popup was actually created +---@return string? skip_reason nil when a popup was created local function restore_item(item, id_remap, root_winid) local loc = location.normalize({ uri = item.uri, range = item.range }, item.provider or "persist") if not loc then - return false + return "invalid location" + end + -- A deleted or moved file would otherwise come back as an empty popup. + if vim.startswith(loc.uri, "file://") and not vim.uv.fs_stat(vim.uri_to_fname(loc.uri)) then + return "file not found" end local parent_id = item.parent_popup_id @@ -47,7 +51,7 @@ local function restore_item(item, id_remap, root_winid) root_winid = root_winid, }) if not model then - return false + return "failed to open popup" end if item.pinned then @@ -56,7 +60,31 @@ local function restore_item(item, id_remap, root_winid) if item.popup_id then id_remap[item.popup_id] = model.id end - return true + return nil +end + +---Maximum number of skipped items listed in the restore warning. +local MAX_REPORTED_SKIPS = 5 + +---@param name string +---@param skipped PeekstackSessionSkippedItem[] +---@return string +local function format_skipped(name, skipped) + local lines = { string.format("Session restored with %d skipped item(s): %s", #skipped, name) } + for idx = 1, math.min(#skipped, MAX_REPORTED_SKIPS) do + local entry = skipped[idx] + local target = "" + if entry.uri then + -- A uri without a scheme makes vim.uri_to_fname() throw. + local ok, fname = pcall(vim.uri_to_fname, entry.uri) + target = ok and vim.fn.fnamemodify(fname, ":~:.") or entry.uri + end + table.insert(lines, string.format(" %s: %s", target, entry.reason)) + end + if #skipped > MAX_REPORTED_SKIPS then + table.insert(lines, string.format(" ... and %d more", #skipped - MAX_REPORTED_SKIPS)) + end + return table.concat(lines, "\n") end ---@param success boolean @@ -162,14 +190,27 @@ function M.restore(name, opts) ---@type table local id_remap = {} local restored_count = 0 + ---@type PeekstackSessionSkippedItem[] + local skipped = {} for _, item in ipairs(session.items) do -- Isolate each item: a single corrupt entry (bad type or a push failure) -- must not abort restoring the rest of the session. + local reason if is_valid_item(item) then - local ok, restored = pcall(restore_item, item, id_remap, root_winid) - if ok and restored then - restored_count = restored_count + 1 + local ok, result = pcall(restore_item, item, id_remap, root_winid) + if ok then + reason = result + else + reason = tostring(result) end + else + reason = "invalid entry" + end + if reason then + local uri = type(item) == "table" and type(item.uri) == "string" and item.uri or nil + table.insert(skipped, { uri = uri, reason = reason }) + else + restored_count = restored_count + 1 end end @@ -177,11 +218,9 @@ function M.restore(name, opts) stack.reflow(root_winid) end - local skipped = #session.items - restored_count - if not silent then - if skipped > 0 then - notify.warn(string.format("Session restored with %d skipped item(s): %s", skipped, resolved_name)) + if #skipped > 0 then + notify.warn(format_skipped(resolved_name, skipped)) else notify.info("Session restored: " .. resolved_name) end @@ -190,6 +229,7 @@ function M.restore(name, opts) user_events.emit("PeekstackRestore", { session = resolved_name, item_count = restored_count, + skipped = skipped, }) finish(restored_count > 0) end) diff --git a/lua/peekstack/types.lua b/lua/peekstack/types.lua index 5becf63..f641c54 100644 --- a/lua/peekstack/types.lua +++ b/lua/peekstack/types.lua @@ -32,6 +32,10 @@ ---@field buffer_mode? "copy"|"source" ---@field parent_popup_id? integer +---@class PeekstackSessionSkippedItem +---@field uri? string missing when the stored entry had no usable uri +---@field reason string why the item was not restored + ---@class PeekstackSessionMeta ---@field created_at integer ---@field updated_at integer diff --git a/tests/marks_provider_spec.lua b/tests/marks_provider_spec.lua index 1b74f86..8d59652 100644 --- a/tests/marks_provider_spec.lua +++ b/tests/marks_provider_spec.lua @@ -127,6 +127,28 @@ describe("marks provider", function() vim.api.nvim_buf_delete(bufnr, { force = true }) end) + it("adds special marks only when they are also listed in include", function() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "line1", "line2", "line3" }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_buf_set_mark(bufnr, "<", 2, 0, {}) + + local function has_mark(include) + local locations = marks_util.collect("buffer", bufnr, { include = include, include_special = true }) + for _, loc in ipairs(locations) do + if loc.text and loc.text:find("[<]", 1, true) then + return true + end + end + return false + end + + assert.is_false(has_mark("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")) + assert.is_true(has_mark("<")) + + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + it("returns locations with valid structure", function() local bufnr = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "hello world", "second line" }) diff --git a/tests/persist_sessions_spec.lua b/tests/persist_sessions_spec.lua index 3e8bcb0..c11b921 100644 --- a/tests/persist_sessions_spec.lua +++ b/tests/persist_sessions_spec.lua @@ -635,6 +635,78 @@ describe("peekstack.persist.sessions", function() assert.is_nil(sessions.broken) end) + it("should report each skipped item with its reason when restoring", function() + local kept = make_file("restore_kept", { "line" }) + local missing = vim.fn.tempname() .. "_restore_missing.lua" + local range = { + start = { line = 0, character = 0 }, + ["end"] = { line = 0, character = 0 }, + } + write_and_wait(test_scope, { + version = 2, + sessions = { + partial = { + items = { + { uri = vim.uri_from_fname(kept), range = range }, + { uri = vim.uri_from_fname(missing), range = range }, + { range = range }, + { uri = "broken", range = range }, + 42, + }, + meta = { created_at = 1, updated_at = 1 }, + }, + }, + }) + persist._reset_cache() + + local original_notify = vim.notify + local warnings = {} + vim.notify = function(msg, level) + if level == vim.log.levels.WARN and msg:find("skipped item(s)", 1, true) then + table.insert(warnings, msg) + end + end + local event_data = nil + local group = vim.api.nvim_create_augroup("PeekstackRestoreSkipTest", { clear = true }) + vim.api.nvim_create_autocmd("User", { + group = group, + pattern = "PeekstackRestore", + callback = function(args) + event_data = args.data + end, + }) + + local restored = nil + persist.restore("partial", { + on_done = function(result) + restored = result + end, + }) + local waited = vim.wait(wait_timeout_ms, function() + return restored ~= nil + end, wait_interval_ms) + vim.notify = original_notify + vim.api.nvim_del_augroup_by_id(group) + assert.is_true(waited, "Timed out waiting for restore") + + assert.is_true(restored) + assert.equals(1, #stack.list()) + assert.equals(1, #warnings) + assert.truthy(warnings[1]:find("4 skipped item(s): partial", 1, true)) + assert.truthy(warnings[1]:find(vim.fn.fnamemodify(missing, ":~:.") .. ": file not found", 1, true)) + assert.truthy(warnings[1]:find(": invalid entry", 1, true)) + assert.truthy(warnings[1]:find("broken: failed to open popup", 1, true)) + + assert.is_not_nil(event_data) + assert.equals(1, event_data.item_count) + assert.same({ + { uri = vim.uri_from_fname(missing), reason = "file not found" }, + { reason = "invalid entry" }, + { uri = "broken", reason = "failed to open popup" }, + { reason = "invalid entry" }, + }, event_data.skipped) + end) + it("should rename a session", function() push_popup("rename_session", { title = "Rename me" }) persist.save_current("old_name", { silent = true, sync = true }) @@ -725,13 +797,13 @@ describe("peekstack.persist.sessions", function() batched = { items = { { - uri = "file:///tmp/a.lua", + uri = vim.uri_from_fname(make_file("a")), range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, provider = "test", ts = os.time(), }, { - uri = "file:///tmp/b.lua", + uri = vim.uri_from_fname(make_file("b")), range = { start = { line = 1, character = 0 }, ["end"] = { line = 1, character = 1 } }, provider = "test", ts = os.time(), @@ -793,7 +865,7 @@ describe("peekstack.persist.sessions", function() optional_fields = { items = { { - uri = "file:///tmp/optional.lua", + uri = vim.uri_from_fname(make_file("optional")), range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, ts = os.time(), }, @@ -1041,7 +1113,7 @@ describe("peekstack.persist.sessions", function() restore_fields = { items = { { - uri = "file:///tmp/a.lua", + uri = vim.uri_from_fname(make_file("a")), range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, provider = "test", ts = os.time(), @@ -1104,14 +1176,14 @@ describe("peekstack.persist.sessions", function() remap_parent = { items = { { - uri = "file:///tmp/parent.lua", + uri = vim.uri_from_fname(make_file("parent")), range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, provider = "test", ts = os.time(), popup_id = 100, }, { - uri = "file:///tmp/child.lua", + uri = vim.uri_from_fname(make_file("child")), range = { start = { line = 1, character = 0 }, ["end"] = { line = 1, character = 1 } }, provider = "test", ts = os.time(), @@ -1176,7 +1248,7 @@ describe("peekstack.persist.sessions", function() orphan_parent = { items = { { - uri = "file:///tmp/orphan.lua", + uri = vim.uri_from_fname(make_file("orphan")), range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, provider = "test", ts = os.time(), @@ -1228,6 +1300,8 @@ describe("peekstack.persist.sessions", function() it("should skip corrupt items and still restore valid ones", function() local original_push = stack.push local original_reflow = stack.reflow + local valid_a = vim.uri_from_fname(make_file("valid_a")) + local valid_b = vim.uri_from_fname(make_file("valid_b")) write_and_wait(test_scope, { version = 2, @@ -1235,7 +1309,7 @@ describe("peekstack.persist.sessions", function() corrupt_items = { items = { { - uri = "file:///tmp/valid_a.lua", + uri = valid_a, range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, provider = "test", ts = os.time(), @@ -1248,13 +1322,13 @@ describe("peekstack.persist.sessions", function() }, -- Non-table range: fails the coarse type check. { - uri = "file:///tmp/corrupt.lua", + uri = vim.uri_from_fname(make_file("corrupt")), range = 42, provider = "test", ts = os.time(), }, { - uri = "file:///tmp/valid_b.lua", + uri = valid_b, range = { start = { line = 2, character = 0 }, ["end"] = { line = 2, character = 1 } }, provider = "test", ts = os.time(), @@ -1292,8 +1366,8 @@ describe("peekstack.persist.sessions", function() assert.is_true(restored) assert.equals(2, #pushed) - assert.equals("file:///tmp/valid_a.lua", pushed[1]) - assert.equals("file:///tmp/valid_b.lua", pushed[2]) + assert.equals(valid_a, pushed[1]) + assert.equals(valid_b, pushed[2]) assert.equals(1, reflow_calls) end) @@ -1308,6 +1382,9 @@ describe("peekstack.persist.sessions", function() it("should isolate a push failure to a single item when restoring", function() local original_push = stack.push local original_reflow = stack.reflow + local ok1 = vim.uri_from_fname(make_file("ok1")) + local boom = vim.uri_from_fname(make_file("boom")) + local ok2 = vim.uri_from_fname(make_file("ok2")) write_and_wait(test_scope, { version = 2, @@ -1315,19 +1392,19 @@ describe("peekstack.persist.sessions", function() push_failure = { items = { { - uri = "file:///tmp/ok1.lua", + uri = ok1, range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, provider = "test", ts = os.time(), }, { - uri = "file:///tmp/boom.lua", + uri = boom, range = { start = { line = 1, character = 0 }, ["end"] = { line = 1, character = 1 } }, provider = "test", ts = os.time(), }, { - uri = "file:///tmp/ok2.lua", + uri = ok2, range = { start = { line = 2, character = 0 }, ["end"] = { line = 2, character = 1 } }, provider = "test", ts = os.time(), @@ -1343,7 +1420,7 @@ describe("peekstack.persist.sessions", function() local ok, err = pcall(function() stack.push = function(loc, _opts) - if loc.uri == "file:///tmp/boom.lua" then + if loc.uri == boom then error("simulated push failure") end table.insert(pushed, loc.uri) @@ -1369,8 +1446,8 @@ describe("peekstack.persist.sessions", function() -- The failing item is isolated; the surrounding valid items still restore. assert.is_true(restored) assert.equals(2, #pushed) - assert.equals("file:///tmp/ok1.lua", pushed[1]) - assert.equals("file:///tmp/ok2.lua", pushed[2]) + assert.equals(ok1, pushed[1]) + assert.equals(ok2, pushed[2]) assert.equals(1, reflow_calls) end) @@ -1392,7 +1469,7 @@ describe("peekstack.persist.sessions", function() all_fail = { items = { { - uri = "file:///tmp/unrestorable.lua", + uri = vim.uri_from_fname(make_file("unrestorable")), range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 1 } }, provider = "test", ts = os.time(),