SciTE/scite-debug/scite_lua/debug/debugger.lua
-- A simple gdb interface for SciTE
-- Steve Donovan, 2007
-- changes:
-- (1) debug.backtrace.depth will configure depth of stack frame dump (default is 20)
-- (2) initially only adds Run and Breakpoint to the menu
-- (3) first generalized version
local GTK = scite_GetProp('PLAT_GTK')

function do_set_menu()
--~ 	scite_Command {
--~ 	  'Step|do_step|Alt+C',
--~ 	  'Step Over|do_next|Alt+N',
--~ 	  'Go To|do_temp_breakpoint|Alt+G',
--~ 	  'Kill|do_kill|Alt+K',
--~ 	  'Inspect|do_inspect|Alt+I',
--~ 	  'Locals|do_locals|Alt+Ctrl+L',
--~       'Watch|do_watch|Alt+W',
--~ 	  'Backtrace|do_backtrace|Alt+Ctrl+B',
--~       'Step Out|do_finish|Alt+M',
--~ 	  'Up|do_up|Alt+U',
--~ 	  'Down|do_down|Alt+D',
--~ 	}
end

-- only bring up the absolute minimum commands initially....
scite_Command {
--~   'Run|do_run|*{savebefore:yes}|Alt+R',
--~   'Breakpoint|do_breakpoint|F9'
}

scite_require 'extlib.lua'

local lua_prompt = '(lua)'
local prompt
local prompt_len
local sub = string.sub
local find = string.find
local len = string.len
local gsub = string.gsub
local status = 'dead'
local last_command
local last_breakpoint
local traced
local dbg

function dbg_last_command()
    return dbg.last_command
end

function dbg_status()
    return status
end

function dbg_obj()
	return dbg
end

function debug_line_handler(line)
	local state = dbg_status()
	local dbg = dbg_obj()
    if state ~= 'dead' then
        dbg.last_command = '<inter>'
        spawner_command(line)
	end
end

local debug_status = scite_GetProp('debug.status',false)

-- *doc* you can add $(status.msg) to your statusbar.text.1 property if
-- you want to see debugger status.
-- (see SciTEGlobal.properties for examples)
function set_status(s)
    if s ~= status then
        if debug_status then print('setting status to '..s) end
        status = s
		local str = s
		if s == 'dead' then str = '' end
        props['status.msg'] = str
        scite.UpdateStatusBar(true)
    end
end

function dbg_status()
    return status
end

------- Generic debugger interface, based on GDB ------
Dbg = class()

function Dbg:init(root)
end

function Dbg:default_target()
    local ext = self.no_target_ext
    if ext then
        local res = props['FileName']
        if ext ~= '' then res = res..'.'..ext end
        return res
    else
        return props['FileNameExt']
    end
end

function Dbg:step()
	dbg_command('step')
end

function Dbg:step_over()
	dbg_command('next')
end

function Dbg:continue()
	dbg_command('cont')
end

function Dbg:quit()
	spawner_command('quit')
    if not self.no_quit_confirm then
        spawner_command('y')
    end
end

function Dbg:set_breakpoint(file,lno)
	dbg_command('break',file..':'..lno)
end

-- generally there are two ways to kill breakpoints in debuggers;
-- either by number or by explicit file:line.
function Dbg:clear_breakpoint(file,line,num)
	if file then
        dbg_command('clear',file..':'..line)
	else
		print ('no breakpoint at '..file..':'..line)
	end
end

-- run until the indicated file:line is reached
function Dbg:goto(file,lno)
	dbg_command('tbreak',file..':'..lno)
	dbg_command('continue')
end

function Dbg:set_display_handler(fun)
	-- 0.8 change: if a handler is already been set, don't try to set a new one!
	if self.result_handler then return end
	self.result_handler = fun
end

function Dbg:inspect(word)
	dbg_command('print',word)
end

local skip_file_pattern
local do_skip_includes

--- *doc* you can choose a directory pattern for files which you don't want to skip through
--- for Unix, this is usually easy, but for mingw you have to supply the path to
--- your gcc directory.
function Dbg:auto_skip_over_file(file)
    if not do_skip_includes then return end
    return find(file,skip_file_pattern)
