Making textadept work with relative file references.

From: uten navn <>
Date: Mon, 28 Mar 2011 12:14:56 +0200

I wanted to make the session information relative to the the location of
textadept. Now I can easely switch between textadept versions and also keep
multiple binaries (linux, windows) in the same location having them pick upp
the same textadept/lua files. And restore my session independently of the
platform I was working on last. I synchronize my working area between
several machines with wuala.

To make this happen I have added the following functions stringSplit,
pathSplit, pathRelative, pathBackTrack. On my system I have those functions
in a file _USERHOME/modules/pathutils.lua. I'm not sure where they belongs
in textadept.

I have also modified the load and save functions in session.lua. The usage
of a variable named filename in both the function call and locally inside
the function with different meaning gave me some grief (head spinning) so I
have changed the call argument name to filenameSession. And I have added
code to make the filenames in the session file relative to textadept _HOME.

I have used this file-structure to test this:

I initiate my selected textadept version like this:
cd [somepath]/textadept
./app_5.7_beta/textadept -u ../data/
Or on windows (XP personal) ( win+r )
c:\my-apps\textadept\app_3.7_beta\textadept.exe -u ../data/

1: Modify ./core/args.lua
-- If userhome is given as a relative path (like -u ../data/) we need to add
the full path at line 82. lfs does not understand paths like this ../data/
userhome = arg[i + 1]
if not lfs.attributes(userhome) then userhome = _G._HOME .. '/' .. userhome

-- I have added userhome .. "/core/?.lua to the front of package.path. I
have done this to be able to fiddle with core modules in my own path.
package.path = userhome .. "/core/?.lua;" .. package.path

2: Modify ./modules/textadept/session.lua
I have saved my modified version in _USERHOME/modules/textadept/session.lua

