SciTE/scite-debug/scite_lua/debug/luagdb.lua
-- GDB-style interface to clidebug
scite_require 'gdb.lua'
local find = string.find
local match = string.match
local push = table.insert
local pop = table.remove
local esc = string.char(26)
local gprefix = esc..esc
--local ferr = io.stderr

local GTK = scite_GetProp('PLAT_GTK')

-- the convention with cli debug is that all Windows filenames are lowercase!
local function canonical(file)
	if not GTK then file = file:lower() end
	return file
end

local function fpath(file)
	return canonical(fullpath(file))
end

function slashify(s)
	return s:gsub('\\','\\\\')
end

local function file_is_lua (file)
    return extension_of(file) == 'lua'
end

local info_line_success = '^Line %d+ of'
local info_line_error = '^No line number information available'

function info_line_handler(line,dbg)
  local success = line:find(info_line_success)
  if success then -- we can set a break command
    --dbg:queue_command('tbreak *'..dbg.addr)
	spawner_command('tbreak *'..dbg.addr)
    dbg.addresses[dbg.addr] = true     
  end
  -- either way, we want to get out of GDB mode at the earliest opportunity!
  --dbg:queue_command 'continue' 
  spawner_command('continue')
end

LGdb = class(Gdb)

function LGdb.discriminator(target)
	local res = find(target,'^:gdb') == 1
	return res
end

function LGdb:init (root)
    Gdb.init(self,root)
    self.root = root
    self.target_dir = canonical(props['FileDir'])
    self.no_target_ext = false
    -- this is added to the package.path of the Lua program
    print(extman_Path())
    self.clidebug_path = scite_GetProp('clidebug.path',join(extman_Path(),'lua_clidebugger'))
	self.clidebug_debugger = canonical(join(self.clidebug_path,'debugger.lua'))
    
--    self.no_quit_confirm = true
	self.skip_system_extension = ".lua"	
    self.deferred_stack = {}
    
    self.postprocess_command['info line'] = {pattern=info_line_success,
        action=info_line_handler, alt_pat=info_line_error}
    
    self.addresses = {}
    self.mode = 'gdb'
    
    -- GDB likes forward slashes, on both platforms...
    local dbgl_file = join(self.clidebug_path,"dbgl.c"):gsub('\\','/')
    
    -- this is a persistent event handler which monitors every program break,
    -- and keeps track of whether we are in GDB or clidebug. Will raise
    -- the events 'gdb' and 'lua' accordingly.
    self:set_event('break',function(file,line)
        local new_mode
        local lf = file_is_lua(file)
        -- don't respond to any breaks in dbgl.c
        if not lf and file == dbgl_file then
            return true,true
        end
        if self.mode ~= 'lua' and lf then
            new_mode = 'lua'
        end
        if self.mode == 'lua' and not lf then
            new_mode = 'gdb'
        end
        if new_mode then
            self.mode = new_mode
            self:raise_event(new_mode)
        end
        return true
    end)
    
end

function LGdb:check_breakpoint (b)
    return not file_is_lua(b.file)
end

function LGdb:parameter_string ()
    local parms = Gdb.parameter_string(self)
    -- we have to modify the package path and cpath for this process so that
    -- the clidebug and dbgl packages are visible.
    local so = choose(GTK,'?.so;','?.dll;')
	local ppath = "'"..slashify(join(self.clidebug_path,'?.lua;')).."'"
    local cpath = "'"..slashify(join(self.clidebug_path,so)).."'"
    local p = 'package'
    local cmdline
    -- if the target isn't Lua, then we assume it's a program that hosts Lua and that
    --  there's an explicit clidebug initialization somewhere in a user Lua script.
    if self.not_lua then
        cmdline = ''    
    else
        cmdline = ('-e "%s.path=%s..%s.path; %s.cpath=%s..%s.cpath; GDB=true; WIN=%s" -lclidebug %s'):format(
			p,ppath,p,p,cpath,p,choose(GTK,'false','true'),self.lua_target)        
    end
	print('*',cmdline)
	return cmdline..' '..parms
end