end

function Dbg:finish()
    dbg_command('finish')
end

function Dbg:locals()
	dbg_command('info locals')
end

function Dbg:watch(word)
	dbg_command('display',word)
end

function Dbg:up()
	dbg_command('up')
end

function Dbg:down()
	dbg_command('down')
end

function Dbg:backtrace(depth)
	dbg_command('backtrace',depth)
end

function Dbg:frame(f)
	dbg_command('frame',f)
end

function Dbg:detect_frame(line)
	local _,_,frame = find(line,'#(%d+)')
	if _ then
		dbg:frame(frame)
	end
end

function Dbg:special_debugger_setup(out)
end

function Dbg:breakpoint_confirmation(line)
	-- breakpoint defintion confirmation
	-- ISSUE: only picking up for breakpoints added _during_ session!
    local _,_,bnum = find(line,"Breakpoint (%d+) at")
	if  _ then
		if last_breakpoint then
			print('breakpoint:',last_breakpoint.line,bnum)
			last_breakpoint.num = bnum
		end
	end
end

function quote(s)
    return '"'..s..'"'
end

function Dbg:find_execution_break(line)
    local _,_,file,lineno = find(line,self.break_line)
    if _ then return file,lineno end
end

function Dbg:check_breakpoint (b)
    return true
end

-- add our currently defined breakpoints
function Dbg:dump_breakpoints(out)
	for b in Breakpoints() do
        if self:check_breakpoint(b) then
            local f = basename(b.file)
            print (b.file,f)
            out:write('break '..f..':'..b.line..'\n')
        end
	end
end

function Dbg:run_program(out,parms)
	out:write('run '..parms..'\n')
end

function Dbg:detect_program_crash(line)
	return false
end

----- Debugger commands --------
local spawner_obj

local function launch_debugger()
	if do_launch() then
		set_status('running')
		return true
	else
		print 'Unable to debug program!'
		return false
	end
end

local function try_start_debugger()
	if not dbg then
		return launch_debugger()
	else
		return true
	end
end

function do_step()
    if not try_start_debugger() then return end
	dbg:step()
end

function do_run()
    if status == 'dead' then
		launch_debugger()
    else
        RemoveLastMarker(true)
        dbg:continue()
        set_status('running')
    end
end

function do_kill()
    if not dbg then return end
	if status == 'running' then
		-- this actually kills the debugger process
		spawner_obj:kill()
	else
		-- this will ask the debugger nicely to exit
		dbg:quit()
	end
    closing_process()
end

function do_next()
    if not try_start_debugger() then return end
	dbg:step_over()
end

function breakpoint_from_position(lno)
	for b in Breakpoints() do
		if b.file == scite_CurrentFile() and b.line == lno then
			return b
		end
	end
	return nil
end

function do_breakpoint()
	local lno = current_line() + 1
	local file = props['FileNameExt']
	-- do we have already have a breakpoint here?
	local brk = breakpoint_from_position(lno)
	if brk then
		local bnum = brk.num
		brk:delete()
		if status ~= 'dead' then
			dbg:clear_breakpoint(file,lno,bnum)
		end
	else
		last_breakpoint = SetBreakMarker(lno)
		if  last_breakpoint then
			if status ~= 'dead' then
				dbg:set_breakpoint(file,lno)
			end
		end
	end
end

function do_temp_breakpoint()
    if not try_start_debugger() then return end
	local lno = current_line() + 1
	local file = props['FileNameExt']
	dbg:goto(file,lno)
end

local function char_at(p)
    return string.char(editor.CharAt[p])
end

-- used to pick up current expression from current document position
-- We use the selection, if available, and otherwise pick up the word;
-- if it seems to be a field expression, look for the object before.
local function current_expr(pos)
    local s = editor:GetSelText()
    if s == '' then -- no selection, so find the word
        pos = pos or editor.CurrentPos
        local p1 = editor:WordStartPosition(pos,true)
        local p2 = editor:WordEndPosition(pos,true)
        -- is this a field of some object?
        while true do
            if  char_at(p1-1) == '.' then -- generic member access
                p1 = editor:WordStartPosition(p1-2,true)
            elseif char_at(p1-1) == '>' and char_at(p1-2) == '-' then --C/C++ pointer
                p1 = editor:WordStartPosition(p1-3,true)
            else
                break
            end
        end
        return editor:textrange(p1,p2)
    else
        return s
    end