My _USERHOME/modules/pathutils.lua
--[[ I have placed these functions in the above referenced
  -- Splits a string according to a given split pattern.
  -- Compatibility: Lua-5.1
  -- SOURCE: function split(str, pat) in:
  -- TODO: StringSplit should have a common textadept/lua place to live
  -- @param str The string to be splitted
  -- @param pat The splitt pattern.
  -- @return array containing the splitted string without pat parts.
  -- @usage stringSplit('c:\\windows\\system32\\kernel32.dll', '[^\\/]+')
  function stringSplit(str, pat)
     local t = {} -- NOTE: use {n = 0} in Lua-5.0
     local fpat = "(.-)" .. pat
     local last_end = 1
     local s, e, cap = str:find(fpat, 1)
     while s do
        if s ~= 1 or cap ~= "" then
        last_end = e+1
        s, e, cap = str:find(fpat, last_end)
     if last_end <= #str then
        cap = str:sub(last_end)
        table.insert(t, cap)
     return t

  -- Split a path formated string into it''s folder/file parts
  -- TODO: pathSplit belongs in a utility module/file?
  -- @param path The path to be splitted in it's respective parts.
  -- @return array with the path parts.
  -- @usage parts = pathSplit('c:\\users\\guest/documents/myNotes.txt')
  function pathSplit(path)
    local pat = '[\\/]+'
    return stringSplit(path, pat)
  -- Compares the path parts of two paths and returns the common part and
the unique parts.
  -- TODO: pathRelative belongs in a utility module/file?
  -- @param path1 A path formated string
  -- @param path2 A path formated string
  -- @param sep Optional path sepperator. Defaults to the pattern '/'. The
returned paths use this seperator. If the returned result is to be passed on
to cmd.exe you need to use sep='\\'
  -- @return commonPath, path1r, path2r
  -- @usage commonPath, path1r, path2r =
assert(commonPath=='c:/users/guest/'); assert(
path1r=='/apps/textadeapt/3.7Beta/'); assert(
  function pathRelative(path1, path2, sep)
    sep = sep or "/"

    local a1 = pathSplit(path1)
    local a2 = pathSplit(path2)
    local i
    local c=0
    local maxn=0
    local pathCommon
    local path1r = ''
    local path2r = ''
    maxn = table.maxn(a1)
    if maxn > table.maxn(a2) then maxn = table.maxn(a2) end
    for i = 1, maxn do
      if a1[i] == a2[i] and c==0 then
        if pathCommon == nil then
          pathCommon = a1[i]
          pathCommon = pathCommon .. sep .. a1[i]
        if c==0 then c=i end
          path1r = path1r .. sep .. a1[i]
          path2r = path2r .. sep .. a2[i]

    if table.maxn(a1) > table.maxn(a2) then
      for i = table.maxn(a2)+1, table.maxn(a1) do
        path1r = path1r .. sep .. a1[i]
    elseif table.maxn(a1) < table.maxn(a2) then
      for i = table.maxn(a1)+1, table.maxn(a2) do
        path2r = path2r .. sep .. a2[i]
    return pathCommon, path1r, path2r
  -- @param path The path we want to walk/refrence backwards.
  -- @param backcmd The pattern used to walk/reference backwards. Defaults
to ..
  -- @return The command needed to walk/reference backwards
  -- @usage ret = pathBackTrack('/apps/textadept/3.7Beta/'); assert(ret ==
  -- TODO: pathBackTrack belongs in a utility module/file
  function pathBackTrack(path, backcmd)
    backcmd = backcmd or '%.%.'
    return string.gsub(path, '[^\\/]+', backcmd)


My modified _HOME/modules/textadept/session.lua

-- Copyright 2007-2011 Mitchell mitchell<att> See LICENSE.

require 'pathutils' -- On my system: _USERHOME/modules/pathutils.lua

local L = _G.locale.localize

-- Session support for the textadept module.
module('_m.textadept.session', package.seeall)
-- Markdown:
-- ## Settings
-- * `DEFAULT_SESSION`: The path to the default session file.
-- * `SAVE_ON_QUIT`: Save the session when quitting. Defaults to true and
can be
--   disabled by passing the command line switch '-n' or '--nosession' to
--   Textadept.
-- settings
-- end settings
-- Loads a Textadept session file.
-- Textadept restores split views, opened buffers, cursor information, and
-- project manager details.
-- @param filenameSession The absolute path to the session file to load.
Defaults to
--   DEFAULT_SESSION if not specified.
-- @return true if the session file was opened and read; false otherwise.
-- @usage _m.textadept.session.load(filename)
function load(filenameSession)
  local not_found = {}
  local f = or DEFAULT_SESSION, 'rb')
  if not f then
    return false
  local current_view, splits = 1, { [0] = {} }
  local lfs_attributes = lfs.attributes
  for line in f:lines() do
    if line:find('^buffer:') then
      local anchor, current_pos, first_visible_line, filename =
        line:match('^buffer: (%d+) (%d+) (%d+) (.+)$')
      filename2, foo = string.gsub(filename, "%[TEXTADEPTHOME%]", _G._HOME)
      if not filename2:find('^%[.+%]$') then
        -- message and error buffer?
        if lfs_attributes(filename2) then
          not_found[#not_found + 1] = filename2
        --any file
        buffer._type = filename2
        events.emit('file_opened', filename2)
      -- Restore saved buffer selection and view.
      local anchor = tonumber(anchor) or 0
      local current_pos = tonumber(current_pos) or 0
      local first_visible_line = tonumber(first_visible_line) or 0
      local buffer = buffer
      buffer._anchor, buffer._current_pos = anchor, current_pos
      buffer._first_visible_line = first_visible_line
      buffer:set_sel(anchor, current_pos)
    elseif line:find('^%s*split%d:') then
      -- Restore splitt's'
      local level, num, type, size =
        line:match('^(%s*)split(%d): (%S+) (%d+)')
      local view = splits[#level] and splits[#level][tonumber(num)] or 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
    elseif line:find('^current_view:') then
      -- Set current view/focus buffer
      local view_idx = line:match('^current_view: (%d+)')
      current_view = tonumber(view_idx) or 1
    elseif line:find('^size:') then
      --Set window size
      local width, height = line:match('^size: (%d+) (%d+)$')
      if width and height then gui.size = { width, height } end
  -- Close session file
  -- Update GUI
  if #not_found > 0 then
    -- Display error/notify msg.
               '--title', L('Session Files Not Found'),
               '--text', L('The following session files were not found'),
               string.format('%s', table.concat(not_found, '\n')))
  return true
  events.connect('arg_none', function() if SAVE_ON_QUIT then load() end end)
-- Saves a Textadept session to a file.
-- Saves split views, opened buffers, cursor information, and project
-- details.
-- @param filenameSession The absolute path to the session file to save.
Defaults to
--   either the current session file or DEFAULT_SESSION if not specified.
-- @usage
function save(filenameSession)
  local session = {}
  local buffer_line = "buffer: %d %d %d %s" -- anchor, cursor, line,
  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 _, buffer in ipairs(_BUFFERS) do
    local filename = buffer.filename or buffer._type
    --Filter out [MESSAGES] buffers
    if not filename:match('^%[.*%]$') then
      local current = buffer.doc_pointer == gui.focused_doc_pointer
      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
      -- Make a relative file reference
      local filename2
      local pathCommon, path1r, path2r
      pathCommon, path1r, path2r = pathRelative(_G._HOME, filename)
      if pathCommon == nil then
        -- can''t find common path so use original filename
        filename2 = filename
        filename2 = "[TEXTADEPTHOME]/" .. pathBackTrack(path1r) .. path2r
      -- Buld array with session info
      session[#session + 1] = buffer_line:format(buffer[anchor] or 0,
                                                 buffer[current_pos] or 0,
                                                 buffer[top_line] or 0,
  -- 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 = (' '):rep(level)
    session[#session + 1] = split_line:format(spaces, number, vertical,
    spaces = (' '):rep(level + 1)
    if type(c1) == 'table' then
      write_split(c1, level + 1, 1)
      session[#session + 1] = view_line:format(spaces, 1, c1)
    if type(c2) == 'table' then
      write_split(c2, level + 1, 2)
      session[#session + 1] = view_line:format(spaces, 2, c2)
  end -- local function write_split end
  local splits = gui.get_split_table()
  if type(splits) == 'table' then
    write_split(splits, 0, 0)
    session[#session + 1] = view_line:format('', 1, splits)
  -- Write out the current focused view.
  local current_view = view
  for i = 1, #_VIEWS do
    if _VIEWS[i] == current_view then
      current_view = i
  session[#session + 1] = ("current_view: %d"):format(current_view)
  -- Write out other things.
  local size = gui.size
  session[#session + 1] = ("size: %d %d"):format(size[1], size[2])
  -- Write the session.
  if f then
    f:write(table.concat(session, '\n'))
end --function save
  events.connect('quit', function() if SAVE_ON_QUIT then save() end end, 1)
  local function no_session() SAVE_ON_QUIT = false end
  args.register('-n', '--nosession', 0, no_session, 'No session
if not package.loaded['session'] then
  print("Run self test: session.lua")
  print("End self test: session.lua SUCCESS")
A typical session file looks like this
buffer: 3907 3907 109 [TEXTADEPTHOME]//../data/modules/textadept/session.lua
buffer: 306 306 0 [TEXTADEPTHOME]//../data/session
buffer: 395 395 0 [TEXTADEPTHOME]//../data/init.lua
buffer: 2288 2288 48 [TEXTADEPTHOME]//core/args.lua
buffer: 0 0 0 [TEXTADEPTHOME]//../app_3.7_beta_2_org/core/args.lua
buffer: 4789 4789 65
buffer: 2781 2781 78 [TEXTADEPTHOME]//../data/modules/pathutils.lua
buffer: 2013 2013 5 [TEXTADEPTHOME]//../data/modules/pathutils-test.lua
split0: true 705
 view1: 1
 split2: false 451
  view1: 8
  view2: 9
current_view: 1
size: 1280 949
Not sure if it matters but I have moved require 'args' as my first require
in _HOME/core/init.lua
I hope that was all
Best regards
Received on Mon 28 Mar 2011 - 06:14:56 EDT

This archive was generated by hypermail 2.2.0 : Thu 08 Mar 2012 - 12:03:48 EST