function LGdb:command_line(target)
    local gtarget,ltarget = target:match('^:gdb;([^;]+);(.*)')
    self.lua_target = ltarget
	print('+',gtarget,ltarget)
    local idx = gtarget:find('%[h%]$')
    if idx then
        gtarget = gtarget:sub(1,idx-1)
        self.not_lua = true
    end
    --- we are going to embed a clidebug session inside a GDB session, so it's
    --- necessary to explicitly create the clidebug.cmd file. This folows the 
    --- sequence in create_existing_breakpoints() in debugger.lua
    local lua_cmd = join(self.root,'clidebug.cmd')
    local out = io.open(lua_cmd,'w')
	for b in Breakpoints() do
        if file_is_lua(b.file) then
            out:write('break '..canonical(b.file)..':'..b.line..'\n')
        end
	end	
	out:write('rootpath '..self.target_dir..'\n')
    if not self.not_lua then
        out:write('run\n')    
    end
    out:close()    
    return  Gdb.command_line(self,gtarget)
end

-- need to put at least one breakpoint into the system, so that we can drop into
-- gdb mode when necessary.  Under Windows, you definitely do not want a separate
-- console window, since we want to capture the result of running clidebug inside GDB.
function LGdb:special_debugger_setup(out)
    Gdb.special_debugger_setup(self,out)
	if not GTK then
		out:write('set new-console off\n')
	end
	-- a useful command when in C Lua code.
    out:write[[
define lstack
    p debug_lua_stack($arg0)
end
    ]]
    out:write('directory ',self.clidebug_path,'\n')
    -- clidebug will use this break to get us into gdb
    out:write('break dbgl.c:9\n')    
end

function LGdb:set_breakpoint(file,lno)
    local lf = file_is_lua(file)
    -- clidebug works best with absolute paths
    if lf then file = fullpath(file) end
    -- if we are in the wrong mode, then the actual setting of a breakpoint
    -- needs to happen when we next switch to the correct mode.
    if (self.mode == 'lua') ~= lf then
        local function set_break ()
            Gdb.set_breakpoint(self,file,lno)
        end        
        if self.mode == 'lua' then
            spawner_command('debugbreak')
            self:set_event('break',set_break)
            self:queue_command 'continue'
        else
            self:set_event('lua',set_break)
        end
    else
        Gdb.set_breakpoint(self,file,lno)
    end
end

function LGdb:goto_file_line(file,line)	
	ProcessOutput(gprefix..self.target_dir..'/'..file..":"..line..'\n')
end

-- there is some clidebugger hackery going on here. It will put us into its own version of debug.stacktrace,
-- and we need to put the program into frame #3, which is where the wobby originally happened. The usual Lua
-- error message is put out by the 'Message: ' line, which we use to capture the file:line needed to jump to.
-- The jumping is achieved by pushing the correct break pattern back into the input above (there must be
-- a more elegant way of doing this!)
local fmsg,lmsg

function LGdb:find_execution_break(line)
    local _,_,file,lineno = find(line,self.break_line)
    if _ then
        -- has our program thrown a wobbly in Lua?
		if file == self.clidebug_debugger and fmsg then 
			self:frame(3)
			return fmsg,lmsg,true
		else
			return file,lineno
		end
	else
		fmsg,lmsg = match(line,'Message: (%S+):(%d+)')
		if fmsg then return end        
        -- clidebug emits this pattern when Lua is entering a C function
        -- we have to check whether this function has any debug symbols
        -- before trying to step into it.
        local addr = match(line,'//@//%s(.+)')
        if addr then
            self.addr = strip_eol(addr)
            -- at this point, we have entered GDB at debug_break (forced by clidebug)
            local cached = self.addresses[self.addr]
            if cached == nil then
                -- haven't met this function before; check for line info.
                self:set_event('break',function()                    
                    dbg_command('info line','*'..self.addr)
                    -- info_line_handler() above will process the output...
                end)                        
            elseif cached == true then
                --we know this function has line numbers defined
                self:set_event('break',function()
                    self:queue_command('tbreak *'..self.addr)
                    self:queue_command('continue')
                end)
            end
            return
        end
	end
end

register_debugger('luagdb','lua',LGdb)