end

function actually_inspect(w)
    if len(w) > 0 then
        dbg:inspect(w)
    end
end

function do_inspect()
    if not dbg then return end
    local w = current_expr()
	scite.Prompt("Inspect which expression:",w,"actually_inspect")
end

function do_locals()
    if not dbg then return end
	dbg:locals()
end

function actually_watch(w)
    dbg:watch(w)
end

function do_watch()
    if not dbg then return end
	scite.Prompt("Watch which expression:",current_expr(),"actually_watch")
end

function do_backtrace()
    if not dbg then return end
	dbg:backtrace(scite_GetProp('debug.backtrace.depth','20'))
end

function do_up()
    if not dbg then return end
	dbg:up()
end

function do_down()
    if not dbg then return end
	dbg:down()
end

function do_finish()
    if not dbg then return end
    dbg:finish()
end

local root

function Dbg:parameter_string()
	-- any parameters defined with View|Parameters
    local parms = ' '
    local i = 1
    local parm = props[i]
    while parm ~= '' do
        if find(parm,'%s') then
			-- if it's already quoted, then preserve the quotes
 			if find(parm,'"') == 1 then
 				parm = gsub(parm,'"','\\"')
 			end
            parm = '"'..parm..'"'
        end
        parms = parms..' '..parm
        i = i + 1
        parm = props[i]
    end
    return parms
end

local menu_init = false
local debug_verbose
local debuggers = {}
local append = table.insert
local remove = table.remove

---- event handling

-- If an event returns true, then this event will persist.
-- The return value of this function is true if any event returns an extra true result
-- Note: we iterate over a copy of the list, because this is the only way I've
-- found to make this method re-enterant. With this scheme it is
-- safe to raise an event within an event handler.
function Dbg:raise_event (event,...)
    local events = self.events
    if not events then return end
    -- not recommended for big tables!
    local cpy = {unpack(events)}
    local ignore
    for i,evt in ipairs(cpy) do
        if evt.event == event then
            local keep,want_to_ignore = evt.handler(...)
            if not keep then
               remove(events,i)
            end
            ignore = ignore or want_to_ignore
        end
    end
    return ignore
end

function Dbg:set_event (name,handler)
    if not self.events then self.events = {} end
    append(self.events,{event=name,handler=handler})
end

function Dbg:queue_command (cmd)
    self:set_event('prompt',function() spawner_command(cmd) end)
end

function create_existing_breakpoints()
	local out = io.open(dbg.cmd_file,"w")
	dbg:special_debugger_setup(out)
	dbg:dump_breakpoints(out)
    local parms = dbg:parameter_string()
	dbg:run_program(out,parms)
	out:close();
end

-- you may register more than one debugger class (@dclass) but such classes must
-- have a static method discriminate() which will be passed the full target name.
function register_debugger(name,ext,dclass)
    if type(ext) == 'table' then
        for i,v in ipairs(ext) do
            register_debugger(name,v,dclass) --**
        end
    else
		if not debuggers[ext] then
			debuggers[ext] = {dclass}
		else
			if not dclass.discriminator then
				error("Multiple debuggers registered for this extension, with no discriminator function")
			end
			append(debuggers[ext],dclass)
		end
    end
end

function create_debugger(ext,target)
	local dclasses = debuggers[ext]
    if not dclasses then dclasses = debuggers['*'] end
	if #dclasses == 1 then -- there is only one possible debugger for this extension!
		return dclasses[1]
	else -- there are several registered. We need to call the discriminator!
		for i,d in ipairs(dclasses) do
			if d.discriminator(target) then
				return d
			end
		end
	end
	error("unable to find appropriate debugger")
end

local initialized
local was_error = false
local continued_line, end_line_action, postproc

