aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/textadept/session.lua
blob: db610f73d9ce9aa1cd5d6bdfc4b6619a4774a38a (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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
-- Copyright 2007-2022 Mitchell. See LICENSE.

local M = {}

--[[ This comment is for LuaDoc.
---
-- Session support for Textadept.
-- @field save_on_quit (bool)
--   Save the session when quitting.
--   The default value is `true` unless the user passed the command line switch `-n` or
--   `--nosession` to Textadept.
-- @field _G.events.SESSION_SAVE (string)
--   Emitted when saving a session.
--   Arguments:
--
--   * `session`: Table of session data to save. All handlers will have access to this same
--     table, and Textadept's default handler reserves the use of some keys.
--     Note that functions, userdata, and circular table values cannot be saved. The latter
--     case is not recognized at all, so beware.
-- @field _G.events.SESSION_LOAD (string)
--   Emitted when loading a session.
--   Arguments:
--
--   * `session`: Table of session data to load. All handlers will have access to this same table.
module('textadept.session')]]

M.save_on_quit = true

-- Events.
local session_events = {'session_save', 'session_load'}
for _, v in ipairs(session_events) do events[v:upper()] = v end

local session_file = _USERHOME .. (not CURSES and '/session' or '/session_term')

---
-- Loads session file *filename* or the user-selected session, returning `true` if a session
-- file was opened and read.
-- Textadept restores split views, opened buffers, cursor information, recent files, and bookmarks.
-- @param filename Optional absolute path to the session file to load. If `nil`, the user is
--   prompted for one.
-- @return `true` if the session file was opened and read; `nil` otherwise.
-- @usage textadept.session.load(filename)
-- @name load
function M.load(filename)
  local dir, name = session_file:match('^(.-[/\\]?)([^/\\]+)$')
  if not assert_type(filename, 'string/nil', 1) then
    filename = ui.dialogs.fileselect{
      title = _L['Load Session'], with_directory = dir, with_file = name
    }
    if not filename then return end
  end
  if session_file ~= filename then M.save(session_file) end
  local f = loadfile(filename, 't', {})
  if not f or not io.close_all_buffers() then return end -- fail silently
  local session = f()
  local not_found = {}

  -- Unserialize cwd.
  if session.cwd then lfs.chdir(session.cwd) end

  -- Unserialize buffers.
  for _, buf in ipairs(session.buffers) do
    if lfs.attributes(buf.filename) then
      io.open_file(buf.filename)
      buffer:set_sel(buf.anchor, buf.current_pos)
      view.first_visible_line = buf.top_line
      for _, line in ipairs(buf.bookmarks) do
        buffer:marker_add(line, textadept.bookmarks.MARK_BOOKMARK)
      end
    elseif buf.filename:find('^%[.+%]$') then
      buffer.new()._type = buf.filename
      buffer:set_save_point()
      events.emit(events.FILE_OPENED, buf.filename) -- close initial buffer
    else
      not_found[#not_found + 1] = buf.filename
    end
  end

  -- Unserialize UI state.
  ui.maximized = session.ui.maximized
  if not ui.maximized then ui.size = session.ui.size end

  -- Unserialize views.
  local function unserialize_split(split)
    if type(split) ~= 'table' then
      view:goto_buffer(_BUFFERS[math.min(split, #_BUFFERS)])
    else
      for i, view in ipairs{view:split(split.vertical)} do
        view.size = split.size
        ui.goto_view(view)
        unserialize_split(split[i])
      end
    end
  end
  unserialize_split(session.views[1])
  ui.goto_view(_VIEWS[math.min(session.views.current, #_VIEWS)])

  -- Unserialize recent files.
  io.recent_files = session.recent_files

  -- Unserialize user data.
  events.emit(events.SESSION_LOAD, session)

  if #not_found > 0 then
    ui.dialogs.msgbox{
      title = _L['Session Files Not Found'],
      text = _L['The following session files were not found'],
      informative_text = table.concat(not_found, '\n'):iconv('UTF-8', _CHARSET),
      icon = 'dialog-warning', width = CURSES and ui.size[1] - 2 or nil
    }
  end
  session_file = filename
  return true
end
-- Load session when no args are present.
local function load_default_session() if M.save_on_quit then M.load(session_file) end end
events.connect(events.ARG_NONE, load_default_session)

-- Returns value *val* serialized as a string.
-- This is a very simple implementation suitable for session saving only.
-- Ignores function, userdata, and thread types, and does not handle circular tables.
local function _tostring(val)
  if type(val) == 'table' then
    local t = {}
    for k, v in pairs(val) do t[#t + 1] = string.format('[%s]=%s,', _tostring(k), _tostring(v)) end
    return string.format('{%s}', table.concat(t))
  elseif type(val) == 'function' or type(val) == 'userdata' or type(val) == 'thread' then
    val = nil
  end
  return type(val) == 'string' and string.format('%q', val) or tostring(val)
end

---
-- Saves the session to file *filename* or the user-selected file.
-- Saves split views, opened buffers, cursor information, recent files, and bookmarks.
-- Upon quitting, the current session is saved to *filename* again, unless
-- `textadept.session.save_on_quit` is `false`.
-- @param filename Optional absolute path to the session file to save. If `nil`, the user is
--   prompted for one.
-- @usage textadept.session.save(filename)
-- @name save
function M.save(filename)
  local dir, name = session_file:match('^(.-[/\\]?)([^/\\]+)$')
  if not assert_type(filename, 'string/nil', 1) then
    filename = ui.dialogs.filesave{
      title = _L['Save Session'], with_directory = dir, with_file = name
    }
    if not filename then return end
  end
  local session = {}

  -- Serialize user data.
  events.emit(events.SESSION_SAVE, session)

  -- Serialize cwd.
  session.cwd = lfs.currentdir()

  -- Serialize buffers.
  session.buffers = {}
  for _, buffer in ipairs(_BUFFERS) do
    if not buffer.filename and not buffer._type then goto continue end
    local current = buffer == view.buffer
    session.buffers[#session.buffers + 1] = {
      filename = buffer.filename or buffer._type,
      anchor = current and buffer.anchor or buffer._anchor or 1,
      current_pos = current and buffer.current_pos or buffer._current_pos or 1,
      top_line = current and view.first_visible_line or buffer._top_line or 1
    }
    local bookmarks = {}
    local BOOKMARK_BIT = 1 << textadept.bookmarks.MARK_BOOKMARK - 1
    local line = buffer:marker_next(1, BOOKMARK_BIT)
    while line ~= -1 do
      bookmarks[#bookmarks + 1] = line
      line = buffer:marker_next(line + 1, BOOKMARK_BIT)
    end
    session.buffers[#session.buffers].bookmarks = bookmarks
    ::continue::
  end

  -- Serialize UI state.
  session.ui = {maximized = ui.maximized, size = ui.size}

  -- Serialize views.
  local function serialize_split(split)
    return split.buffer and _BUFFERS[split.buffer] or {
      serialize_split(split[1]), serialize_split(split[2]), vertical = split.vertical,
      size = split.size
    }
  end
  session.views = {serialize_split(ui.get_split_table()), current = _VIEWS[view]}

  -- Serialize recent files.
  session.recent_files = io.recent_files

  -- Write the session.
  assert(io.open(filename, 'wb')):write('return ', _tostring(session)):close()
  session_file = filename
end
-- Saves session on quit.
events.connect(events.QUIT, function() if M.save_on_quit then M.save(session_file) end end, 1)

-- Does not save session on quit.
args.register('-n', '--nosession', 0, function() M.save_on_quit = false end,
  'No session functionality')
-- Loads the given session on startup.
args.register('-s', '--session', 1, function(name)
  if not lfs.attributes(name) then name = string.format('%s/%s', _USERHOME, name) end
  M.load(name)
  events.disconnect(events.ARG_NONE, load_default_session)
end, 'Load session')

return M