1// Aseprite
2// Copyright (C) 2021-2022 Igara Studio S.A.
3//
4// This program is distributed under the terms of
5// the End-User License Agreement for Aseprite.
6
7#ifdef HAVE_CONFIG_H
8#include "config.h"
9#endif
10
11#ifndef ENABLE_SCRIPTING
12 #error ENABLE_SCRIPTING must be defined
13#endif
14
15#include "app/commands/debugger.h"
16
17#include "app/app.h"
18#include "app/context.h"
19#include "app/script/engine.h"
20#include "app/ui/skin/skin_theme.h"
21#include "base/convert_to.h"
22#include "base/file_content.h"
23#include "base/fs.h"
24#include "base/split_string.h"
25#include "base/trim_string.h"
26#include "fmt/format.h"
27#include "ui/entry.h"
28#include "ui/listbox.h"
29#include "ui/listitem.h"
30#include "ui/manager.h"
31#include "ui/message.h"
32#include "ui/message_loop.h"
33#include "ui/paint_event.h"
34#include "ui/size_hint_event.h"
35
36#ifdef ENABLE_SCRIPTING
37 #include "app/script/luacpp.h"
38#else
39 #error Compile the debugger only when ENABLE_SCRIPTING is defined
40#endif
41
42#include "debugger.xml.h"
43
44#include <unordered_map>
45
46namespace app {
47
48using namespace ui;
49using namespace app::skin;
50
51namespace {
52
53struct FileContent {
54 base::buffer content;
55 std::vector<const uint8_t*> lines;
56
57 FileContent() { }
58 FileContent(const FileContent&) = delete;
59 FileContent& operator=(const FileContent&) = delete;
60
61 void clear() {
62 content.clear();
63 lines.clear();
64 }
65
66 void setContent(const std::string& c) {
67 content = base::buffer(
68 (const uint8_t*)c.c_str(),
69 (const uint8_t*)c.c_str() + c.size() + 1); // Include nul char
70
71 update();
72 }
73
74 void readContentFromFile(const std::string& filename) {
75 if (base::is_file(filename))
76 content = base::read_file_content(filename);
77 else
78 content.clear();
79
80 // Ensure one last nul char to print as const char* the last line
81 content.push_back(0);
82
83 update();
84 }
85
86private:
87 void update() {
88 ASSERT(content.back() == 0);
89
90 // Replace all '\r' to ' ' (for Windows EOL and to avoid to paint
91 // a square on each newline)
92 for (auto& chr : content) {
93 if (chr == '\r')
94 chr = ' ';
95 }
96
97 // Generate the lines array
98 lines.clear();
99 for (size_t i=0; i<content.size(); ++i) {
100 lines.push_back(&content[i]);
101
102 size_t j = i;
103 for (; j<content.size() && content[j] != '\n'; ++j)
104 ;
105 if (j < content.size())
106 content[j] = 0;
107 i = j;
108 }
109 }
110};
111
112using FileContentPtr = std::shared_ptr<FileContent>;
113
114// Cached file content of each visited file in the debugger.
115// key=filename -> value=FileContent
116std::unordered_map<std::string, FileContentPtr> g_fileContent;
117
118class DebuggerSource : public Widget {
119public:
120 DebuggerSource() {
121 }
122
123 void clearFile() {
124 m_fileContent.reset();
125 m_maxLineWidth = 0;
126
127 if (View* view = View::getView(this))
128 view->updateView();
129 }
130
131 void setFileContent(const FileContentPtr& fileContent) {
132 m_fileContent = fileContent;
133 m_maxLineWidth = 0;
134
135 if (View* view = View::getView(this))
136 view->updateView();
137
138 setCurrentLine(1);
139 }
140
141 void setFile(const std::string& filename,
142 const std::string& content) {
143 FileContentPtr newFileContent(new FileContent);
144 if (!content.empty()) {
145 newFileContent->setContent(content);
146 }
147 else {
148 newFileContent->readContentFromFile(filename);
149 }
150 g_fileContent[filename] = newFileContent;
151
152 setFileContent(newFileContent);
153 }
154
155 void setCurrentLine(int currentLine) {
156 m_currentLine = currentLine;
157 if (m_currentLine > 0) {
158 if (View* view = View::getView(this)) {
159 const gfx::Rect vp = view->viewportBounds();
160 const int th = textHeight();
161 int y = m_currentLine*th - vp.h/2 + th/2;
162 if (y < 0)
163 y = 0;
164 view->setViewScroll(gfx::Point(0, y));
165 }
166 }
167 invalidate();
168 }
169
170protected:
171 bool onProcessMessage(Message* msg) override {
172 switch (msg->type()) {
173
174 case kMouseWheelMessage: {
175 View* view = View::getView(this);
176 if (view) {
177 auto mouseMsg = static_cast<MouseMessage*>(msg);
178 gfx::Point scroll = view->viewScroll();
179
180 if (mouseMsg->preciseWheel())
181 scroll += mouseMsg->wheelDelta();
182 else
183 scroll += mouseMsg->wheelDelta() * textHeight()*3;
184
185 view->setViewScroll(scroll);
186 }
187 break;
188 }
189
190 }
191 return Widget::onProcessMessage(msg);
192 }
193
194 void onPaint(PaintEvent& ev) override {
195 auto theme = SkinTheme::get(this);
196 Graphics* g = ev.graphics();
197 View* view = View::getView(this);
198 gfx::Color linesBg = theme->colors.textboxCodeFace();
199 gfx::Color bg = theme->colors.textboxFace();
200 gfx::Color fg = theme->colors.textboxText();
201 int nlines = (m_fileContent ? m_fileContent->lines.size(): 0);
202
203 gfx::Rect vp;
204 if (view)
205 vp = view->viewportBounds().offset(-bounds().origin());
206 else
207 vp = clientBounds();
208
209 auto f = font();
210 gfx::Rect linesVp(0, vp.y, getLineNumberColumnWidth(), vp.h);
211
212 // Fill background
213 g->fillRect(bg, vp);
214 g->fillRect(linesBg, linesVp);
215
216 if (m_fileContent) {
217 auto icon = theme->parts.debugContinue()->bitmap(0);
218 gfx::Point pt = clientBounds().origin();
219 for (int i = 0; i < nlines; ++i) {
220 if (i+1 == m_currentLine) {
221 g->drawRgbaSurface(icon, pt.x+linesVp.w, pt.y);
222 }
223
224 // Draw the line number
225 {
226 auto lineNumText = base::convert_to<std::string>(i+1);
227 int lw = Graphics::measureUITextLength(lineNumText, f);
228 g->drawText(
229 lineNumText.c_str(),
230 fg, linesBg,
231 gfx::Point(pt.x+linesVp.w-lw-2*guiscale(), pt.y));
232 }
233
234 // Draw the this line of source code
235 const char* line = (const char*)m_fileContent->lines[i];
236 g->drawText(line, fg, bg,
237 gfx::Point(pt.x + icon->width() + linesVp.w, pt.y));
238
239 pt.y += textHeight();
240 }
241 }
242 }
243
244 void onSizeHint(SizeHintEvent& ev) override {
245 if (m_fileContent) {
246 if (m_maxLineWidth == 0) {
247 auto f = font();
248 std::string tmp;
249 for (const uint8_t* line : m_fileContent->lines) {
250 ASSERT(line);
251 tmp.assign((const char*)line);
252 m_maxLineWidth = std::max(m_maxLineWidth, Graphics::measureUITextLength(tmp, f));
253 }
254 }
255
256 ev.setSizeHint(gfx::Size(m_maxLineWidth + getLineNumberColumnWidth(),
257 m_fileContent->lines.size() * textHeight()));
258 }
259 }
260
261private:
262
263 int getLineNumberColumnWidth() const {
264 auto f = font();
265 int nlines = (m_fileContent ? m_fileContent->lines.size(): 0);
266 return
267 Graphics::measureUITextLength(base::convert_to<std::string>(nlines), f)
268 + 4*guiscale(); // TODO configurable from the theme?
269 }
270
271 FileContentPtr m_fileContent;
272 int m_currentLine = -1;
273 int m_maxLineWidth = 0;
274};
275
276class StacktraceBox : public ListBox {
277public:
278 class Item : public ListItem {
279 public:
280 Item(lua_Debug* ar, const int stackLevel)
281 : m_fn(ar->short_src)
282 , m_ln(ar->currentline)
283 , m_stackLevel(stackLevel) {
284 std::string lineContent;
285
286 auto it = g_fileContent.find(m_fn);
287 if (it != g_fileContent.end()) {
288 const int i = ar->currentline - 1;
289 if (i >= 0 && i < it->second->lines.size())
290 lineContent.assign((const char*)it->second->lines[i]);
291 }
292 base::trim_string(lineContent, lineContent);
293
294 setText(fmt::format(
295 "{}:{}: {}", base::get_file_name(m_fn), m_ln, lineContent));
296 }
297
298 const std::string& filename() const { return m_fn; }
299 const int lineNumber() const { return m_ln; }
300 const int stackLevel() const { return m_stackLevel; }
301
302 private:
303 std::string m_fn;
304 int m_ln;
305 int m_stackLevel;
306 };
307
308 StacktraceBox() {
309 }
310
311 void clear() {
312 while (auto item = lastChild()) {
313 removeChild(item);
314 item->deferDelete();
315 }
316 }
317
318 void update(lua_State* L) {
319 clear();
320
321 lua_Debug ar;
322 int level = 0;
323 while (lua_getstack(L, level, &ar)) {
324 lua_getinfo(L, "lnS", &ar);
325 if (ar.currentline > 0)
326 addChild(new Item(&ar, level));
327 ++level;
328 }
329 }
330
331};
332
333// TODO similar to DevConsoleView::CommmandEntry, merge both widgets
334// or remove the DevConsoleView
335class EvalEntry : public Entry {
336public:
337 EvalEntry() : Entry(2048, "") {
338 setFocusStop(true);
339 }
340
341 obs::signal<void(const std::string&)> Execute;
342
343protected:
344 bool onProcessMessage(Message* msg) override {
345 switch (msg->type()) {
346 case kKeyDownMessage:
347 if (hasFocus()) {
348 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
349 KeyScancode scancode = keymsg->scancode();
350
351 switch (scancode) {
352 case kKeyEnter:
353 case kKeyEnterPad: {
354 std::string cmd = text();
355 setText("");
356 Execute(cmd);
357 return true;
358 }
359 }
360 }
361 break;
362 }
363 return Entry::onProcessMessage(msg);
364 }
365};
366
367} // anonmous namespace
368
369class Debugger : public gen::Debugger
370 , public script::EngineDelegate
371 , public script::DebuggerDelegate {
372 enum State {
373 Hidden,
374 WaitingStart,
375 WaitingHook,
376 WaitingNextCommand,
377 ProcessingCommand,
378 };
379
380 enum Button {
381 None = -1,
382 Start = 0, // Start/Pause/Continue
383 StepInto,
384 StepOver,
385 StepOut,
386 Breakpoint,
387 };
388
389public:
390
391 Debugger() {
392 control()->ItemChange.connect([this] {
393 auto button = (Button)control()->selectedItem();
394 control()->deselectItems();
395 onControl(button);
396 });
397
398 breakpoint()->setVisible(false); // TODO make this visible
399 breakpoint()->ItemChange.connect([this] {
400 breakpoint()->deselectItems();
401 onControl(Button::Breakpoint);
402 });
403
404 Close.connect([this]{
405 m_state = State::Hidden;
406
407 auto app = App::instance();
408 app->scriptEngine()->setDelegate(m_oldDelegate);
409 app->scriptEngine()->stopDebugger();
410
411 // Clear the cached content of all debugged files
412 g_fileContent.clear();
413
414 // Clear console & locals.
415 console()->setText(std::string());
416 clearLocals();
417 });
418
419 m_stacktrace.Change.connect([this]{
420 onStacktraceChange();
421 });
422
423 m_evalEntry.Execute.connect([this](const std::string& expr){
424 onEvalExpression(expr);
425 });
426
427 mainArea()->setVisible(false);
428
429 sourcePlaceholder()->attachToView(&m_sourceViewer);
430 sourcePlaceholder()->setVisible(false);
431
432 stackPlaceholder()->attachToView(&m_stacktrace);
433 stackPlaceholder()->setVisible(false);
434
435 outputButtons()->ItemChange.connect([this]{ onOutputButtonChange(); });
436 outputButtons()->setSelectedItem(0);
437 onOutputButtonChange();
438
439 console()->setVisible(false);
440 locals()->setVisible(false);
441
442 evalPlaceholder()->addChild(&m_evalEntry);
443 }
444
445 void openDebugger() {
446 m_state = State::WaitingHook;
447
448 updateControls();
449 openWindow();
450
451 auto app = App::instance();
452 m_oldDelegate = app->scriptEngine()->delegate();
453 app->scriptEngine()->setDelegate(this);
454 app->scriptEngine()->startDebugger(this);
455 }
456
457 void onControl(Button button) {
458 ASSERT(m_state != State::Hidden);
459
460 m_lastCommand = button;
461
462 switch (button) {
463
464 case Button::Start:
465 if (m_state == State::WaitingStart) {
466 m_state = State::WaitingHook;
467 m_commandStackLevel = m_stackLevel = 0;
468 m_sourceViewer.clearFile();
469 }
470 else {
471 m_state = State::WaitingStart;
472 m_commandStackLevel = m_stackLevel = 0;
473 m_sourceViewer.clearFile();
474 }
475 break;
476
477 case Button::StepInto:
478 case Button::StepOver:
479 case Button::StepOut:
480 m_state = State::ProcessingCommand;
481 m_commandStackLevel = m_stackLevel;
482 break;
483
484 case Button::Breakpoint:
485 // m_state = State::WaitingNextCommand;
486 // TODO
487 break;
488 }
489
490 updateControls();
491 }
492
493 void updateControls() {
494 bool isRunning = (m_state == State::WaitingHook ||
495 m_state == State::ProcessingCommand);
496 bool canRunCommands = (m_state == State::WaitingNextCommand);
497
498 auto theme = SkinTheme::get(this);
499 control()->getItem(0)->setIcon(
500 (isRunning ? theme->parts.debugPause() :
501 theme->parts.debugContinue()));
502
503 control()->getItem(1)->setEnabled(canRunCommands);
504 control()->getItem(2)->setEnabled(canRunCommands);
505 control()->getItem(3)->setEnabled(canRunCommands);
506 breakpoint()->getItem(0)->setEnabled(canRunCommands);
507
508 // Calling this we update the mouse widget and we can click the
509 // same button several times.
510 //
511 // TODO why is this needed? shouldn't be this automatic from the
512 // ui::Manager side?
513 if (auto man = manager())
514 man->_updateMouseWidgets();
515 }
516
517 // script::EngineDelegate impl
518 void onConsoleError(const char* text) override {
519 m_fileOk = false;
520
521 onConsolePrint(text);
522
523 // Get error filename and number
524 {
525 std::vector<std::string> parts;
526 base::split_string(text, parts, { ":" });
527 if (parts.size() >= 3) {
528 const std::string& fn = parts[0];
529 const std::string& ln = parts[1];
530 if (base::is_file(fn)) {
531 m_sourceViewer.setFile(fn, std::string());
532 m_sourceViewer.setCurrentLine(
533 std::strtol(ln.c_str(), nullptr, 10));
534
535 sourcePlaceholder()->setVisible(true);
536 layout();
537 }
538 }
539 }
540
541 // Stop debugger
542 auto app = App::instance();
543 waitNextCommand(app->scriptEngine()->luaState());
544 updateControls();
545 }
546
547 void onConsolePrint(const char* text) override {
548 console()->setVisible(true);
549 consoleView()->setViewScroll(gfx::Point(0, 0));
550
551 if (text) {
552 std::string stext(console()->text());
553 stext += text;
554 stext += '\n';
555 console()->setText(stext);
556 }
557
558 layout();
559
560 consoleView()->setViewScroll(
561 gfx::Point(0, consoleView()->getScrollableSize().h));
562 }
563
564 // script::DebuggerDelegate impl
565 void hook(lua_State* L, lua_Debug* ar) override {
566 lua_getinfo(L, "lnS", ar);
567
568 switch (ar->event) {
569 case LUA_HOOKCALL: ++m_stackLevel; break;
570 case LUA_HOOKRET: --m_stackLevel; break;
571 case LUA_HOOKLINE:
572 case LUA_HOOKCOUNT:
573 case LUA_HOOKTAILCALL:
574 break;
575 }
576
577 switch (m_state) {
578
579 case State::WaitingStart:
580 // Do nothing (the execution continues regularly, unexpected
581 // script being executed)
582 return;
583
584 case State::WaitingHook:
585 if (ar->event == LUA_HOOKLINE)
586 waitNextCommand(L);
587 else
588 return;
589 break;
590
591 case State::ProcessingCommand:
592 switch (m_lastCommand) {
593
594 case Button::Start:
595 if (ar->event == LUA_HOOKLINE) {
596 // TODO Wait next error...
597 }
598 else {
599 return;
600 }
601 return;
602
603 case Button::StepInto:
604 if (ar->event == LUA_HOOKLINE) {
605 waitNextCommand(L);
606 }
607 else {
608 return;
609 }
610 break;
611
612 case Button::StepOver:
613 if (ar->event == LUA_HOOKLINE &&
614 m_stackLevel == m_commandStackLevel) {
615 waitNextCommand(L);
616 }
617 else {
618 return;
619 }
620 break;
621
622 case Button::StepOut:
623 if (ar->event == LUA_HOOKLINE &&
624 m_stackLevel < m_commandStackLevel) {
625 waitNextCommand(L);
626 }
627 else {
628 return;
629 }
630 break;
631
632 case Button::Breakpoint:
633 if (ar->event != LUA_HOOKLINE) {
634 // TODO
635 return;
636 }
637 break;
638 }
639 break;
640 }
641
642 updateControls();
643
644 if (m_state == State::WaitingNextCommand) {
645 mainArea()->setVisible(true);
646 sourcePlaceholder()->setVisible(true);
647 stackPlaceholder()->setVisible(true);
648 layout();
649
650 if (m_lastFile != ar->short_src &&
651 base::is_file(ar->short_src)) {
652 m_lastFile = ar->short_src;
653 m_sourceViewer.setFile(m_lastFile, std::string());
654 }
655 if (m_lastFile == ar->short_src) {
656 m_sourceViewer.setCurrentLine(ar->currentline);
657 }
658
659 if (!m_expanded) {
660 m_expanded = true;
661 gfx::Rect bounds = this->bounds();
662 if (m_sourceViewer.isVisible()) {
663 bounds.w = std::max(bounds.w, 256*guiscale());
664 bounds.h = std::max(bounds.h, 256*guiscale());
665 }
666 expandWindow(bounds.size());
667 invalidate();
668 }
669
670 MessageLoop loop(Manager::getDefault());
671 while (m_state == State::WaitingNextCommand)
672 loop.pumpMessages();
673 }
674 }
675
676 void startFile(const std::string& file,
677 const std::string& content) override {
678 m_stackLevel = 0;
679 m_fileOk = true;
680 m_sourceViewer.setFile(file, content);
681 }
682
683 void endFile(const std::string& file) override {
684 if (m_fileOk)
685 m_sourceViewer.clearFile();
686 m_stacktrace.clear();
687 m_lastFile.clear();
688 m_stackLevel = 0;
689 }
690
691private:
692 void waitNextCommand(lua_State* L) {
693 m_state = State::WaitingNextCommand;
694 m_stacktrace.update(L);
695 updateLocals(L, 0);
696 }
697
698 void onOutputButtonChange() {
699 consoleView()->setVisible(isConsoleSelected());
700 localsView()->setVisible(isLocalsSelected());
701 layout();
702 }
703
704 bool isConsoleSelected() const {
705 return (outputButtons()->selectedItem() == 0);
706 }
707
708 bool isLocalsSelected() const {
709 return (outputButtons()->selectedItem() == 1);
710 }
711
712 void onStacktraceChange() {
713 if (auto item = dynamic_cast<StacktraceBox::Item*>(m_stacktrace.getSelectedChild())) {
714 auto it = g_fileContent.find(item->filename());
715 if (it != g_fileContent.end())
716 m_sourceViewer.setFileContent(it->second);
717 else
718 m_sourceViewer.setFile(item->filename(), std::string());
719 m_sourceViewer.setCurrentLine(item->lineNumber());
720
721 auto app = App::instance();
722 updateLocals(app->scriptEngine()->luaState(), item->stackLevel());
723 }
724 }
725
726 void onEvalExpression(const std::string& expr) {
727 auto app = App::instance();
728 app->scriptEngine()->evalCode(expr);
729 }
730
731 void clearLocals() {
732 while (auto item = locals()->lastChild()) {
733 locals()->removeChild(item);
734 item->deferDelete();
735 }
736 }
737
738 void updateLocals(lua_State* L, int level) {
739 clearLocals();
740
741 lua_Debug ar;
742 if (lua_getstack(L, level, &ar)) {
743 for (int n=1; ; ++n) {
744 const char* name = lua_getlocal(L, &ar, n);
745 if (!name)
746 break;
747
748 // These special names are returned by luaG_findlocal()
749 if (strcmp(name, "(temporary)") == 0 ||
750 strcmp(name, "(C temporary)") == 0) {
751 lua_pop(L, 1);
752 continue;
753 }
754
755 std::string v = "?";
756 switch (lua_type(L, -1)) {
757 case LUA_TNONE:
758 v = "none";
759 break;
760 case LUA_TNIL:
761 v = "nil";
762 break;
763 case LUA_TBOOLEAN:
764 v = (lua_toboolean(L, -1) ? "true": "false");
765 break;
766 case LUA_TLIGHTUSERDATA:
767 v = "lightuserdata";
768 break;
769 case LUA_TNUMBER:
770 v = lua_tostring(L, -1);
771 break;
772 case LUA_TSTRING:
773 v = lua_tostring(L, -1);
774 break;
775 case LUA_TTABLE:
776 v = "table";
777 break;
778 case LUA_TFUNCTION:
779 v = "function";
780 break;
781 case LUA_TUSERDATA:
782 v = "userdata";
783 break;
784 case LUA_TTHREAD:
785 v = "thread";
786 break;
787 }
788 std::string itemText = fmt::format("{}={}", name, v);
789 lua_pop(L, 1);
790
791 locals()->addChild(new ListItem(itemText));
792 }
793 }
794
795 locals()->setVisible(true);
796 localsView()->updateView();
797 }
798
799 EngineDelegate* m_oldDelegate = nullptr;
800 bool m_expanded = false;
801 State m_state = State::Hidden;
802 Button m_lastCommand = Button::None;
803 DebuggerSource m_sourceViewer;
804 StacktraceBox m_stacktrace;
805 EvalEntry m_evalEntry;
806 std::string m_lastFile;
807 int m_commandStackLevel = 0;
808 int m_stackLevel = 0;
809 bool m_fileOk = true;
810};
811
812DebuggerCommand::DebuggerCommand()
813 : Command(CommandId::Debugger(), CmdRecordableFlag)
814{
815}
816
817void DebuggerCommand::closeDebugger(Context* ctx)
818{
819 if (ctx->isUIAvailable()) {
820 if (m_debugger)
821 m_debugger->closeWindow(nullptr);
822 }
823}
824
825void DebuggerCommand::onExecute(Context* ctx)
826{
827 if (ctx->isUIAvailable()) {
828 auto app = App::instance();
829
830 // Create the debugger window for the first time
831 if (!m_debugger) {
832 m_debugger.reset(new Debugger);
833 app->Exit.connect([this]{
834 m_debugger.reset();
835 });
836 }
837
838 if (!m_debugger->isVisible()) {
839 m_debugger->openDebugger();
840 }
841 else {
842 m_debugger->closeWindow(nullptr);
843 }
844 }
845}
846
847Command* CommandFactory::createDebuggerCommand()
848{
849 return new DebuggerCommand;
850}
851
852} // namespace app
853