Discard missing files from saved sessions

Sanitize nonexistent unmodified file buffers and stale argument-list entries after session load and before automatic save.
This commit is contained in:
Ray Elliott 2026-10-28 19:38:17 +00:00
commit aedc1d7b86
3 changed files with 62 additions and 6 deletions

1
LOG.md
View file

@ -226,6 +226,7 @@ Record every decision here with a short rationale. Append new entries; do not re
- 2026-10-28: Replaced the migration-era `AGENTS.md` with a concise maintenance guide for the established configuration. Removed completed phase tracking, obsolete validation-workspace instructions, resolved issue notes, and the target-tree description; retained current architectural guardrails, documentation expectations, a smoke-test command, and a short history note. - 2026-10-28: Replaced the migration-era `AGENTS.md` with a concise maintenance guide for the established configuration. Removed completed phase tracking, obsolete validation-workspace instructions, resolved issue notes, and the target-tree description; retained current architectural guardrails, documentation expectations, a smoke-test command, and a short history note.
- 2026-10-28: Retired the completed migration's legacy configuration, vendored plugin archive, UltiSnips collection, validation workspaces, root test assets, superseded planning documents, and phase labels in active Lua comments. Git history now provides the historical archive. Persistent undo storage moved from the configuration repository to `stdpath('state')/undo`; the active README, architectural log, lockfile, colorscheme, spell data, and templates remain. - 2026-10-28: Retired the completed migration's legacy configuration, vendored plugin archive, UltiSnips collection, validation workspaces, root test assets, superseded planning documents, and phase labels in active Lua comments. Git history now provides the historical archive. Persistent undo storage moved from the configuration repository to `stdpath('state')/undo`; the active README, architectural log, lockfile, colorscheme, spell data, and templates remain.
- 2026-10-28: Diagnosed the Tree-sitter `TSNode:range()` failure as the known incompatibility between Neovim 0.12 and nvim-treesitter's locked legacy `master` branch. Migrated nvim-treesitter and nvim-treesitter-textobjects to their rewritten `main` branches: native Neovim highlighting and incremental selection replace the old modules, textobjects use explicit mappings, indentation uses the new plugin expression when a query exists, and autotag retains its direct supported setup. Installed the verified official Tree-sitter CLI 0.26.9 binary in `~/.local/bin` after the system package required interactive sudo and Cargo compilation required unavailable libclang. Reinstalled all configured parsers in the new site directory and removed obsolete generated parser copies from the plugin checkout. The plugin audit confirmed every plugin is current for its configured branch or release constraint; nvim-surround remains on the latest release rather than its newer unreleased `main` commit. - 2026-10-28: Diagnosed the Tree-sitter `TSNode:range()` failure as the known incompatibility between Neovim 0.12 and nvim-treesitter's locked legacy `master` branch. Migrated nvim-treesitter and nvim-treesitter-textobjects to their rewritten `main` branches: native Neovim highlighting and incremental selection replace the old modules, textobjects use explicit mappings, indentation uses the new plugin expression when a query exists, and autotag retains its direct supported setup. Installed the verified official Tree-sitter CLI 0.26.9 binary in `~/.local/bin` after the system package required interactive sudo and Cargo compilation required unavailable libclang. Reinstalled all configured parsers in the new site directory and removed obsolete generated parser copies from the plugin checkout. The plugin audit confirmed every plugin is current for its configured branch or release constraint; nvim-surround remains on the latest release rather than its newer unreleased `main` commit.
- 2026-10-28: Fixed stale deleted files persisting across session recreation. Deleting `Session.vim` did not clear the in-memory argument list or listed buffers, so `:mksession` serialized the removed diagnostics fixture again. Session load and exit-time save now discard unmodified nonexistent local-file buffers and missing argument-list entries before restoring or writing the session; real files, URI buffers, unnamed buffers, and modified buffers remain protected.
## Project-Local Configuration ## Project-Local Configuration

View file

@ -20,6 +20,7 @@ Session support is automatic but user-controlled:
- On startup: auto-loads `Session.vim` if it exists (unless files specified on command line) - On startup: auto-loads `Session.vim` if it exists (unless files specified on command line)
- On exit: auto-saves to `Session.vim` if it already exists - On exit: auto-saves to `Session.vim` if it already exists
- Netrw and its preview windows are omitted from the saved layout; generated `NetrwTreeListing` buffers are not restorable - Netrw and its preview windows are omitted from the saved layout; generated `NetrwTreeListing` buffers are not restorable
- Missing files are removed from restored buffers and argument lists, so deleted files cannot become persistent empty session buffers
- Sessions are per-directory and opt-in (just create one to start using) - Sessions are per-directory and opt-in (just create one to start using)
**Stop using a session**: **Stop using a session**:

View file

