New initial Lua debugger version (#1967)

Incomplete version of the Lua debugger.
Some available features:

* Break in next executed instruction
* Step in, over, out
* See & navigate stacktrace
* See local variables

Some missing features:

* Breakpoints
* Eval user expressions with local variables

The UX needs some improvement yet.
This commit is contained in:
David Capello 2021-10-04 21:17:33 -03:00
parent 3de3ef5635
commit 212e9fbe6c
20 changed files with 1025 additions and 8 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -249,6 +249,12 @@
<part id="pal_presets" x="160" y="200" w="5" h="5" />
<part id="pal_options" x="168" y="200" w="5" h="5" />
<part id="pal_resize" x="176" y="200" w="5" h="5" />
<part id="debug_continue" x="208" y="240" w="7" h="7" />
<part id="debug_pause" x="208" y="247" w="7" h="7" />
<part id="debug_step_into" x="222" y="240" w="7" h="7" />
<part id="debug_step_over" x="215" y="240" w="7" h="7" />
<part id="debug_step_out" x="229" y="240" w="7" h="7" />
<part id="debug_breakpoint" x="236" y="240" w="7" h="7" />
<part id="selection_replace" x="176" y="160" w="7" h="7" />
<part id="selection_add" x="184" y="160" w="7" h="7" />
<part id="selection_subtract" x="192" y="160" w="7" h="7" />

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2018-2020 Igara Studio S.A. -->
<!-- Copyright (C) 2018-2021 Igara Studio S.A. -->
<!-- Copyright (C) 2001-2018 David Capello -->
<gui>
<!-- Keyboard shortcuts -->
@ -637,6 +637,8 @@
<menu id="scripts_menu" text="@.file_scripts" group="file_scripts">
<item command="OpenScriptFolder" text="@.file_open_script_folder" />
<item command="Refresh" text="@.file_rescan_script_folder" />
<separator />
<item command="Debugger" text="@.file_debugger" />
</menu>
<separator group="file_app" />
<item command="Exit" text="@.file_exit" />

View File

@ -255,6 +255,7 @@ CopyColors = Copy Colors
CopyMerged = Copy Merged
CropSprite = Crop Sprite
Cut = Cut
Debugger = Debugger
DeselectMask = Deselect Mask
Despeckle = Despeckle
DeveloperConsole = Developer Console
@ -460,6 +461,17 @@ Zoom_In = Zoom In
Zoom_Out = Zoom Out
Zoom_Set = Zoom {0}%
[debugger]
title = Debugger
continue = Start or Continue Debugging
step_into = Step Into
step_over = Step Over
step_out = Step Out
toggle_breakpoint = Toggle Breakpoint
stacktrace = Stacktrace
console = Console
locals = Locals
[document_tab_popup_menu]
duplicate_view = Duplicate &View
open_with_os = &Open with OS
@ -813,6 +825,7 @@ file_repeat_last_export = Repeat &Last Export
file_scripts = Scri&pts
file_open_script_folder = &Open Scripts Folder
file_rescan_script_folder = &Rescan Scripts Folder
file_debugger = &Debugger
file_exit = E&xit
edit = &Edit
edit_undo = &Undo

43
data/widgets/debugger.xml Normal file
View File

@ -0,0 +1,43 @@
<!-- Aseprite -->
<!-- Copyright (C) 2021 by Igara Studio S.A. -->
<gui>
<window id="debugger" text="@.title">
<vbox childspacing="0">
<hbox>
<buttonset columns="4" id="control">
<item icon="debug_continue" tooltip="@.continue" tooltip_dir="bottom" />
<item icon="debug_step_into" tooltip="@.step_into" tooltip_dir="bottom" />
<item icon="debug_step_over" tooltip="@.step_over" tooltip_dir="bottom" />
<item icon="debug_step_out" tooltip="@.step_out" tooltip_dir="bottom" />
</buttonset>
<buttonset columns="1" id="breakpoint">
<item icon="debug_breakpoint" tooltip="@.toggle_breakpoint" tooltip_dir="bottom" />
</buttonset>
</hbox>
<splitter horizontal="true" noborders="true" childspacing="2" expansive="true">
<view id="source_placeholder" />
<splitter vertical="true" noborders="true" childspacing="2" expansive="true">
<vbox id="stack_part">
<separator text="@.stacktrace" horizontal="true" />
<view id="stack_placeholder" expansive="true" />
</vbox>
<vbox id="output_part">
<hbox>
<buttonset id="output_buttons" columns="2">
<item text="@.console" />
<item text="@.locals" />
</buttonset>
</hbox>
<view id="console_view" expansive="true">
<textbox id="console" />
</view>
<view id="locals_view" expansive="true">
<listbox id="locals" expansive="true" />
</view>
<vbox id="eval_placeholder" noborders="true" />
</vbox>
</splitter>
</splitter>
</vbox>
</window>
</gui>

View File

@ -145,6 +145,7 @@ if(ENABLE_SCRIPTING)
set(scripting_files_ui
commands/cmd_developer_console.cpp
commands/cmd_open_script_folder.cpp
commands/debugger.cpp
ui/devconsole_view.cpp)
endif()
if(ENABLE_WEBSOCKET)

View File

@ -95,6 +95,9 @@ namespace {
class ConsoleEngineDelegate : public script::EngineDelegate {
public:
void onConsoleError(const char* text) override {
onConsolePrint(text);
}
void onConsolePrint(const char* text) override {
m_console.printf("%s\n", text);
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -17,6 +18,10 @@
#include "app/ui/main_window.h"
#include "ui/alert.h"
#ifdef ENABLE_SCRIPTING
#include "app/commands/debugger.h"
#endif
namespace app {
class ExitCommand : public Command {
@ -39,6 +44,13 @@ void ExitCommand::onExecute(Context* ctx)
if (Job::runningJobs() > 0)
return;
#ifdef ENABLE_SCRIPTING
if (auto debuggerCommand = dynamic_cast<DebuggerCommand*>(
Commands::instance()->byId(CommandId::Debugger()))) {
debuggerCommand->closeDebugger(ctx);
}
#endif
if (ctx->hasModifiedDocuments()) {
Command* closeAll = Commands::instance()->byId(CommandId::CloseAllFiles());
Params params;

View File

@ -166,6 +166,7 @@ FOR_EACH_COMMAND(Zoom)
#ifdef ENABLE_SCRIPTING
#ifdef ENABLE_UI
FOR_EACH_COMMAND(Debugger)
FOR_EACH_COMMAND(DeveloperConsole)
FOR_EACH_COMMAND(OpenScriptFolder)
#endif

View File

@ -0,0 +1,827 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifndef ENABLE_SCRIPTING
#error ENABLE_SCRIPTING must be defined
#endif
#include "app/commands/debugger.h"
#include "app/app.h"
#include "app/context.h"
#include "app/script/engine.h"
#include "app/ui/skin/skin_theme.h"
#include "base/clamp.h"
#include "base/convert_to.h"
#include "base/file_content.h"
#include "base/fs.h"
#include "base/split_string.h"
#include "base/trim_string.h"
#include "fmt/format.h"
#include "ui/entry.h"
#include "ui/listbox.h"
#include "ui/listitem.h"
#include "ui/manager.h"
#include "ui/message.h"
#include "ui/message_loop.h"
#include "ui/paint_event.h"
#include "ui/size_hint_event.h"
#ifdef ENABLE_SCRIPTING
#include "app/script/luacpp.h"
#else
#error Compile the debugger only when ENABLE_SCRIPTING is defined
#endif
#include "debugger.xml.h"
#include <unordered_map>
namespace app {
using namespace ui;
using namespace app::skin;
namespace {
struct FileContent {
base::buffer content;
std::vector<const uint8_t*> lines;
FileContent() { }
FileContent(const FileContent&) = delete;
FileContent& operator=(const FileContent&) = delete;
void clear() {
content.clear();
lines.clear();
}
void setContent(const std::string& c) {
content = base::buffer(
(const uint8_t*)c.c_str(),
(const uint8_t*)c.c_str() + c.size() + 1); // Include nul char
update();
}
void readContentFromFile(const std::string& filename) {
if (base::is_file(filename))
content = base::read_file_content(filename);
else
content.clear();
// Ensure one last nul char to print as const char* the last line
content.push_back(0);
update();
}
private:
void update() {
ASSERT(content.back() == 0);
// Replace all '\r' to ' ' (for Windows EOL and to avoid to paint
// a square on each newline)
for (auto& chr : content) {
if (chr == '\r')
chr = ' ';
}
// Generate the lines array
lines.clear();
for (size_t i=0; i<content.size(); ++i) {
lines.push_back(&content[i]);
size_t j = i;
for (; j<content.size() && content[j] != '\n'; ++j)
;
if (j < content.size())
content[j] = 0;
i = j;
}
}
};
using FileContentPtr = std::shared_ptr<FileContent>;
// Cached file content of each visited file in the debugger.
// key=filename -> value=FileContent
std::unordered_map<std::string, FileContentPtr> g_fileContent;
class DebuggerSource : public Widget {
public:
DebuggerSource() {
}
void clearFile() {
m_fileContent.reset();
m_maxLineWidth = 0;
if (View* view = View::getView(this))
view->updateView();
}
void setFileContent(const FileContentPtr& fileContent) {
m_fileContent = fileContent;
m_maxLineWidth = 0;
if (View* view = View::getView(this))
view->updateView();
setCurrentLine(1);
}
void setFile(const std::string& filename,
const std::string& content) {
FileContentPtr newFileContent(new FileContent);
if (!content.empty()) {
newFileContent->setContent(content);
}
else {
newFileContent->readContentFromFile(filename);
}
g_fileContent[filename] = newFileContent;
setFileContent(newFileContent);
}
void setCurrentLine(int currentLine) {
m_currentLine = currentLine;
if (m_currentLine > 0) {
if (View* view = View::getView(this)) {
const gfx::Rect vp = view->viewportBounds();
const int th = textHeight();
int y = m_currentLine*th - vp.h/2 + th/2;
if (y < 0)
y = 0;
view->setViewScroll(gfx::Point(0, y));
}
}
invalidate();
}
protected:
bool onProcessMessage(Message* msg) override {
return Widget::onProcessMessage(msg);
}
void onPaint(PaintEvent& ev) override {
auto theme = static_cast<SkinTheme*>(this->theme());
Graphics* g = ev.graphics();
View* view = View::getView(this);
gfx::Color linesBg = theme->colors.textboxCodeFace();
gfx::Color bg = theme->colors.textboxFace();
gfx::Color fg = theme->colors.textboxText();
int nlines = (m_fileContent ? m_fileContent->lines.size(): 0);
gfx::Rect vp;
if (view)
vp = view->viewportBounds().offset(-bounds().origin());
else
vp = clientBounds();
auto f = font();
gfx::Rect linesVp(
vp.x, vp.y,
Graphics::measureUITextLength(base::convert_to<std::string>(nlines), f)
+ 4*guiscale(), // TODO configurable from the theme?
vp.h);
vp.x += linesVp.w;
vp.w -= linesVp.w;
// Fill background
g->fillRect(linesBg, linesVp);
g->fillRect(bg, vp);
if (m_fileContent) {
auto icon = theme->parts.debugContinue()->bitmap(0);
gfx::Point pt = clientBounds().origin();
for (int i = 0; i < nlines; ++i) {
if (i+1 == m_currentLine) {
g->drawRgbaSurface(icon, pt.x+linesVp.w, pt.y);
}
// Draw the line number
{
auto lineNumText = base::convert_to<std::string>(i+1);
int lw = Graphics::measureUITextLength(lineNumText, f);
g->drawText(
lineNumText.c_str(),
fg, linesBg,
gfx::Point(pt.x+linesVp.w-lw-2*guiscale(), pt.y));
}
// Draw the this line of source code
const char* line = (const char*)m_fileContent->lines[i];
g->drawText(line, fg, bg,
gfx::Point(pt.x + icon->width() + linesVp.w, pt.y));
pt.y += textHeight();
}
}
}
void onSizeHint(SizeHintEvent& ev) override {
if (m_fileContent) {
if (m_maxLineWidth == 0) {
auto f = font();
std::string tmp;
for (const uint8_t* line : m_fileContent->lines) {
ASSERT(line);
tmp.assign((const char*)line);
m_maxLineWidth = std::max(m_maxLineWidth, Graphics::measureUITextLength(tmp, f));
}
}
ev.setSizeHint(gfx::Size(m_maxLineWidth,
m_fileContent->lines.size() * textHeight()));
}
}
private:
FileContentPtr m_fileContent;
int m_currentLine = -1;
int m_maxLineWidth = 0;
};
class StacktraceBox : public ListBox {
public:
class Item : public ListItem {
public:
Item(lua_Debug* ar, const int stackLevel)
: m_fn(ar->short_src)
, m_ln(ar->currentline)
, m_stackLevel(stackLevel) {
std::string lineContent;
auto it = g_fileContent.find(m_fn);
if (it != g_fileContent.end()) {
const int i = ar->currentline - 1;
if (i >= 0 && i < it->second->lines.size())
lineContent.assign((const char*)it->second->lines[i]);
}
base::trim_string(lineContent, lineContent);
setText(fmt::format(
"{}:{}: {}", base::get_file_name(m_fn), m_ln, lineContent));
}
const std::string& filename() const { return m_fn; }
const int lineNumber() const { return m_ln; }
const int stackLevel() const { return m_stackLevel; }
private:
std::string m_fn;
int m_ln;
int m_stackLevel;
};
StacktraceBox() {
}
void clear() {
while (auto item = firstChild()) {
removeChild(item);
item->deferDelete();
}
}
void update(lua_State* L) {
clear();
lua_Debug ar;
int level = 0;
while (lua_getstack(L, level, &ar)) {
lua_getinfo(L, "lnS", &ar);
if (ar.currentline > 0)
addChild(new Item(&ar, level));
++level;
}
}
};
// TODO similar to DevConsoleView::CommmandEntry, merge both widgets
// or remove the DevConsoleView
class EvalEntry : public Entry {
public:
EvalEntry() : Entry(2048, "") {
setFocusStop(true);
}
obs::signal<void(const std::string&)> Execute;
protected:
bool onProcessMessage(Message* msg) override {
switch (msg->type()) {
case kKeyDownMessage:
if (hasFocus()) {
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
KeyScancode scancode = keymsg->scancode();
switch (scancode) {
case kKeyEnter:
case kKeyEnterPad: {
std::string cmd = text();
setText("");
Execute(cmd);
return true;
}
}
}
break;
}
return Entry::onProcessMessage(msg);
}
};
} // anonmous namespace
class Debugger : public gen::Debugger
, public script::EngineDelegate
, public script::DebuggerDelegate {
enum State {
Hidden,
WaitingStart,
WaitingHook,
WaitingNextCommand,
ProcessingCommand,
};
enum Button {
None = -1,
Start = 0, // Start/Pause/Continue
StepInto,
StepOver,
StepOut,
Breakpoint,
};
public:
Debugger() {
control()->ItemChange.connect([this] {
auto button = (Button)control()->selectedItem();
control()->deselectItems();
onControl(button);
});
breakpoint()->setVisible(false); // TODO make this visible
breakpoint()->ItemChange.connect([this] {
breakpoint()->deselectItems();
onControl(Button::Breakpoint);
});
Close.connect([this]{
m_state = State::Hidden;
auto app = App::instance();
app->scriptEngine()->setDelegate(m_oldDelegate);
app->scriptEngine()->stopDebugger();
// Clear the cached content of all debugged files
g_fileContent.clear();
// Clear console & locals.
console()->setText(std::string());
clearLocals();
});
m_stacktrace.Change.connect([this]{
onStacktraceChange();
});
m_evalEntry.Execute.connect([this](const std::string& expr){
onEvalExpression(expr);
});
sourcePlaceholder()->attachToView(&m_sourceViewer);
sourcePlaceholder()->setVisible(false);
stackPlaceholder()->attachToView(&m_stacktrace);
stackPlaceholder()->setVisible(false);
outputButtons()->ItemChange.connect([this]{ onOutputButtonChange(); });
outputButtons()->setSelectedItem(0);
onOutputButtonChange();
console()->setVisible(false);
locals()->setVisible(false);
evalPlaceholder()->addChild(&m_evalEntry);
}
void openDebugger() {
m_state = State::WaitingHook;
updateControls();
openWindow();
auto app = App::instance();
m_oldDelegate = app->scriptEngine()->delegate();
app->scriptEngine()->setDelegate(this);
app->scriptEngine()->startDebugger(this);
}
void onControl(Button button) {
ASSERT(m_state != State::Hidden);
m_lastCommand = button;
switch (button) {
case Button::Start:
if (m_state == State::WaitingStart) {
m_state = State::WaitingHook;
m_commandStackLevel = m_stackLevel = 0;
m_sourceViewer.clearFile();
}
else {
m_state = State::WaitingStart;
m_commandStackLevel = m_stackLevel = 0;
m_sourceViewer.clearFile();
}
break;
case Button::StepInto:
case Button::StepOver:
case Button::StepOut:
m_state = State::ProcessingCommand;
m_commandStackLevel = m_stackLevel;
break;
case Button::Breakpoint:
// m_state = State::WaitingNextCommand;
// TODO
break;
}
updateControls();
}
void updateControls() {
bool isRunning = (m_state == State::WaitingHook ||
m_state == State::ProcessingCommand);
bool canRunCommands = (m_state == State::WaitingNextCommand);
auto theme = static_cast<SkinTheme*>(this->theme());
control()->getItem(0)->setIcon(
(isRunning ? theme->parts.debugPause() :
theme->parts.debugContinue()));
control()->getItem(1)->setEnabled(canRunCommands);
control()->getItem(2)->setEnabled(canRunCommands);
control()->getItem(3)->setEnabled(canRunCommands);
breakpoint()->getItem(0)->setEnabled(canRunCommands);
// Calling this we update the mouse widget and we can click the
// same button several times.
//
// TODO why is this needed? shouldn't be this automatic from the
// ui::Manager side?
if (auto man = manager())
man->_updateMouseWidgets();
}
// script::EngineDelegate impl
void onConsoleError(const char* text) override {
m_fileOk = false;
onConsolePrint(text);
// Get error filename and number
{
std::vector<std::string> parts;
base::split_string(text, parts, { ":" });
if (parts.size() >= 3) {
const std::string& fn = parts[0];
const std::string& ln = parts[1];
if (base::is_file(fn)) {
m_sourceViewer.setFile(fn, std::string());
m_sourceViewer.setCurrentLine(
std::strtol(ln.c_str(), nullptr, 10));
sourcePlaceholder()->setVisible(true);
layout();
}
}
}
// Stop debugger
auto app = App::instance();
waitNextCommand(app->scriptEngine()->luaState());
updateControls();
}
void onConsolePrint(const char* text) override {
console()->setVisible(true);
consoleView()->setViewScroll(gfx::Point(0, 0));
if (text) {
std::string stext(console()->text());
stext += text;
stext += '\n';
console()->setText(stext);
}
layout();
consoleView()->setViewScroll(
gfx::Point(0, consoleView()->getScrollableSize().h));
}
// script::DebuggerDelegate impl
void hook(lua_State* L, lua_Debug* ar) override {
lua_getinfo(L, "lnS", ar);
switch (ar->event) {
case LUA_HOOKCALL: ++m_stackLevel; break;
case LUA_HOOKRET: --m_stackLevel; break;
case LUA_HOOKLINE:
case LUA_HOOKCOUNT:
case LUA_HOOKTAILCALL:
break;
}
switch (m_state) {
case State::WaitingStart:
// Do nothing (the execution continues regularly, unexpected
// script being executed)
return;
case State::WaitingHook:
if (ar->event == LUA_HOOKLINE)
waitNextCommand(L);
else
return;
break;
case State::ProcessingCommand:
switch (m_lastCommand) {
case Button::Start:
if (ar->event == LUA_HOOKLINE) {
// TODO Wait next error...
}
else {
return;
}
return;
case Button::StepInto:
if (ar->event == LUA_HOOKLINE) {
waitNextCommand(L);
}
else {
return;
}
break;
case Button::StepOver:
if (ar->event == LUA_HOOKLINE &&
m_stackLevel == m_commandStackLevel) {
waitNextCommand(L);
}
else {
return;
}
break;
case Button::StepOut:
if (ar->event == LUA_HOOKLINE &&
m_stackLevel < m_commandStackLevel) {
waitNextCommand(L);
}
else {
return;
}
break;
case Button::Breakpoint:
if (ar->event != LUA_HOOKLINE) {
// TODO
return;
}
break;
}
break;
}
updateControls();
if (m_state == State::WaitingNextCommand) {
sourcePlaceholder()->setVisible(true);
stackPlaceholder()->setVisible(true);
layout();
if (m_lastFile != ar->short_src &&
base::is_file(ar->short_src)) {
m_lastFile = ar->short_src;
m_sourceViewer.setFile(m_lastFile, std::string());
}
if (m_lastFile == ar->short_src) {
m_sourceViewer.setCurrentLine(ar->currentline);
}
if (!m_expanded) {
m_expanded = true;
gfx::Rect bounds = this->bounds();
if (m_sourceViewer.isVisible()) {
bounds.w = std::max(bounds.w, 256*guiscale());
bounds.h = std::max(bounds.h, 256*guiscale());
}
setBounds(bounds);
invalidate();
}
MessageLoop loop(Manager::getDefault());
while (m_state == State::WaitingNextCommand)
loop.pumpMessages();
}
}
void startFile(const std::string& file,
const std::string& content) override {
m_stackLevel = 0;
m_fileOk = true;
m_sourceViewer.setFile(file, content);
}
void endFile(const std::string& file) override {
if (m_fileOk)
m_sourceViewer.clearFile();
m_stacktrace.clear();
m_lastFile.clear();
m_stackLevel = 0;
}
private:
void waitNextCommand(lua_State* L) {
m_state = State::WaitingNextCommand;
m_stacktrace.update(L);
updateLocals(L, 0);
}
void onOutputButtonChange() {
consoleView()->setVisible(isConsoleSelected());
localsView()->setVisible(isLocalsSelected());
layout();
}
bool isConsoleSelected() const {
return (outputButtons()->selectedItem() == 0);
}
bool isLocalsSelected() const {
return (outputButtons()->selectedItem() == 1);
}
void onStacktraceChange() {
if (auto item = dynamic_cast<StacktraceBox::Item*>(m_stacktrace.getSelectedChild())) {
auto it = g_fileContent.find(item->filename());
if (it != g_fileContent.end())
m_sourceViewer.setFileContent(it->second);
else
m_sourceViewer.setFile(item->filename(), std::string());
m_sourceViewer.setCurrentLine(item->lineNumber());
auto app = App::instance();
updateLocals(app->scriptEngine()->luaState(), item->stackLevel());
}
}
void onEvalExpression(const std::string& expr) {
auto app = App::instance();
app->scriptEngine()->evalCode(expr);
}
void clearLocals() {
while (auto item = locals()->firstChild()) {
locals()->removeChild(item);
item->deferDelete();
}
}
void updateLocals(lua_State* L, int level) {
clearLocals();
lua_Debug ar;
if (lua_getstack(L, level, &ar)) {
for (int n=1; ; ++n) {
const char* name = lua_getlocal(L, &ar, n);
if (!name)
break;
// These special names are returned by luaG_findlocal()
if (strcmp(name, "(temporary)") == 0 ||
strcmp(name, "(C temporary)") == 0) {
lua_pop(L, 1);
continue;
}
std::string v = "?";
switch (lua_type(L, -1)) {
case LUA_TNONE:
v = "none";
break;
case LUA_TNIL:
v = "nil";
break;
case LUA_TBOOLEAN:
v = (lua_toboolean(L, -1) ? "true": "false");
break;
case LUA_TLIGHTUSERDATA:
v = "lightuserdata";
break;
case LUA_TNUMBER:
v = lua_tostring(L, -1);
break;
case LUA_TSTRING:
v = lua_tostring(L, -1);
break;
case LUA_TTABLE:
v = "table";
break;
case LUA_TFUNCTION:
v = "function";
break;
case LUA_TUSERDATA:
v = "userdata";
break;
case LUA_TTHREAD:
v = "thread";
break;
}
std::string itemText = fmt::format("{}={}", name, v);
lua_pop(L, 1);
locals()->addChild(new ListItem(itemText));
}
}
locals()->setVisible(true);
localsView()->updateView();
}
EngineDelegate* m_oldDelegate = nullptr;
bool m_expanded = false;
State m_state = State::Hidden;
Button m_lastCommand = Button::None;
DebuggerSource m_sourceViewer;
StacktraceBox m_stacktrace;
EvalEntry m_evalEntry;
std::string m_lastFile;
int m_commandStackLevel = 0;
int m_stackLevel = 0;
bool m_fileOk = true;
};
DebuggerCommand::DebuggerCommand()
: Command(CommandId::Debugger(), CmdRecordableFlag)
{
}
void DebuggerCommand::closeDebugger(Context* ctx)
{
if (ctx->isUIAvailable()) {
if (m_debugger)
m_debugger->closeWindow(nullptr);
}
}
void DebuggerCommand::onExecute(Context* ctx)
{
if (ctx->isUIAvailable()) {
auto app = App::instance();
// Create the debugger window for the first time
if (!m_debugger) {
m_debugger.reset(new Debugger);
app->Exit.connect([this]{
m_debugger.reset();
});
}
if (!m_debugger->isVisible()) {
m_debugger->openDebugger();
}
else {
m_debugger->closeWindow(nullptr);
}
}
}
Command* CommandFactory::createDebuggerCommand()
{
return new DebuggerCommand;
}
} // namespace app

View File

@ -0,0 +1,33 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_COMMANDS_DEBUGGER_H_INCLUDED
#define APP_COMMANDS_DEBUGGER_H_INCLUDED
#pragma once
#include "app/commands/command.h"
#include <memory>
namespace app {
class Debugger;
class DebuggerCommand : public Command {
public:
DebuggerCommand();
void closeDebugger(Context* ctx);
protected:
void onExecute(Context* context) override;
std::unique_ptr<Debugger> m_debugger;
};
} // namespace app
#endif

View File

@ -46,6 +46,9 @@ base::Chrono luaClock;
// Stack of script filenames that are being executed.
std::stack<std::string> current_script_dirs;
// Just one debugger delegate is possible.
DebuggerDelegate* g_debuggerDelegate = nullptr;
class AddScriptFilename {
public:
AddScriptFilename(const std::string& fn) {
@ -444,7 +447,7 @@ bool Engine::evalCode(const std::string& code,
lua_pcall(L, 0, 1, 0)) {
const char* s = lua_tostring(L, -1);
if (s)
onConsolePrint(s);
onConsoleError(s);
ok = false;
m_returnCode = -1;
}
@ -467,7 +470,7 @@ bool Engine::evalCode(const std::string& code,
lua_pop(L, 1);
}
catch (const std::exception& ex) {
onConsolePrint(ex.what());
onConsoleError(ex.what());
ok = false;
m_returnCode = -1;
}
@ -492,7 +495,44 @@ bool Engine::evalFile(const std::string& filename,
AddScriptFilename add(absFilename);
set_app_params(L, params);
return evalCode(buf.str(), "@" + absFilename);
if (g_debuggerDelegate)
g_debuggerDelegate->startFile(absFilename, buf.str());
bool result = evalCode(buf.str(), "@" + absFilename);
if (g_debuggerDelegate)
g_debuggerDelegate->endFile(absFilename);
return result;
}
void Engine::startDebugger(DebuggerDelegate* debuggerDelegate)
{
g_debuggerDelegate = debuggerDelegate;
lua_Hook hook = [](lua_State* L, lua_Debug* ar) {
int ret = lua_getinfo(L, "l", ar);
if (ret == 0 || ar->currentline < 0)
return;
g_debuggerDelegate->hook(L, ar);
};
lua_sethook(L, hook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKLINE | LUA_MASKCOUNT, 1);
}
void Engine::stopDebugger()
{
lua_sethook(L, nullptr, 0, 0);
}
void Engine::onConsoleError(const char* text)
{
if (text && m_delegate)
m_delegate->onConsoleError(text);
else
onConsolePrint(text);
}
void Engine::onConsolePrint(const char* text)

View File

@ -23,10 +23,12 @@
#include "gfx/fwd.h"
#include <cstdio>
#include <string>
#include <functional>
#include <map>
#include <string>
struct lua_State;
struct lua_Debug;
namespace base {
class Version;
@ -61,9 +63,19 @@ namespace app {
class EngineDelegate {
public:
virtual ~EngineDelegate() { }
virtual void onConsoleError(const char* text) = 0;
virtual void onConsolePrint(const char* text) = 0;
};
class DebuggerDelegate {
public:
virtual ~DebuggerDelegate() { }
virtual void hook(lua_State* L, lua_Debug* ar) = 0;
virtual void startFile(const std::string& file,
const std::string& content) = 0;
virtual void endFile(const std::string& file) = 0;
};
class Engine {
public:
Engine();
@ -90,7 +102,11 @@ namespace app {
lua_State* luaState() { return L; }
void startDebugger(DebuggerDelegate* debuggerDelegate);
void stopDebugger();
private:
void onConsoleError(const char* text);
void onConsolePrint(const char* text);
lua_State* L;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -148,7 +148,10 @@ void ButtonSet::Item::onPaint(ui::PaintEvent& ev)
if (m_icon) {
os::Surface* bmp = m_icon->bitmap(0);
if (isSelected() && hasCapture())
if (!isEnabled())
g->drawColoredRgbaSurface(bmp, theme->colors.disabled(),
iconRc.x, iconRc.y);
else if (isSelected() && hasCapture())
g->drawColoredRgbaSurface(bmp, theme->colors.buttonSelectedText(),
iconRc.x, iconRc.y);
else if (m_mono)

View File

@ -149,6 +149,11 @@ void DevConsoleView::onExecuteCommand(const std::string& cmd)
m_engine->evalCode(cmd);
}
void DevConsoleView::onConsoleError(const char* text)
{
onConsolePrint(text);
}
void DevConsoleView::onConsolePrint(const char* text)
{
if (text)

View File

@ -42,6 +42,7 @@ namespace app {
void onTabPopup(Workspace* workspace) override;
// EngineDelegate impl
virtual void onConsoleError(const char* text) override;
virtual void onConsolePrint(const char* text) override;
protected:

View File

@ -1,4 +1,5 @@
// Aseprite Code Generator
// Copyright (c) 2021 Igara Studio S.A.
// Copyright (c) 2014-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -153,6 +154,9 @@ static Item convert_to_item(TiXmlElement* elem)
if (name == "splitter")
return item.typeIncl("ui::Splitter",
"ui/splitter.h");
if (name == "textbox")
return item.typeIncl("ui::TextBox",
"ui/textbox.h");
if (name == "tipwindow")
return item.typeIncl("ui::TipWindow",
"ui/tooltips.h");

View File

@ -1192,6 +1192,12 @@ void Manager::_closeWindow(Window* window, bool redraw_background)
}
}
void Manager::_updateMouseWidgets()
{
// Update mouse widget.
updateMouseWidgets(ui::get_mouse_position());
}
bool Manager::onProcessMessage(Message* msg)
{
switch (msg->type()) {

View File

@ -104,6 +104,7 @@ namespace ui {
void _openWindow(Window* window);
void _closeWindow(Window* window, bool redraw_background);
void _updateMouseWidgets();
protected:
bool onProcessMessage(Message* msg) override;

2
third_party/lua vendored

@ -1 +1 @@
Subproject commit 9b86693268347da63de3fcbbd48c049e188da145
Subproject commit 23b40393119035938e2506bbdc3278c63fda643f