SciTE/scite-debug/extman.lua
-- Extman is a Lua script manager for SciTE. It enables multiple scripts to capture standard events
-- without interfering with each other. For instance, scite_OnDoubleClick() will register handlers
-- for scripts that need to know when a double-click event has happened. (To know whether it
-- was in the output or editor pane, just test editor.Focus).  It provides a useful function scite_Command
-- which allows you to define new commands without messing around with property files (see the
-- examples in the scite_lua directory.)
-- extman defines three new convenience handlers as well:
--scite_OnWord (called when user has entered a word)
--scite_OnEditorLine (called when a line is entered into the editor)
--scite_OnOutputLine (called when a line is entered into the output pane)

-- this is an opportunity for you to make regular Lua packages available to SciTE
--~ package.path = package.path..';C:\\lang\\lua\\lua\\?.lua'
--~ package.cpath = package.cpath..';c:\\lang\\lua\\?.dll'

-- useful function for getting a property, or a default if not present.
function scite_GetProp(key,default)
   local val = props[key]
   if val and val ~= '' then return val
   else return default end
end

function scite_GetPropBool(key,default)
    local res = scite_GetProp(key,default)
    if not res or res == '0' or res == 'false' then return false
    else return true
    end
end

local GTK = scite_GetProp('PLAT_GTK')

local _MarginClick,_DoubleClick,_SavePointLeft = {},{},{}
local _SavePointReached,_Open,_SwitchFile = {},{},{}
local _BeforeSave,_Save,_Char = {},{},{}
local _Word,_LineEd,_LineOut = {},{},{}
local _OpenSwitch = {}
local _UpdateUI = {}
local _UserListSelection
-- new with 1.74!
local _Key = {}
local _DwellStart = {}
local _Close = {}
-- new
local _remove = {}
local append = table.insert
local find = string.find
local size = table.getn
local sub = string.sub
local gsub = string.gsub


-- file must be quoted if it contains spaces!
function quote_if_needed(target)
    local quote = '"'
    if find(target,'%s') and sub(target,1,1) ~= quote then
        target = quote..target..quote
    end
    return target
end

function OnUserListSelection(tp,str)
  if _UserListSelection then
     local callback = _UserListSelection
     _UserListSelection = nil
     return callback(str)
  else return false end
end

local function DispatchOne(handlers,arg)
  for i,handler in pairs(handlers) do
    local fn = handler
    if _remove[fn] then
        handlers[i] = nil
       _remove[fn] = nil
    end
    local ret = fn(arg)
    if ret then return ret end
  end
  return false
end

local function Dispatch4(handlers,arg1,arg2,arg3,arg4)
    for i,handler in pairs(handlers) do
        local fn = handler
        if _remove[fn] then
            handlers[i] = nil
            _remove[fn] = nil
        end
        local ret = fn(arg1,arg2,arg3,arg4)
        if ret then return ret end
    end
    return false
end

DoDispatchOne = DispatchOne -- export this!

-- these are the standard SciTE Lua callbacks  - we use them to call installed extman handlers!
function OnMarginClick()
  return DispatchOne(_MarginClick)
end

function OnDoubleClick()
  return DispatchOne(_DoubleClick)
end

function OnSavePointLeft()
  return DispatchOne(_SavePointLeft)
end

function OnSavePointReached()
  return DispatchOne(_SavePointReached)
end

function OnChar(ch)
  return DispatchOne(_Char,ch)
end

function OnSave(file)
  return DispatchOne(_Save,file)
end

function OnBeforeSave(file)
  return DispatchOne(_BeforeSave,file)
end

function OnSwitchFile(file)
  return DispatchOne(_SwitchFile,file)
end

function OnOpen(file)
  return DispatchOne(_Open,file)
end

function OnUpdateUI()
  if editor.Focus then
    return DispatchOne(_UpdateUI)
  else
    return false
  end
end

-- new with 1.74
function OnKey(key,shift,ctrl,alt)
    return Dispatch4(_Key,key,shift,ctrl,alt)
end

function OnDwellStart(pos,s)
    return Dispatch4(_DwellStart,pos,s)
end

function OnClose()
    return DispatchOne(_Close)
end

-- may optionally ask that this handler be immediately
-- removed after it's called
local function append_unique(tbl,fn,rem)
  local once_only
  if type(fn) == 'string' then
     once_only = fn == 'once'
     fn = rem
     rem = nil
     if once_only then
        _remove[fn] = fn
    end
  else
    _remove[fn] = nil
  end
  local idx
  for i,handler in pairs(tbl) do
     if handler == fn then idx = i; break end
  end
  if idx then
    if rem then
      table.remove(tbl,idx)
    end
  else
    if not rem then
      append(tbl,fn)
    end
  end
end
ex_append_unique = append_unique

-- this is how you register your own handlers with extman
function scite_OnMarginClick(fn,rem)
  append_unique(_MarginClick,fn,rem)
end

function scite_OnDoubleClick(fn,rem)
  append_unique(_DoubleClick,fn,rem)
end

function scite_OnSavePointLeft(fn,rem)
  append_unique(_SavePointLeft,fn,rem)
end

function scite_OnSavePointReached(fn,rem)
  append_unique(_SavePointReached,fn,rem)
end

function scite_OnOpen(fn,rem)
  append_unique(_Open,fn,rem)
end

function scite_OnSwitchFile(fn,rem)
  append_unique(_SwitchFile,fn,rem)
end

function scite_OnBeforeSave(fn,rem)
  append_unique(_BeforeSave,fn,rem)
end

function scite_OnSave(fn,rem)
  append_unique(_Save,fn,rem)
end

function scite_OnUpdateUI(fn,rem)
  append_unique(_UpdateUI,fn,rem)
end

function scite_OnChar(fn,rem)
  append_unique(_Char,fn,rem)
end

function scite_OnOpenSwitch(fn,rem)
  append_unique(_OpenSwitch,fn,rem)
end

--new 1.74
function scite_OnKey(fn,rem)
    append_unique(_Key,fn,rem)
end

function scite_OnDwellStart(fn,rem)
    append_unique(_DwellStart,fn,rem)
end

function scite_OnClose(fn,rem)
    append_unique(_Close,fn,rem)
end

local function buffer_switch(f)
--- OnOpen() is also called if we move to a new folder
   if not find(f,'[\\/]$') then
      DispatchOne(_OpenSwitch,f)
   end
end

scite_OnOpen(buffer_switch)
scite_OnSwitchFile(buffer_switch)

local next_user_id = 13 -- arbitrary

-- the handler is always reset!
function scite_UserListShow(list,start,fn)
  local separators = {' ', ';', '@', '?', '~', ':'}
  local separator
  local s = table.concat(list)
  for i, sep in ipairs(separators) do
    if not string.find(s, sep, 1, true) then
      s = table.concat(list, sep, start)
      separator = sep
      break
    end
  end
  -- we could not find a good separator, set it arbitrarily
  if not separator then
    separator = '@'
    s = table.concat(list, separator, start)
  end
  _UserListSelection = fn
  local pane = editor
  if not pane.Focus then pane = output end
  pane.AutoCSeparator = string.byte(separator)
  pane:UserListShow(next_user_id,s)
  pane.AutoCSeparator = string.byte(' ')
  return true
end

 local word_start,in_word,current_word
-- (Nicolas) this is in Ascii as SciTE always passes chars in this "encoding" to OnChar
local wordchars = '[A-Za-z--]'  -- wuz %w

 local function on_word_char(s)
     if not in_word then
        if find(s,wordchars) then
      -- we have hit a word!
         word_start = editor.CurrentPos
         in_word = true
         current_word = s
      end
    else -- we're in a word
   -- and it's another word character, so collect
     if find(s,wordchars) then
       current_word = current_word..s
     else
       -- leaving a word; call the handler
       local word_end = editor.CurrentPos
       DispatchOne(_Word, {word=current_word,
               startp=word_start,endp=editor.CurrentPos,
               ch = s
            })
       in_word = false
     end
    end
  -- don't interfere with usual processing!
    return false
  end

function scite_OnWord(fn,rem)
  append_unique(_Word,fn,rem)
  if not rem then
     scite_OnChar(on_word_char)
  else
     scite_OnChar(on_word_char,'remove')
  end
end

local last_pos = 0

function get_line(pane,lineno)
    if not pane then pane = editor end
    if not lineno then
        local line_pos = pane.CurrentPos
        lineno = pane:LineFromPosition(line_pos)-1
    end
    -- strip linefeeds (Windows is a special case as usual!)
    local endl = 2
    if pane.EOLMode == 0 then endl = 3 end
    local line = pane:GetLine(lineno)
    if not line then return nil end
    return string.sub(line,1,-endl)
end

-- export this useful function...
scite_Line = get_line

local function on_line_char(ch,was_output)
    if ch == '\n' then
        local in_editor = editor.Focus
        if in_editor and not was_output then
            DispatchOne(_LineEd,get_line(editor))
            return false -- DO NOT interfere with any editor processing!
        elseif not in_editor and was_output then
            DispatchOne(_LineOut,get_line(output))
            return true -- prevent SciTE from trying to evaluate the line
        end
    end
    return false