function do_launch()
	if not menu_init then
		do_set_menu()
		menu_init = true
	end
	scite.MenuCommand(IDM_SAVE)
	local no_host_symbols
    traced = false
	debug_verbose = true
   	-- *doc* detect the debugger we want to use, based on target extension
    -- if there is no explicit target, then use the current file.
    local target = scite_GetProp('debug.target')
    local ext
    if target then
	    -- @doc the target may not actually have debug symbols, in the case
		-- where we are debugging some dynamic libraries. Indicate this
		-- by prefixing target with [n]
		if target:find('^%[n%]') then
			target = target:sub(4)
			no_host_symbols = true
		end
        ext = extension_of(target)
    else
        ext = props['FileExt']
    end
	dbg = create_debugger(ext,choose(target,target,props['FileName']))
	dbg.host_symbols = not no_host_symbols
    -- this isn't ideal!
    root = props['TMP']
    dbg:init(root)
    do_skip_includes = scite_GetProp('debug.skip.includes',false)
    if do_skip_includes then
		local inc_path
        if GTK then inc_path = '^/usr/' else inc_path = '<<<DONUT>>>' end
		local file_pat_prop = 'debug.skip.file.matching'
		if dbg.skip_system_extension then
			file_pat_prop = file_pat_prop..dbg.skip_system_extension
		end
		skip_file_pattern = scite_GetProp(file_pat_prop,inc_path)
    end
    -- *doc* the default target depends on the debugger (it wd have extension for pydb, etc)
    if not target then target = dbg:default_target() end
    target = quote_if_needed(target)
    -- *doc* this determines the time before calltips appear; you can set this as a SciTE property.
	if props['dwell.period'] == '' then props['dwell.period'] = 500 end
    -- get the debugger process command string
    local dbg_cmd = dbg:command_line(target)
    print(dbg_cmd)
    continued_line = nil
    -- first create the cmd file for the debugger
	create_existing_breakpoints()
	scite_InteractivePromptHandler (dbg.prompt,debug_line_handler)
    --- and go!!
	scite.SetDirectory(props['FileDir'])
    spawner.verbose(scite_GetPropBool('debug.spawner.verbose',false))
--	spawner.fulllines(1)
	spawner_obj = spawner.new(dbg_cmd)
	spawner_obj:set_output('ProcessChunk')
	spawner_obj:set_result('ProcessResult')
	return spawner_obj:run()
end

-- speaking to the spawned process goes through a named pipe on both
-- platforms.
local pipe = nil
local last_command_line

function dbg_command_line(s)
    if status == 'active' or status == 'error' then
        spawner_command(s)
        last_command_line = s
        if dbg.trailing_prompt then
            last_command_line = dbg.prompt..last_command_line
        end
    end
end

function spawner_command(line)
    if not dbg then return end
	spawner_obj:write(line..'\n')
end

--local ferr = io.stderr

function dbg_command(s,argument)
    if not dbg then return end
	dbg.last_command = s
    dbg.last_arg = argument
    if argument then s = s..' '..argument end
    dbg_command_line(s)
end

-- *doc* currently, only win32-spawner understands the !up command; I can't
-- find the Unix/GTK equivalent! It is meant to bring the debugger
-- SciTE instance to the front.
function raise_scite()
	--spawner.foreground()
end

-- output of inspected variables goes here; this mechanism allows us
-- to redirect command output (to a tooltip in this case)
function display(s)
	if dbg.result_handler then
		dbg.result_handler(s)
        dbg.result_handler = nil
	else
		print(s)
	end
end

function closing_process()
    print 'quitting debugger'
--	spawner_obj:close()
    set_status('dead')
    RemoveLastMarker(true)
	scite_LeaveInteractivePrompt()
    dbg = nil
end

local function finish_pending_actions()
    if continued_line then
        end_line_action(continued_line,dbg)
        continued_line = nil
        if postproc.once then dbg.last_command = ''  end
    end
end

local function set_error_state()
    if was_error then
        set_status('error')
        was_error = false
    else
        set_status('active')
    end
end

local function auto_backtrace()
    if status == 'error' and not traced then
        raise_scite()
        do_backtrace()
        traced = true
    end
end

local current_file

local function error(s)
    io.stderr:write(s..'\n')
end

local was_prompt

function ProcessOutput(line)
    -- on the GTK version, commands are currently echoed....
    if last_command_line and find(line,last_command_line) then
        return
     end
	 -- Debuggers (esp. clidebug) can emit spurious blank lines. This makes them quieter!
	 if was_prompt and line:find('^%s*$') then
		was_prompt = false
		return
	end
--~  	 trace('*'..line)
    -- sometimes it's useful to know when the debugger process has properly started
    if dbg.detect_start and find(line,dbg.detect_start) then
        dbg:handle_debug_start()
        dbg.detect_start = nil
        return
    end
	-- detecting end of program execution
    local prog_ended,process_fininished = dbg:detect_program_end(line)
	if prog_ended then
		if not processed_finished then spawner_command('quit') end
        set_status('dead')
        closing_process()
		return
	end
    -- ignore prompt; this is the point at which we know that commands have finished
	if find(line,dbg.prompt) then
        dbg:raise_event 'prompt'
        finish_pending_actions()
        if was_error then set_error_state() end
        auto_backtrace()
		was_prompt = true
		return
	end

    -- the result of some commands require postprocessing;
	-- it will collect multi-line output together!
    postproc = dbg.postprocess_command[dbg.last_command]
    if postproc then
        local tline = rtrim(line)
        if find(tline,postproc.pattern)
			or (postproc.alt_pat and find(tline,postproc.alt_pat)) then
            if not postproc.single_pattern then
                finish_pending_actions()
                continued_line = tline
                end_line_action = postproc.action
            else
                postproc.action(tline,dbg)
            end
        else
            if continued_line then continued_line = continued_line..tline end
        end
    end
	-- did we get a confirmation message about a created breakpoint?
    dbg:breakpoint_confirmation(line)
	-- have we crashed?
	if dbg:detect_program_crash(line) then
		was_error = true
	end
	-- looking for break at line pattern
	local file,lineno,explicit_error = dbg:find_execution_break(line)
	if file and status ~= 'dead' then
        if dbg.check_skip_always or current_file ~= file then
            current_file = file
            if dbg:auto_skip_over_file(file) then
                dbg:finish()
                spawner_command('step') --??
                return
            end
        end
		-- a debugger can indicate an explicit error, rather than depending on
		-- detect_program_crash()
		if explicit_error then
			was_error = true
		end
        set_error_state()
        -- if any of the break events wishes, we can ignore this break...
        if not dbg:raise_event ('break',file,lineno,status)  then
            OpenAtPos(file,lineno,status)
            raise_scite()
            auto_backtrace()
            dbg.last_comand = ''
            -- may schedule a command to be executed after the error backtrace
            if type(explicit_error) == 'string' then
                dbg:queue_command(explicit_error)
            end
        end
	else
        local cmd = dbg.last_command
        if (debug_verbose or dbg.last_command == '<inter>') and
			not (dbg.silent_command[cmd] or dbg.postprocess_command[cmd]) then
				trace(line)
        end
	end
end

function ProcessChunk(s)
	local i1 = 1
	local i2 = find(s,'\n',i1)
	while i2 do
		local line = sub(s,i1,i2)
		ProcessOutput(line)
		i1 = i2 + 1
		i2 = find(s,'\n',i1)
	end
	if i1 <= len(s) then
		local line = sub(s,i1)
		ProcessOutput(line)
	end
end

function ProcessResult(res)
	if status ~= 'dead' then
		closing_process()
	end
end

--- *doc* currently, double-clicking in the output pane will try to recognize
--- a stack frame pattern and move to that frame if possible.
scite_OnDoubleClick(function()
	if output.Focus and status == 'active' or status == 'error' then
		dbg:detect_frame(output:GetLine(current_output_line()))
	end
end)

-- *doc* if your scite has OnDwellStart, then the current symbol under the mouse
-- pointer will be evaluated and shown in a calltip.
local _pos

function calltip(s)
    editor:CallTipShow(_pos,s)
end

scite_OnDwellStart(function (pos,s)
    if status == 'active' or status == 'error' then
        if s ~= '' then
			s = current_expr(pos)
            _pos = pos
            dbg:set_display_handler(calltip)
			dbg:inspect(s)
        else
           editor:CallTipCancel()
        end
        return true
    end
end)