@ -104,6 +104,7 @@ vim.api.nvim_create_autocmd("FileType", {
-- Session management: auto-load on startup, auto-save on exit (only if Session.vim exists) -- Session management: auto-load on startup, auto-save on exit (only if Session.vim exists)
local session_aug = vim.api.nvim_create_augroup("SessionManagement", { clear = true }) local session_aug = vim.api.nvim_create_augroup("SessionManagement", { clear = true })
local sanitize_session_files
-- Auto-load Session.vim if it exists in the current directory -- Auto-load Session.vim if it exists in the current directory
vim.api.nvim_create_autocmd("VimEnter", { vim.api.nvim_create_autocmd("VimEnter", {
@ -126,6 +127,8 @@ vim.api.nvim_create_autocmd("SessionLoadPost", {
group = session_aug, group = session_aug,
pattern = "*", pattern = "*",
callback = function() callback = function()
sanitize_session_files()
-- Trigger BufReadPost for all loaded buffers to ensure Treesitter/LSP attach -- Trigger BufReadPost for all loaded buffers to ensure Treesitter/LSP attach
vim.schedule(function() vim.schedule(function()
for _, buf in ipairs(vim.api.nvim_list_bufs()) do for _, buf in ipairs(vim.api.nvim_list_bufs()) do
@ -142,13 +145,17 @@ local function is_netrw_session_buffer(buf)
and (vim.bo[buf].filetype == "netrw" or vim.api.nvim_buf_get_name(buf):match("NetrwTreeListing$") ~= nil) and (vim.bo[buf].filetype == "netrw" or vim.api.nvim_buf_get_name(buf):match("NetrwTreeListing$") ~= nil)
end end
local function session_fallback_buffer() local function session_fallback_buffer(excluded)
excluded = excluded or {}
for _, buf in ipairs(vim.api.nvim_list_bufs()) do for _, buf in ipairs(vim.api.nvim_list_bufs()) do
local name = vim.api.nvim_buf_get_name(buf)
if if
vim.api.nvim_buf_is_valid(buf) vim.api.nvim_buf_is_valid(buf)
and not excluded[buf]
and vim.bo[buf].buflisted and vim.bo[buf].buflisted
and vim.bo[buf].buftype == "" and vim.bo[buf].buftype == ""
and vim.api.nvim_buf_get_name(buf) ~= "" and name ~= ""
and vim.uv.fs_stat(name) ~= nil
and not is_netrw_session_buffer(buf) and not is_netrw_session_buffer(buf)
then then
return buf return buf
@ -158,6 +165,55 @@ local function session_fallback_buffer()
return vim.api.nvim_create_buf(true, false) return vim.api.nvim_create_buf(true, false)
end end
local function is_local_file_path(path)
return path ~= "" and path:match("^%a[%w+.-]*://") == nil
end
sanitize_session_files = function()
local missing = {}
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buftype == "" then
local name = vim.api.nvim_buf_get_name(buf)
if is_local_file_path(name) and vim.uv.fs_stat(name) == nil and not vim.bo[buf].modified then
missing[buf] = true
end
end
end
if next(missing) then
local fallback
for _, win in ipairs(vim.api.nvim_list_wins()) do
local buf = vim.api.nvim_win_get_buf(win)
if missing[buf] then
fallback = fallback or session_fallback_buffer(missing)
vim.api.nvim_win_set_buf(win, fallback)
end
end
for buf in pairs(missing) do
if vim.api.nvim_buf_is_valid(buf) then
pcall(vim.api.nvim_buf_delete, buf, { force = true })
end
end
end
local args = vim.fn.argv()
local existing = {}
for _, arg in ipairs(args) do
local path = is_local_file_path(arg) and vim.fn.fnamemodify(arg, ":p") or nil
if not path or vim.uv.fs_stat(path) ~= nil then
table.insert(existing, arg)
end
end
if #existing ~= #args then
vim.cmd("%argdelete")
for _, arg in ipairs(existing) do
vim.cmd.argadd({ args = { arg } })
end
end
end
local function close_session_tab(tab) local function close_session_tab(tab)
if vim.api.nvim_tabpage_close then if vim.api.nvim_tabpage_close then
pcall(vim.api.nvim_tabpage_close, tab, true) pcall(vim.api.nvim_tabpage_close, tab, true)
@ -191,10 +247,7 @@ local function omit_netrw_from_session()
if vim.api.nvim_tabpage_is_valid(tab) then if vim.api.nvim_tabpage_is_valid(tab) then
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tab)) do for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tab)) do
if if vim.api.nvim_win_is_valid(win) and is_netrw_session_buffer(vim.api.nvim_win_get_buf(win)) then
vim.api.nvim_win_is_valid(win)
and is_netrw_session_buffer(vim.api.nvim_win_get_buf(win))
then
if #vim.api.nvim_tabpage_list_wins(tab) > 1 then if #vim.api.nvim_tabpage_list_wins(tab) > 1 then
pcall(vim.api.nvim_win_close, win, true) pcall(vim.api.nvim_win_close, win, true)
elseif #vim.api.nvim_list_tabpages() > 1 then elseif #vim.api.nvim_list_tabpages() > 1 then
@ -217,6 +270,7 @@ vim.api.nvim_create_autocmd("VimLeavePre", {
-- Only save if Session.vim exists (user manually created it) -- Only save if Session.vim exists (user manually created it)
if vim.fn.filereadable("Session.vim") == 1 then if vim.fn.filereadable("Session.vim") == 1 then
omit_netrw_from_session() omit_netrw_from_session()
sanitize_session_files()
vim.cmd("mksession! Session.vim") vim.cmd("mksession! Session.vim")
end end
end, end,