end

local function on_line_editor_char(ch)
  return on_line_char(ch,false)
end

local function on_line_output_char(ch)
  return on_line_char(ch,true)
end

local function set_line_handler(fn,rem,handler,on_char)
  append_unique(handler,fn,rem)
  if not rem then
    scite_OnChar(on_char)
  else
    scite_OnChar(on_char,'remove')
  end
end

function scite_OnEditorLine(fn,rem)
  set_line_handler(fn,rem,_LineEd,on_line_editor_char)
end

-- with this scheme, there is a primary handler, and secondary prompt handlers
-- can temporarily take charge of input. There is only one prompt in charge
-- at any particular time, however.
local primary_handler

function scite_OnOutputLine(fn,rem)
    if not rem then
        if not primary_handler then primary_handler = fn end
    end
    _LineOut = {}
    set_line_handler(fn,rem,_LineOut,on_line_output_char)
    if rem and fn ~= primary_handler then
        set_line_handler(primary_handler,false,_LineOut,on_line_output_char)
    end
end

local path_pattern
local tempfile
local dirsep

if GTK then
    tempfile = '/tmp/.scite-temp-files'
    path_pattern = '(.*)/[^%./]+%.%w+$'
    dirsep = '/'
else
    tempfile = '\\scite_temp1'
    path_pattern = '(.*)\\[^%.\\]+%.%w+$'
    dirsep = '\\'
end

function path_of(s)
    local _,_,res = find(s,path_pattern)
    if _ then return res else return s end
end

local extman_path = path_of(props['ext.lua.startup.script'])
local lua_path = scite_GetProp('ext.lua.directory',extman_path..dirsep..'scite_lua')

function extman_Path()
    return extman_path
end

-- this version of scite-gdb uses the new spawner extension library.
local fn,err,spawner_path
if package then loadlib = package.loadlib end
-- by default, the spawner lib sits next to extman.lua
spawner_path = scite_GetProp('spawner.extension.path',extman_path)
if GTK then
    fn,err = loadlib(spawner_path..'/unix-spawner-ex.so','luaopen_spawner')
else
    fn,err = loadlib(spawner_path..'\\spawner-ex.dll','luaopen_spawner')
end
if fn then
    fn() -- register spawner
else
    print('cannot load spawner '..err)
end

-- a general popen function that uses the spawner library if found; otherwise falls back
-- on os.execute
function scite_Popen(cmd)
    if spawner then
        return spawner.popen(cmd)
    else
        cmd = cmd..' > '..tempfile
        if  GTK then -- io.popen is dodgy; don't use it!
            os.execute(cmd)
        else
            if Execute then -- scite_other was found!
                Execute(cmd)
            else
                os.execute(cmd)
            end
       end
       return io.open(tempfile)
    end
end

function dirmask(mask,isdir)
    local attrib = ''
    if isdir then
        if not GTK then
            attrib = ' /A:D '
        else
            attrib = ' -F '
        end
    end
    if not GTK then
        mask = gsub(mask,'/','\\')
        return 'dir /b '..attrib..quote_if_needed(mask)
    else
        return 'ls -1 '..attrib..quote_if_needed(mask)
    end
end

