Forked from
tome / Tales of MajEyal
3508 commits behind the upstream repository.
-
Hachem_Muche authored
string.fromFunction(fct, fmt) returns a string abbreviation for a function, including filepath, line numbers string.fromValue(v, recurse, offset, prefix, suffix) -- enhancement of tostring function with more thorough handling of tables and functions string.fromTable(src, recurse, offset, prefix, suffix, key_recurse): returns a single-line string representation of a table, including string representations of subtables and functions. The recursion level for both keys and values can be set. The converted string is lua source code compatible, depending on arguments.
Hachem_Muche authoredstring.fromFunction(fct, fmt) returns a string abbreviation for a function, including filepath, line numbers string.fromValue(v, recurse, offset, prefix, suffix) -- enhancement of tostring function with more thorough handling of tables and functions string.fromTable(src, recurse, offset, prefix, suffix, key_recurse): returns a single-line string representation of a table, including string representations of subtables and functions. The recursion level for both keys and values can be set. The converted string is lua source code compatible, depending on arguments.
DebugConsole.lua 17.61 KiB
-- TE4 - T-Engine 4
-- Copyright (C) 2009 - 2017 Nicolas Casalini
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org
require "engine.class"
require "engine.Dialog"
--- Debug Console
-- @classmod engine.DebugConsole
-- @inherit engine.Dialog
module(..., package.seeall, class.inherit(engine.Dialog))
-- Globals to all instances of the console
--- Current scroll offset
offset = 0
--- History of debug console defaults to help text for easy usage
history = {
[[<<<<<------------------------------------------------------------------------------------->>>>>]],
[[< Welcome to the T-Engine Lua Console >]],
[[<--------------------------------------------------------------------------------------------->]],
[[< You have access to the T-Engine global namespace. >]],
[[< To execute commands, simply type them and hit Enter. >]],
[[< To see the return values of a command, start the line off with a "=" character. >]],
[[< For a table, this will not show keys inherited from a metatable (usually class functions). >]],
[[<--------------------------------------------------------------------------------------------->]],
[[< Here are some useful keyboard shortcuts: >]],
[[< Left/right arrows :=: Move the cursor position left/right >]],
[[< Ctrl+A or Home :=: Move the cursor to the beginning of the line >]],
[[< Ctrl+E or End :=: Move the cursor to the end of the line >]],
[[< Ctrl+K or Ctrl+End :=: Move the cursor to the end of the line >]],
[[< Ctrl+Backspace :=: Delete to beginning of line >]],
[[< Ctrl+Del :=: Delete to end of line >]],
[[< Up/down arrows :=: Move between previous/later executed lines >]],
[[< Ctrl+Space :=: Print help for the function to the left of the cursor >]],
[[< Ctrl+Shift+Space :=: Print the entire definition for the function >]],
[[< Tab :=: Auto-complete path strings or tables at the cursor >]],
[[< Page Up :=: Scrolls up 75% of the history >]],
[[< Page Down :=: Scrolls down 75% of the history >]],
[[<<<<<------------------------------------------------------------------------------------->>>>>]],
}
--- String representation of current line
line = ""
--- Integer Position in current line
line_pos = 0
--- Integer command selection
com_sel = 0
--- Registered commands
commands = {}
local find_base
--- Parses a string for autocompletion
-- @local
-- @string remaining the string to parse, also used for recursion
-- @return[1] nil
-- @return[1] error object
-- @return[2] nil
-- @return[2] "%s does not exist."
-- @return[3] nil
-- @return[3] "%s is not a valid path"
-- @return[4] head
-- @return[4] tail
find_base = function(remaining)
-- Check if we are in a string by counting quotation marks
local _, nsinglequote = remaining:gsub("\'", "")
local _, ndoublequote = remaining:gsub("\"", "")
if (nsinglequote % 2 ~= 0) or (ndoublequote % 2 ~= 0) then
-- Only auto-complete paths
local path_to_complete
if (nsinglequote % 2 ~= 0) and not (ndoublequote % 2 ~= 0) then
path_to_complete = remaining:match("[^\']+$")
elseif (ndoublequote % 2 ~= 0) and not (nsinglequote % 2 ~= 0) then
path_to_complete = remaining:match("[^\"]+$")
end
if path_to_complete and path_to_complete:sub(1, 1) == "/" then
local tail = path_to_complete:match("[^/]+$") or ""
local head = path_to_complete:sub(1, #path_to_complete - #tail)
if fs.exists(head) then
return head, tail
else
return nil, ([[%s is not a valid path]]):format(head)
end
else
return nil, "Cannot auto-complete strings."
end
end
-- Work from the back of the line to the front
local string_to_complete = remaining:match("[%d%w_%[%]%.:\'\"]+$") or ""
-- Find the trailing tail
local tail = string_to_complete:match("[%d%w_]+$") or ""
local linking_char = string_to_complete:sub(#string_to_complete - #tail, #string_to_complete - #tail)
-- Only handle numerical keys to auto-complete
if linking_char == "[" and not tonumber(tail) then
return find_base(tail)
end
-- Drop the linking character
local head = string_to_complete:sub(1, util.bound(#string_to_complete - #tail - 1, 0))
if #head > 0 then
local f, err = loadstring("return " .. head)
if err then
return nil, err
else
local res = {pcall(f)}
if res[1] and res[2] then
return res[2], tail
else
return nil, ([[%s does not exist.]]):format(head)
end
end
-- Global namespace if there is no head
else
return _G, tail
end
end
--- Init the debug console
function _M:init()
self.cursor = "|"
self.blink_period = 20
self.blink = self.blink_period
local w, h = core.display.size()
engine.Dialog.init(self, "Lua Console", w, h, 0, 0, nil, core.display.newFont("/data/font/DroidSansMono.ttf", 12))
game:onTickEnd(function() self.key:unicodeInput(true) end)
self:keyCommands{
_RETURN = function()
table.insert(_M.commands, _M.line)
_M.com_sel = #_M.commands + 1
table.insert(_M.history, _M.line)
-- Handle assignment and simple printing
if _M.line:match("^=") then _M.line = "return ".._M.line:sub(2) end
local f, err = loadstring(_M.line)
if err then
table.insert(_M.history, err)
else
local res = {pcall(f)}
for i, v in ipairs(res) do
if i > 1 then
table.insert(_M.history, " "..(i-1).." :=: "..tostring(v))
-- Handle printing a table
if type(v) == "table" then
local array = {}
for k, vv in table.orderedPairs(v) do
array[#array+1] = tostring(k).." :=: "..tostring(vv)
end
self:historyColumns(array, 8)
end
end
end
end
_M.line = ""
_M.line_pos = 0
_M.offset = 0
self.changed = true
end,
_UP = function()
_M.com_sel = util.bound(_M.com_sel - 1, 0, #_M.commands)
if _M.commands[_M.com_sel] then
if #_M.line == 0 or _M.line_pos == #_M.line then
_M.line_pos = #_M.commands[_M.com_sel]
end
_M.line = _M.commands[_M.com_sel]
end
self.changed = true
end,
_DOWN = function()
_M.com_sel = util.bound(_M.com_sel + 1, 1, #_M.commands)
if _M.commands[_M.com_sel] then
if #_M.line == 0 or _M.line_pos == #_M.line then
_M.line_pos = #_M.commands[_M.com_sel]
end
_M.line = _M.commands[_M.com_sel]
else
_M.line = ""
_M.line_pos = 0
end
self.changed = true
end,
_LEFT = function()
_M.line_pos = util.bound(_M.line_pos - 1, 0, #_M.line)
self.changed = true
end,
_RIGHT = function()
_M.line_pos = util.bound(_M.line_pos + 1, 0, #_M.line)
self.changed = true
end,
_HOME = function()
_M.line_pos = 0
self.changed = true
end,
[{"_a","ctrl"}] = function()
_M.line_pos = 0
self.changed = true
end,
_END = function()
_M.line_pos = #_M.line
self.changed = true
end,
[{"_e","ctrl"}] = function()
_M.line_pos = #_M.line
self.changed = true
end,
_ESCAPE = function()
game:unregisterDialog(self)
end,
_BACKSPACE = function()
if _M.line_pos > 0 then
local st = core.key.modState("ctrl") and 0 or _M.line_pos - 1
for i = _M.line_pos - 1, st, -1 do
_M.line = _M.line:sub(1, _M.line_pos - 1) .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = _M.line_pos - 1
end
end
self.changed = true
end,
_DELETE = function()
local st = core.key.modState("ctrl") and #_M.line or _M.line_pos
for i = _M.line_pos, st do
_M.line = _M.line:sub(1, _M.line_pos) .. _M.line:sub(_M.line_pos + 2)
end
self.changed = true
end,
[{"_END", "ctrl"}] = function()
_M.line = _M.line:sub(1, _M.line_pos)
self.changed = true
end,
[{"_k", "ctrl"}] = function()
_M.line = _M.line:sub(1, _M.line_pos)
self.changed = true
end,
__TEXTINPUT = function(c)
_M.line = _M.line:sub(1, _M.line_pos) .. c .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = util.bound(_M.line_pos + 1, 0, #_M.line)
self.changed = true
end,
[{"_v", "ctrl"}] = function(c)
local s = core.key.getClipboard()
if s then
_M.line = _M.line:sub(1, _M.line_pos) .. s .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = util.bound(_M.line_pos + #s, 0, #_M.line)
self.changed = true
end
end,
[{"_c", "ctrl"}] = function(c)
core.key.setClipboard(_M.line)
end,
_TAB = function()
self:autoComplete()
end,
[{"_SPACE", "ctrl"}] = function(c)
core.key.flush() -- flush input buffer
local base, remaining = find_base(_M.line:sub(1,_M.line_pos))
local func = base[remaining]
if not func or type(func) ~= "function" then
table.insert(_M.history, "<<<<< No function found >>>>>")
return
end
local lines, fname, lnum = self:functionHelp(func)
if not lines then
table.insert(_M.history, ([[<<<<< %s >>>>>]]):format(fname))
return
end
table.insert(_M.history, ([[<<<<< Help found in %s at line %d. >>>>>]]):format(fname, lnum))
for _, line in ipairs(lines) do
table.insert(_M.history, " " .. line:gsub("\t", " "))
end
end,
[{"_SPACE", "ctrl", "shift"}] = function(c)
core.key.flush() -- flush input buffer
local base, remaining = find_base(_M.line:sub(1,_M.line_pos))
local func = base[remaining]
if not func or type(func) ~= "function" then
table.insert(_M.history, "<<<<< No function found >>>>>")
return
end
local lines, fname, lnum = self:functionHelp(func, true)
if not lines then
table.insert(_M.history, ([[<<<<< %s >>>>>]]):format(fname))
return
end
table.insert(_M.history, ([[<<<<< Definition found in %s at line %d. >>>>>]]):format(fname, lnum))
for _, line in ipairs(lines) do
table.insert(_M.history, " " .. line:gsub("\t", " "))
end
end,
_PAGEUP = function()
local num_lines = math.floor(self.h / self.font_h * 0.75)
self:scrollUp(num_lines)
end,
_PAGEDOWN = function()
local num_lines = math.floor(self.h / self.font_h * 0.75)
self:scrollUp(-num_lines)
end,
}
-- Scroll message log
self:mouseZones{
{ x=0, y=0, w=game.w, h=game.h, mode={button=true}, norestrict=true, fct=function(button) if button ~= "none" then game:unregisterDialog(self) end end},
{ x=0, y=0, w=self.iw, h=self.ih, mode={button=true}, fct=function(button, x, y, xrel, yrel, tx, ty)
if button == "wheelup" then self:scrollUp(1) end
if button == "wheeldown" then self:scrollUp(-1) end
end },
}
end
--- Display function
function _M:display()
-- Blinking cursor
self.blink = self.blink - 1
if self.blink <= 0 then
self.cursor = self.cursor == "|" and " " or "|"
self.blink = self.blink_period
self.changed = true
end
engine.Dialog.display(self)
end
--- @param s screen
-- @param w width
-- @param h height
function _M:drawDialog(s, w, h)
local buffer = (self.ih % self.font_h) / 2
local i, dh = #_M.history - _M.offset, self.ih - buffer - self.font:lineSkip()
-- Start at the bottom and work up
-- Draw the current command
s:drawStringBlended(self.font, _M.line:sub(1, _M.line_pos) .. self.cursor .. _M.line:sub(_M.line_pos+1), 0, dh, 255, 255, 255)
dh = dh - self.font:lineSkip()
-- Now draw the history with any offset
while dh > buffer do
if not _M.history[i] then break end
s:drawStringBlended(self.font, _M.history[i], 0, dh, 255, 255, 255)
i = i - 1
dh = dh - self.font:lineSkip()
end
self.changed = false
end
--- Scroll the zone
-- @int i number representing how many lines to scroll
function _M:scrollUp(i)
_M.offset = _M.offset + i
if _M.offset > #_M.history - 1 then _M.offset = #_M.history - 1 end
if _M.offset < 0 then _M.offset = 0 end
self.changed = true
end
--- Autocomplete the current line
-- Will handle either tables (eg. mod.cla -> mod.class) or paths (eg. "/mod/cla" -> "/mod/class/")
function _M:autoComplete()
local base, to_complete = find_base(_M.line:sub(1, _M.line_pos))
if not base then
if to_complete then
table.insert(_M.history, ([[<<<<< %s >>>>>]]):format(to_complete))
self.changed = true
end
return
end
-- Autocomplete a table
local set = {}
if type(base) == "table" then
local recurs_bases
recurs_bases = function(base)
if type(base) ~= "table" then return end
for k, v in pairs(base) do
-- Need to handle numbers, too
if type(k) == "number" and tonumber(to_complete) then
if tostring(k):match("^" .. to_complete) then
set[tostring(k)] = true
end
elseif type(k) == "string" then
if k:match("^" .. to_complete) then
set[k] = true
end
end
end
-- Check the metatable __index
local mt = getmetatable(base)
if mt and mt.__index and type(mt.__index) == "table" then
recurs_bases(mt.__index)
end
end
recurs_bases(base)
-- Autocomplete a path
elseif type(base) == "string" then
-- Make sure the directory exists
if fs.exists(base) then
for i, fname in ipairs(fs.list(base)) do
if fname:sub(1, #to_complete) == to_complete then
-- Add a "/" to directories
if fs.isdir(base.."/"..fname) then
set[fname.."/"] = true
else
set[fname] = true
end
end
end
end
else
return
end
-- Convert to a sorted array
local array = {}
for k, _ in pairs(set) do
array[#array+1] = k
end
table.sort(array, function(a, b) return a < b end)
-- If there is one possibility, complete it
if #array == 1 then
-- Special case for a table...
if array[1] == to_complete and type(base[to_complete]) == "table" then
_M.line = _M.line:sub(1, _M.line_pos) .. "." .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = _M.line_pos + 1
elseif array[1] == to_complete and type(base[to_complete]) == "function" then
_M.line = _M.line:sub(1, _M.line_pos) .. "(" .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = _M.line_pos + 1
else
_M.line = _M.line:sub(1, _M.line_pos - #to_complete) .. array[1] .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = _M.line_pos - #to_complete + #array[1]
end
elseif #array > 1 then
table.insert(_M.history, "<<<<< Auto-complete possibilities: >>>>>")
self:historyColumns(array)
-- Find the longest common substring and complete it
local substring = array[1]:sub(#to_complete+1)
for i=2,#array do
local min_len = math.min(#array[i]-#to_complete, #substring)
for j=1,min_len do
if substring:sub(j, j) ~= array[i]:sub(#to_complete+j, #to_complete+j) then
substring = substring:sub(1, util.bound(j-1, 0))
break
end
end
if #substring == 0 then break end
end
-- Complete to the longest common substring
if #substring > 0 then
_M.line = _M.line:sub(1, _M.line_pos) .. substring .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = _M.line_pos + #substring
end
else
table.insert(_M.history, "<<<<< No auto-complete possibilities. >>>>>")
end
self.changed = true
end
--- Prints comments for a function
-- @func func only works on a function obviously
-- @param[type=boolean] verbose give extra junk
function _M:functionHelp(func, verbose)
if type(func) ~= "function" then return nil, "Can only give help on functions." end
local info = debug.getinfo(func, "S")
-- Check the path exists
local fpath = string.gsub(info.source,"@","")
if not fs.exists(fpath) then return nil, ([[%s does not exist.]]):format(fpath) end
local f = fs.open(fpath, "r")
local lines = {}
local line_num = 0
local line
while true do
line = f:readLine()
if line then
line_num = line_num + 1
if line_num == info.linedefined then
lines[#lines+1] = line
break
elseif line:sub(1,2) == "--" then
lines[#lines+1] = line
else
lines = {}
end
else
break
end
end
if verbose then
for i=info.linedefined+1,info.lastlinedefined do
line = f:readLine()
lines[#lines+1] = line
end
end
f:close()
return lines, info.short_src, info.linedefined
end
--- Add a list of strings to the history with multiple columns
-- @param[type=table] strings Array of strings to add to the history
-- @int offset Number of spaces to add on the left-hand side
function _M:historyColumns(strings, offset)
local offset_str = string.rep(" ", offset and offset or 0)
local ox, oy = self.font:size(offset_str)
local longest_key = ""
local width = 0 --
local max_width = 80 -- Maximum field width to print
for i, k in ipairs(strings) do
if #k > width then
longest_key = k
width = #k
if width >= max_width then
width = max_width
break
end
end
end
local tx, ty = self.font:size(string.sub(longest_key,1,width) .. "... ")
local num_columns = math.floor((self.w - ox) / tx)
local num_rows = math.ceil(#strings / num_columns)
local line_format = offset_str..string.rep("%-"..tostring(math.min(max_width+5,width+5)).."s ", num_columns) --
for i=1,num_rows do
vals = {}
for j=1,num_columns do
vals[j] = strings[i + (j - 1) * num_rows] or ""
--Truncate and annotate if too long
if #vals[j] > width then
vals[j] = string.sub(vals[j],1,width) .. "..."
end
end
table.insert(_M.history, line_format:format(unpack(vals)))
end
end