Skip to content

Instantly share code, notes, and snippets.

@pkulchenko
Created August 6, 2014 04:45
Show Gist options
  • Save pkulchenko/a5deb69a80378b5ed087 to your computer and use it in GitHub Desktop.
Save pkulchenko/a5deb69a80378b5ed087 to your computer and use it in GitHub Desktop.
Watch window background color fix
-- Copyright 2011-14 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 debugger = setmetatable(ide.debugger, ide.proto.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.watchCtrl = nil -- the watch ctrl that shows watch information
debugger.stackCtrl = nil -- the stack ctrl that shows stack information
debugger.toggleview = {
stackpanel = false, watchpanel = false, toolbar = false }
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)()
do
local getBitmap = (ide.app.createbitmap or wx.wxArtProvider.GetBitmap)
local size = wx.wxSize(16,16)
local imglist = wx.wxImageList(16,16)
imglist:Add(getBitmap("GO-FORWARD", "OTHER", size)) -- 0 = stack call
imglist:Add(getBitmap("LIST-VIEW", "OTHER", size)) -- 1 = local variables
imglist:Add(getBitmap("REPORT-VIEW", "OTHER", size)) -- 2 = upvalues
debugger.imglist = imglist
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 activate = {CHECKONLY = 1, NOREPORT = 2}
local function serialize(value, options) return mobdebug.line(value, options) end
local stackmaxlength = ide.config.debugger.stackmaxlength or 400
local stackmaxnum = ide.config.debugger.stackmaxnum or 400
local stackmaxlevel = ide.config.debugger.stackmaxlevel or 3
local params = {comment = false, nocode = true, maxlevel = stackmaxlevel, maxnum = stackmaxnum}
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 function trimToMaxLength(...)
local t = {...}
for i = 1, #t do
t[i] = t[i]:sub(1, stackmaxlength)..(#t[i] > stackmaxlength and '...' or '')
end
return unpack(t)
end
-- on some Linux versions using wxwidgets 2.9.5 wxListCtrl doesn't report
-- background color even after the color being explicitly set
-- (Linux cipripc 3.13.0-24-generic #47-Ubuntu SMP Fri May 2 23:30:00 UTC 2014 x86_64 GNU/Linux);
-- check if the fix is needed in this case and use stackCtrl color instead.
local bgcolorfixneeded
if ide.osname == 'Unix' then
local lc, color = wx.wxListCtrl(), wx.wxRED
lc:SetBackgroundColour(color)
fixneeded = lc:GetBackgroundColour():GetAsString() ~= color:GetAsString()
end
local q = EscapeMagic
local function updateWatchesSync(num)
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())
if shown and debugger.server and not debugger.running
and not debugger.scratchpad and not (debugger.options or {}).noeval then
local bgcl = watchCtrl:GetBackgroundColour()
if fixneeded then bgcl = debugger.stackCtrl:GetBackgroundColour() end
local hicl = wx.wxColour(math.floor(bgcl:Red()*.9),
math.floor(bgcl:Green()*.9), math.floor(bgcl:Blue()*.9))
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 hicl or bgcl)
end
watchCtrl:SetItem(idx, 1, newval)
end
end
end
end
local simpleType = {['nil'] = true, ['string'] = true, ['number'] = true, ['boolean'] = true}
local stackItemValue = {}
local callData = {}
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
local pane = ide.frame.uimgr:GetPane("stackpanel")
local shown = stackCtrl and (pane:IsOk() and pane:IsShown() or not pane:IsOk() and stackCtrl:IsShown())
if shown 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 root = stackCtrl:AddRoot("Stack")
stackItemValue = {} -- reset cache of items in the stack
callData = {} -- reset call cache
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]
-- 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, 0)
-- 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(frame[2]) do
-- format the variable name, value as a single line and,
-- if not a simple type, the string value.
-- comment can be not necessarily a string for tables with metatables
-- that provide its own __tostring method
local value, comment = val[1], fixUTF8(trimToMaxLength(tostring(val[2])))
local text = ("%s = %s%s"):
format(name, fixUTF8(trimToMaxLength(serialize(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
-- add the upvalues for this call stack level to the tree item
for name,val in pairs(frame[3]) do
local value, comment = val[1], fixUTF8(trimToMaxLength(tostring(val[2])))
local text = ("%s = %s%s"):
format(name, fixUTF8(trimToMaxLength(serialize(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:SetScrollPos(wx.wxHORIZONTAL, 0, true)
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 debuggerToggleViews(show)
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
debugger.toggleview[view] = not pane:IsShown()
if debugger.toggleview[view] and (needed or bar)
and (not bar or not ide.frame:IsFullScreen()) 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
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, activatehow)
if not file then return end
-- 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
local activated
local indebugger = file:find('mobdebug%.lua$')
local fileName = wx.wxFileName(file)
for _, document in pairs(ide.openDocuments) do
local editor = document.editor
-- 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.
if document.filePath and fileName:SameAs(wx.wxFileName(document.filePath))
or content and content:gsub("[\n\r]","") == editor:GetText():gsub("[\n\r]","") then
ClearAllCurrentLineMarkers()
if line then
if line == 0 then -- special case; find the first executable line
line = math.huge
local func = loadstring(editor:GetText())
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
local line = line - 1 -- editor line operations are zero-based
editor:MarkerAdd(line, CURRENT_LINE_MARKER)
-- 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
local selection = document.index
RequestAttention()
notebook:SetSelection(selection)
SetEditorSelection(selection)
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 document.filePath then
local filePath = document.filePath
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
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 filePath and 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 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
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
DisplayShell(fixUTF8(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
local function stoppedAtBreakpoint(file, line)
-- if this document can be activated and the current line has a breakpoint
local editor = 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 > -1 and breakpoint == current
end
local function mapRemotePath(basedir, file, line, method)
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 found a local mapping under basedir
local activated = longestpath and activateDocument(longestpath, line, method or activate.NOREPORT)
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))
return longestpath
end
return nil
end
debugger.listen = function(start)
if start == false then
if debugger.listening then
debugger.terminate() -- terminate if running
copas.removeserver(debugger.listening)
DisplayOutputLn(TR("Debugger server stopped at %s:%d.")
:format(debugger.hostname, debugger.portnumber))
debugger.listening = false
else
DisplayOutputLn(TR("Can't stop debugger server as it is not started."))
end
return
end
local server, err = socket.bind("*", debugger.portnumber)
if not server then
DisplayOutputLn(TR("Can't start debugger server at %s:%d: %s.")
:format(debugger.hostname, debugger.portnumber, err or TR("unknown error")))
return
end
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)
-- ignore errors that happen because debugging session is
-- terminated during handshake (server == nil in this case).
if debugger.server then
DisplayOutputLn(TR("Can't start debugging session due to internal error '%s'."):format(error))
end
debugger.terminate()
end)
local options = debugger.options or {}
-- this may be a remote call without using an interpreter and as such
-- debugger.options may not be set, but runonstart is still configured.
if not options.runstart then options.runstart = ide.config.debugger.runonstart end
-- support allowediting as set in the interpreter or config
if not options.allowediting then options.allowediting = ide.config.debugger.allowediting end
if not debugger.scratchpad and not options.allowediting then
SetAllEditorsReadOnly(true)
end
debugger.server = copas.wrap(skt)
debugger.socket = skt
debugger.loop = false
debugger.scratchable = false
debugger.stats = {line = 0}
debugger.missing = {}
debugger.editormap = {}
local wxfilepath = GetEditorFileAndCurInfo()
local startfile = 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)
local init = options.init or ide.config.debugger.init
if init then
local _, _, err = debugger.execute(init)
if err then DisplayOutputLn(TR("Ignored error in debugger initialization code: %s."):format(err)) end
end
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
DisplayOutputLn(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
DisplayOutputLn("Output filter failed: "..res)
return
end
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()
elseif options.runstart and not debugger.scratchpad
and stoppedAtBreakpoint(file, line) then
activateDocument(file, line)
options.runstart = false
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 err then
DisplayOutputLn(TR("Can't debug the script in the active editor window.")
.." "..TR("Compilation error")
..":\n"..err)
return debugger.terminate()
elseif options.runstart then
local file = (mapRemotePath(basedir, file, line or 0, activate.CHECKONLY)
or file or startfile)
if stoppedAtBreakpoint(file, line or 0) then
activateDocument(file, line or 0)
options.runstart = false
end
elseif file and line then
local activated = 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 = activateDocument(startpath..file, line, activate.NOREPORT)
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.
-- 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 mapRemotePath(basedir, file, line, activate.NOREPORT) then
activated = true
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
else
debugger.scratchable = true
activateDocument(startfile, 0) -- find the appropriate line
end
end
if (not options.noshell and not debugger.scratchpad) then
ShellSupportRemote(debugger.shell)
end
debuggerToggleViews(true)
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 = server
end
debugger.handle = function(command, server, options)
local verbose = ide.config.debugger.verbose
local osexit, gprint
osexit, os.exit = os.exit, function () end
gprint, _G.print = _G.print, function (...)
if verbose then 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
_G.print = gprint
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))
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
debugger.handleAsync = function(command)
if debugger.server and not debugger.running then
copas.addthread(function () debugger.handle(command) end)
end
end
debugger.handleDirect = function(command)
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
debugger.loadfile = function(file)
return debugger.handle("load " .. file)
end
debugger.loadstring = function(file, string)
return debugger.handle("loadstring '" .. file .. "' " .. string)
end
do
local nextupdatedelta = 0.250
local nextupdate = TimeGet() + 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
debugger.update = function()
if debugger.server or debugger.listening and TimeGet() > nextupdate then
copas.step(0)
nextupdate = TimeGet() + nextupdatedelta
end
-- if there is any pending activation
if debugger.activate then
local file, line, content = unpack(debugger.activate)
if content then
local editor = NewFile()
editor:SetText(content)
if not ide.config.debugger.allowediting
and not (debugger.options or {}).allowediting then
editor:SetReadOnly(true)
end
forceUpdateOnWrap(editor)
activateDocument(file, line)
else
local editor = LoadFile(file)
if editor then
forceUpdateOnWrap(editor)
activateDocument(file, line)
end
end
debugger.activate = nil
end
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.detach = function() debugger.exec("done") 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)
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
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
function DebuggerAddStackWindow()
return ide:AddPanel(debugger.stackCtrl, "stackpanel", TR("Stack"))
end
function DebuggerAddWatchWindow()
return ide:AddPanel(debugger.watchCtrl, "watchpanel", TR("Watch"))
end
local width, height = 360, 200
local function debuggerCreateStackWindow()
local stackCtrl = wx.wxTreeCtrl(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)
debugger.stackCtrl = stackCtrl
stackCtrl:AssignImageList(debugger.imglist)
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 = fixUTF8(trimToMaxLength(serialize(value, params)))
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
if num > stackmaxnum then break end
end
return true
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(debugger.basedir, file)
if file then
local editor = LoadFile(file,nil,true)
editor:SetFocus()
if line then editor:GotoPos(editor:PositionFromLine(line-1)) end
end
end)
local layout = ide:GetSetting("/view", "uimgrlayout")
if layout and not layout:find("stackpanel") then
ide.frame.bottomnotebook:AddPage(stackCtrl, TR("Stack"), true)
return
end
DebuggerAddStackWindow()
end
local function debuggerCreateWatchWindow()
local watchCtrl = wx.wxListCtrl(ide.frame, 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)
local watchMenu = wx.wxMenu {
{ ID_ADDWATCH, TR("&Add Watch")..KSC(ID_ADDWATCH) },
{ ID_EDITWATCH, TR("&Edit Watch")..KSC(ID_EDITWATCH) },
{ ID_DELETEWATCH, TR("&Delete Watch")..KSC(ID_DELETEWATCH) },
}
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 = ""
local function addWatch()
local row = watchCtrl:InsertItem(watchCtrl:GetItemCount(), TR("Expr"))
watchCtrl:SetItem(row, 0, defaultExpr)
watchCtrl:SetItem(row, 1, TR("Value"))
watchCtrl:EditLabel(row)
end
local function editWatch()
local row = findSelectedWatchItem()
if row >= 0 then watchCtrl:EditLabel(row) end
end
local function deleteWatch()
local row = findSelectedWatchItem()
if row >= 0 then watchCtrl:DeleteItem(row) end
end
watchCtrl:Connect(wx.wxEVT_CONTEXT_MENU,
function () watchCtrl:PopupMenu(watchMenu) end)
watchCtrl:Connect(ID_ADDWATCH, wx.wxEVT_COMMAND_MENU_SELECTED, addWatch)
watchCtrl:Connect(ID_EDITWATCH, wx.wxEVT_COMMAND_MENU_SELECTED, editWatch)
watchCtrl:Connect(ID_EDITWATCH, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(watchCtrl:GetSelectedItemCount() > 0) end)
watchCtrl:Connect(ID_DELETEWATCH, wx.wxEVT_COMMAND_MENU_SELECTED, deleteWatch)
watchCtrl:Connect(ID_DELETEWATCH, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(watchCtrl:GetSelectedItemCount() > 0) end)
watchCtrl:Connect(wx.wxEVT_COMMAND_LIST_ITEM_ACTIVATED,
function (event) watchCtrl:EditLabel(event:GetIndex()) 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)
local layout = ide:GetSetting("/view", "uimgrlayout")
if layout and not layout:find("watchpanel") then
ide.frame.bottomnotebook:AddPage(watchCtrl, TR("Watch"), true)
return
end
DebuggerAddWatchWindow()
end
debuggerCreateStackWindow()
debuggerCreateWatchWindow()
----------------------------------------------
-- public api
DebuggerRefreshPanels = updateStackAndWatches
function DebuggerAddWatch(watch)
local mgr = ide.frame.uimgr
local pane = mgr:GetPane("watchpanel")
if (pane:IsOk() and not pane:IsShown()) then
pane:Show()
mgr:Update()
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 DebuggerAttachDefault(options)
debugger.options = options
if (debugger.listening) then return end
debugger.listen()
end
function DebuggerShutdown()
if debugger.server then debugger.terminate() end
if debugger.pid then killClient() end
end
function DebuggerStop(resetpid)
if (debugger.server) then
debugger.server = nil
SetAllEditorsReadOnly(false)
ShellSupportRemote(nil)
ClearAllCurrentLineMarkers()
DebuggerScratchpadOff()
debuggerToggleViews(false)
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
-- reset pid for "running" (not debugged) processes
if resetpid then debugger.pid = nil end
end
local function debuggerMakeFileName(editor)
return ide:GetDocument(editor):GetFilePath()
or ide:GetDocument(editor):GetFileName()
or ide.config.default.fullname
end
function DebuggerToggleBreakpoint(editor, line)
local markers = editor:MarkerGet(line)
local filePath = debugger.editormap and debugger.editormap[editor]
or debuggerMakeFileName(editor)
if bit.band(markers, BREAKPOINT_MARKER_VALUE) > 0 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
if scratchpadEditor.spec.apitype
and scratchpadEditor.spec.apitype == "lua"
and not ide.interpreter.skipcompile
and not CompileProgram(scratchpadEditor, { jumponerror = false, reportstats = false })
then return end
local code = StripShebang(scratchpadEditor:GetText())
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)
-- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment