-- Copyright 2011-18 Paul Kulchenko, ZeroBrane LLC -- Original authors: Lomtik Software (J. Winwood & John Labenski) -- Luxinia Dev (Eike Decker & Christoph Kubisch) -- Integration with MobDebug --------------------------------------------------------- local copas = require "copas" local socket = require "socket" local mobdebug = require "mobdebug" local unpack = table.unpack or unpack local ide = ide local protodeb = setmetatable(ide:GetDebugger(), ide.proto.Debugger) local debugger = protodeb 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.watchCtrl = nil -- the watch ctrl that shows watch information debugger.stackCtrl = nil -- the stack ctrl that shows stack information debugger.toggleview = { bottomnotebook = true, -- output/console is "on" by default stackpanel = false, watchpanel = false, toolbar = false } debugger.needrefresh = {} -- track components that may need a refresh debugger.hostname = ide.config.debugger.hostname or (function() local hostname = socket.dns.gethostname() return hostname and socket.dns.toip(hostname) and hostname or "localhost" end)() debugger.imglist = ide:CreateImageList("STACK", "VALUE-CALL", "VALUE-LOCAL", "VALUE-UP") local image = { STACK = 0, LOCAL = 1, UPVALUE = 2 } 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 activate = {CHECKONLY = "checkonly", NOREPORT = "noreport", CLEARALL = "clearall"} local function serialize(value, options) return mobdebug.line(value, options) end local function displayError(...) return ide:GetOutput():Error(...) end local function fixUTF8(...) local t = {...} -- convert to escaped decimal code as these can only appear in strings local function fix(s) return '\\'..string.byte(s) end for i = 1, #t do t[i] = FixUTF8(t[i], fix) end return unpack(t) end local q = EscapeMagic local MORE = "{...}" function debugger:init(init) local o = {} -- merge known self and init values for k, v in pairs(self) do o[k] = v end for k, v in pairs(init or {}) do o[k] = v end return setmetatable(o, {__index = protodeb}) end function debugger:updateWatchesSync(onlyitem) local debugger = self local watchCtrl = debugger.watchCtrl local pane = ide.frame.uimgr:GetPane("watchpanel") local shown = watchCtrl and (pane:IsOk() and pane:IsShown() or not pane:IsOk() and watchCtrl:IsShown()) local canupdate = (debugger.server and not debugger.running and not debugger.scratchpad and not (debugger.options or {}).noeval) if shown and canupdate then local bgcl = watchCtrl:GetBackgroundColour() local hicl = wx.wxColour(math.floor(bgcl:Red()*.9), math.floor(bgcl:Green()*.9), math.floor(bgcl:Blue()*.9)) local root = watchCtrl:GetRootItem() if not root or not root:IsOk() then return end local params = debugger:GetDataOptions({maxlength=false}) local item = onlyitem or watchCtrl:GetFirstChild(root) while true do if not item:IsOk() then break end local expression = watchCtrl:GetItemExpression(item) if expression then local _, values, error = debugger:evaluate(expression, params) local curchildren = watchCtrl:GetItemChildren(item) if error then error = error:gsub("%[.-%]:%d+:%s+","") watchCtrl:SetItemValueIfExpandable(item, nil) else if #values == 0 then values = {'nil'} end local _, res = LoadSafe("return "..values[1]) watchCtrl:SetItemValueIfExpandable(item, res) end local newval = fixUTF8(expression .. ' = ' .. (error and ('error: '..error) or table.concat(values, ", "))) local val = watchCtrl:GetItemText(item) watchCtrl:SetItemBackgroundColour(item, val ~= newval and hicl or bgcl) watchCtrl:SetItemText(item, newval) if onlyitem or val ~= newval then local newchildren = watchCtrl:GetItemChildren(item) if next(curchildren) ~= nil and next(newchildren) == nil then watchCtrl:SetItemHasChildren(item, true) watchCtrl:CollapseAndReset(item) watchCtrl:SetItemHasChildren(item, false) elseif next(curchildren) ~= nil and next(newchildren) ~= nil then watchCtrl:CollapseAndReset(item) watchCtrl:Expand(item) end end end if onlyitem then break end item = watchCtrl:GetNextSibling(item) end debugger.needrefresh.watches = false elseif not shown and canupdate then debugger.needrefresh.watches = true end end local callData = {} function debugger:updateStackSync() local debugger = self local stackCtrl = debugger.stackCtrl local pane = ide.frame.uimgr:GetPane("stackpanel") local shown = stackCtrl and (pane:IsOk() and pane:IsShown() or not pane:IsOk() and stackCtrl:IsShown()) local canupdate = debugger.server and not debugger.running and not debugger.scratchpad if shown and canupdate then local stack, _, err = debugger:stack(debugger:GetDataOptions({maxlength=false})) if not stack or #stack == 0 then stackCtrl:DeleteAll() if err then -- report an error if any stackCtrl:AppendItem(stackCtrl:AddRoot("Stack"), "Error: " .. err, image.STACK) end return end stackCtrl:Freeze() stackCtrl:DeleteAll() local forceexpand = ide.config.debugger.maxdatalevel == 1 local params = debugger:GetDataOptions({maxlevel=false}) local maxlen = tonumber(ide.config.debugger.maxdatalength) local root = stackCtrl:AddRoot("Stack") callData = {} -- reset call cache for _,frame in ipairs(stack) do -- check if the stack includes expected structures if type(frame) ~= "table" or type(frame[1]) ~= "table" or #frame[1] < 7 then break end -- "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] -- format the function name to a readable user string 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") -- format the function treeitem text string, including the function name 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[7]..":"..call[3]..")" or " (defined in "..call[7]..")")) -- create the new tree item for this level of the call stack local callitem = stackCtrl:AppendItem(root, text, image.STACK) -- register call data to provide stack navigation callData[callitem:GetValue()] = { call[2], call[4] } -- add the local variables to the call stack item for name,val in pairs(type(frame[2]) == "table" and frame[2] or {}) do -- format the variable name, value as a single line and, -- if not a simple type, the string value. local value = val[1] if type(value) == "string" and maxlen and #value > maxlen then value = value:sub(1,maxlen) end local text = ("%s = %s"):format(name, fixUTF8(serialize(value, params))) local item = stackCtrl:AppendItem(callitem, text, image.LOCAL) stackCtrl:SetItemValueIfExpandable(item, value, forceexpand) stackCtrl:SetItemName(item, name) end -- add the upvalues for this call stack level to the tree item for name,val in pairs(type(frame[3]) == "table" and frame[3] or {}) do local value = val[1] if type(value) == "string" and maxlen and #value > maxlen then value = value:sub(1,maxlen) end local text = ("%s = %s"):format(name, fixUTF8(serialize(value, params))) local item = stackCtrl:AppendItem(callitem, text, image.UPVALUE) stackCtrl:SetItemValueIfExpandable(item, value, forceexpand) stackCtrl:SetItemName(item, name) end stackCtrl:SortChildren(callitem) stackCtrl:Expand(callitem) end stackCtrl:EnsureVisible(stackCtrl:GetFirstChild(root)) stackCtrl:Thaw() stackCtrl:SetScrollPos(wx.wxHORIZONTAL, 0, true) debugger.needrefresh.stack = false elseif not shown and canupdate then debugger.needrefresh.stack = true end end function debugger:updateStackAndWatches() local debugger = self -- check if the debugger is running and may be waiting for a response. -- allow that request to finish, otherwise this function does nothing. if debugger.running then debugger:Update() end if debugger.server and not debugger.running then copas.addthread(function() local debugger = debugger debugger:updateStackSync() debugger:updateWatchesSync() end) end end function debugger:updateWatches(item) local debugger = self -- check if the debugger is running and may be waiting for a response. -- allow that request to finish, otherwise this function does nothing. if debugger.running then debugger:Update() end if debugger.server and not debugger.running then copas.addthread(function() local debugger = debugger debugger:updateWatchesSync(item) end) end end function debugger:updateStack() local debugger = self -- check if the debugger is running and may be waiting for a response. -- allow that request to finish, otherwise this function does nothing. if debugger.running then debugger:Update() end if debugger.server and not debugger.running then copas.addthread(function() local debugger = debugger debugger:updateStackSync() end) end end function debugger:toggleViews(show) local debugger = self -- don't toggle if the current state is the same as the new one local shown = debugger.toggleview.shown if (show and shown) or (not show and not shown) then return end debugger.toggleview.shown = nil local mgr = ide.frame.uimgr local refresh = false for view, needed in pairs(debugger.toggleview) do local bar = view == 'toolbar' local pane = mgr:GetPane(view) if show then -- starting debugging and pane is not shown -- show toolbar during debugging if hidden and not fullscreen debugger.toggleview[view] = (not pane:IsShown() and (not bar or not ide.frame:IsFullScreen())) if debugger.toggleview[view] and (needed or bar) then pane:Show() refresh = true end else -- completing debugging and pane is shown debugger.toggleview[view] = pane:IsShown() and needed if debugger.toggleview[view] then pane:Hide() refresh = true end end end if refresh then mgr:Update() end if show then debugger.toggleview.shown = true end end local function killProcess(pid) if not pid then return false end if wx.wxProcess.Exists(pid) then local _ = wx.wxLogNull() -- disable error popup; will report as needed -- 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(pid, wx.wxSIGKILL, wx.wxKILL_CHILDREN) if ret == wx.wxKILL_OK then ide:Print(TR("Program stopped (pid: %d)."):format(pid)) elseif ret ~= wx.wxKILL_NO_PROCESS then wx.wxMilliSleep(250) if wx.wxProcess.Exists(pid) then displayError(TR("Unable to stop program (pid: %d), code %d."):format(pid, ret)) return false end end end return true end function debugger:ActivateDocument(file, line, activatehow) if activatehow == activate.CLEARALL then ClearAllCurrentLineMarkers() end local debugger = self if not file then return end line = tonumber(line) -- file can be a filename or serialized file content; deserialize first. -- check if the filename starts with '"' and is deserializable -- to avoid showing filenames that may look like valid lua code -- (for example: 'mobdebug.lua'). local content if not wx.wxFileName(file):FileExists() and file:find('^"') then local ok, res = LoadSafe("return "..file) if ok then content = res end end -- in some cases filename can be returned quoted if the chunk is loaded with -- loadstring(chunk, "filename") instead of loadstring(chunk, "@filename") if content then -- if the returned content can be matched with a file, it's a file name local fname = GetFullPathIfExists(debugger.basedir, content) or content if wx.wxFileName(fname):FileExists() then file, content = fname, nil end elseif not wx.wxIsAbsolutePath(file) and debugger.basedir then file = debugger.basedir .. file end if PackageEventHandle("onDebuggerPreActivate", debugger, file, line) == false then return end local activated = false local indebugger = file:find('mobdebug%.lua$') local fileName = wx.wxFileName(file) local fileNameLower = wx.wxFileName(file:lower()) for _, document in pairs(ide:GetDocuments()) do local editor = document:GetEditor() -- either the file name matches, or the content; -- when checking for the content remove all newlines as they may be -- reported differently from the original by the Lua engine. local ignorecase = ide.config.debugger.ignorecase or (debugger.options or {}).ignorecase local filePath = document:GetFilePath() if filePath and (fileName:SameAs(wx.wxFileName(filePath)) or ignorecase and fileNameLower:SameAs(wx.wxFileName(filePath:lower()))) or content and content:gsub("[\n\r]","") == editor:GetTextDyn():gsub("[\n\r]","") then ClearAllCurrentLineMarkers() if line then if line == 0 then -- special case; find the first executable line line = math.huge local loadstring = loadstring or load local func = loadstring(editor:GetTextDyn()) if func then -- .activelines == {[3] = true, [4] = true, ...} for l in pairs(debug.getinfo(func, "L").activelines) do if l < line then line = l end end end if line == math.huge then line = 1 end end if debugger.runtocursor then local ed, ln = unpack(debugger.runtocursor) if ed:GetId() == editor:GetId() and ln == line then -- remove run-to breakpoint at this location debugger:breakpointToggle(ed, ln, false) debugger.runtocursor = nil end end local line = line - 1 -- editor line operations are zero-based editor:MarkerAdd(line, CURRENT_LINE_MARKER) editor:Refresh() -- needed for background markers that don't get refreshed (wx2.9.5) -- expand fold if the activated line is in a folded fragment if not editor:GetLineVisible(line) then editor:ToggleFold(editor:GetFoldParent(line)) end -- found and marked what we are looking for; -- don't need to activate with CHECKONLY (this assumes line is given) if activatehow == activate.CHECKONLY then return editor end local firstline = editor:DocLineFromVisible(editor:GetFirstVisibleLine()) local lastline = math.min(editor:GetLineCount(), editor:DocLineFromVisible(editor:GetFirstVisibleLine() + editor:LinesOnScreen())) -- if the line is already on the screen, then don't enforce policy if line <= firstline or line >= lastline then editor:EnsureVisibleEnforcePolicy(line) end end document:SetActive() ide:RequestAttention() if content then -- it's possible that the current editor tab already has -- breakpoints that have been set based on its filepath; -- if the content has been matched, then existing breakpoints -- need to be removed and new ones set, based on the content. if not debugger.editormap[editor] and filePath then local line = editor:MarkerNext(0, BREAKPOINT_MARKER_VALUE) while filePath and line ~= -1 do debugger:handle("delb " .. filePath .. " " .. (line+1)) debugger:handle("setb " .. file .. " " .. (line+1)) line = editor:MarkerNext(line + 1, BREAKPOINT_MARKER_VALUE) end end -- keep track of those editors that have been activated based on -- content rather than file names as their breakpoints have to be -- specified in a different way debugger.editormap[editor] = file end activated = editor break end end if not (activated or indebugger or debugger.loop or activatehow == activate.CHECKONLY) and (ide.config.editor.autoactivate or content and activatehow == activate.NOREPORT) then -- found file, but can't activate yet (because this part may be executed -- in a different coroutine), so schedule pending activation. if content or wx.wxFileName(file):FileExists() then debugger.activate = {file, line, content} return true -- report successful activation, even though it's pending end -- only report files once per session and if not asked to skip if not debugger.missing[file] and activatehow ~= activate.NOREPORT then debugger.missing[file] = true displayError(TR("Couldn't activate file '%s' for debugging; continuing without it.") :format(file)) end end PackageEventHandle("onDebuggerActivate", debugger, file, line, activated) return activated end function debugger:reSetBreakpoints() local debugger = self -- 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:GetDocuments()) do local editor = document:GetEditor() local filePath = document:GetFilePath() local line = editor:MarkerNext(0, BREAKPOINT_MARKER_VALUE) while filePath and line ~= -1 do debugger:handle("setb " .. filePath .. " " .. (line+1)) line = editor:MarkerNext(line + 1, BREAKPOINT_MARKER_VALUE) end end end end function debugger:shell(expression, isstatement) local debugger = self local loadstring = loadstring or load -- check if the debugger is running and may be waiting for a response. -- allow that request to finish, otherwise this function 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 -- default options for shell commands local params = debugger:GetDataOptions({ comment=true, maxlength=false, maxlevel=false, numformat=false}) -- any explicit options for this command for k, v in pairs(loadstring("return"..(expression:match("--%s*(%b{})%s*$") or "{}"))()) do params[k] = v end copas.addthread(function() local debugger = debugger -- 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, params) if not forceexpression and err then local _, values2, err2 = debugger:execute(expression, params) -- since the remote execution may fail during compilation- and run-time, -- and some expressions may fail in both cases, try to report the "best" error. -- for example, `x[1]` fails as statement, and may also fail if `x` is `nil`. -- in this case, the first (expression) error is returned if it's not a -- statement and compiles as an expression without errors. -- the order of statement and expression checks can't be reversed as errors from -- code fragments that fail with both, will be always reported as expressions. if not (err2 and not isstatement and loadstring("return "..expression)) then addedret, values, err = false, values2, err2 end end if err then if addedret then err = err:gsub('^%[string "return ', '[string "') end ide:GetConsole():Error(err) elseif addedret or #values > 0 then if forceexpression then -- display elements as multi-line 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 '') .. serialize(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 ide:GetConsole():Print(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 debugger:updateStackSync() debugger:updateWatchesSync() end end) elseif debugger.server then ide:GetConsole():Error(TR("Can't evaluate the expression while the application is running.")) end end function debugger:stoppedAtBreakpoint(file, line) -- if this document can be activated and the current line has a breakpoint local editor = self:ActivateDocument(file, line, activate.CHECKONLY) if not editor then return false end local current = editor:MarkerNext(0, CURRENT_LINE_MARKER_VALUE) local breakpoint = editor:MarkerNext(current, BREAKPOINT_MARKER_VALUE) return breakpoint ~= wx.wxNOT_FOUND and breakpoint == current end function debugger:mapRemotePath(basedir, file, line, method) local debugger = self if not file then return end -- 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 the mapped directory empty or the same as the basedir, nothing to do if not remotedir or remotedir == "" or wx.wxFileName(remotedir):SameAs(wx.wxFileName(debugger.basedir)) then return end -- if found a local mapping under basedir local activated = longestpath and (debugger:ActivateDocument(longestpath, line, method or activate.NOREPORT) -- local file may exist, but not activated when not (auto-)opened, still need to remap or wx.wxFileName(longestpath):FileExists()) 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 debugger:reSetBreakpoints() ide:Print(TR("Mapped remote request for '%s' to '%s'."):format(remotedir, debugger.basedir)) return longestpath end return nil end function debugger:Listen(start) local debugger = ide:GetDebugger() if start == false then if debugger.listening then debugger:terminate() -- terminate if running copas.removeserver(debugger.listening) ide:Print(TR("Debugger server stopped at %s:%d.") :format(debugger.hostname, debugger.portnumber)) debugger.listening = false else displayError(TR("Can't stop debugger server as it is not started.")) end return end if debugger.listening then return end local server, err = socket.bind("*", debugger.portnumber) if not server then displayError(TR("Can't start debugger server at %s:%d: %s.") :format(debugger.hostname, debugger.portnumber, err or TR("unknown error"))) return end ide:Print(TR("Debugger server started at %s:%d."):format(debugger.hostname, debugger.portnumber)) copas.autoclose = false copas.addserver(server, function (skt) local debugger = ide:GetDebugger() local options = debugger.options or {} if options.refuseonconflict == nil then options.refuseonconflict = ide.config.debugger.refuseonconflict end -- pull any pending data not processed yet if debugger.running then debugger:Update() end if debugger.server and options.refuseonconflict then displayError(TR("Refused a request to start a new debugging session as there is one in progress already.")) return end -- error handler is set per-copas-thread copas.setErrorHandler(function(error) -- ignore errors that happen because debugging session is -- terminated during handshake (server == nil in this case). if debugger.server then displayError(TR("Can't start debugging session due to internal error '%s'."):format(error)) end debugger:terminate() end) -- this may be a remote call without using an interpreter and as such -- debugger.options may not be set, but runonstart is still configured. local runstart = options.runstart if runstart == nil then runstart = ide.config.debugger.runonstart end -- support allowediting as set in the interpreter or config if options.allowediting == nil then options.allowediting = ide.config.debugger.allowediting end if not debugger.scratchpad and not options.allowediting then SetAllEditorsReadOnly(true) end debugger = ide:SetDebugger(debugger:init({ server = copas.wrap(skt), socket = skt, loop = false, scratchable = false, stats = {line = 0}, missing = {}, editormap = {}, runtocursor = nil, })) if PackageEventHandle("onDebuggerPreLoad", debugger, options) == false then return end local editor = ide:GetEditor() local startfile = ide:GetProjectStartFile() or options.startwith or (editor and SaveIfModified(editor) and ide:GetDocument(editor):GetFilePath()) if not startfile then displayError(TR("Can't start debugging without an opened file or with the current file not being saved.")) return debugger:terminate() end local startpath = wx.wxFileName(startfile):GetPath(wx.wxPATH_GET_VOLUME + wx.wxPATH_GET_SEPARATOR) local basedir = options.basedir or ide:GetProject() 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) local init = options.init or ide.config.debugger.init if init then local _, _, err = debugger:execute(init) if err then displayError(TR("Ignored error in debugger initialization code: %s."):format(err)) end end debugger:reSetBreakpoints() local redirect = ide.config.debugger.redirect or options.redirect if redirect then debugger:handle("output stdout " .. redirect, nil, { handler = function(m) -- 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 ok, res = LoadSafe("return "..m) if ok then ide:Print(res) return end end if ide.config.debugger.outputfilter then local ok, res = pcall(ide.config.debugger.outputfilter, m) if ok then m = res else displayError("Output filter failed: "..res) return end end if m then ide:GetOutput():Write(m) end end}) end if (options.startwith) then local file, line, err = debugger:loadfile(options.startwith) if err then displayError(TR("Can't run the entry point script ('%s').") :format(options.startwith) .." "..TR("Compilation error") ..":\n"..err) return debugger:terminate() elseif runstart and not debugger.scratchpad then if debugger:stoppedAtBreakpoint(file, line) then debugger:ActivateDocument(file, line) runstart = false end elseif file and line and not debugger:ActivateDocument(file, line) then displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).") :format(file, line)) end elseif not 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 err then displayError(TR("Can't start debugging for '%s'."):format(startfile) .." "..TR("Compilation error") ..":\n"..err) return debugger:terminate() elseif runstart then local file = (debugger:mapRemotePath(basedir, file, line or 0, activate.CHECKONLY) or file or startfile) if debugger:stoppedAtBreakpoint(file, line or 0) then debugger:ActivateDocument(file, line or 0) runstart = false end elseif file and line then local activated = debugger:ActivateDocument(file, line, activate.NOREPORT) -- if not found, check using full file path and reset basedir if not activated and not wx.wxIsAbsolutePath(file) then activated = debugger:ActivateDocument(startpath..file, line, activate.NOREPORT) if activated then debugger.basedir = startpath debugger:handle("basedir " .. debugger.basedir) -- reset breakpoints again as basedir has changed debugger: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. -- also check for absolute path as it may need to be remapped -- when autoactivation is disabled. if not activated and (not wx.wxFileName(file):FileExists() or wx.wxIsAbsolutePath(file)) then if debugger:mapRemotePath(basedir, file, line, activate.NOREPORT) then activated = true end end if not activated then displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).") :format(file, line)) end -- debugger may still be available for scratchpad, -- if the interpreter signals scratchpad support, so enable it. debugger.scratchable = ide.interpreter.scratchextloop ~= nil else debugger.scratchable = true local activated = debugger:ActivateDocument(startfile, 0) -- find the appropriate line if not activated then displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).") :format(startfile, '?')) end end end if (not options.noshell and not debugger.scratchpad) then ide:GetConsole():SetRemote(debugger:GetConsole()) end debugger:toggleViews(true) debugger:updateStackSync() debugger:updateWatchesSync() ide:Print(TR("Debugging session started in '%s'."):format(debugger.basedir)) if debugger.scratchpad then debugger.scratchpad.updated = true elseif runstart then ClearAllCurrentLineMarkers() debugger:Run() end -- request attention if the debugging is stopped if not debugger.running then ide:RequestAttention() end -- refresh toolbar and menus in case the main app is not active ide:GetMainFrame():UpdateWindowUI(wx.wxUPDATE_UI_FROMIDLE) ide:GetToolBar():UpdateWindowUI(wx.wxUPDATE_UI_FROMIDLE) PackageEventHandle("onDebuggerLoad", debugger, options) end) debugger.listening = server end local function nameOutputTab(name) local nbk = ide.frame.bottomnotebook local index = nbk:GetPageIndex(ide:GetOutput()) if index ~= wx.wxNOT_FOUND then nbk:SetPageText(index, name) end end local ok, winapi = pcall(require, 'winapi') if not ok then winapi = nil end function debugger:handle(command, server, options) local debugger = self local verbose = ide.config.debugger.verbose options = options or {} options.verbose = verbose and (function(...) ide:Print(...) end) or false local ip, port = debugger.socket:getpeername() PackageEventHandle("onDebuggerCommand", debugger, command, server or debugger.server, options) debugger.running = true debugger:UpdateStatus("running") if verbose then ide:Print(("[%s:%s] Debugger sent (command):"):format(ip, port), command) end local file, line, err = mobdebug.handle(command, server or debugger.server, options) if verbose then ide:Print(("[%s:%s] Debugger received (file, line, err):"):format(ip, port), file, line, err) end debugger.running = false -- only set suspended if the debugging hasn't been terminated debugger:UpdateStatus(debugger.server and "suspended" or "stopped") -- some filenames may be represented in a different code page; check and re-encode as UTF8 local codepage = ide:GetCodePage() if codepage and type(file) == "string" and FixUTF8(file) == nil and winapi then file = winapi.encode(codepage, winapi.CP_UTF8, file) end return file, line, err end function debugger:exec(command, func) local debugger = self if debugger.server and not debugger.running then copas.addthread(function() local debugger = debugger -- execute a custom function (if any) in the context of this thread if type(func) == 'function' then func() end 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 displayError(err) end debugger:teardown() 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 local activated = debugger:ActivateDocument(file, line) -- activation has been canceled; nothing else needs to be done if activated == nil then return end if activated then -- move cursor to the activated line if it's a breakpoint if ide.config.debugger.linetobreakpoint and command ~= "step" and debugger:stoppedAtBreakpoint(file, line) and not debugger.breaking and ide:IsValidCtrl(activated) then activated:GotoLine(line-1) end debugger.stats.line = debugger.stats.line + 1 if debugger.loop then debugger:updateStackSync() debugger:updateWatchesSync() else debugger: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 displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).") :format(file, line)) debugger:updateStackAndWatches() 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 function debugger:handleAsync(command) local debugger = self if debugger.server and not debugger.running then copas.addthread(function() local debugger = debugger debugger:handle(command) end) end end function debugger:handleDirect(command) local debugger = self local sock = debugger.socket if debugger.server and sock then local running = debugger.running -- this needs to be short as it will block the UI sock:settimeout(0.25) debugger:handle(command, sock) sock:settimeout(0) -- restore running status debugger.running = running end end function debugger:loadfile(file) local debugger = self local f, l, err = debugger:handle("load " .. file) if not f and wx.wxFileExists(file) and err and err:find("Cannot open file") then local content = FileRead(file) if content then return debugger:loadstring(file, content) end end return f, l, err end function debugger:loadstring(file, string) local debugger = self return debugger:handle("loadstring '" .. file .. "' " .. string) end do local nextupdatedelta = 0.250 local nextupdate = ide:GetTime() + nextupdatedelta local function forceUpdateOnWrap(editor) -- http://www.scintilla.org/ScintillaDoc.html#LineWrapping -- Scintilla doesn't perform wrapping immediately after a content change -- for performance reasons, so the activation calculations can be wrong -- if there is wrapping that pushes the current line out of the screen. -- force editor update that performs wrapping recalculation. if ide.config.editor.usewrap then editor:Update(); editor:Refresh() end end function debugger:Update() local debugger = self local smth = false if debugger.server or debugger.listening and ide:GetTime() > nextupdate then smth = copas.step(0) nextupdate = ide:GetTime() + nextupdatedelta end -- if there is any pending activation if debugger.activate then local file, line, content = unpack(debugger.activate) debugger.activate = nil if content then local editor = NewFile() editor:SetTextDyn(content) if not ide.config.debugger.allowediting and not (debugger.options or {}).allowediting then editor:SetReadOnly(true) end forceUpdateOnWrap(editor) debugger:ActivateDocument(file, line) else local editor = LoadFile(file) if editor then forceUpdateOnWrap(editor) debugger:ActivateDocument(file, line) end end end return smth end end function debugger:terminate() local debugger = self if debugger.server then if killProcess(ide:GetLaunchedProcess()) then -- if there is PID, try local kill ide:SetLaunchedProcess(nil) else -- otherwise, try graceful exit for the remote process debugger:detach("exit") end debugger:teardown() end end function debugger:Step() return self:exec("step") end function debugger:trace() local debugger = self debugger.loop = true debugger:exec("step") end function debugger:RunTo(editor, line) local debugger = self -- check if the location is valid for a breakpoint if editor:IsLineEmpty(line-1) then return end local ed, ln = unpack(debugger.runtocursor or {}) local same = ed and ln and ed:GetId() == editor:GetId() and ln == line -- check if there is already a breakpoint in the "run to" location; -- if so, don't mark the location as "run to" as it will stop there anyway if bit.band(editor:MarkerGet(line-1), BREAKPOINT_MARKER_VALUE) > 0 and not same then debugger.runtocursor = nil debugger:Run() return end -- save the location of the breakpoint debugger.runtocursor = {editor, line} -- set breakpoint and execute run debugger:exec("run", function() -- if run-to-cursor location is already set, then remove the breakpoint, -- but only if this location is different if ed and ln and not same then debugger:breakpointToggle(ed, ln, false) -- remove earlier run-to breakpoint debugger:Wait() end if not same then debugger:breakpointToggle(editor, line, true) -- set new run-to breakpoint debugger:Wait() end end) end function debugger:Wait() local debugger = self -- wait for all results to come back while debugger.running do debugger:Update() end end function debugger:Over() return self:exec("over") end function debugger:Out() return self:exec("out") end function debugger:Run() return self:exec("run") end function debugger:detach(cmd) local debugger = self if not debugger.server then return end if debugger.running then debugger:handleDirect(cmd or "done") debugger:teardown() else debugger:exec(cmd or "done") end end local function todeb(params) return params and " -- "..mobdebug.line(params, {comment = false}) or "" end function debugger:evaluate(exp, params) return self:handle('eval ' .. exp .. todeb(params)) end function debugger:execute(exp, params) return self:handle('exec '.. exp .. todeb(params)) end function debugger:stack(params) return self:handle('stack' .. todeb(params)) end function debugger:Break(command) local debugger = self -- 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 function debugger:breakpoint(file, line, state) local debugger = self if debugger.running then return debugger:handleDirect((state and "asetb " or "adelb ") .. file .. " " .. line) end return debugger:handleAsync((state and "setb " or "delb ") .. file .. " " .. line) end function debugger:EvalAsync(var, callback, params) local debugger = self if debugger.server and not debugger.running and callback and not debugger.scratchpad and not (debugger.options or {}).noeval then copas.addthread(function() local debugger = debugger local _, values, err = debugger:evaluate(var, params) if err then callback(nil, (err:gsub("%[.-%]:%d+:%s*","error: "))) else callback(#values > 0 and values[1] or 'nil') end end) end end local width, height = 360, 200 local keyword = {} for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end local function stringifyKeyIntoPrefix(name, num) return (type(name) == "number" and (num and num == name and '' or ("[%s] = "):format(name)) or type(name) == "string" and (name:match("^[%l%u_][%w_]*$") and not keyword[name] and ("%s = "):format(name) or ("[%q] = "):format(name)) or ("[%s] = "):format(tostring(name))) end local function debuggerCreateStackWindow() local stackCtrl = ide:CreateTreeCtrl(ide.frame, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxSize(width, height), wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS + wx.wxTR_SINGLE + wx.wxTR_HIDE_ROOT + wx.wxNO_BORDER) local debugger = ide:GetDebugger() debugger.stackCtrl = stackCtrl stackCtrl:SetImageList(debugger.imglist) local names = {} function stackCtrl:SetItemName(item, name) local nametype = type(name) names[item:GetValue()] = ( (nametype == 'string' or nametype == 'number' or nametype == 'boolean') and name or nil ) end function stackCtrl:GetItemName(item) return names[item:GetValue()] end local expandable = {} -- special value local valuecache = {} function stackCtrl:SetItemValueIfExpandable(item, value, delayed) local maxlvl = tonumber(ide.config.debugger.maxdatalevel) -- don't make empty tables expandable if expansion is disabled (`maxdatalevel` is false) local isexpandable = type(value) == 'table' and (next(value) ~= nil or delayed and maxlvl ~= nil) if isexpandable then -- cache table value to expand when requested valuecache[item:GetValue()] = next(value) == nil and expandable or value elseif type(value) ~= 'table' then valuecache[item:GetValue()] = nil end self:SetItemHasChildren(item, isexpandable) end function stackCtrl:IsExpandable(item) return valuecache[item:GetValue()] == expandable end function stackCtrl:DeleteAll() self:DeleteAllItems() valuecache = {} names = {} end function stackCtrl:GetItemChildren(item) return valuecache[item:GetValue()] or {} end function stackCtrl:IsFrame(item) return (item and item:IsOk() and self:GetItemParent(item):IsOk() and self:GetItemParent(item):GetValue() == self:GetRootItem():GetValue()) end function stackCtrl:GetItemFullExpression(item) local expr = '' while item:IsOk() and not self:IsFrame(item) do local name = self:GetItemName(item) -- check if it's a top item, as it needs to be used as is; -- convert `(*vararg num)` to `select(num, ...)` expr = (self:IsFrame(self:GetItemParent(item)) and name:gsub("^%(%*vararg (%d+)%)$", "select(%1, ...)") or (type(name) == 'string' and '[%q]' or '[%s]'):format(tostring(name))) ..expr item = self:GetItemParent(item) end return expr, item:IsOk() and item or nil end function stackCtrl:GetItemPos(item) if not item:IsOk() then return end local pos = 0 repeat pos = pos + 1 item = self:GetPrevSibling(item) until not item:IsOk() return pos end function stackCtrl:ExpandItemValue(item) local expr, itemframe = self:GetItemFullExpression(item) local stack = self:GetItemPos(itemframe) local debugger = ide:GetDebugger() 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() local debugger = debugger local value, _, err = debugger:evaluate(expr, {maxlevel = 1, stack = stack}) if err then err = err:gsub("%[.-%]:%d+:%s+","") -- this may happen when attempting to expand a sub-element referenced by a key -- that can't be evaluated, like a table, function, or userdata if err ~= "attempt to index a nil value" then self:SetItemText(item, 'error: '..err) else local name = self:GetItemName(item) local text = stringifyKeyIntoPrefix(name, self:GetItemPos(item)).."{}" self:SetItemText(item, text) self:SetItemValueIfExpandable(item, {}) self:Expand(item) end else local ok, res = LoadSafe("return "..tostring(value)) if ok then self:SetItemValueIfExpandable(item, res) self:Expand(item) local name = self:GetItemName(item) if not name then -- this is an empty table, so replace MORE indicator with the empty table self:SetItemText(item, (self:GetItemText(item):gsub(q(MORE), "{}"))) return end -- update cache in the parent local parent = self:GetItemParent(item) valuecache[parent:GetValue()][name] = res local params = debugger:GetDataOptions({maxlevel=false}) -- now update all serialized values in the tree starting from the expanded item while item:IsOk() and not self:IsFrame(item) do local value = valuecache[item:GetValue()] local strval = fixUTF8(serialize(value, params)) local name = self:GetItemName(item) local text = (self:IsFrame(self:GetItemParent(item)) and name.." = " or stringifyKeyIntoPrefix(name, self:GetItemPos(item))) ..strval self:SetItemText(item, text) item = self:GetItemParent(item) end end end end) end 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 if stackCtrl:IsExpandable(item_id) then return stackCtrl:ExpandItemValue(item_id) end local image = stackCtrl:GetItemImage(item_id) local num, maxnum = 1, ide.config.debugger.maxdatanum local params = debugger:GetDataOptions({maxlevel = false}) stackCtrl:Freeze() for name,value in pairs(stackCtrl:GetItemChildren(item_id)) do local item = stackCtrl:AppendItem(item_id, "", image) stackCtrl:SetItemValueIfExpandable(item, value, true) local strval = stackCtrl:IsExpandable(item) and MORE or fixUTF8(serialize(value, params)) stackCtrl:SetItemText(item, stringifyKeyIntoPrefix(name, num)..strval) stackCtrl:SetItemName(item, name) num = num + 1 if num > maxnum then break end end stackCtrl:Thaw() return true end) stackCtrl:Connect(wx.wxEVT_SET_FOCUS, function(event) local debugger = ide:GetDebugger() if debugger.needrefresh.stack then debugger:updateStack() debugger.needrefresh.stack = false end end) -- register navigation callback stackCtrl:Connect(wx.wxEVT_LEFT_DCLICK, function (event) local item_id = stackCtrl:HitTest(event:GetPosition()) if not item_id or not item_id:IsOk() then event:Skip() return end local coords = callData[item_id:GetValue()] if not coords then event:Skip() return end local file, line = coords[1], coords[2] if file:match("@") then file = string.sub(file, 2) end file = GetFullPathIfExists(ide:GetDebugger().basedir, file) if file then local editor = LoadFile(file,nil,true) editor:SetFocus() if line then editor:GotoLine(line-1) editor:EnsureVisibleEnforcePolicy(line-1) -- make sure the line is visible (unfolded) end end end) local layout = ide:GetSetting("/view", "uimgrlayout") if layout and not layout:find("stackpanel") then ide:AddPanelDocked(ide.frame.bottomnotebook, stackCtrl, "stackpanel", TR("Stack")) else ide:AddPanel(stackCtrl, "stackpanel", TR("Stack")) end end local function debuggerCreateWatchWindow() local watchCtrl = ide:CreateTreeCtrl(ide.frame, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxSize(width, height), wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS + wx.wxTR_SINGLE + wx.wxTR_HIDE_ROOT + wx.wxTR_EDIT_LABELS + wx.wxNO_BORDER) local debugger = ide:GetDebugger() debugger.watchCtrl = watchCtrl local root = watchCtrl:AddRoot("Watch") watchCtrl:SetImageList(debugger.imglist) local defaultExpr = "watch expression" local expressions = {} -- table to keep track of expressions function watchCtrl:SetItemExpression(item, expr, value) expressions[item:GetValue()] = expr self:SetItemText(item, expr .. ' = ' .. (value or '?')) self:SelectItem(item, true) local debugger = ide:GetDebugger() if not value then debugger:updateWatches(item) end end function watchCtrl:GetItemExpression(item) return expressions[item:GetValue()] end local names = {} function watchCtrl:SetItemName(item, name) local nametype = type(name) names[item:GetValue()] = ( (nametype == 'string' or nametype == 'number' or nametype == 'boolean') and name or nil ) end function watchCtrl:GetItemName(item) return names[item:GetValue()] end local expandable = {} -- special value local valuecache = {} function watchCtrl:SetItemValueIfExpandable(item, value, delayed) local maxlvl = tonumber(ide.config.debugger.maxdatalevel) -- don't make empty tables expandable if expansion is disabled (`maxdatalevel` is false) local isexpandable = type(value) == 'table' and (next(value) ~= nil or delayed and maxlvl ~= nil) if isexpandable then -- cache table value to expand when requested valuecache[item:GetValue()] = next(value) == nil and expandable or value elseif type(value) ~= 'table' then valuecache[item:GetValue()] = nil end self:SetItemHasChildren(item, isexpandable) end function watchCtrl:IsExpandable(item) return valuecache[item:GetValue()] == expandable end function watchCtrl:GetItemChildren(item) return valuecache[item:GetValue()] or {} end function watchCtrl:IsWatch(item) return (item and item:IsOk() and self:GetItemParent(item):IsOk() and self:GetItemParent(item):GetValue() == root:GetValue()) end function watchCtrl:IsEditable(item) return (item and item:IsOk() and (self:IsWatch(item) or self:GetItemName(item) ~= nil)) end function watchCtrl:GetItemFullExpression(item) local expr = '' while true do local name = self:GetItemName(item) expr = (self:IsWatch(item) and ('({%s})[1]'):format(self:GetItemExpression(item)) or (type(name) == 'string' and '[%q]' or '[%s]'):format(tostring(name)) )..expr if self:IsWatch(item) then break end item = self:GetItemParent(item) if not item:IsOk() then break end end return expr, item:IsOk() and item or nil end function watchCtrl:CopyItemValue(item) local expr = self:GetItemFullExpression(item) local debugger = ide:GetDebugger() 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() local debugger = debugger local _, values, error = debugger:evaluate(expr) ide:CopyToClipboard(error and error:gsub("%[.-%]:%d+:%s+","") or (#values == 0 and 'nil' or fixUTF8(values[1]))) end) end end function watchCtrl:UpdateItemValue(item, value) local expr, itemupd = self:GetItemFullExpression(item) local debugger = ide:GetDebugger() 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() local debugger = debugger local _, _, err = debugger:execute(expr..'='..value) if err then watchCtrl:SetItemText(item, 'error: '..err:gsub("%[.-%]:%d+:%s+","")) elseif itemupd then debugger:updateWatchesSync(itemupd) end debugger:updateStackSync() end) end end function watchCtrl:GetItemPos(item) if not item:IsOk() then return end local pos = 0 repeat pos = pos + 1 item = self:GetPrevSibling(item) until not item:IsOk() return pos end function watchCtrl:ExpandItemValue(item) local expr = self:GetItemFullExpression(item) local debugger = ide:GetDebugger() 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() local debugger = debugger local value, _, err = debugger:evaluate(expr, {maxlevel = 1}) if err then self:SetItemText(item, 'error: '..err:gsub("%[.-%]:%d+:%s+","")) else local ok, res = LoadSafe("return "..tostring(value)) if ok then self:SetItemValueIfExpandable(item, res) self:Expand(item) local name = self:GetItemName(item) if not name then self:SetItemText(item, (self:GetItemText(item):gsub(q(MORE), "{}"))) return end -- update cache in the parent local parent = self:GetItemParent(item) valuecache[parent:GetValue()][name] = res local params = debugger:GetDataOptions({maxlevel=false}) -- now update all serialized values in the tree starting from the expanded item while item:IsOk() do local value = valuecache[item:GetValue()] local strval = fixUTF8(serialize(value, params)) local name = self:GetItemName(item) local text = (self:IsWatch(item) and self:GetItemExpression(item).." = " or stringifyKeyIntoPrefix(name, self:GetItemPos(item))) ..strval self:SetItemText(item, text) if self:IsWatch(item) then break end item = self:GetItemParent(item) end end end end) end end watchCtrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_EXPANDING, function (event) local item_id = event:GetItem() local count = watchCtrl:GetChildrenCount(item_id, false) if count > 0 then return true end if watchCtrl:IsExpandable(item_id) then return watchCtrl:ExpandItemValue(item_id) end local image = watchCtrl:GetItemImage(item_id) local num, maxnum = 1, ide.config.debugger.maxdatanum local params = debugger:GetDataOptions({maxlevel = false}) watchCtrl:Freeze() for name,value in pairs(watchCtrl:GetItemChildren(item_id)) do local item = watchCtrl:AppendItem(item_id, "", image) watchCtrl:SetItemValueIfExpandable(item, value, true) local strval = watchCtrl:IsExpandable(item) and MORE or fixUTF8(serialize(value, params)) watchCtrl:SetItemText(item, stringifyKeyIntoPrefix(name, num)..strval) watchCtrl:SetItemName(item, name) num = num + 1 if num > maxnum then break end end watchCtrl:Thaw() return true end) watchCtrl:Connect(wx.wxEVT_COMMAND_TREE_DELETE_ITEM, function (event) local value = event:GetItem():GetValue() expressions[value] = nil valuecache[value] = nil names[value] = nil end) watchCtrl:Connect(wx.wxEVT_SET_FOCUS, function(event) local debugger = ide:GetDebugger() if debugger.needrefresh.watches then debugger:updateWatches() debugger.needrefresh.watches = false end end) local item -- wx.wxEVT_CONTEXT_MENU is only triggered over tree items on OSX, -- but it needs to be also triggered below any item to add a watch, -- so use RIGHT_DOWN instead watchCtrl:Connect(wx.wxEVT_RIGHT_DOWN, function (event) -- store the item to be used in edit/delete actions item = watchCtrl:HitTest(watchCtrl:ScreenToClient(wx.wxGetMousePosition())) local editlabel = watchCtrl:IsWatch(item) and TR("&Edit Watch") or TR("&Edit Value") local menu = ide:MakeMenu { { ID.ADDWATCH, TR("&Add Watch")..KSC(ID.ADDWATCH) }, { ID.EDITWATCH, editlabel..KSC(ID.EDITWATCH) }, { ID.DELETEWATCH, TR("&Delete Watch")..KSC(ID.DELETEWATCH) }, { ID.COPYWATCHVALUE, TR("&Copy Value")..KSC(ID.COPYWATCHVALUE) }, } PackageEventHandle("onMenuWatch", menu, watchCtrl, event) watchCtrl:PopupMenu(menu) item = nil end) watchCtrl:Connect(ID.ADDWATCH, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) watchCtrl:SetFocus() watchCtrl:EditLabel(watchCtrl:AppendItem(root, defaultExpr, image.LOCAL)) end) watchCtrl:Connect(ID.EDITWATCH, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) watchCtrl:EditLabel(item or watchCtrl:GetSelection()) end) watchCtrl:Connect(ID.EDITWATCH, wx.wxEVT_UPDATE_UI, function (event) event:Enable(watchCtrl:IsEditable(item or watchCtrl:GetSelection())) end) watchCtrl:Connect(ID.DELETEWATCH, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) watchCtrl:Delete(item or watchCtrl:GetSelection()) end) watchCtrl:Connect(ID.DELETEWATCH, wx.wxEVT_UPDATE_UI, function (event) event:Enable(watchCtrl:IsWatch(item or watchCtrl:GetSelection())) end) watchCtrl:Connect(ID.COPYWATCHVALUE, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) watchCtrl:CopyItemValue(item or watchCtrl:GetSelection()) end) watchCtrl:Connect(ID.COPYWATCHVALUE, wx.wxEVT_UPDATE_UI, function (event) -- allow copying only when the debugger is available local debugger = ide:GetDebugger() event:Enable(item:IsOk() and debugger.server and not debugger.running and (not debugger.scratchpad or debugger.scratchpad.paused)) end) local label watchCtrl:Connect(wx.wxEVT_COMMAND_TREE_BEGIN_LABEL_EDIT, function (event) local item = event:GetItem() if not (item:IsOk() and watchCtrl:IsEditable(item)) then event:Veto() return end label = watchCtrl:GetItemText(item) if watchCtrl:IsWatch(item) then local expr = watchCtrl:GetItemExpression(item) if expr then watchCtrl:SetItemText(item, expr) end else local prefix = stringifyKeyIntoPrefix(watchCtrl:GetItemName(item)) local val = watchCtrl:GetItemText(item):gsub(q(prefix),'') watchCtrl:SetItemText(item, val) end end) watchCtrl:Connect(wx.wxEVT_COMMAND_TREE_END_LABEL_EDIT, function (event) event:Veto() local item = event:GetItem() if event:IsEditCancelled() then if watchCtrl:GetItemText(item) == defaultExpr then -- when Delete is called from END_EDIT, it causes infinite loop -- on OSX (wxwidgets 2.9.5) as Delete calls END_EDIT again. -- disable handlers during Delete and then enable back. watchCtrl:SetEvtHandlerEnabled(false) watchCtrl:Delete(item) watchCtrl:SetEvtHandlerEnabled(true) else watchCtrl:SetItemText(item, label) end else if watchCtrl:IsWatch(item) then watchCtrl:SetItemExpression(item, event:GetLabel()) else watchCtrl:UpdateItemValue(item, event:GetLabel()) end end event:Skip() end) local layout = ide:GetSetting("/view", "uimgrlayout") if layout and not layout:find("watchpanel") then ide:AddPanelDocked(ide.frame.bottomnotebook, watchCtrl, "watchpanel", TR("Watch")) else ide:AddPanel(watchCtrl, "watchpanel", TR("Watch")) end end debuggerCreateStackWindow() debuggerCreateWatchWindow() ---------------------------------------------- -- public api function debugger:RefreshPanels() return self:updateStackAndWatches() end function debugger:BreakpointSet(...) return self:breakpoint(...) end local statuses = { running = TR("Output (running)"), suspended = TR("Output (suspended)"), stopped = TR("Output"), } function debugger:UpdateStatus(status) local debugger = self if not status then status = debugger.running and "running" or debugger.server and "suspended" or "stopped" end if PackageEventHandle("onDebuggerStatusUpdate", debugger, status) == false then return end nameOutputTab(statuses[status] or statuses.stopped) end function debugger:OutputSet(stream, mode, options) return self:handle(("output %s %s"):format(stream, mode), nil, options) end function DebuggerAttachDefault(options) ide:GetDebugger():SetOptions(options) end function debugger:SetOptions(options) self.options = options end function debugger:Stop() local debugger = self -- terminate the local session (if still active) if killProcess(ide:GetLaunchedProcess()) then ide:SetLaunchedProcess(nil) end debugger:terminate() end function debugger:Shutdown() self:Stop() PackageEventHandle("onDebuggerShutdown", self) end function debugger:teardown() local debugger = self if debugger.server then local lines = TR("traced %d instruction", debugger.stats.line):format(debugger.stats.line) ide:Print(TR("Debugging session completed (%s)."):format(lines)) debugger:UpdateStatus(ide:GetLaunchedProcess() and "running" or "stopped") if debugger.runtocursor then local ed, ln = unpack(debugger.runtocursor) debugger:breakpointToggle(ed, ln, false) -- remove current run-to breakpoint end if PackageEventHandle("onDebuggerPreClose", debugger) ~= false then SetAllEditorsReadOnly(false) ide:GetConsole():SetRemote(nil) ClearAllCurrentLineMarkers() debugger:toggleViews(false) PackageEventHandle("onDebuggerClose", debugger) end debugger.server = nil debugger:ScratchpadOff() 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. debugger:ScratchpadOff() end end local function debuggerMakeFileName(editor) return ide:GetDocument(editor):GetFilePath() or ide:GetDocument(editor):GetFileName() or ide:GetDefaultFileName() end function debugger:breakpointToggle(editor, line, value) local debugger = self local file = debugger.editormap and debugger.editormap[editor] or debuggerMakeFileName(editor) debugger:BreakpointSet(file, line, value) end -- scratchpad functions function debugger:ScratchpadRefresh() local debugger = self if debugger.scratchpad and debugger.scratchpad.updated and not debugger.scratchpad.paused then local scratchpadEditor = debugger.scratchpad.editor if scratchpadEditor.spec.apitype and scratchpadEditor.spec.apitype == "lua" and not ide.interpreter.skipcompile and not CompileProgram(scratchpadEditor, { jumponerror = false, reportstats = false }) then debugger.scratchpad.updated = false return end local code = StripShebang(scratchpadEditor:GetTextDyn()) if debugger.scratchpad.running then -- break the current execution first -- don't try too frequently to avoid overwhelming the debugger local now = ide:GetTime() if now - debugger.scratchpad.running > 0.250 then debugger:Break() debugger.scratchpad.running = now end else local filePath = debuggerMakeFileName(scratchpadEditor) -- 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 ' .. ide:GetTime() local stopper = "error('" .. errormsg .. "')" -- store if interpreter requires a special handling for external loop local extloop = ide.interpreter.scratchextloop local function reloadScratchpadCode() local debugger = debugger debugger.scratchpad.running = ide:GetTime() debugger.scratchpad.updated = false debugger.scratchpad.runs = (debugger.scratchpad.runs or 0) + 1 ide:GetOutput():Erase() -- 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 displayError(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 function debugger:ScratchpadOn(editor) local debugger = self -- 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) ide:GetConsole():SetRemote(nil) -- disable remote shell debugger:ScratchpadRefresh() elseif not ProjectDebug(true, "scratchpad") then debugger.scratchpad = nil return end end local scratchpadEditor = editor for _, numberStyle in ipairs(scratchpadEditor.spec.isnumber) do scratchpadEditor:StyleSetUnderline(numberStyle, true) end debugger.scratchpad.margin = scratchpadEditor:GetAllMarginWidth() 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) local isnumber = scratchpadEditor.spec.isnumber -- are we over a number in the scratchpad? if not, it's not our event if not (scratchpad and isnumber[bit.band(scratchpadEditor:GetStyleAt(pos),ide.STYLEMASK)]) then event:Skip() return end -- find start position and length of the number local text = scratchpadEditor:GetTextDyn() local nstart = pos while nstart >= 0 and isnumber[bit.band(scratchpadEditor:GetStyleAt(nstart),ide.STYLEMASK)] do nstart = nstart - 1 end local nend = pos while nend < string.len(text) and isnumber[bit.band(scratchpadEditor:GetStyleAt(nend),ide.STYLEMASK)] do nend = nend + 1 end -- check if there is minus sign right before the number and include it if nstart >= 0 and scratchpadEditor:GetTextRangeDyn(nstart,nstart+1) == '-' then nstart = nstart - 1 end scratchpad.start = nstart + 1 scratchpad.length = nend - nstart - 1 scratchpad.origin = scratchpadEditor:GetTextRangeDyn(nstart+1,nend) if tonumber(scratchpad.origin) then scratchpad.point = point scratchpadEditor:BeginUndoAction() 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:EndUndoAction() 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 scratchpadEditor.spec.isnumber[bit.band(scratchpadEditor:GetStyleAt(pos),ide.STYLEMASK)]) 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 debugger:ScratchpadOff() local debugger = self if not debugger.scratchpad then return end for scratchpadEditor in pairs(debugger.scratchpad.editors) do for _, numberStyle in ipairs(scratchpadEditor.spec.isnumber) do scratchpadEditor:StyleSetUnderline(numberStyle, false) end 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 debugger = ide:SetDebugger(setmetatable({}, {__index = protodeb})) ide:AddPackage('core.debugger', { onEditorMarkerUpdate = function(self, editor, marker, line, value) if marker ~= BREAKPOINT_MARKER then return end local debugger = ide:GetDebugger() if value == false then -- if there is pending "run-to-cursor" call at this location, remove it local ed, ln = unpack(debugger.runtocursor or {}) local same = ed and ln and ed:GetId() == editor:GetId() and ln == line if same then debugger.runtocursor = nil end elseif editor:IsLineEmpty(line-1) then return false -- don't set marker here end return debugger:breakpointToggle(editor, line, value) end, })