summaryrefslogtreecommitdiffstats
path: root/.config/nvim/plugin/50-session.lua
blob: 39b4c0911854a03a22bd5f4f8e432b95790e0912 (plain)
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 })