-- p = globtopattern(g)
--
-- Converts glob string (g) into Lua pattern string (p).
-- Always succeeds.
--
-- Warning: could be better tested.
--
-- (c) 2008 D.Manura, Licensed under the same terms as Lua (MIT License).
local function globtopattern(g)
  -- Some useful references:
  -- - apr_fnmatch in Apache APR.  For example,
  --   http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html
  --   which cites POSIX 1003.2-1992, section B.6.

  local p = "^"  -- pattern being built
  local i = 0    -- index in g
  local c        -- char at index i in g.

  -- unescape glob char
  local function unescape()
    if c == '\\' then
      i = i + 1; c = g:sub(i,i)
      if c == '' then
        p = '[^]'
        return false
      end
    end
    return true
  end

  -- escape pattern char
  local function escape(c)
    return c:match("^%w$") and c or '%' .. c
  end

  -- Convert tokens at end of charset.
  local function charset_end()
    while 1 do
      if c == '' then
        p = '[^]'
        break
      elseif c == ']' then
        p = p .. ']'
        break
      else
        if not unescape() then break end
        local c1 = c
        i = i + 1; c = g:sub(i,i)
        if c == '' then
          p = '[^]'
          break
        elseif c == '-' then
          i = i + 1; c = g:sub(i,i)
          if c == '' then
            p = '[^]'
            break
          elseif c == ']' then
            p = p .. escape(c1) .. '%-]'
            break
          else
            if not unescape() then break end
            p = p .. escape(c1) .. '-' .. escape(c)
          end
        elseif c == ']' then
          p = p .. escape(c1) .. ']'
          break
        else
          p = p .. escape(c1)
          i = i - 1 -- put back
        end
      end
      i = i + 1; c = g:sub(i,i)
    end
  end

  -- Convert tokens in charset.
  local function charset()
    p = p .. '['
    i = i + 1; c = g:sub(i,i)
    if c == '' or c == ']' then
      p = p .. '[^]'
    elseif c == '^' or c == '!' then
      p = p .. '^'
      i = i + 1; c = g:sub(i,i)
      if c == ']' then
        -- ignored
      else
        charset_end()
      end
    else
      charset_end()
    end
  end

  -- Convert tokens.
  while 1 do
    i = i + 1; c = g:sub(i,i)
    if c == '' then
      p = p .. '$'
      break
    elseif c == '?' then
      p = p .. '.'
    elseif c == '*' then
      p = p .. '.*'
    elseif c == '[' then
      charset()
    elseif c == '\\' then
      i = i + 1; c = g:sub(i,i)
      if c == '' then
        p = p .. '\\$'
        break
      end
      p = p .. escape(c)
    else
      p = p .. escape(c)
    end
  end
  return p
end

-- grab all files matching @mask, which is assumed to be a path with a wildcard.
-- 2008-06-27 Now uses David Manura's globtopattern(), which is not fooled by cases
-- like test.lua and test.lua~ !
function scite_Files(mask)
    local f,path,pat,cmd,_
    if not GTK then
        cmd = dirmask(mask)
        path = mask:match('(.*\\)')  or '.\\'
        local file = mask:match('([^\\]*)$')
        pat = globtopattern(file)
    else
        cmd = 'ls -1 '..mask
        path = ''
    end
    f = scite_Popen(cmd)
    local files = {}
    if not f then return files end

    for line in f:lines() do
        if not pat or line:match(pat) then
            append(files,path..line)
        end
    end
    f:close()
    return files
end

-- grab all directories in @path, excluding anything that matches @exclude_path
-- As a special exception, will also any directory called 'examples' ;)
function scite_Directories(path,exclude_pat)
    local cmd
    --print(path)
    if not GTK then
        cmd = dirmask(path..'\\*.',true)
    else
        cmd = dirmask(path,true)
    end
    path = path..dirsep
    local f = scite_Popen(cmd)
    local files = {}
    if not f then return files end
    for line in f:lines() do
--        print(line)
        if GTK then
            if line:sub(-1,-1) == dirsep then
                line = line:sub(1,-2)
            else
                line = nil
            end
        end
        if line and not line:find(exclude_pat) and line ~= 'examples' then
            append(files,path..line)
        end
    end
    f:close()
    return files
end

function scite_FileExists(f)
  local f = io.open(f)
  if not f then return false
  else
    f:close()
    return true
  end
end

function scite_CurrentFile()
    return props['FilePath']
end

-- (Nicolas)
if GTK then
    function scite_DirectoryExists(path)
        return os.execute('test -d "'..path..'"') == 0
    end
else
    -- what is the Win32 equivalent??
    function scite_DirectoryExists(path)
        return true
    end
end

function split(s,delim)
    res = {}
    while true do
        p = find(s,delim)
        if not p then
            append(res,s)
            return res
        end
        append(res,sub(s,1,p-1))
        s = sub(s,p+1)
    end
end

function splitv(s,delim)
    return unpack(split(s,delim))
end

local idx = 10
local shortcuts_used = {}
local alt_letter_map = {}
local alt_letter_map_init = false
local name_id_map = {}

local function set_command(name,cmd,mode)
     local _,_,pattern,md = find(mode,'(.+){(.+)}')
     if not _ then
        pattern = mode
        md = 'savebefore:no'
     end
     local which = '.'..idx..pattern
     props['command.name'..which] = name
     props['command'..which] = cmd
     props['command.subsystem'..which] = '3'
     props['command.mode'..which] = md
     name_id_map[name] = 1100+idx
     return which
end

local function check_gtk_alt_shortcut(shortcut,name)
   -- Alt+<letter> shortcuts don't work for GTK, so handle them directly...
   local _,_,letter = shortcut:find('Alt%+([A-Z])$')
   if _ then
        alt_letter_map[letter:lower()] = name
        if not alt_letter_map_init then
            alt_letter_map_init = true
            scite_OnKey(function(key,shift,ctrl,alt)
                if alt and key < 255 then
                    local ch = string.char(key)
                    if alt_letter_map[ch] then
                        scite_MenuCommand(alt_letter_map[ch])
                    end
                end
            end)
        end
    end
