-- Copyright 2007-2019 Mitchell mitchell.att.foicica.com. 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 max_recent_files (number) -- The maximum number of recent files to save in session files. -- Recent files are stored in [`io.recent_files`](). -- The default value is `10`. module('textadept.session')]] M.save_on_quit = true M.max_recent_files = 10 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; `false` otherwise. -- @usage textadept.session.load(filename) -- @name load function M.load(filename) local dir, name = session_file:match('^(.-[/\\]?)([^/\\]+)$') filename = filename or ui.dialogs.fileselect{ title = _L['Load Session'], with_directory = dir, with_file = name } if not filename then return end local not_found = {} local f = io.open(filename, 'rb') if not f or not io.close_all_buffers() then return false end io.recent_files = {} local current_view, splits = view, {[0] = {}} for line in f:lines() do if line:find('^buffer:') then local patt = '^buffer: (%d+) (%d+) (%d+) (.+)$' local anchor, current_pos, top_line, filename = line:match(patt) if not filename:find('^%[.+%]$') then if lfs.attributes(filename) then io.open_file(filename) else not_found[#not_found + 1] = filename end else buffer.new()._type = filename buffer:set_save_point() events.emit(events.FILE_OPENED, filename) -- close initial untitled buf end -- Restore saved buffer selection and view. buffer:set_sel(tonumber(anchor), tonumber(current_pos)) buffer:line_scroll(0, buffer:visible_from_doc_line(tonumber(top_line)) - buffer.first_visible_line) elseif line:find('^bookmarks:') then local lines = line:match('^bookmarks: (.*)$') for line in lines:gmatch('%d+') do buffer:marker_add(tonumber(line), textadept.bookmarks.MARK_BOOKMARK) end elseif line:find('^size:') then local maximized, width, height = line:match('^size: (%l+) (%d+) (%d+)$') ui.maximized = maximized == 'true' if not ui.maximized then ui.size = {width, height} end elseif line:find('^%s*split%d:') then local level, num, type, size = line:match('^(%s*)split(%d): (%S+) (%d+)') local view = splits[#level] and splits[#level][tonumber(num)] or view ui.goto_view(view) splits[#level + 1] = {view:split(type == 'true')} splits[#level + 1][1].size = tonumber(size) -- could be 1 or 2 elseif line:find('^%s*view%d:') then local level, num, buf_idx = line:match('^(%s*)view(%d): (%d+)$') local view = splits[#level][tonumber(num)] or view buf_idx = tonumber(buf_idx) if buf_idx > #_BUFFERS then buf_idx = #_BUFFERS end view:goto_buffer(_BUFFERS[buf_idx]) elseif line:find('^current_view:') then current_view = _VIEWS[tonumber(line:match('^current_view: (%d+)')) or 1] elseif line:find('^recent:') then -- If a recent file is already open, do not add it to the list again. local recent_file, exists = line:match('^recent: (.+)$'), false for i = 1, #io.recent_files do if io.recent_files[i] == recent_file then exists = true break end end if not exists then io.recent_files[#io.recent_files + 1] = recent_file end end end f:close() ui.goto_view(current_view) 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 = 'gtk-dialog-warning' } 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) --- -- Saves the session to file *filename* or the user-selected file. -- Saves split views, opened buffers, cursor information, recent files, and -- bookmarks. -- @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('^(.-[/\\]?)([^/\\]+)$') filename = filename or ui.dialogs.filesave{ title = _L['Save Session'], with_directory = dir, with_file = name:iconv('UTF-8', _CHARSET) } if not filename then return end local session = {} local buffer_line = 'buffer: %d %d %d %s' -- anchor, cursor, line, filename local split_line = '%ssplit%d: %s %d' -- level, number, type, size local view_line = '%sview%d: %d' -- level, number, doc index -- Write out opened buffers. for i = 1, #_BUFFERS do local buffer = _BUFFERS[i] local filename = buffer.filename or buffer._type if filename then local current = buffer == view.buffer local anchor = current and 'anchor' or '_anchor' local current_pos = current and 'current_pos' or '_current_pos' local top_line = current and 'first_visible_line' or '_top_line' session[#session + 1] = buffer_line:format(buffer[anchor] or 0, buffer[current_pos] or 0, buffer[top_line] or 0, filename) -- Write out bookmarks. local lines = {} local line = buffer:marker_next(0, 1 << textadept.bookmarks.MARK_BOOKMARK) while line >= 0 do lines[#lines + 1] = line line = buffer:marker_next(line + 1, 1 << textadept.bookmarks.MARK_BOOKMARK) end session[#session + 1] = 'bookmarks: '..table.concat(lines, ' ') end end -- Write out window size. Do this before writing split views since split view -- size depends on the window size. local maximized, size = tostring(ui.maximized), ui.size session[#session + 1] = string.format('size: %s %d %d', maximized, size[1], size[2]) -- Write out split views. local function write_split(split, level, number) local c1, c2 = split[1], split[2] local vertical, size = tostring(split.vertical), split.size local spaces = string.rep(' ', level) session[#session + 1] = split_line:format(spaces, number, vertical, size) spaces = string.rep(' ', level + 1) if c1[1] and c1[2] then write_split(c1, level + 1, 1) else session[#session + 1] = view_line:format(spaces, 1, _BUFFERS[c1.buffer]) end if c2[1] and c2[2] then write_split(c2, level + 1, 2) else session[#session + 1] = view_line:format(spaces, 2, _BUFFERS[c2.buffer]) end end local splits = ui.get_split_table() if splits[1] and splits[2] then write_split(splits, 0, 0) else session[#session + 1] = view_line:format('', 1, _BUFFERS[splits.buffer]) end -- Write out the current focused view. session[#session + 1] = string.format('current_view: %d', _VIEWS[view]) -- Write out other things. for i = 1, #io.recent_files do if i > M.max_recent_files then break end session[#session + 1] = string.format('recent: %s', io.recent_files[i]) end -- Write the session. local f = io.open(filename, 'wb') if f then f:write(table.concat(session, '\n')):close() end 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 = _USERHOME..'/'..name end M.load(name) events.disconnect(events.ARG_NONE, load_default_session) end, 'Load session') return M