Files
OpenRA/src/editor/debugger.lua

1178 lines
44 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 CURRENT_LINE_MARKER = StylesGetMarker("currentline")
local CURRENT_LINE_MARKER_VALUE = 2^CURRENT_LINE_MARKER
local BREAKPOINT_MARKER = StylesGetMarker("breakpoint")
local BREAKPOINT_MARKER_VALUE = 2^BREAKPOINT_MARKER
local function q(s) return s:gsub('([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') end
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
-- comment can be not necessarily a string for tables with metatables
-- that provide its own __tostring method
local value, comment = val[1], tostring(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, skipauto)
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
RequestAttention()
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 or indebugger or debugger.loop or skipauto)
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 or debugger.scratchpad.paused) 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 addedret 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 not (options.run or debugger.scratchpad) then
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, true)
-- 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, true)
if activated then
debugger.basedir = startpath
debugger.handle("basedir " .. debugger.basedir)
-- reset breakpoints again as basedir has changed
reSetBreakpoints()
end
end
-- if not found and the files doesn't exist, it may be
-- a remote call; try to map it to the project folder
if not activated and not wx.wxFileName(file):FileExists() then
-- file is /foo/bar/my.lua; basedir is d:\local\path\
-- check for d:\local\path\my.lua, d:\local\path\bar\my.lua, ...
-- wxwidgets on Windows handles \\ and / as separators, but on OSX
-- and Linux it only handles 'native' separator;
-- need to translate for GetDirs to work.
local file = file:gsub("\\", "/")
local parts = wx.wxFileName(file):GetDirs()
local name = wx.wxFileName(file):GetFullName()
-- find the longest remote path that can be mapped locally
local longestpath, remotedir
while true do
local mapped = GetFullPathIfExists(basedir, name)
if mapped then
longestpath = mapped
remotedir = file:gsub(q(name):gsub("/", ".").."$", "")
end
if #parts == 0 then break end
name = table.remove(parts, #parts) .. "/" .. name
end
-- if found a local mapping under basedir
activated = longestpath and activateDocument(longestpath, line, true)
if activated then
-- find remote basedir by removing the tail from remote file
debugger.handle("basedir " .. debugger.basedir .. "\t" .. remotedir)
-- reset breakpoints again as remote basedir has changed
reSetBreakpoints()
DisplayOutputLn(TR("Mapped remote request for '%s' to '%s'.")
:format(remotedir, debugger.basedir))
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 (debugger.scratchpad) then
debugger.scratchpad.updated = true
else
if (options.runstart) then
ClearAllCurrentLineMarkers()
debugger.run()
end
if (options.run) then
local file, line = debugger.handle("run")
activateDocument(file, line)
end
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
-- clear markers before running the command
-- don't clear if running trace as the marker is then invisible,
-- and it needs to be visible during tracing
if not debugger.loop then ClearAllCurrentLineMarkers() end
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
elseif not debugger.server then
-- it is possible that while debugger.handle call was executing
-- the debugging was terminated; simply return in this case.
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
-- clear the marker as it wasn't cleared earlier
if debugger.loop then ClearAllCurrentLineMarkers() end
-- 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
-- wait for a little bit as in some rare cases when closing the debugger
-- with a running application under OSX, the process crashes (wxlua2.8.12).
if ide.osname == "Macintosh" then wx.wxMilliSleep(100) 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
function DebuggerRefreshScratchpad()
if debugger.scratchpad and debugger.scratchpad.updated and not debugger.scratchpad.paused 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