end

local function set_shortcut(shortcut,name,which)
    if shortcut == 'Context' then
        local usr = 'user.context.menu'
        if props[usr] == '' then -- force a separator
            props[usr] = '|'
        end
        props[usr] = props[usr]..'|'..name..'|'..(1100+idx)..'|'
    else
       local cmd = shortcuts_used[shortcut]
       if cmd then
            print('Error: shortcut already used in "'..cmd..'"')
       else
           shortcuts_used[shortcut] = name
           if GTK then check_gtk_alt_shortcut(shortcut,name) end
           props['command.shortcut'..which] = shortcut
       end
     end
end

-- allows you to bind given Lua functions to shortcut keys
-- without messing around in the properties files!
-- Either a string or a table of strings; the string format is either
--      menu text|Lua command|shortcut
-- or
--      menu text|Lua command|mode|shortcut
-- where 'mode' is the file extension which this command applies to,
-- e.g. 'lua' or 'c', optionally followed by {mode specifier}, where 'mode specifier'
-- is the same as documented under 'command.mode'
-- 'shortcut' can be a usual SciTE key specifier, like 'Alt+R' or 'Ctrl+Shift+F1',
-- _or_ it can be 'Context', meaning that the menu item should also be added
-- to the right-hand click context menu.
function scite_Command(tbl)
  if type(tbl) == 'string' then
     tbl = {tbl}
  end
  for i,v in pairs(tbl) do
     local name,cmd,mode,shortcut = splitv(v,'|')
     if not shortcut then
        shortcut = mode
        mode = '.*'
     else
        mode = '.'..mode
     end
     -- has this command been defined before?
     local old_idx = 0
     for ii = 10,idx do
        if props['command.name.'..ii..mode] == name then old_idx = ii end
     end
     if old_idx == 0 then
        local which = set_command(name,cmd,mode)
         if shortcut then
            set_shortcut(shortcut,name,which)
        end
        idx = idx + 1
    end
  end
end

-- use this to launch Lua Tool menu commands directly by name
-- (commands are not guaranteed to work properly if you just call the Lua function)
function scite_MenuCommand(cmd)
    if type(cmd) == 'string' then
        cmd = name_id_map[cmd]
        if not cmd then return end
    end
    scite.MenuCommand(cmd)
end

local loaded = {}
local current_filepath

-- this will quietly fail....
local function silent_dofile(f)
    if scite_FileExists(f) then
        if not loaded[f] then
            dofile(f)
            loaded[f] = true
        end
        return true
    end
    return false
end

function scite_dofile(f)
    f = extman_path..'/'..f
    silent_dofile(f)
end

function scite_require(f)
    local path = lua_path..dirsep..f
    if not silent_dofile(path) then
        silent_dofile(current_filepath..dirsep..f)
    end
end

if not GTK then
    scite_dofile 'scite_other.lua'
end

if not scite_DirectoryExists(lua_path) then
    print('Error: directory '..lua_path..' not found')
    return
end

function load_script_list(script_list,path)
    if not script_list then
      print('Error: no files found in '..path)
    else
      current_filepath = path
      for i,file in pairs(script_list) do
        silent_dofile(file)
      end
    end
end

-- Load all scripts in the lua_path (usually 'scite_lua'), including within any subdirectories
-- that aren't 'examples' or begin with a '_'
local script_list = scite_Files(lua_path..dirsep..'*.lua')
load_script_list(script_list,lua_path)
local dirs = scite_Directories(lua_path,'^_')
for i,dir in ipairs(dirs) do
    load_script_list(scite_Files(dir..dirsep..'*.lua'),dir)
end

function scite_WordAtPos(pos)
    if not pos then pos = editor.CurrentPos end
    local p2 = editor:WordEndPosition(pos,true)
    local p1 = editor:WordStartPosition(pos,true)
    if p2 > p1 then
        return editor:textrange(p1,p2)
    end
end

function scite_GetSelOrWord()
    local s = editor:GetSelText()
    if s == '' then
        return scite_WordAtPos()
    else
        return s
    end
end

--~ scite_Command 'Reload Script|reload_script|Shift+Ctrl+R'

--~ function reload_script()
--~    current_file = scite_CurrentFile()
--~    print('Reloading... '..current_file)
--~    loaded[current_file] = false
--~    silent_dofile(current_file)
--~ end

--~ require"remdebug.engine"
--~ remdebug.engine.start()

