1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
--
-- 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 })
|