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 | |
46 | namespace app { |
47 | |
48 | using namespace ui; |
49 | using namespace app::skin; |
50 | |
51 | namespace { |
52 | |
53 | struct 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 | |
86 | private: |
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 | |
112 | using FileContentPtr = std::shared_ptr<FileContent>; |
113 | |
114 | // Cached file content of each visited file in the debugger. |
115 | // key=filename -> value=FileContent |
116 | std::unordered_map<std::string, FileContentPtr> g_fileContent; |
117 | |
118 | class DebuggerSource : public Widget { |
119 | public: |
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 | |
170 | protected: |
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 | |
261 | private: |
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 | |
276 | class StacktraceBox : public ListBox { |
277 | public: |
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 |
335 | class EvalEntry : public Entry { |
336 | public: |
337 | EvalEntry() : Entry(2048, "" ) { |
338 | setFocusStop(true); |
339 | } |
340 | |
341 | obs::signal<void(const std::string&)> Execute; |
342 | |
343 | protected: |
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 | |
369 | class 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 | |
389 | public: |
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 | |
691 | private: |
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 | |
812 | DebuggerCommand::DebuggerCommand() |
813 | : Command(CommandId::Debugger(), CmdRecordableFlag) |
814 | { |
815 | } |
816 | |
817 | void DebuggerCommand::closeDebugger(Context* ctx) |
818 | { |
819 | if (ctx->isUIAvailable()) { |
820 | if (m_debugger) |
821 | m_debugger->closeWindow(nullptr); |
822 | } |
823 | } |
824 | |
825 | void 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 | |
847 | Command* CommandFactory::createDebuggerCommand() |
848 | { |
849 | return new DebuggerCommand; |
850 | } |
851 | |
852 | } // namespace app |
853 | |