-- -- Session plugin -- local session_dir = vim.fn.stdpath("state") .. "/sessions" local session_default = session_dir .. "/default.vim" if not vim.uv.fs_stat(session_dir) then vim.uv.fs_mkdir(session_dir, tonumber("755", 8)) vim.notify("Sessions save directory created at " .. session_dir) end -- :mksession doesn't persist everything we want (e.g. tab-local t:tabname), so we keep a -- sidecar JSON object alongside the .vim session for extra data. Tab names live under the -- `tabnames` key, stored positionally because :mksession recreates tabs in their original -- order, making the index stable across reloads. local function sidecar_path(path) return (path:gsub("%.vim$", "")) .. ".json" end local function read_sidecar(path) local f = io.open(sidecar_path(path), "r") if not f then return {} end local content = f:read("*a") f:close() local ok, data = pcall(vim.json.decode, content) return (ok and type(data) == "table") and data or {} end local function write_sidecar(path, data) local f = io.open(sidecar_path(path), "w") if f then f:write(vim.json.encode(data)) f:close() end end -- :mksession can't restore man:// buffers (they're :buftype=nofile, not backed by a file), so -- we record them ourselves, nested as tab index -> window number -> { buffer name, cursor line }. -- Both indices are positional for the same reason as tab names above: :mksession recreates tabs -- and their windows in order, so the ordinals are stable across reloads. Nesting by tab matters -- because window numbers restart at 1 in each tabpage and would otherwise collide. The indices -- are stringified so the sparse table encodes as a JSON object, not a null-padded array. -- -- Limitation: the cursor line is an index into the *rendered* man page, whose line wrapping -- depends on MANWIDTH. If MANWIDTH differs on load, the page re-wraps and the saved line points -- elsewhere. (It never changes in this setup, but the dependency is real.) local function man_windows() local pages = {} for ti, tp in ipairs(vim.api.nvim_list_tabpages()) do for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tp)) do local name = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(win)) if name:match("^man://") then local tk = tostring(ti) pages[tk] = pages[tk] or {} pages[tk][tostring(vim.api.nvim_win_get_number(win))] = { name = name, line = vim.api.nvim_win_get_cursor(win)[1] } end end end return pages end local function save_session(path) vim.cmd.mksession({ path, bang = true }) local names = {} for i, tp in ipairs(vim.api.nvim_list_tabpages()) do -- pcall: nvim_tabpage_get_var errors when the variable isn't set on that tab local ok, name = pcall(vim.api.nvim_tabpage_get_var, tp, "tabname") names[i] = (ok and type(name) == "string") and name or "" end write_sidecar(path, { tabnames = names, manpages = man_windows() }) end -- Reopen the man:// buffers recorded by man_windows(). Keys come back from JSON as strings, -- hence the tonumber(). :edit man://... re-renders the page via the man plugin's BufReadCmd; we -- run it window-scoped so the layout restored by :mksession is left untouched, then clamp the -- saved cursor line to the (possibly re-wrapped) page. local function restore_man_windows(manpages) if not manpages then return end local tabpages = vim.api.nvim_list_tabpages() for tk, wins in pairs(manpages) do local tp = tabpages[tonumber(tk)] if tp then local by_number = {} for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tp)) do by_number[vim.api.nvim_win_get_number(win)] = win end for wk, info in pairs(wins) do local win = by_number[tonumber(wk)] if win then vim.api.nvim_win_call(win, function() vim.cmd.edit({ args = { info.name } }) end) local buf = vim.api.nvim_win_get_buf(win) local line = math.max(1, math.min(info.line or 1, vim.api.nvim_buf_line_count(buf))) vim.api.nvim_win_set_cursor(win, { line, 0 }) end end end end end local function load_session(path) vim.cmd.source(path) local data = read_sidecar(path) if data.tabnames then for i, tp in ipairs(vim.api.nvim_list_tabpages()) do if data.tabnames[i] and data.tabnames[i] ~= "" then vim.api.nvim_tabpage_set_var(tp, "tabname", data.tabnames[i]) end end vim.cmd.redrawtabline() end restore_man_windows(data.manpages) end local function reload_session(path) save_session(path) vim.cmd.restart({ args = { "+qall", "SessionLoad", path } }) end local function delete_session(path) vim.fs.rm(path) local sidecar = sidecar_path(path) if vim.uv.fs_stat(sidecar) then vim.fs.rm(sidecar) end end local function session_completefunc(arg_lead, _, _) local completions = {} for path in vim.fs.dir(session_dir) do if string.match(path, "^" .. arg_lead) and string.match(path, ".vim$") then completions[#completions + 1] = path:sub(1, -5) end end return completions end local function session_op(base, op) local path = #base > 0 and base or session_default if not string.match(path, "^" .. session_dir) then path = session_dir .. "/" .. path end if not string.match(path, "%.vim$") then path = path .. ".vim" end op(path) end vim.api.nvim_create_user_command("SessionSave", function(ev) session_op(ev.args, save_session) end, { desc = "Save session", nargs = "?", complete = session_completefunc }) vim.api.nvim_create_user_command("SessionLoad", function(ev) session_op(ev.args, load_session) end, { desc = "Load session", nargs = "?", complete = session_completefunc }) vim.api.nvim_create_user_command("SessionDelete", function(ev) session_op(ev.args, delete_session) end, { desc = "Delete session", nargs = "?", complete = session_completefunc }) vim.api.nvim_create_user_command("SessionRestart", function(ev) session_op(ev.args, reload_session) end, { desc = "Reload session", nargs = "?", complete = session_completefunc }) vim.api.nvim_create_user_command("SessionExitSave", function(ev) session_op(ev.args, save_session) vim.cmd.qall() end, { desc = "Save session and exit", nargs = "?", complete = session_completefunc }) vim.api.nvim_create_user_command("SessionExitNoSave", function() vim.cmd.qall() end, { desc = "Exit without saving session", nargs = "?", complete = session_completefunc })