Skip to content

Instantly share code, notes, and snippets.

@JustAPerson
Created September 19, 2012 02:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JustAPerson/3747403 to your computer and use it in GitHub Desktop.
Save JustAPerson/3747403 to your computer and use it in GitHub Desktop.
Debugger attempt for Textadept
-- ~/.textadept/modules/lua/debugger2.lua
-- My derived work, based on ~/.textadept/modules/lua/debugger.lua
require 'textadept.debugger'
local mobdebug = require 'lua.mobdebug'
local handle = mobdebug.handle;
local debugger = _M.textadept.debugger.new('lua');
_M.lua.debugger = debugger;
function debugger.get_stacklevel()
end
function debugger.get_env()
end
function debugger.debug_hook(event, line, thread, level)
mobdebug.handle(event);
end
function debugger:handle_start(filename)
filename = filename or buffer.filename;
os.execute("gnome-terminal -x lua %s " .. filename .. " read read")
end
function debugger:handle_stop()
error("FIXME HANDLE_STOP")
end
function debugger:handle_continue()
handle('run');
end
function debugger:handle_step_into()
handle('step');
end
function debugger:handle_step_over()
handle('over');
end
function debugger:handle_step_out()
handle('out');
end
function debugger:handle_set_watch(expr)
handle('setw ' .. expr);
self.watches[expr] = #self.watches + 1;
end
function debugger:handle_delete_watch()
handle('delw ' .. self.watches[expr])
self.watches[expr] = nil;
end
function debugger:get_call_stack()
error("FIXME GET_CALL_STACK")
end
function debugger:set_stack()
error("FIXME SET_STACK")
end
function debugger:handle_inspect(symbol, pos)
error("FIXME HANDLE_INSPECT")
end
function debugger:handle_command(text)
error("FIXME HANDLE_COMMAND");
end
-- Key commands.
keys.lua[keys.LANGUAGE_MODULE_PREFIX].d = {
d = debugger.start,
q = debugger.stop,
c = debugger.continue,
n = debugger.step_over,
s = debugger.step_into,
o = debugger.step_out,
i = debugger.inspect,
l = debugger.call_stack,
b = debugger.toggle_breakpoint,
B = debugger.delete_breakpoint,
w = debugger.set_watch,
W = debugger.delete_watch
}
-- Context menu.
local SEPARATOR = { '' }
_M.lua.context_menu = {
{ 'Undo', buffer.undo },
{ 'Redo', buffer.redo },
SEPARATOR,
{ 'Cut', buffer.cut },
{ 'Copy', buffer.copy },
{ 'Paste', buffer.paste },
{ 'Delete', buffer.clear },
SEPARATOR,
{ 'Select All', buffer.select_all },
SEPARATOR,
{ title = 'De_bug',
{ 'Start _Debugging', debugger.start },
{ 'Sto_p Debugging', debugger.stop },
SEPARATOR,
{ 'Debug _Continue', debugger.continue },
{ 'Debug Step _Over', debugger.step_over },
{ 'Debug Step _Into', debugger.step_into },
{ 'Debug Step Ou_t', debugger.step_out },
SEPARATOR,
{ 'Debug I_nspect', debugger.inspect },
{ 'Debug Call Stac_k...', debugger.call_stack },
SEPARATOR,
{ 'Toggle _Breakpoint', debugger.toggle_breakpoint },
{ '_Delete Breakpoint...', debugger.delete_breakpoint },
{ 'Set _Watch Expression', debugger.set_watch },
{ 'D_elete Watch Expression...', debugger.delete_watch },
}
}
-- ~/.textadept/modules/lua/mobdebug.lua
-- Get the official file
-- https://github.com/pkulchenko/MobDebug/blob/master/src/mobdebug.lua
-- ~/.textadept/modules/textadept/debugger.lua
-- Copyright 2007-2011 Mitchell mitchell<att>caladbolg.net. See LICENSE.
local _m = _M;
local L = _L;
local events = events
---
-- Language debugging support for the textadept module.
module('_M.textadept.debugger', package.seeall)
-- Markdown:
-- ## Overview
--
-- The debugger interface allows Textadept to communicate with a debugger for
-- stepping through code graphically. It supports many common actions such as
-- setting breakpoints and watch expressions, continuing, and stepping into,
-- over, and out of functions. More actions can be added for specific debugger
-- instances. This document provides the information necessary in order to
-- implement a new debugger for a language. For illustrative purposes, a
-- debugger for C/C++ that uses the GNU Debugger (gdb) will be created.
--
-- ## Implementing a Debugger
--
-- Debugger instances exist per-languge and are typically defined in a
-- [language-specific module](../manual/7_Modules.html#language_specific).
-- First check to see if the module for your language has a debugger. If not,
-- you will need to implement one. The language modules included with Textadept
-- have debuggers so they can be used for reference. If your language uses a
-- debugger like gdb, you can copy and modify C/C++'s debugger, saving some time
-- and effort.
--
-- #### Introduction
--
-- Open the languge-specific module for editing and create a new instance of a
-- debugger.
--
-- $> # from either _HOME or _USERHOME:
-- $> cd modules/cpp/
-- $> textadept init.lua
--
-- debugger = _m.textadept.debugger.new('cpp')
--
-- Where 'cpp' is replaced by your language's name.
--
-- #### Syntax Options
--
-- The syntax of different languages varies so the debugger must be configured
-- for your language in order to function properly.
--
-- ##### symbol_chars
--
-- In addition to the usual `[%w_%.]` symbol characters, C/C++ also allows
-- symbols to contain `->`.
--
-- debugger.symbol_chars = '[%w_%.%->]'
--
-- #### Debugger Functions
--
-- The debugger interface provides the framework for a debugger, but the
-- instance-specific functions still have to be defined.
--
-- ##### handle_start
--
-- ##### handle_set_breakpoint
--
-- ##### handle_delete_breakpoint
--
-- ##### handle_set_watch
--
-- ##### handle_delete_watch
--
-- ##### handle_stop
--
-- ##### handle_continue
--
-- ##### handle_step_into
--
-- ##### handle_step_over
--
-- ##### handle_step_out
--
-- ##### handle_inspect
--
-- ##### handle_command
--
-- ##### Other Functions
--
-- It is your responsibility to handle any errors that occur and stop the
-- debugger (`debugger:stop()`) if necessary to avoid unexpected behavior.
--
-- #### Summary
--
-- The above method of setting syntax options and defining debugger functions is
-- sufficient for most language debuggers. The rest of this document is devoted
-- to more complex techniques.
--
-- ## Settings
--
-- * `MARK_BREAKPOINT_COLOR` [number]: The color used for a breakpoint line in
-- `0xBBGGRR` format.
-- * `MARK_BREAKPOINT_ALPHA` [number]: The alpha used for a breakpoint line. The
-- default is `128`.
-- * `MARK_DEBUGLINE_COLOR` [number]: The color used for the current debug line
-- in `0xBBGGRR` format.
-- * `MARK_DEBUGLINE_ALPHA` [number]: The alpha used for the current debug line.
-- The default is `128`.
-- Settings.
MARK_BREAKPOINT_COLOR = 0x6D6DD9
MARK_BREAKPOINT_ALPHA = 0x80
MARK_DEBUGLINE_COLOR = 0x6DD96D
MARK_DEBUGLINE_ALPHA = 0x80
-- End settings.
local debuggers = {}
local MARK_BREAKPOINT = _SCINTILLA.next_marker_number()
local MARK_DEBUGLINE = _SCINTILLA.next_marker_number()
-- Gets the debugger for the current language, if any.
-- This is necessary for menu items and key commands assigned to generic
-- debugger functions without regard to the current language.
-- @return debugger or `nil`
local function get_debugger()
local m = _m[buffer:get_lexer()]
return m and m.debugger
end
---
-- Sets a breakpoint on the given line of the current file.
-- The debugger will break when that point in execution is reached.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param file The file to set the breakpoint in. Defaults to the current file.
-- @param line The line number to break on. Line numbers start at 1.
function set_breakpoint(debugger, file, line)
debugger = debugger or get_debugger()
if not debugger or not line then return end
if not file then file = buffer.filename end
print("set_breakpoint", debugger, file, line)
if debugger.debugging then
if not debugger:handle_set_breakpoint(file, line) then return end
end
if not debugger.breakpoints[file] then debugger.breakpoints[file] = {} end
debugger.breakpoints[file][line] = true
print("set_breakpoint2", file, buffer.filename);
if file == buffer.filename then
print("YOOOOO", buffer:marker_add(line - 1, MARK_BREAKPOINT));
end
end
---
-- Called to add a breakpoint to the debugger.
-- This method should be replaced with your own that is specific to the debugger
-- instance. You do not have to modify the `debugger.breakpoints` table; you
-- only have to add the breakpoint to the debugger instance.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param file The file to add the breakpoint to.
-- @param line The line number to add the breakpoint at. Line numbers start at
-- 1.
-- @return `true` if successful; `false` otherwise
function handle_set_breakpoint(debugger, file, line) return true end
-- Returns a sorted list of breakpoints for a filtered list.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param file Optional file to list breakpoints from. Otherwise lists all
-- breakpoints in all files.
local function get_breakpoints(debugger, file)
local breakpoints = {}
if not file then
for file, file_breakpoints in pairs(debugger.breakpoints) do
for line, breakpoint in pairs(file_breakpoints) do
if breakpoint then breakpoints[#breakpoints + 1] = file..':'..line end
end
end
else
for line, breakpoint in pairs(debugger.breakpoints[file] or {}) do
if breakpoint then breakpoints[#breakpoints + 1] = file..':'..line end
end
end
table.sort(breakpoints)
return breakpoints
end
---
-- Deletes a breakpoint on the given line of the current file.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param file The file to delete the breakpoint from. Defaults to the current
-- file.
-- @param line The line number to break on. Line numbers start at 1.
function delete_breakpoint(debugger, file, line)
debugger = debugger or get_debugger()
if not debugger then return end
if not file or not line then
local result = gui.filteredlist('Delete Breakpoint', 'Breakpoint:',
get_breakpoints(debugger, file), false,
'--select-multiple')
if not result then return end
for breakpoint in result:gmatch('[^\n]+') do
file, line = breakpoint:match('^(.+):(%d+)$')
line = tonumber(line)
debugger:delete_breakpoint(file, line)
end
return
end
if debugger.breakpoints[file] then
if debugger.debugging then debugger:handle_delete_breakpoint() end
debugger.breakpoints[file][line] = nil
end
if file == buffer.filename then
buffer:marker_delete(line - 1, MARK_BREAKPOINT)
end
end
---
-- Toggles a breakpoint on the given line of the current file.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param file The file to toggle the breakpoint in. Defaults to the current
-- file.
-- @param line The line number to toggle the break on. Defaults to the current
-- line. Line numbers start at 1.
function toggle_breakpoint(debugger, file, line)
print("toggle_breakpoint", debugger, file, line)
debugger = debugger or get_debugger();
if not debugger then return end
if not file then file = buffer.filename end
if not line then line = buffer:line_from_position(buffer.current_pos) + 1 end
if debugger.breakpoints[file] and debugger.breakpoints[file][line] then
delete_breakpoint(debugger, file, line)
else
set_breakpoint(debugger, file, line)
end
end
---
-- Called to delete a breakpoint from the debugger.
-- This method should be replaced with your own that is specific to the debugger
-- instance. You do not have to modify the `debugger.breakpoints` table; you
-- only have to remove the breakpoint from the debugger instance.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param file The file to delete the breakpoint from.
-- @param line The line number to delete the breakpoint at. Line numbers start
-- at 1.
function handle_delete_breakpoint(debugger, file, line) end
---
-- Sets a watch expression.
-- The debugger will break when the expression evaluates to `true`.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param expr The expression to watch. If `nil`, prompts the user for one.
-- @return watch expression ID number
function set_watch(debugger, expr)
debugger = debugger or get_debugger()
if not debugger then return end
if not expr then
local out = gui.dialog('standard-inputbox',
'--title', 'Set Watch',
'--text', 'Expression:',
'--no-newline')
local response, value = out:match('^([^\n]+)\n(.-)$')
if response ~= '1' or value == '' then return end
expr = value
end
if debugger.debugging and not debugger:handle_set_watch(expr) then return end
debugger.watches[#debugger.watches + 1] = expr
debugger.watches[expr] = #debugger.watches
return #debugger.watches
end
---
-- Called to add a watch expression to the debugger.
-- This method should be replaced with your own that is specific to the debugger
-- instance. You do not have to modify the `debugger.watches` table; you only
-- have to add the watch to the debugger instance.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param expr The expression to watch.
-- @return `true` if successful; `false` otherwise
function handle_set_watch(debugger, expr) return true end
---
-- Deletes a watch expression.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param i The ID number of the watch expression. If `nil`, prompts the user
-- for one.
function delete_watch(debugger, i)
debugger = debugger or get_debugger()
if not debugger then return end
if not i then
local w = {}
for i, watch in ipairs(debugger.watches) do w[i] = watch end
i = gui.filteredlist('Delete Watch', 'Expression:', w, true)
if not i then return end
i = i + 1
end
if debugger.watches[i] then
if debugger.debugging then debugger:handle_delete_watch(i) end
debugger.watches[debugger.watches[i]] = nil
table.remove(debugger.watches, i)
end
end
---
-- Called to delete a watch from the debugger.
-- This method should be replaced with your own that is specific to the debugger
-- instance. You do not have to modify the `debugger.watches` table; you only
-- have to remove the watch from the debugger instance.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param i The ID number of the watch expression.
function handle_delete_watch(debugger, i) end
---
-- Start the debugger.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param ... Any additional parameters passed to `handle_start()`.
-- @see handle_start
function start(debugger, ...)
print("debugger.start", ...)
debugger = debugger or get_debugger()
if not debugger or debugger.debugging then return end
debugger.debugging = true
local ok, err = pcall(debugger.handle_start, debugger, ...)
if not ok then
debugger:stop()
error(err)
end
-- Load breakpoints and watches.
for file, breakpoints in pairs(debugger.breakpoints) do
if type(breakpoints) == 'table' then
for line, breakpoint in pairs(breakpoints) do
if breakpoint then debugger:handle_set_breakpoint(file, line) end
end
end
end
for _, expr in ipairs(debugger.watches) do debugger:handle_set_watch(expr) end
debugger:continue() -- start executing immediately
end
---
-- Called when starting the debugger.
-- This method should be replaced with your own that is specific to the debugger
-- instance. `debugger:stop()` is called automatically if an error occurs.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param ... Any additional parameters passed from `start()`.
function handle_start(debugger, ...) end
---
-- Stop the debugger.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param ... Any additional parameters passed to `handle_stop()`.
-- @see handle_stop
function stop(debugger, ...)
debugger = debugger or get_debugger()
if not debugger or not debugger.debugging then return end
debugger.debugging = false
pcall(debugger.handle_stop, debugger, ...)
buffer:marker_delete_all(MARK_DEBUGLINE)
if debugger.state and debugger.state.error then
buffer:annotation_clear_all()
end
debugger.state = nil
end
---
-- Called when stopping the debugger.
-- This method should be replaced with your own that is specific to the debugger
-- instance.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param ... Any additional parameters passed from `stop()`.
function handle_stop(debugger, ...) end
-- Perform a debugger function.
-- If an error occurs, the debugger is stopped.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param f The string function name.
-- @param ... Any additional parameters to pass to the function.
local function perform(debugger, f, ...)
debugger = debugger or get_debugger()
if not debugger or not debugger.debugging then return end
local ok, err = pcall(debugger[f], debugger, ...)
if not ok then
debugger:stop()
error(err .. debug.traceback())
end
end
---
-- Continue debugger execution until the next breakpoint.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param ... Any additional parameters passed to `handle_continue()`.
-- @see handle_continue
function continue(debugger, ...) perform(debugger, 'handle_continue', ...) end
---
-- Called when continuing execution until the next breakpoint.
-- This method should be replaced with your own that is specific to the debugger
-- instance. `debugger:stop()` is called automatically if an error occurs.
-- Call `debugger:update_state()` after handling the continue.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param ... Any additional parameters passed from `continue()`.
-- @see update_state
function handle_continue(debugger, ...) end
---
-- Continue debugger execution by one line, stepping into functions.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param ... Any additional parameters passed to `handle_step_into()`.
-- @see handle_step_into
function step_into(debugger, ...) perform(debugger, 'handle_step_into', ...) end
---
-- Called when continuing execution by one line, stepping into functions.
-- This method should be replaced with your own that is specific to the debugger
-- instance. `debugger:stop()` is called automatically if an error occurs.
-- Call `debugger:update_state()` after handling the step into.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param ... Any additional parameters passed from `step_into()`.
-- @see update_state
function handle_step_into(debugger, ...) end
---
-- Continue debugger execution by one line, stepping over functions.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param ... Any additional parameters passed to `handle_step_over()`.
-- @see handle_step_over
function step_over(debugger, ...) perform(debugger, 'handle_step_over', ...) end
---
-- Called when continuing execution by one line, stepping over functions.
-- This method should be replaced with your own that is specific to the debugger
-- instance. `debugger:stop()` is called automatically if an error occurs.
-- Call `debugger:update_state()` after handling the step over.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param ... Any additional parameters passed from `step_over()`.
-- @see update_state
function handle_step_over(debugger, ...) end
---
-- Continue debugger execution, stepping out of the current function.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param ... Any additional parameters passed to `handle_step_out()`.
-- @see handle_step_out
function step_out(debugger, ...) perform(debugger, 'handle_step_out', ...) end
---
-- Called when continuing execution, stepping out of the current function.
-- This method should be replaced with your own that is specific to the debugger
-- instance. `debugger:stop()` is called automatically if an error occurs.
-- Call `debugger:update_state()` after handling the step out.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param ... Any additional parameters passed from `step_out()1.
-- @see update_state
function handle_step_out(debugger, ...) end
---
-- Updates the debugger's state and marks the current debug line.
-- This method should be called whenever the debugger's state has changed,
-- typically in the set of `handle_*` functions.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param state A table with at least two fields: `file` and `line`, indicating
-- the debugger's current position. It will be assigned to the `debugger.state`
-- field and may also contain other information useful to the debugger
-- implementation. If an `error` field is present, the error message is shown in
-- an annotation. If state is `nil` or not a table, `debugger:stop()` is called.
-- @see state
function update_state(debugger, state)
debugger.state = state
if type(state) ~= 'table' then
debugger:stop()
if state then error(state) end
return
end
buffer:marker_delete_all(MARK_DEBUGLINE)
local file = state.file:iconv('UTF-8', _CHARSET)
if state.file ~= buffer.filename then io.open_file(file) end
buffer:marker_add(state.line - 1, MARK_DEBUGLINE)
buffer:goto_line(state.line - 1)
if state.error then
buffer:annotation_set_text(state.line - 1, state.error)
buffer.annotation_style[state.line - 1] = 8 -- error style number
end
end
---
-- Show the current call stack in a dropdown box in order to move between
-- frames.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param ... Any additional parameters passed to `get_call_stack()`.
-- @see get_call_stack
-- @see set_stack
function call_stack(debugger, ...)
debugger = debugger or get_debugger()
if not debugger or not debugger.debugging then return end
local stack, pos = debugger:get_call_stack(...)
local out = gui.dialog('standard-dropdown',
'--title', 'Call Stack',
'--items', stack,
'--selected', pos or 0)
local response, level = out:match('^(%d+)\n(%d+)')
if response == '1' then debugger:set_stack(tonumber(level)) end
end
---
-- Called when showing the current call stack.
-- This method should be replaced with your own that is specific to the debugger
-- instance. It is your responsibility to handle any errors that occur and stop
-- the debugger if necessary to avoid unexpected behavior.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param ... Any additional parameters passed from `call_stack()`.
-- @return a table of string stack positions and a number indicating the current
-- stack position. The number defaults to `0`, the first table value.
function get_call_stack(debugger, ...) end
---
-- Called when changing stack frames.
-- This method should be replaced with your own that is specific to the debugger
-- instance. It is your responsibility to handle any errors that occur and stop
-- the debugger if necessary to avoid unexpected behavior.
-- Call `debugger:update_state()` after changing stack frames.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param level The level of the stack frame to change to.
function set_stack(debugger, level) end
---
-- Inspects a symbol at the given position.
-- Symbols can have any character in the `debugger.symbol_chars` pattern and
-- must have a style defined in the `debugger.inspect_styles` table.
-- @param debugger The debugger returned by `debugger.new()`. Defaults to the
-- debugger for the current language.
-- @param pos The buffer position to inspect at.
-- @see handle_inspect
function inspect(debugger, pos)
local buffer = buffer
debugger = debugger or get_debugger()
if not pos then pos = buffer.current_pos end
if debugger and debugger.debugging and pos > 0 and
debugger.inspect_styles[buffer:get_style_name(buffer.style_at[pos])] then
local s = buffer:position_from_line(buffer:line_from_position(pos))
local e = buffer:word_end_position(pos, true)
local line = buffer:text_range(s, e)
debugger:handle_inspect(line:match(debugger.symbol_chars..'+$'), pos)
end
end
---
-- Called when inspecting a symbol during a debug session.
-- This method should be replaced with your own that is specific to the debugger
-- instance. Usually a call tip is displayed with the symbol's value.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param symbol The symbol being inspected.
-- @param position The buffer position at inspection. This is useful for
-- displaying a call tip.
-- @see buffer.call_tip_show
function handle_inspect(debugger, symbol, position) end
---
-- Called when a command in the command entry is entered during a debug session.
-- This method should be replaced with your own that is specific to the debugger
-- instance. It is your responsibility to handle any errors that occur and stop
-- the debugger if necessary to avoid unexpected behavior.
-- If you want, call `gui.command_entry.focus()` to hide the command entry while
-- it still has focus.
-- @param debugger The debugger returned by `debugger.new()`.
-- @param text The command text.
-- @return `true` if the command was consumed
function handle_command(debugger, text) end
---
-- Creates a new debugger for the given lexer language.
-- Only one debugger can exist per language.
-- @param lang The lexer language to create a debugger for.
-- @return debugger
-- @usage local debugger = _m.textadept.debugger.new('lua')
function new(lang)
local debugger = debuggers[lang]
if debugger then
if debugger.debugging then error('Debugger running') end
debugger.breakpoints = nil
debugger.watches = nil
end
debugger = setmetatable({
lexer = lang,
debugging = false,
symbol_chars = '[%w_%.]',
---
-- The set of breakpoints for the debugger.
-- Each key is a filename that contains a table of line numbers with boolean
-- values indicating whether or not breakpoints are set on those lines. When
-- the debugger reaches a line that has a breakpoint, it breaks.
-- @class table
-- @name breakpoints
breakpoints = {},
---
-- The set of watches for the debugger.
-- This table contains a list of watch expressions and also watch expression
-- keys with values indicating the index of that expression in the table.
-- @class table
-- @name watches
watches = {},
---
-- The current state of the debugger.
-- It is guaranteed to contain `file` and `line` fields, but can also contain
-- other fields useful to the debugger implementation.
-- @class table
-- @name state
-- @field file The file (encoded in _CHARSET, not UTF-8) the debugger is in.
-- @field line The line the debugger is on.
-- @field error If an error occured in the program being debugged, this is an
-- error message that will be displayed as an annotation.
-- @see update_state
state = {},
---
-- The styles a symbol can have in order to determine and show the symbol's
-- value during a debug session.
-- Each key is a style name with a boolean value indicating whether or not the
-- style can contain a symbol. The default contains the `identifier` style.
-- @field identifier Identifiers contain symbols.
inspect_styles = { identifier = true },
super = setmetatable({}, { __index = _M })
}, { __index = _M })
debuggers[lang] = debugger
return debugger
end
-- Sets view properties for debug markers.
local function set_marker_properties()
local buffer = buffer
buffer.marker_back[MARK_BREAKPOINT] = MARK_BREAKPOINT_COLOR;
buffer.marker_alpha[MARK_BREAKPOINT] = MARK_BREAKPOINT_ALPHA;
buffer.marker_back[MARK_DEBUGLINE] = MARK_DEBUGLINE_COLOR;
buffer.marker_alpha[MARK_DEBUGLINE] = MARK_DEBUGLINE_ALPHA;
end
if buffer then set_marker_properties() end
events.connect(events.VIEW_NEW, set_marker_properties)
-- Set breakpoint on margin-click.
events.connect(events.MARGIN_CLICK, function(margin, position, modifiers)
print("MARGIN_CLICK", margin, position, modifiers);
if margin == 1 and modifiers == 0 then
toggle_breakpoint(nil, nil, buffer:line_from_position(position) + 1)
end
end)
-- Update breakpoints after switching buffers.
events.connect(events.BUFFER_AFTER_SWITCH, function()
local debugger = get_debugger()
if not debugger then return end
local file = buffer.filename
if not debugger.breakpoints[file] then return end
local buffer = buffer
-- Delete markers for breakpoints that have been removed.
local line = buffer:marker_next(0, 2^MARK_BREAKPOINT)
while line >= 0 do
if not debugger.breakpoints[file][line + 1] then
buffer:marker_delete(line, MARK_BREAKPOINT)
end
line = buffer:marker_next(line + 1, 2^MARK_BREAKPOINT)
end
-- Add markers for breakpoints that have been added.
for line, v in pairs(debugger.breakpoints[file]) do
if v and buffer:marker_next(line - 1, 2^MARK_BREAKPOINT) ~= line - 1 then
buffer:marker_add(line - 1, MARK_BREAKPOINT)
end
end
end)
-- Inspect symbols and show call tips during mouse dwell events.
events.connect(events.DWELL_START, function(pos) inspect(nil, pos) end)
events.connect(events.DWELL_END, buffer.call_tip_cancel)
-- Handle command entry commands.
events.connect(events.COMMAND_ENTRY_COMMAND, function(text)
local debugger = get_debugger()
if debugger and debugger.debugging and debugger:handle_command(text) then
return true
end
end, 1) -- place before command_entry.lua's handler (if necessary)
-- ~/.textadept/modules/lua/debugger.lua
-- Original (broken) debugger, by Mitchell
-- Copyright 2007-2011 Mitchell mitchell<att>caladbolg.net. See LICENSE.
-- Lua Debugger extension for the Lua module.
-- Markdown:
-- ## Key Commands
--
-- + `Ctrl+L, D, S` (`⌘L, D, S` on Mac OSX): Start debugging.
-- + `Ctrl+L, D, Q` (`⌘L, D, Q` on Mac OSX): Stop debugging.
-- + `Ctrl+L, D, C` (`⌘L, D, C` on Mac OSX): Continue.
-- + `Ctrl+L, D, N` (`⌘L, D, N` on Mac OSX): Step over.
-- + `Ctrl+L, D, S` (`⌘L, D, S` on Mac OSX): Step into.
-- + `Ctrl+L, D, O` (`⌘L, D, O` on Mac OSX): Step out.
-- + `Ctrl+L, D, I` (`⌘L, D, I` on Mac OSX): Inspect.
-- + `Ctrl+L, D, L` (`⌘L, D, L` on Mac OSX): Show call stack.
-- + `Ctrl+L, D, B` (`⌘L, D, B` on Mac OSX): Toggle breakpoint.
-- + `Ctrl+L, D, Shift+B` (`⌘L, D, ⇧B` on Mac OSX): Delete breakpoint.
-- + `Ctrl+L, D, W` (`⌘L, D, W` on Mac OSX): Set watch.
-- + `Ctrl+L, D, Shift+W` (`⌘L, D, ⇧W` on Mac OSX): Delete watch.
require 'textadept.debugger'
local _m = _M;
local debugger = _m.textadept.debugger.new('lua')
_m.lua.debugger = debugger
-- For debugging coroutines within this debugger's coroutines, the current debug
-- hook needs to be propagated so `coroutine.create` and `coroutine.wrap` need
-- to be modified.
local coroutine_create, coroutine_wrap = coroutine.create, coroutine.wrap
function coroutine.create(f)
local hook, mask, count = debug.gethook()
if not hook then return coroutine_create(f) end
local thread
local function thread_hook(event, line) hook(event, line, thread, 3) end
thread = coroutine_create(function(...)
step_level[thread], stack_level[thread] = 0, 0
debug.sethook(thread_hook, mask, count)
return f(...)
end)
return thread
end
function coroutine.wrap(f)
local hook, mask, count = debug.gethook()
if not hook then return coroutine_wrap(f) end
local function thread_hook(event, line) hook(event, line, thread, 3) end
thread = coroutine_wrap(function(...)
step_level[thread], stack_level[thread] = 0, 0
debug.sethook(thread_hook, mask, count)
return f(...)
end)
return thread
end
-- Get the stack at a given stack level for the debugger.
-- @param level The stack level to get the stack at.
-- @return table stack levels
function debugger.get_stack(level)
local stack = {}
while true do
local info = debug.getinfo(level)
if not info then break end
stack[#stack + 1] = info
level = level + 1
end
-- Ignore the last two stack levels which are calls from this debugger.
stack[#stack], stack[#stack - 1] = nil, nil
return stack
end
-- Get the environment at a given stack level for the debugger.
-- The returned table is suitable for setting as a function environment.
-- @param level The stack level to get the environment at.
-- @return table of name-value variable pairs. Also contains _LOCALS, _UPVALUES,
-- _GLOBALS, and _ENV tables containing their respective variable types.
function debugger.get_env(level)
local env = { _LOCALS = {}, _UPVALUES = {}, _GLOBALS = getfenv(0) }
-- Upvalues.
local func = debug.getinfo(level, 'f').func
local i = 1
while true do
local name, value = debug.getupvalue(func, i)
if not name then break end
if name:sub(1, 1) ~= '(' then
env[name], env._UPVALUES[name] = value, value
end
i = i + 1
end
env._ENV = getfenv(func)
-- Local variables (override upvalues as necessary).
i = 1
while true do
local name, value = debug.getlocal(level, i)
if not name then break end
if name:sub(1, 1) ~= '(' then
env[name], env._LOCALS[name] = value, value
end
i = i + 1
end
setmetatable(env, { __index = env._ENV, __newindex = env._ENV })
return env
end
-- Hook called by the Lua debug library used for debugging.
-- @param event The event, either 'call', 'return', 'tail return', 'line', or
-- 'count'.
-- @param line The line number.
-- @param thread The currently running thread. This is non-nil when a coroutine
-- is being run from within the debug coroutine.
-- @param level The level of the currently running thread. This is non-nil when
-- a coroutine is being run from within the debug coroutine.
function debugger.debug_hook(event, line, thread, level)
if not debugger.debugging then return end
debugger.current_thread = thread or 'main'
level = level or 2
local step_level, stack_level = debugger.step_level, debugger.stack_level
local current_thread = debugger.current_thread
if event == 'call' then
stack_level[current_thread] = stack_level[current_thread] + 1
elseif event == 'return' then
stack_level[current_thread] = stack_level[current_thread] - 1
if stack_level[current_thread] < 0 then stack_level[current_thread] = 0 end
if stack_level['main'] == 1 then debugger:stop() end -- finished xpcall()
else
-- Get the filename.
local file = debug.getinfo(level, 'S').source:match('^@?(.+)$')
if file:find('modules[/\\]lua[/\\]debugger%.lua$') then return end
-- Get the stack trace.
local stack = debugger.get_stack(level + 1)
-- Get the current environment.
local env = debugger.get_env(level + 1)
-- Check watches.
local watch_id
for i = 1, #debugger.watches do
local f = debugger.watches[debugger.watches[i]]
if type(f) == 'function' then
local ok, result = pcall(setfenv(f, env))
if ok and result then watch_id = i break end
end
end
-- If at a breakpoint or watch, stepping into, or stepping over, resume the
-- debugger coroutine to get the next instruction.
if debugger.breakpoints[file] and debugger.breakpoints[file][line] or
watch_id or debugger.stepping_into or debugger.stepping_over and
(stack_level[current_thread] <= step_level[current_thread] or
stack_level[current_thread] == 0) then
local command = coroutine.yield {
file = file, line = line, stack = stack, env = env, watch_id = watch_id
}
repeat
local continue = true
if not command then
debugger:stop()
elseif command == 'continue' then
debugger.stepping_into, debugger.stepping_over = false, false
elseif command == 'step_into' then
debugger.stepping_into, debugger.stepping_over = true, false
elseif command == 'step_over' then
debugger.stepping_into, debugger.stepping_over = false, true
step_level[current_thread] = stack_level[current_thread]
elseif command == 'step_out' then
debugger.stepping_into, debugger.stepping_over = false, true
step_level[current_thread] = stack_level[current_thread] - 1
elseif command:find('^set_stack') then
local i = tonumber(command:match('^set_stack (%d+)'))
local info = debug.getinfo(level + i, 'Sl')
if info.what ~= 'C' then
command = coroutine.yield {
file = info.source:match('^@?(.+)$'), line = info.currentline,
stack = stack, stack_pos = i,
env = debugger.get_env(level + i + 1)
}
else
command = coroutine.yield { C = true }
end
continue = false
end
until continue
end
end
end
-- Environment for debug scripts.
-- @class table
-- @name ENV
local ENV = {
'assert', 'collectgarbage', 'dofile', 'error', 'getfenv', 'getmetatable',
'ipairs', 'load', 'loadfile', 'loadstring', 'next', 'pairs', 'pcall', 'print',
'rawequal', 'rawget', 'rawset', 'select', 'setfenv', 'setmetatable',
'tonumber', 'tostring', 'type', 'unpack', 'xpcall', 'coroutine', 'module',
'require', 'table', 'math', 'os', 'debug', 'lpeg', 'lfs',
'_VERSION',
-- Some functions and fields in the following libraries need to be excluded
-- either because they belong to Textadept or they are data references that
-- Textadept's state shares and could cause problems if modified.
string = {
'byte', 'char', 'dump', 'find', 'format', 'gmatch', 'gsub', 'len', 'lower',
'match', 'rep', 'reverse', 'sub', 'upper'
},
io = {
'close', 'flush', 'lines', 'open', 'popen', 'read', 'tmpfile', 'write',
'type'
},
package = { 'cpath', 'loaders', 'loadlib', 'path', 'seeall' }
}
-- Creates the environment for debug scripts.
-- @see ENV
local function create_env()
local env = {}
-- Create the env from ENV.
for k, v in pairs(ENV) do
if type(k) == 'number' then
env[v] = _G[v]
else
env[k] = {}
for k2, v2 in ipairs(v) do env[k][v2] = _G[k][v2] end
end
end
-- Create new references that do not interfere with Textadepts'.
env._G, env.package.loaded, env.package.preload = env, {}, {}
-- Modify input/output functions to interface with Textadept.
env.print = function(...)
debugger.debugging = false -- do not debug the following function calls
local prev_view = #_VIEWS == 1 and 1 or _VIEWS[view]
gui.print(...)
gui.goto_view(prev_view)
debugger.debugging = true -- resume debugging normally
end
env.io.stdin = { read = function(_, ...)
local input = gui.dialog('inputbox', '--title', 'stdin', '--width', 400)
return input:match('([^\n]+)\n$')
end }
env.io.stdout = { write = function(_, ...) env.print(...) end }
env.io.stderr = { write = function(_, ...) env.print('STDERR:', ...) end }
env.io.input = function(f)
if not f then return env.io.stdin end
env.io.stdin = f
end
env.io.output = function(f)
if not f then return env.io.stdout end
env.io.stdout = f
end
env.io.read = function(...) return env.io.input():read(...) end
env.io.write = function(...) return env.io.output():write(...) end
return env
end
-- Implementation for debugger:start().
-- @param filename The file to debug. Defaults to buffer.filename.
function debugger:handle_start(filename)
print("debugger.handle_start", filename)
if not filename then filename = buffer.filename end
local f, err = loadfile(filename)
if not f then error(err) end
self.co = coroutine_create(function(f)
self.current_thread = 'main'
self.stepping_into, self.stepping_over = false, false
self.step_level = { [self.current_thread] = 0 }
self.stack_level = { [self.current_thread] = 1 }
coroutine.yield()
setfenv(f, create_env())
debug.sethook(self.debug_hook, 'clr')
xpcall(f, function(err)
local info = debug.getinfo(2, 'Sl')
-- If the error occurs in C (e.g. via Lua's 'error' function), go up the
-- stack to where the error in Lua occurred.
if info.what == 'C' then info = debug.getinfo(3, 'Sl') end
local file, line = info.source:match('^@?(.+)$'), info.currentline
local stack, env = self.get_stack(3), self.get_env(3)
coroutine.yield {
file = file, line = line, stack = stack, env = env, error = err
}
end)
self:stop()
end)
coroutine.resume(self.co, f)
end
-- Implementation for debugger:stop().
function debugger:handle_stop()
debug.sethook()
coroutine.resume(self.co, false)
end
-- Performs a debugger action.
-- @param action The action to perform: 'continue', 'step_into', 'step_over', or
-- 'step_out'.
local function handle(debugger, action)
print("handle", action);
local ok, state = coroutine.resume(debugger.co, action)
debugger:update_state(state)
end
-- Implementation for debugger:continue().
function debugger:handle_continue() handle(self, 'continue') end
-- Implementation for debugger:step_into().
function debugger:handle_step_into() handle(self, 'step_into') end
-- Implementation for debugger:step_over().
function debugger:handle_step_over() handle(self, 'step_over') end
-- Implementation for debugger:step_out().
function debugger:handle_step_out() handle(self, 'step_out') end
-- Implementation for debugger:set_watch().
-- Loads the given expression as a Lua chunk so it can be evaluated by the debug
-- hook.
function debugger:handle_set_watch(expr)
local f, err = loadstring('return ('..expr..')')
if not f then error(err) end
self.watches[expr] = f
return true
end
-- Implementation for debugger:delete_watch().
function debugger:handle_delete_watch(expr) self.watches[expr] = nil end
-- Implementation for debugger:get_call_stack().
function debugger:get_call_stack()
if not self.state then return end
local stack = self.state.stack
local t = {}
for _, info in ipairs(stack) do
t[#t + 1] = ('(%s) %s:%d'):format(info.name or info.what, info.short_src,
info.currentline)
end
return t, self.state.stack_pos
end
-- Implementation for debugger:set_stack().
function debugger:set_stack(level)
local ok, state = coroutine.resume(debugger.co, 'set_stack '..level)
if not state.C then self:update_state(state) end
end
-- Lua reserved words.
-- Used when displaying table keys in table_tostring().
-- @class table
-- @name reserved
local reserved = {
['and'] = 1, ['break'] = 1, ['do'] = 1, ['else'] = 1, ['elseif'] = 1,
['end'] = 1, ['false'] = 1, ['for'] = 1, ['function'] = 1, ['if'] = 1,
['in'] = 1, ['local'] = 1, ['nil'] = 1, ['not'] = 1, ['or'] = 1,
['repeat'] = 1, ['return'] = 1, ['then'] = 1, ['true'] = 1, ['until'] = 1,
['while'] = 1
}
local truncate_len = 25
-- Prints a value to a string as it might look in Lua syntax.
-- @param value The value to print.
-- @param truncate Flag indicating whether or not to truncate long strings.
-- @param level The table level (non-zero for tables inside tables).
-- @param visited Table of visited tables so recursion does not happen.
local function tostringi(value, truncate, level, visited)
local truncate_len = truncate and truncate_len or math.huge
if type(value) == 'string' then
local v = ('%q'):format(value)
if #v > truncate_len then v = v:sub(1, truncate_len)..' ..."' end
return v
elseif type(value) == 'table' then
if not visited then visited = {} end
local indent = (' '):rep(2 * (level or 0))
local s = { '{ -- '..tostring(value) }
for k, v in pairs(value) do
if type(k) == 'string' then
if #k > truncate_len then k = k:sub(1, truncate_len)..' ...' end
if not k:find('^[%w_]+$') or reserved[k] then k = ('[%q]'):format(k) end
else
k = '['..tostring(k)..']'
end
if type(v) == 'string' then
v = ('%q'):format(v)
if #v > truncate_len then v = v:sub(1, truncate_len)..' ..."' end
elseif type(v) == 'table' and not visited[v] then
visited[v] = true
v = tostringi(v, truncate, (level or 0) + 1, visited)
else
v = tostring(v)
end
s[#s + 1] = ('%s %s = %s,'):format(indent, k, v)
end
s[#s + 1] = ('%s}'):format(indent)
return table.concat(s, '\n')
else
return tostring(value)
end
end
-- Implementation for debugger:inspect().
-- If a table value contains more than 20 lines, it is truncated.
function debugger:handle_inspect(symbol, pos)
if self.state and self.state.env then
local f = loadstring('return '..symbol)
local ok, value = pcall(setfenv(f, self.state.env))
if not ok then return end
value = tostringi(value, true)
local lines, s = 1, value:find('\n')
while s and lines < 20 do
lines = lines + 1
s = value:find('\n', s + 1)
end
if lines >= 20 then value = value:sub(1, s)..'...' end
buffer:call_tip_show(pos, symbol..' = '..value)
end
end
-- Implementation for debugger:command().
-- Handle commands from the command entry during a debug session.
function debugger:handle_command(text)
if not self.state or not self.state.env then return end
gui.command_entry.focus() -- hide
gui.print(text)
if text:find('^%s*=') then
local f, err = loadstring('return '..text:match('^%s*=(.+)$'))
if not f then error(err) end
local values = { setfenv(f, self.state.env)() }
local n = select('#', unpack(values))
for i = 1, n do values[i] = tostringi(values[i]) end
gui.print(table.concat(values, '\t'))
else
local f, err = loadstring(text)
if not f then error(err) end
setfenv(f, self.state.env)()
end
return true
end
-- Adeptsense.
_m.lua.sense.syntax.type_assignments['^(_m%.textadept%.debugger)%.new'] = '%1'
-- Key commands.
keys.lua[keys.LANGUAGE_MODULE_PREFIX].d = {
d = debugger.start,
q = debugger.stop,
c = debugger.continue,
n = debugger.step_over,
s = debugger.step_into,
o = debugger.step_out,
i = debugger.inspect,
l = debugger.call_stack,
b = debugger.toggle_breakpoint,
B = debugger.delete_breakpoint,
w = debugger.set_watch,
W = debugger.delete_watch
}
-- Context menu.
local SEPARATOR = { '' }
_m.lua.context_menu = {
{ 'Undo', buffer.undo },
{ 'Redo', buffer.redo },
SEPARATOR,
{ 'Cut', buffer.cut },
{ 'Copy', buffer.copy },
{ 'Paste', buffer.paste },
{ 'Delete', buffer.clear },
SEPARATOR,
{ 'Select All', buffer.select_all },
SEPARATOR,
{ title = 'De_bug',
{ 'Start _Debugging', debugger.start },
{ 'Sto_p Debugging', debugger.stop },
SEPARATOR,
{ 'Debug _Continue', debugger.continue },
{ 'Debug Step _Over', debugger.step_over },
{ 'Debug Step _Into', debugger.step_into },
{ 'Debug Step Ou_t', debugger.step_out },
SEPARATOR,
{ 'Debug I_nspect', debugger.inspect },
{ 'Debug Call Stac_k...', debugger.call_stack },
SEPARATOR,
{ 'Toggle _Breakpoint', debugger.toggle_breakpoint },
{ '_Delete Breakpoint...', debugger.delete_breakpoint },
{ 'Set _Watch Expression', debugger.set_watch },
{ 'D_elete Watch Expression...', debugger.delete_watch },
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment