1119 lines
41 KiB
Lua
1119 lines
41 KiB
Lua
-- Integration with MobDebug
|
|
-- Copyright 2011-12 Paul Kulchenko, ZeroBrane LLC
|
|
-- Original authors: Lomtik Software (J. Winwood & John Labenski)
|
|
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
|
|
|
|
local copas = require "copas"
|
|
local socket = require "socket"
|
|
local mobdebug = require "mobdebug"
|
|
|
|
local ide = ide
|
|
local debugger = ide.debugger
|
|
debugger.server = nil -- DebuggerServer object when debugging, else nil
|
|
debugger.running = false -- true when the debuggee is running
|
|
debugger.listening = false -- true when the debugger is listening for a client
|
|
debugger.portnumber = ide.config.debugger.port or mobdebug.port -- the port # to use for debugging
|
|
debugger.watchWindow = nil -- the watchWindow, nil when not created
|
|
debugger.watchCtrl = nil -- the child ctrl in the watchWindow
|
|
debugger.stackWindow = nil -- the stackWindow, nil when not created
|
|
debugger.stackCtrl = nil -- the child ctrl in the stackWindow
|
|
debugger.hostname = ide.config.debugger.hostname or (function()
|
|
local addr = wx.wxIPV4address() -- check what address is resolvable
|
|
for _, host in ipairs({wx.wxGetHostName(), wx.wxGetFullHostName()}) do
|
|
if host and #host > 0 and addr:Hostname(host) then return host end
|
|
end
|
|
return "localhost" -- last resort; no known good hostname
|
|
end)()
|
|
|
|
local notebook = ide.frame.notebook
|
|
|
|
local function updateWatchesSync(num)
|
|
local watchCtrl = debugger.watchCtrl
|
|
if watchCtrl and debugger.server and not debugger.running
|
|
and not debugger.scratchpad and not (debugger.options or {}).noeval then
|
|
for idx = 0, watchCtrl:GetItemCount() - 1 do
|
|
if not num or idx == num then
|
|
local expression = watchCtrl:GetItemText(idx)
|
|
local _, values, error = debugger.evaluate(expression)
|
|
if error then error = error:gsub("%[.-%]:%d+:%s+","")
|
|
elseif #values == 0 then values = {'nil'} end
|
|
|
|
local newval = error and ('error: '..error) or values[1]
|
|
-- get the current value from a list item
|
|
do local litem = wx.wxListItem()
|
|
litem:SetMask(wx.wxLIST_MASK_TEXT)
|
|
litem:SetId(idx)
|
|
litem:SetColumn(1)
|
|
watchCtrl:GetItem(litem)
|
|
watchCtrl:SetItemBackgroundColour(idx,
|
|
watchCtrl:GetItem(litem) and newval ~= litem:GetText()
|
|
and ide.config.styles.caretlinebg
|
|
and wx.wxColour(unpack(ide.config.styles.caretlinebg.bg))
|
|
or watchCtrl:GetBackgroundColour())
|
|
end
|
|
|
|
watchCtrl:SetItem(idx, 1, newval)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local simpleType = {['nil'] = true, ['string'] = true, ['number'] = true, ['boolean'] = true}
|
|
local stackItemValue = {}
|
|
local function checkIfExpandable(value, item)
|
|
local expandable = type(value) == 'table' and next(value) ~= nil
|
|
and not stackItemValue[value] -- only expand first time
|
|
if expandable then -- cache table value to expand when requested
|
|
stackItemValue[item:GetValue()] = value
|
|
stackItemValue[value] = item:GetValue() -- to avoid circular refs
|
|
end
|
|
return expandable
|
|
end
|
|
|
|
local function updateStackSync()
|
|
local stackCtrl = debugger.stackCtrl
|
|
if stackCtrl and debugger.server
|
|
and not debugger.running and not debugger.scratchpad then
|
|
local stack, _, err = debugger.stack()
|
|
if not stack or #stack == 0 then
|
|
stackCtrl:DeleteAllItems()
|
|
if err then -- report an error if any
|
|
stackCtrl:AppendItem(stackCtrl:AddRoot("Stack"), "Error: " .. err, 0)
|
|
end
|
|
return
|
|
end
|
|
stackCtrl:Freeze()
|
|
stackCtrl:DeleteAllItems()
|
|
local params = {comment = false, nocode = true}
|
|
local root = stackCtrl:AddRoot("Stack")
|
|
stackItemValue = {} -- reset cache of items in the stack
|
|
for _,frame in ipairs(stack) do
|
|
-- "main chunk at line 24"
|
|
-- "foo() at line 13 (defined at foobar.lua:11)"
|
|
-- call = { source.name, source.source, source.linedefined,
|
|
-- source.currentline, source.what, source.namewhat, source.short_src }
|
|
local call = frame[1]
|
|
local func = call[5] == "main" and "main chunk"
|
|
or call[5] == "C" and (call[1] or "C function")
|
|
or call[5] == "tail" and "tail call"
|
|
or (call[1] or "anonymous function")
|
|
local text = func ..
|
|
(call[4] == -1 and '' or " at line "..call[4]) ..
|
|
(call[5] ~= "main" and call[5] ~= "Lua" and ''
|
|
or (call[3] > 0 and " (defined at "..call[2]..":"..call[3]..")"
|
|
or " (defined in "..call[2]..")"))
|
|
local callitem = stackCtrl:AppendItem(root, text, 0)
|
|
for name,val in pairs(frame[2]) do
|
|
local value, comment = val[1], val[2]
|
|
local text = ("%s = %s%s"):
|
|
format(name, mobdebug.line(value, params),
|
|
simpleType[type(value)] and "" or (" --[["..comment.."]]"))
|
|
local item = stackCtrl:AppendItem(callitem, text, 1)
|
|
if checkIfExpandable(value, item) then
|
|
stackCtrl:SetItemHasChildren(item, true)
|
|
end
|
|
end
|
|
for name,val in pairs(frame[3]) do
|
|
local value, comment = val[1], val[2]
|
|
local text = ("%s = %s%s"):
|
|
format(name, mobdebug.line(value, params),
|
|
simpleType[type(value)] and "" or (" --[["..comment.."]]"))
|
|
local item = stackCtrl:AppendItem(callitem, text, 2)
|
|
if checkIfExpandable(value, item) then
|
|
stackCtrl:SetItemHasChildren(item, true)
|
|
end
|
|
end
|
|
stackCtrl:SortChildren(callitem)
|
|
stackCtrl:Expand(callitem)
|
|
end
|
|
stackCtrl:EnsureVisible(stackCtrl:GetFirstChild(root))
|
|
stackCtrl:Thaw()
|
|
end
|
|
end
|
|
|
|
local function updateStackAndWatches()
|
|
-- check if the debugger is running and may be waiting for a response.
|
|
-- allow that request to finish, otherwise updateWatchesSync() does nothing.
|
|
if debugger.running then debugger.update() end
|
|
if debugger.server and not debugger.running then
|
|
copas.addthread(function() updateStackSync() updateWatchesSync() end)
|
|
end
|
|
end
|
|
|
|
local function updateWatches(num)
|
|
-- check if the debugger is running and may be waiting for a response.
|
|
-- allow that request to finish, otherwise updateWatchesSync() does nothing.
|
|
if debugger.running then debugger.update() end
|
|
if debugger.server and not debugger.running then
|
|
copas.addthread(function() updateWatchesSync(num) end)
|
|
end
|
|
end
|
|
|
|
local function killClient()
|
|
if (debugger.pid) then
|
|
-- using SIGTERM for some reason kills not only the debugee process,
|
|
-- but also some system processes, which leads to a blue screen crash
|
|
-- (at least on Windows Vista SP2)
|
|
local ret = wx.wxProcess.Kill(debugger.pid, wx.wxSIGKILL, wx.wxKILL_CHILDREN)
|
|
if ret == wx.wxKILL_OK then
|
|
DisplayOutputLn(TR("Program stopped (pid: %d)."):format(debugger.pid))
|
|
elseif ret ~= wx.wxKILL_NO_PROCESS then
|
|
DisplayOutputLn(TR("Unable to stop program (pid: %d), code %d.")
|
|
:format(debugger.pid, ret))
|
|
end
|
|
debugger.pid = nil
|
|
end
|
|
end
|
|
|
|
local function activateDocument(file, line)
|
|
if not file then return end
|
|
|
|
if not wx.wxIsAbsolutePath(file) and debugger.basedir then
|
|
file = debugger.basedir .. file
|
|
end
|
|
|
|
local activated
|
|
local indebugger = file:find('mobdebug%.lua$')
|
|
local fileName = wx.wxFileName(file)
|
|
for _, document in pairs(ide.openDocuments) do
|
|
-- skip those tabs that may have file without names (untitled.lua)
|
|
if document.filePath and fileName:SameAs(wx.wxFileName(document.filePath)) then
|
|
local editor = document.editor
|
|
local selection = document.index
|
|
notebook:SetSelection(selection)
|
|
SetEditorSelection(selection)
|
|
ClearAllCurrentLineMarkers()
|
|
if line then
|
|
editor:MarkerAdd(line-1, CURRENT_LINE_MARKER)
|
|
editor:EnsureVisibleEnforcePolicy(line-1)
|
|
end
|
|
activated = editor
|
|
break
|
|
end
|
|
end
|
|
|
|
if not activated and not indebugger and ide.config.editor.autoactivate then
|
|
-- found file, but can't activate yet (because this part may be executed
|
|
-- in a different co-routine), so schedule pending activation.
|
|
if wx.wxFileName(file):FileExists() then
|
|
debugger.activate = {file, line}
|
|
return true -- report successful activation, even though it's pending
|
|
end
|
|
|
|
if not debugger.missing[file] then -- only report files once per session
|
|
debugger.missing[file] = true
|
|
DisplayOutputLn(TR("Couldn't activate file '%s' for debugging; continuing without it.")
|
|
:format(file))
|
|
end
|
|
end
|
|
|
|
return activated ~= nil
|
|
end
|
|
|
|
local function reSetBreakpoints()
|
|
-- remove all breakpoints that may still be present from the last session
|
|
-- this only matters for those remote clients that reload scripts
|
|
-- without resetting their breakpoints
|
|
debugger.handle("delallb")
|
|
|
|
-- go over all windows and find all breakpoints
|
|
if (not debugger.scratchpad) then
|
|
for _, document in pairs(ide.openDocuments) do
|
|
local editor = document.editor
|
|
local filePath = document.filePath
|
|
local line = editor:MarkerNext(0, BREAKPOINT_MARKER_VALUE)
|
|
while line ~= -1 do
|
|
debugger.handle("setb " .. filePath .. " " .. (line+1))
|
|
line = editor:MarkerNext(line + 1, BREAKPOINT_MARKER_VALUE)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
debugger.shell = function(expression, isstatement)
|
|
-- check if the debugger is running and may be waiting for a response.
|
|
-- allow that request to finish, otherwise updateWatchesSync() does nothing.
|
|
if debugger.running then debugger.update() end
|
|
if debugger.server and not debugger.running and not debugger.scratchpad then
|
|
copas.addthread(function ()
|
|
-- exec command is not expected to return anything.
|
|
-- eval command returns 0 or more results.
|
|
-- 'values' has a list of serialized results returned.
|
|
-- as it is not possible to distinguish between 0 results and one
|
|
-- 'nil' value returned, 'nil' is always returned in this case.
|
|
-- the first value returned by eval command is not used;
|
|
-- this may need to be taken into account by other debuggers.
|
|
local addedret, forceexpression = true, expression:match("^%s*=%s*")
|
|
expression = expression:gsub("^%s*=%s*","")
|
|
local _, values, err = debugger.evaluate(expression)
|
|
if not forceexpression and err and
|
|
(err:find("'<eof>' expected near '") or
|
|
err:find("'%(' expected near") or
|
|
err:find("unexpected symbol near '")) then
|
|
_, values, err = debugger.execute(expression)
|
|
addedret = false
|
|
end
|
|
|
|
if err then
|
|
if addedret then err = err:gsub('^%[string "return ', '[string "') end
|
|
DisplayShellErr(err)
|
|
elseif addedret or #values > 0 then
|
|
if forceexpression then -- display elements as multi-line
|
|
local mobdebug = require "mobdebug"
|
|
for i,v in pairs(values) do -- stringify each of the returned values
|
|
local func = loadstring('return '..v) -- deserialize the value first
|
|
if func then -- if it's deserialized correctly
|
|
values[i] = (forceexpression and i > 1 and '\n' or '') ..
|
|
mobdebug.line(func(), {nocode = true, comment = 0,
|
|
-- if '=' is used, then use multi-line serialized output
|
|
indent = forceexpression and ' ' or nil})
|
|
end
|
|
end
|
|
end
|
|
|
|
-- if empty table is returned, then show nil if this was an expression
|
|
if #values == 0 and (forceexpression or not isstatement) then
|
|
values = {'nil'}
|
|
end
|
|
DisplayShell((table.unpack or unpack)(values))
|
|
end
|
|
|
|
-- refresh Stack and Watch windows if executed a statement (and no err)
|
|
if isstatement and not err and not addret and #values == 0 then
|
|
updateStackSync() updateWatchesSync() end
|
|
end)
|
|
end
|
|
end
|
|
|
|
debugger.listen = function()
|
|
local server = socket.bind("*", debugger.portnumber)
|
|
DisplayOutputLn(TR("Debugger server started at %s:%d.")
|
|
:format(debugger.hostname, debugger.portnumber))
|
|
copas.autoclose = false
|
|
copas.addserver(server, function (skt)
|
|
if debugger.server then
|
|
DisplayOutputLn(TR("Refused a request to start a new debugging session as there is one in progress already."))
|
|
return
|
|
end
|
|
|
|
copas.setErrorHandler(function(error)
|
|
DisplayOutputLn(TR("Can't start debugging session due to internal error '%s'."):format(error))
|
|
debugger.terminate()
|
|
end)
|
|
|
|
local options = debugger.options or {}
|
|
if not debugger.scratchpad then SetAllEditorsReadOnly(true) end
|
|
|
|
debugger.server = copas.wrap(skt)
|
|
debugger.socket = skt
|
|
debugger.loop = false
|
|
debugger.scratchable = false
|
|
debugger.stats = {line = 0}
|
|
debugger.missing = {}
|
|
|
|
local wxfilepath = GetEditorFileAndCurInfo()
|
|
local startfile = options.startfile or options.startwith
|
|
or (wxfilepath and wxfilepath:GetFullPath())
|
|
|
|
if not startfile then
|
|
DisplayOutputLn(TR("Can't start debugging without an opened file or with the current file not being saved ('%s').")
|
|
:format(ide.config.default.fullname))
|
|
return debugger.terminate()
|
|
end
|
|
|
|
local startpath = wx.wxFileName(startfile):GetPath(wx.wxPATH_GET_VOLUME + wx.wxPATH_GET_SEPARATOR)
|
|
local basedir = options.basedir or FileTreeGetDir() or startpath
|
|
-- guarantee that the path has a trailing separator
|
|
debugger.basedir = wx.wxFileName.DirName(basedir):GetFullPath()
|
|
|
|
-- load the remote file into the debugger
|
|
-- set basedir first, before loading to make sure that the path is correct
|
|
debugger.handle("basedir " .. debugger.basedir)
|
|
|
|
reSetBreakpoints()
|
|
|
|
if options.redirect then
|
|
debugger.handle("output stdout " .. options.redirect, nil,
|
|
{ handler = function(m)
|
|
if not debugger.server then return end
|
|
|
|
-- if it's an error returned, then handle the error
|
|
if m and m:find("stack traceback:", 1, true) then
|
|
-- this is an error message sent remotely
|
|
local func = loadstring("return "..m)
|
|
if func then
|
|
DisplayOutputLn(func())
|
|
debugger.terminate()
|
|
return
|
|
end
|
|
end
|
|
|
|
if ide.config.debugger.outputfilter then
|
|
m = ide.config.debugger.outputfilter(m)
|
|
elseif m then
|
|
local max = 240
|
|
m = #m < max+4 and m or m:sub(1,max) .. "...\n"
|
|
end
|
|
if m then DisplayOutputNoMarker(m) end
|
|
end})
|
|
end
|
|
|
|
if (options.startwith) then
|
|
local file, line, err = debugger.loadfile(options.startwith)
|
|
if err then
|
|
DisplayOutputLn(TR("Can't run the entry point script ('%s').")
|
|
:format(options.startwith)
|
|
.." "..TR("Compilation error")
|
|
..":\n"..err)
|
|
return debugger.terminate()
|
|
end
|
|
elseif (options.run) then
|
|
-- do nothing here
|
|
elseif (debugger.scratchpad) then
|
|
debugger.scratchpad.updated = true
|
|
else
|
|
local file, line, err = debugger.loadfile(startfile)
|
|
-- "load" can work in two ways: (1) it can load the requested file
|
|
-- OR (2) it can "refuse" to load it if the client was started
|
|
-- with start() method, which can't load new files
|
|
-- if file and line are set, this indicates option #2
|
|
if file and line then
|
|
local activated = activateDocument(file, line)
|
|
|
|
-- if not found, check using full file path and reset basedir
|
|
if not activated and not wx.wxIsAbsolutePath(file) then
|
|
activated = activateDocument(startpath..file, line)
|
|
if activated then
|
|
debugger.basedir = startpath
|
|
debugger.handle("basedir " .. debugger.basedir)
|
|
-- reset breakpoints again as basedir has changed
|
|
reSetBreakpoints()
|
|
end
|
|
end
|
|
|
|
if not activated then
|
|
DisplayOutputLn(TR("Can't find file '%s' in the current project to activate for debugging. Update the project or open the file in the editor before debugging.")
|
|
:format(file))
|
|
return debugger.terminate()
|
|
end
|
|
|
|
-- debugger may still be available for scratchpad,
|
|
-- if the interpreter signals scratchpad support, so enable it.
|
|
debugger.scratchable = ide.interpreter.scratchextloop ~= nil
|
|
elseif err then
|
|
DisplayOutputLn(TR("Can't debug the script in the active editor window.")
|
|
.." "..TR("Compilation error")
|
|
..":\n"..err)
|
|
return debugger.terminate()
|
|
else
|
|
debugger.scratchable = true
|
|
activateDocument(startfile, 1)
|
|
end
|
|
end
|
|
|
|
if (not options.noshell and not debugger.scratchpad) then
|
|
ShellSupportRemote(debugger.shell)
|
|
end
|
|
|
|
updateStackSync()
|
|
updateWatchesSync()
|
|
|
|
DisplayOutputLn(TR("Debugging session started in '%s'."):format(debugger.basedir))
|
|
|
|
if (options.runstart) then
|
|
ClearAllCurrentLineMarkers()
|
|
debugger.run()
|
|
end
|
|
|
|
if (options.run) then
|
|
local file, line = debugger.handle("run")
|
|
activateDocument(file, line)
|
|
end
|
|
end)
|
|
debugger.listening = true
|
|
end
|
|
|
|
debugger.handle = function(command, server, options)
|
|
local verbose = ide.config.debugger.verbose
|
|
local osexit, gprint
|
|
osexit, os.exit = os.exit, function () end
|
|
if (verbose) then
|
|
gprint, _G.print = _G.print, function (...) DisplayOutputLn(...) end
|
|
end
|
|
|
|
debugger.running = true
|
|
if verbose then DisplayOutputLn("Debugger sent (command):", command) end
|
|
local file, line, err = mobdebug.handle(command, server or debugger.server, options)
|
|
if verbose then DisplayOutputLn("Debugger received (file, line, err):", file, line, err) end
|
|
debugger.running = false
|
|
|
|
os.exit = osexit
|
|
if (verbose) then _G.print = gprint end
|
|
return file, line, err
|
|
end
|
|
|
|
debugger.exec = function(command)
|
|
if debugger.server and not debugger.running then
|
|
copas.addthread(function ()
|
|
local out
|
|
local attempts = 0
|
|
while true do
|
|
debugger.breaking = false
|
|
local file, line, err = debugger.handle(out or command)
|
|
if out then out = nil end
|
|
if line == nil then
|
|
if err then DisplayOutputLn(err) end
|
|
DebuggerStop()
|
|
return
|
|
else
|
|
if activateDocument(file, line) then
|
|
debugger.stats.line = debugger.stats.line + 1
|
|
if debugger.loop then
|
|
updateStackSync()
|
|
updateWatchesSync()
|
|
else
|
|
updateStackAndWatches()
|
|
return
|
|
end
|
|
else
|
|
-- we may be in some unknown location at this point;
|
|
-- If this happens, stop and report allowing users to set
|
|
-- breakpoints and step through.
|
|
if debugger.breaking then
|
|
DisplayOutputLn(TR("Debugging suspended at %s:%s (couldn't activate the file).")
|
|
:format(file, line))
|
|
return
|
|
end
|
|
-- redo now; if the call is from the debugger, then repeat
|
|
-- the same command, except when it was "run" (switch to 'step');
|
|
-- this is needed to "break" execution that happens in on() call.
|
|
-- in all other cases get out of this file.
|
|
-- don't get out of "mobdebug", because it may happen with
|
|
-- start() or on() call, which will get us out of the current
|
|
-- file, which is not what we want.
|
|
-- Some engines (Corona SDK) report =?:0 as the current location.
|
|
-- repeat the same command, but check if this has been tried
|
|
-- too many times already; if so, get "out"
|
|
out = ((tonumber(line) == 0 and attempts < 10) and command
|
|
or (file:find('mobdebug%.lua$')
|
|
and (command == 'run' and 'step' or command) or "out"))
|
|
attempts = attempts + 1
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
debugger.handleAsync = function(command)
|
|
if debugger.server and not debugger.running then
|
|
copas.addthread(function () debugger.handle(command) end)
|
|
end
|
|
end
|
|
|
|
debugger.loadfile = function(file)
|
|
return debugger.handle("load " .. file)
|
|
end
|
|
debugger.loadstring = function(file, string)
|
|
return debugger.handle("loadstring '" .. file .. "' " .. string)
|
|
end
|
|
debugger.update = function()
|
|
copas.step(0)
|
|
-- if there are any pending activations
|
|
if debugger.activate then
|
|
local file, line = (table.unpack or unpack)(debugger.activate)
|
|
if LoadFile(file) then activateDocument(file, line) end
|
|
debugger.activate = nil
|
|
end
|
|
end
|
|
debugger.terminate = function()
|
|
if debugger.server then
|
|
if debugger.pid then -- if there is PID, try local kill
|
|
killClient()
|
|
else -- otherwise, try graceful exit for the remote process
|
|
debugger.breaknow("exit")
|
|
end
|
|
DebuggerStop()
|
|
end
|
|
end
|
|
debugger.step = function() debugger.exec("step") end
|
|
debugger.trace = function()
|
|
debugger.loop = true
|
|
debugger.exec("step")
|
|
end
|
|
debugger.over = function() debugger.exec("over") end
|
|
debugger.out = function() debugger.exec("out") end
|
|
debugger.run = function() debugger.exec("run") end
|
|
debugger.evaluate = function(expression) return debugger.handle('eval ' .. expression) end
|
|
debugger.execute = function(expression) return debugger.handle('exec ' .. expression) end
|
|
debugger.stack = function() return debugger.handle('stack') end
|
|
debugger.breaknow = function(command)
|
|
-- stop if we're running a "trace" command
|
|
debugger.loop = false
|
|
|
|
-- force suspend command; don't use copas interface as it checks
|
|
-- for the other side "reading" and the other side is not reading anything.
|
|
-- use the "original" socket to send "suspend" command.
|
|
-- this will only break on the next Lua command.
|
|
if debugger.socket then
|
|
local running = debugger.running
|
|
-- this needs to be short as it will block the UI
|
|
debugger.socket:settimeout(0.25)
|
|
local file, line, err = debugger.handle(command or "suspend", debugger.socket)
|
|
debugger.socket:settimeout(0)
|
|
-- restore running status
|
|
debugger.running = running
|
|
debugger.breaking = true
|
|
-- don't need to do anything else as the earlier call (run, step, etc.)
|
|
-- will get the results (file, line) back and will update the UI
|
|
return file, line, err
|
|
end
|
|
end
|
|
debugger.breakpoint = function(file, line, state)
|
|
debugger.handleAsync((state and "setb " or "delb ") .. file .. " " .. line)
|
|
end
|
|
debugger.quickeval = function(var, callback)
|
|
if debugger.server and not debugger.running
|
|
and not debugger.scratchpad and not (debugger.options or {}).noeval then
|
|
copas.addthread(function ()
|
|
local _, values, err = debugger.evaluate(var)
|
|
local val = err
|
|
and err:gsub("%[.-%]:%d+:%s*","error: ")
|
|
or (var .. " = " .. (#values > 0 and values[1] or 'nil'))
|
|
if callback then callback(val) end
|
|
end)
|
|
end
|
|
end
|
|
|
|
----------------------------------------------
|
|
-- public api
|
|
|
|
function DebuggerAttachDefault(options)
|
|
debugger.options = options
|
|
if (debugger.listening) then return end
|
|
debugger.listen()
|
|
end
|
|
|
|
function DebuggerShutdown()
|
|
if debugger.server then debugger.terminate() end
|
|
if debugger.pid then killClient() end
|
|
end
|
|
|
|
function DebuggerStop()
|
|
if (debugger.server) then
|
|
debugger.server = nil
|
|
debugger.pid = nil
|
|
SetAllEditorsReadOnly(false)
|
|
ShellSupportRemote(nil)
|
|
ClearAllCurrentLineMarkers()
|
|
DebuggerScratchpadOff()
|
|
local lines = TR("traced %d instruction", debugger.stats.line):format(debugger.stats.line)
|
|
DisplayOutputLn(TR("Debugging session completed (%s)."):format(lines))
|
|
else
|
|
-- it's possible that the application couldn't start, or that the
|
|
-- debugger in the application didn't start, which means there is
|
|
-- no debugger.server, but scratchpad may still be on. Turn it off.
|
|
DebuggerScratchpadOff()
|
|
end
|
|
end
|
|
|
|
function DebuggerCloseStackWindow()
|
|
if (debugger.stackWindow) then
|
|
SettingsSaveFramePosition(debugger.stackWindow, "StackWindow")
|
|
debugger.stackCtrl = nil
|
|
debugger.stackWindow = nil
|
|
end
|
|
end
|
|
|
|
function DebuggerCloseWatchWindow()
|
|
if (debugger.watchWindow) then
|
|
SettingsSaveFramePosition(debugger.watchWindow, "WatchWindow")
|
|
debugger.watchCtrl = nil
|
|
debugger.watchWindow = nil
|
|
end
|
|
end
|
|
|
|
-- need imglist to be a file local variable as SetImageList takes ownership
|
|
-- of it and if done inside a function, icons do not work as expected
|
|
local imglist = wx.wxImageList(16,16)
|
|
do
|
|
local getBitmap = (ide.app.createbitmap or wx.wxArtProvider.GetBitmap)
|
|
local size = wx.wxSize(16,16)
|
|
-- 0 = stack call
|
|
imglist:Add(getBitmap(wx.wxART_GO_FORWARD, wx.wxART_OTHER, size))
|
|
-- 1 = local variables
|
|
imglist:Add(getBitmap(wx.wxART_LIST_VIEW, wx.wxART_OTHER, size))
|
|
-- 2 = upvalues
|
|
imglist:Add(getBitmap(wx.wxART_REPORT_VIEW, wx.wxART_OTHER, size))
|
|
end
|
|
|
|
function DebuggerCreateStackWindow()
|
|
if (debugger.stackWindow) then return updateStackAndWatches() end
|
|
local width = 360
|
|
local stackWindow = wx.wxFrame(ide.frame, wx.wxID_ANY,
|
|
TR("Stack Window"),
|
|
wx.wxDefaultPosition, wx.wxSize(width, 200),
|
|
wx.wxDEFAULT_FRAME_STYLE + wx.wxFRAME_FLOAT_ON_PARENT)
|
|
|
|
debugger.stackWindow = stackWindow
|
|
|
|
local stackCtrl = wx.wxTreeCtrl(stackWindow, wx.wxID_ANY,
|
|
wx.wxDefaultPosition, wx.wxDefaultSize,
|
|
wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS + wx.wxTR_SINGLE + wx.wxTR_HIDE_ROOT)
|
|
|
|
debugger.stackCtrl = stackCtrl
|
|
|
|
stackCtrl:SetImageList(imglist)
|
|
stackWindow:CentreOnParent()
|
|
SettingsRestoreFramePosition(stackWindow, "StackWindow")
|
|
stackWindow:Show(true)
|
|
|
|
stackWindow:Connect(wx.wxEVT_CLOSE_WINDOW,
|
|
function (event)
|
|
DebuggerCloseStackWindow()
|
|
stackWindow = nil
|
|
stackCtrl = nil
|
|
event:Skip()
|
|
end)
|
|
|
|
stackCtrl:Connect( wx.wxEVT_COMMAND_TREE_ITEM_EXPANDING,
|
|
function (event)
|
|
local item_id = event:GetItem()
|
|
local count = stackCtrl:GetChildrenCount(item_id, false)
|
|
if count > 0 then return true end
|
|
|
|
local image = stackCtrl:GetItemImage(item_id)
|
|
local num = 1
|
|
for name,value in pairs(stackItemValue[item_id:GetValue()]) do
|
|
local strval = mobdebug.line(value, {comment = false, nocode = true})
|
|
local text = type(name) == "number"
|
|
and (num == name and strval or ("[%s] = %s"):format(name, strval))
|
|
or ("%s = %s"):format(tostring(name), strval)
|
|
local item = stackCtrl:AppendItem(item_id, text, image)
|
|
if checkIfExpandable(value, item) then
|
|
stackCtrl:SetItemHasChildren(item, true)
|
|
end
|
|
num = num + 1
|
|
end
|
|
|
|
stackCtrl:SortChildren(item_id)
|
|
return true
|
|
end)
|
|
stackCtrl:Connect( wx.wxEVT_COMMAND_TREE_ITEM_COLLAPSED,
|
|
function() return true end)
|
|
|
|
updateStackAndWatches()
|
|
end
|
|
|
|
function DebuggerCreateWatchWindow()
|
|
if (debugger.watchWindow) then return updateWatches() end
|
|
local width = 360
|
|
local watchWindow = wx.wxFrame(ide.frame, wx.wxID_ANY,
|
|
TR("Watch Window"),
|
|
wx.wxDefaultPosition, wx.wxSize(width, 200),
|
|
wx.wxDEFAULT_FRAME_STYLE + wx.wxFRAME_FLOAT_ON_PARENT)
|
|
|
|
debugger.watchWindow = watchWindow
|
|
|
|
local watchMenu = wx.wxMenu{
|
|
{ ID_ADDWATCH, TR("&Add Watch")..KSC(ID_ADDWATCH) },
|
|
{ ID_EDITWATCH, TR("&Edit Watch")..KSC(ID_EDITWATCH) },
|
|
{ ID_REMOVEWATCH, TR("&Remove Watch")..KSC(ID_REMOVEWATCH) },
|
|
{ ID_EVALUATEWATCH, TR("Evaluate &Watches")..KSC(ID_EVALUATEWATCH) }}
|
|
|
|
local watchMenuBar = wx.wxMenuBar()
|
|
watchMenuBar:Append(watchMenu, TR("&Watches"))
|
|
watchWindow:SetMenuBar(watchMenuBar)
|
|
|
|
local watchCtrl = wx.wxListCtrl(watchWindow, wx.wxID_ANY,
|
|
wx.wxDefaultPosition, wx.wxDefaultSize,
|
|
wx.wxLC_REPORT + wx.wxLC_EDIT_LABELS)
|
|
|
|
debugger.watchCtrl = watchCtrl
|
|
|
|
local info = wx.wxListItem()
|
|
info:SetMask(wx.wxLIST_MASK_TEXT + wx.wxLIST_MASK_WIDTH)
|
|
info:SetText(TR("Expression"))
|
|
info:SetWidth(width * 0.32)
|
|
watchCtrl:InsertColumn(0, info)
|
|
|
|
info:SetText(TR("Value"))
|
|
info:SetWidth(width * 0.56)
|
|
watchCtrl:InsertColumn(1, info)
|
|
|
|
watchWindow:CentreOnParent()
|
|
SettingsRestoreFramePosition(watchWindow, "WatchWindow")
|
|
watchWindow:Show(true)
|
|
|
|
local function findSelectedWatchItem()
|
|
local count = watchCtrl:GetSelectedItemCount()
|
|
if count > 0 then
|
|
for idx = 0, watchCtrl:GetItemCount() - 1 do
|
|
if watchCtrl:GetItemState(idx, wx.wxLIST_STATE_FOCUSED) ~= 0 then
|
|
return idx
|
|
end
|
|
end
|
|
end
|
|
return -1
|
|
end
|
|
|
|
local defaultExpr = ""
|
|
|
|
watchWindow:Connect(wx.wxEVT_CLOSE_WINDOW,
|
|
function (event)
|
|
DebuggerCloseWatchWindow()
|
|
watchWindow = nil
|
|
watchCtrl = nil
|
|
event:Skip()
|
|
end)
|
|
|
|
watchWindow:Connect(ID_ADDWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
|
function ()
|
|
local row = watchCtrl:InsertItem(watchCtrl:GetItemCount(), TR("Expr"))
|
|
watchCtrl:SetItem(row, 0, defaultExpr)
|
|
watchCtrl:SetItem(row, 1, TR("Value"))
|
|
watchCtrl:EditLabel(row)
|
|
end)
|
|
|
|
watchWindow:Connect(ID_EDITWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
|
function ()
|
|
local row = findSelectedWatchItem()
|
|
if row >= 0 then
|
|
watchCtrl:EditLabel(row)
|
|
end
|
|
end)
|
|
watchWindow:Connect(ID_EDITWATCH, wx.wxEVT_UPDATE_UI,
|
|
function (event)
|
|
event:Enable(watchCtrl:GetSelectedItemCount() > 0)
|
|
end)
|
|
|
|
watchWindow:Connect(ID_REMOVEWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
|
function ()
|
|
local row = findSelectedWatchItem()
|
|
if row >= 0 then
|
|
watchCtrl:DeleteItem(row)
|
|
end
|
|
end)
|
|
watchWindow:Connect(ID_REMOVEWATCH, wx.wxEVT_UPDATE_UI,
|
|
function (event)
|
|
event:Enable(watchCtrl:GetSelectedItemCount() > 0)
|
|
end)
|
|
|
|
watchWindow:Connect(ID_EVALUATEWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
|
function () updateWatches() end)
|
|
watchWindow:Connect(ID_EVALUATEWATCH, wx.wxEVT_UPDATE_UI,
|
|
function (event)
|
|
event:Enable(watchCtrl:GetItemCount() > 0)
|
|
end)
|
|
|
|
watchCtrl:Connect(wx.wxEVT_COMMAND_LIST_END_LABEL_EDIT,
|
|
function (event)
|
|
local row = event:GetIndex()
|
|
if event:IsEditCancelled() then
|
|
if watchCtrl:GetItemText(row) == defaultExpr then
|
|
watchCtrl:DeleteItem(row)
|
|
end
|
|
else
|
|
watchCtrl:SetItem(row, 0, event:GetText())
|
|
updateWatches(row)
|
|
end
|
|
event:Skip()
|
|
end)
|
|
end
|
|
|
|
function DebuggerAddWatch(watch)
|
|
if (not debugger.watchWindow) then DebuggerCreateWatchWindow() end
|
|
|
|
local watchCtrl = debugger.watchCtrl
|
|
-- check if this expression is already on the list
|
|
for idx = 0, watchCtrl:GetItemCount() - 1 do
|
|
if watchCtrl:GetItemText(idx) == watch then return end
|
|
end
|
|
|
|
local row = watchCtrl:InsertItem(watchCtrl:GetItemCount(), TR("Expr"))
|
|
watchCtrl:SetItem(row, 0, watch)
|
|
watchCtrl:SetItem(row, 1, TR("Value"))
|
|
|
|
updateWatches(row)
|
|
end
|
|
|
|
function DebuggerMakeFileName(editor, filePath)
|
|
return filePath or ide.config.default.fullname
|
|
end
|
|
|
|
function DebuggerToggleBreakpoint(editor, line)
|
|
-- ignore requests to toggle when the debugger is running
|
|
if debugger.server and debugger.running then return end
|
|
local markers = editor:MarkerGet(line)
|
|
if markers >= CURRENT_LINE_MARKER_VALUE then
|
|
markers = markers - CURRENT_LINE_MARKER_VALUE
|
|
end
|
|
local id = editor:GetId()
|
|
local filePath = DebuggerMakeFileName(editor, ide.openDocuments[id].filePath)
|
|
if markers >= BREAKPOINT_MARKER_VALUE then
|
|
editor:MarkerDelete(line, BREAKPOINT_MARKER)
|
|
if debugger.server then
|
|
debugger.breakpoint(filePath, line+1, false)
|
|
end
|
|
else
|
|
editor:MarkerAdd(line, BREAKPOINT_MARKER)
|
|
if debugger.server then
|
|
debugger.breakpoint(filePath, line+1, true)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- scratchpad functions
|
|
|
|
local function q(s) return s:gsub('([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') end
|
|
function DebuggerRefreshScratchpad()
|
|
if debugger.scratchpad and debugger.scratchpad.updated then
|
|
|
|
local scratchpadEditor = debugger.scratchpad.editor
|
|
local compiled, code = CompileProgram(scratchpadEditor, true)
|
|
if not compiled then return end
|
|
|
|
if debugger.scratchpad.running then
|
|
-- break the current execution first
|
|
-- don't try too frequently to avoid overwhelming the debugger
|
|
local now = TimeGet()
|
|
if now - debugger.scratchpad.running > 0.250 then
|
|
debugger.breaknow()
|
|
debugger.scratchpad.running = now
|
|
end
|
|
else
|
|
local clear = ide.frame.menuBar:IsChecked(ID_CLEAROUTPUT)
|
|
local filePath = DebuggerMakeFileName(scratchpadEditor,
|
|
ide.openDocuments[scratchpadEditor:GetId()].filePath)
|
|
|
|
-- wrap into a function call to make "return" to work with scratchpad
|
|
code = "(function()"..code.."\nend)()"
|
|
|
|
-- this is a special error message that is generated at the very end
|
|
-- of each script to avoid exiting the (debugee) scratchpad process.
|
|
-- these errors are handled and not reported to the user
|
|
local errormsg = 'execution suspended at ' .. TimeGet()
|
|
local stopper = "error('" .. errormsg .. "')"
|
|
-- store if interpreter requires a special handling for external loop
|
|
local extloop = ide.interpreter.scratchextloop
|
|
|
|
local function reloadScratchpadCode()
|
|
debugger.scratchpad.running = TimeGet()
|
|
debugger.scratchpad.updated = false
|
|
debugger.scratchpad.runs = (debugger.scratchpad.runs or 0) + 1
|
|
|
|
if clear then ClearOutput() end
|
|
|
|
-- the code can be running in two ways under scratchpad:
|
|
-- 1. controlled by the application, requires stopper (most apps)
|
|
-- 2. controlled by some external loop (for example, love2d).
|
|
-- in the first case we need to reload the app after each change
|
|
-- in the second case, we need to load the app once and then
|
|
-- "execute" new code to reflect the changes (with some limitations).
|
|
local _, _, err
|
|
if extloop then -- if the execution is controlled by an external loop
|
|
if debugger.scratchpad.runs == 1
|
|
then _, _, err = debugger.loadstring(filePath, code)
|
|
else _, _, err = debugger.execute(code) end
|
|
else _, _, err = debugger.loadstring(filePath, code .. stopper) end
|
|
|
|
-- when execute() is used, it's not possible to distinguish between
|
|
-- compilation and run-time error, so just report as "Scratchpad error"
|
|
local prefix = extloop and TR("Scratchpad error") or TR("Compilation error")
|
|
|
|
if not err then
|
|
_, _, err = debugger.handle("run")
|
|
prefix = TR("Execution error")
|
|
end
|
|
if err and not err:find(errormsg) then
|
|
local fragment, line = err:match('.-%[string "([^\010\013]+)"%]:(%d+)%s*:')
|
|
-- make the code shorter to better see the error message
|
|
if prefix == TR("Scratchpad error") and fragment and #fragment > 30 then
|
|
err = err:gsub(q(fragment), function(s) return s:sub(1,30)..'...' end)
|
|
end
|
|
DisplayOutputLn(prefix
|
|
..(line and (" "..TR("on line %d"):format(line)) or "")
|
|
..":\n"..err:gsub('stack traceback:.+', ''):gsub('\n+$', ''))
|
|
end
|
|
debugger.scratchpad.running = false
|
|
end
|
|
|
|
copas.addthread(reloadScratchpadCode)
|
|
end
|
|
end
|
|
end
|
|
|
|
local numberStyle = wxstc.wxSTC_LUA_NUMBER
|
|
|
|
function DebuggerScratchpadOn(editor)
|
|
-- first check if there is already scratchpad editor.
|
|
-- this may happen when more than one editor is being added...
|
|
|
|
if debugger.scratchpad and debugger.scratchpad.editors then
|
|
debugger.scratchpad.editors[editor] = true
|
|
else
|
|
debugger.scratchpad = {editor = editor, editors = {[editor] = true}}
|
|
|
|
-- check if the debugger is already running; this happens when
|
|
-- scratchpad is turned on after external script has connected
|
|
if debugger.server then
|
|
debugger.scratchpad.updated = true
|
|
ClearAllCurrentLineMarkers()
|
|
SetAllEditorsReadOnly(false)
|
|
ShellSupportRemote(nil) -- disable remote shell
|
|
DebuggerRefreshScratchpad()
|
|
elseif not ProjectDebug(true, "scratchpad") then
|
|
debugger.scratchpad = nil
|
|
return
|
|
end
|
|
end
|
|
|
|
local scratchpadEditor = editor
|
|
scratchpadEditor:StyleSetUnderline(numberStyle, true)
|
|
debugger.scratchpad.margin = scratchpadEditor:GetMarginWidth(0) +
|
|
scratchpadEditor:GetMarginWidth(1) + scratchpadEditor:GetMarginWidth(2)
|
|
|
|
scratchpadEditor:Connect(wxstc.wxEVT_STC_MODIFIED, function(event)
|
|
local evtype = event:GetModificationType()
|
|
if (bit.band(evtype,wxstc.wxSTC_MOD_INSERTTEXT) ~= 0 or
|
|
bit.band(evtype,wxstc.wxSTC_MOD_DELETETEXT) ~= 0 or
|
|
bit.band(evtype,wxstc.wxSTC_PERFORMED_UNDO) ~= 0 or
|
|
bit.band(evtype,wxstc.wxSTC_PERFORMED_REDO) ~= 0) then
|
|
debugger.scratchpad.updated = true
|
|
debugger.scratchpad.editor = scratchpadEditor
|
|
end
|
|
event:Skip()
|
|
end)
|
|
|
|
scratchpadEditor:Connect(wx.wxEVT_LEFT_DOWN, function(event)
|
|
local scratchpad = debugger.scratchpad
|
|
|
|
local point = event:GetPosition()
|
|
local pos = scratchpadEditor:PositionFromPoint(point)
|
|
|
|
-- are we over a number in the scratchpad? if not, it's not our event
|
|
if ((not scratchpad) or
|
|
(bit.band(scratchpadEditor:GetStyleAt(pos),31) ~= numberStyle)) then
|
|
event:Skip()
|
|
return
|
|
end
|
|
|
|
-- find start position and length of the number
|
|
local text = scratchpadEditor:GetText()
|
|
|
|
local nstart = pos
|
|
while nstart >= 0
|
|
and (bit.band(scratchpadEditor:GetStyleAt(nstart),31) == numberStyle)
|
|
do nstart = nstart - 1 end
|
|
|
|
local nend = pos
|
|
while nend < string.len(text)
|
|
and (bit.band(scratchpadEditor:GetStyleAt(nend),31) == numberStyle)
|
|
do nend = nend + 1 end
|
|
|
|
-- check if there is minus sign right before the number and include it
|
|
if nstart >= 0 and scratchpadEditor:GetTextRange(nstart,nstart+1) == '-' then
|
|
nstart = nstart - 1
|
|
end
|
|
scratchpad.start = nstart + 1
|
|
scratchpad.length = nend - nstart - 1
|
|
scratchpad.origin = scratchpadEditor:GetTextRange(nstart+1,nend)
|
|
if tonumber(scratchpad.origin) then
|
|
scratchpad.point = point
|
|
scratchpadEditor:CaptureMouse()
|
|
end
|
|
end)
|
|
|
|
scratchpadEditor:Connect(wx.wxEVT_LEFT_UP, function(event)
|
|
if debugger.scratchpad and debugger.scratchpad.point then
|
|
debugger.scratchpad.point = nil
|
|
scratchpadEditor:ReleaseMouse()
|
|
wx.wxSetCursor(wx.wxNullCursor) -- restore cursor
|
|
else event:Skip() end
|
|
end)
|
|
|
|
scratchpadEditor:Connect(wx.wxEVT_MOTION, function(event)
|
|
local point = event:GetPosition()
|
|
local pos = scratchpadEditor:PositionFromPoint(point)
|
|
local scratchpad = debugger.scratchpad
|
|
local ipoint = scratchpad and scratchpad.point
|
|
|
|
-- record the fact that we are over a number or dragging slider
|
|
scratchpad.over = scratchpad and
|
|
(ipoint ~= nil or (bit.band(scratchpadEditor:GetStyleAt(pos),31) == numberStyle))
|
|
|
|
if ipoint then
|
|
local startpos = scratchpad.start
|
|
local endpos = scratchpad.start+scratchpad.length
|
|
|
|
-- calculate difference in point position
|
|
local dx = point.x - ipoint.x
|
|
|
|
-- calculate the number of decimal digits after the decimal point
|
|
local origin = scratchpad.origin
|
|
local decdigits = #(origin:match('%.(%d+)') or '')
|
|
|
|
-- calculate new value
|
|
local value = tonumber(origin) + dx * 10^-decdigits
|
|
|
|
-- convert new value back to string to check the number of decimal points
|
|
-- this is needed because the rate of change is determined by the
|
|
-- current value. For example, for number 1, the next value is 2,
|
|
-- but for number 1.1, the next is 1.2 and for 1.01 it is 1.02.
|
|
-- But if 1.01 becomes 1.00, the both zeros after the decimal point
|
|
-- need to be preserved to keep the increment ratio the same when
|
|
-- the user wants to release the slider and start again.
|
|
origin = tostring(value)
|
|
local newdigits = #(origin:match('%.(%d+)') or '')
|
|
if decdigits ~= newdigits then
|
|
origin = origin .. (origin:find('%.') and '' or '.') .. ("0"):rep(decdigits-newdigits)
|
|
end
|
|
|
|
-- update length
|
|
scratchpad.length = #origin
|
|
|
|
-- update the value in the document
|
|
scratchpadEditor:SetTargetStart(startpos)
|
|
scratchpadEditor:SetTargetEnd(endpos)
|
|
scratchpadEditor:ReplaceTarget(origin)
|
|
else event:Skip() end
|
|
end)
|
|
|
|
scratchpadEditor:Connect(wx.wxEVT_SET_CURSOR, function(event)
|
|
if (debugger.scratchpad and debugger.scratchpad.over) then
|
|
event:SetCursor(wx.wxCursor(wx.wxCURSOR_SIZEWE))
|
|
elseif debugger.scratchpad and ide.osname == 'Unix' then
|
|
-- restore the cursor manually on Linux since event:Skip() doesn't reset it
|
|
local ibeam = event:GetX() > debugger.scratchpad.margin
|
|
event:SetCursor(wx.wxCursor(ibeam and wx.wxCURSOR_IBEAM or wx.wxCURSOR_RIGHT_ARROW))
|
|
else event:Skip() end
|
|
end)
|
|
|
|
return true
|
|
end
|
|
|
|
function DebuggerScratchpadOff()
|
|
if not debugger.scratchpad then return end
|
|
|
|
for scratchpadEditor in pairs(debugger.scratchpad.editors) do
|
|
scratchpadEditor:StyleSetUnderline(numberStyle, false)
|
|
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wxstc.wxEVT_STC_MODIFIED)
|
|
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_MOTION)
|
|
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_LEFT_DOWN)
|
|
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_LEFT_UP)
|
|
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_SET_CURSOR)
|
|
end
|
|
|
|
wx.wxSetCursor(wx.wxNullCursor) -- restore cursor
|
|
|
|
debugger.scratchpad = nil
|
|
debugger.terminate()
|
|
|
|
-- disable menu if it is still enabled
|
|
-- (as this may be called when the debugger is being shut down)
|
|
local menuBar = ide.frame.menuBar
|
|
if menuBar:IsChecked(ID_RUNNOW) then menuBar:Check(ID_RUNNOW, false) end
|
|
|
|
return true
|
|
end
|