summaryrefslogtreecommitdiff
path: root/Userland/DevTools
diff options
context:
space:
mode:
authorFalseHonesty <thefalsehonesty@gmail.com>2021-04-12 21:02:47 -0400
committerLinus Groh <mail@linusgroh.de>2021-04-25 19:03:57 +0200
commit60d329a18624ffc1ce84498ce1378f078cc8baaa (patch)
tree85c7751298caec2c6f536a4160b2c4ba70d5e73a /Userland/DevTools
parentbee16bb83ac935fe80a8aa9bf4a381b091952045 (diff)
downloadserenity-60d329a18624ffc1ce84498ce1378f078cc8baaa.zip
HackStudio: Add evaluate expression popup to debugger
This implements a dialog that can be used to evaluate a JS expression in the HackStudio's Debugger context. It also implements simple C++ Variable <-> JS Value conversion, allowing for JS expressions to read/write variables in the debugger scope. Currently, C++ structs are mapped to JS objects by way of a JS proxy, however this leads to issues when printing, so this will be changed in a later commit.
Diffstat (limited to 'Userland/DevTools')
-rw-r--r--Userland/DevTools/HackStudio/CMakeLists.txt4
-rw-r--r--Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.cpp145
-rw-r--r--Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.h34
-rw-r--r--Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.cpp151
-rw-r--r--Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.h33
-rw-r--r--Userland/DevTools/HackStudio/Editor.cpp11
-rw-r--r--Userland/DevTools/HackStudio/Editor.h1
7 files changed, 378 insertions, 1 deletions
diff --git a/Userland/DevTools/HackStudio/CMakeLists.txt b/Userland/DevTools/HackStudio/CMakeLists.txt
index e2f41f2a10..7342dc1de8 100644
--- a/Userland/DevTools/HackStudio/CMakeLists.txt
+++ b/Userland/DevTools/HackStudio/CMakeLists.txt
@@ -5,13 +5,15 @@ compile_gml(Dialogs/NewProjectDialog.gml Dialogs/NewProjectDialogGML.h new_proje
set(SOURCES
CodeDocument.cpp
- ClassViewWidget.cpp
+ ClassViewWidget.cpp
CursorTool.cpp
Debugger/BacktraceModel.cpp
Debugger/DebugInfoWidget.cpp
Debugger/Debugger.cpp
+ Debugger/DebuggerGlobalJSObject.cpp
Debugger/DisassemblyModel.cpp
Debugger/DisassemblyWidget.cpp
+ Debugger/EvaluateExpressionDialog.cpp
Debugger/RegistersModel.cpp
Debugger/VariablesModel.cpp
Dialogs/NewProjectDialog.cpp
diff --git a/Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.cpp b/Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.cpp
new file mode 100644
index 0000000000..eda05da06f
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.cpp
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2021, Hunter Salyer <thefalsehonesty@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "DebuggerGlobalJSObject.h"
+#include "Debugger.h"
+#include <LibJS/Runtime/Object.h>
+#include <LibJS/Runtime/ProxyObject.h>
+
+namespace HackStudio {
+
+DebuggerGlobalJSObject::DebuggerGlobalJSObject()
+{
+ auto regs = Debugger::the().session()->get_registers();
+ auto lib = Debugger::the().session()->library_at(regs.eip);
+ if (!lib)
+ return;
+ m_variables = lib->debug_info->get_variables_in_current_scope(regs);
+}
+
+JS::Value DebuggerGlobalJSObject::get(const JS::PropertyName& name, JS::Value receiver, bool without_side_effects) const
+{
+ if (m_variables.is_empty() || !name.is_string())
+ return JS::Object::get(name, receiver, without_side_effects);
+
+ auto it = m_variables.find_if([&](auto& variable) {
+ return variable->name == name.as_string();
+ });
+ if (it.is_end())
+ return JS::Object::get(name, receiver, without_side_effects);
+ auto& target_variable = **it;
+ auto js_value = debugger_to_js(target_variable);
+ if (js_value.has_value())
+ return js_value.value();
+ auto error_string = String::formatted("Variable {} of type {} is not convertible to a JS Value", name.as_string(), target_variable.type_name);
+ vm().throw_exception<JS::TypeError>(const_cast<DebuggerGlobalJSObject&>(*this), error_string);
+ return {};
+}
+
+bool DebuggerGlobalJSObject::put(const JS::PropertyName& name, JS::Value value, JS::Value receiver)
+{
+ if (m_variables.is_empty() || !name.is_string())
+ return JS::Object::put(name, value, receiver);
+
+ auto it = m_variables.find_if([&](auto& variable) {
+ return variable->name == name.as_string();
+ });
+ if (it.is_end())
+ return JS::Object::put(name, value, receiver);
+ auto& target_variable = **it;
+ auto debugger_value = js_to_debugger(value, target_variable);
+ if (debugger_value.has_value())
+ return Debugger::the().session()->poke((u32*)target_variable.location_data.address, debugger_value.value());
+ auto error_string = String::formatted("Cannot convert JS value {} to variable {} of type {}", value.to_string_without_side_effects(), name.as_string(), target_variable.type_name);
+ vm().throw_exception<JS::TypeError>(const_cast<DebuggerGlobalJSObject&>(*this), error_string);
+ return {};
+}
+
+Optional<JS::Value> DebuggerGlobalJSObject::debugger_to_js(const Debug::DebugInfo::VariableInfo& variable) const
+{
+ if (variable.location_type != Debug::DebugInfo::VariableInfo::LocationType::Address)
+ return {};
+
+ auto variable_address = variable.location_data.address;
+
+ if (variable.is_enum_type() || variable.type_name == "int") {
+ auto value = Debugger::the().session()->peek((u32*)variable_address);
+ VERIFY(value.has_value());
+ return JS::Value((i32)value.value());
+ }
+
+ if (variable.type_name == "char") {
+ auto value = Debugger::the().session()->peek((u32*)variable_address);
+ VERIFY(value.has_value());
+ return JS::Value((char)value.value());
+ }
+
+ if (variable.type_name == "bool") {
+ auto value = Debugger::the().session()->peek((u32*)variable_address);
+ VERIFY(value.has_value());
+ return JS::Value(value.value() != 0);
+ }
+
+ auto& global = const_cast<DebuggerGlobalJSObject&>(*this);
+
+ auto* object = JS::Object::create_empty(global);
+ auto* handler = JS::Object::create_empty(global);
+ auto proxy = JS::ProxyObject::create(global, *object, *handler);
+
+ auto set = [&](JS::VM& vm, JS::GlobalObject&) {
+ auto property = vm.argument(1).value_or(JS::js_undefined());
+ if (!property.is_string())
+ return JS::Value(false);
+ auto property_name = property.as_string().string();
+
+ auto value = vm.argument(2).value_or(JS::js_undefined());
+ dbgln("prop name {}", property_name);
+
+ auto it = variable.members.find_if([&](auto& variable) {
+ dbgln("candidate debugger var name: {}", variable->name);
+ return variable->name == property_name;
+ });
+ if (it.is_end())
+ return JS::Value(false);
+ auto& member = **it;
+ dbgln("Found var {}", member.name);
+
+ auto new_value = js_to_debugger(value, member);
+ Debugger::the().session()->poke((u32*)member.location_data.address, new_value.value());
+
+ return JS::Value(true);
+ };
+
+ handler->define_native_function("set", move(set), 4);
+
+ for (auto& member : variable.members) {
+ auto member_value = debugger_to_js(member);
+ if (!member_value.has_value())
+ continue;
+ object->put(member.name, member_value.value());
+ }
+ return proxy;
+}
+
+Optional<u32> DebuggerGlobalJSObject::js_to_debugger(JS::Value value, const Debug::DebugInfo::VariableInfo& variable) const
+{
+ if (value.is_string() && variable.type_name == "char") {
+ auto string = value.as_string().string();
+ if (string.length() != 1)
+ return {};
+ return string[0];
+ }
+
+ if (value.is_number() && (variable.is_enum_type() || variable.type_name == "int"))
+ return value.as_u32();
+
+ if (value.is_boolean() && variable.type_name == "bool")
+ return value.as_bool();
+
+ return {};
+}
+
+}
diff --git a/Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.h b/Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.h
new file mode 100644
index 0000000000..f8412986e6
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021, Hunter Salyer <thefalsehonesty@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Weakable.h>
+#include <LibDebug/DebugInfo.h>
+#include <LibJS/Runtime/GlobalObject.h>
+
+namespace HackStudio {
+
+class DebuggerGlobalJSObject final
+ : public JS::GlobalObject
+ , public Weakable<DebuggerGlobalJSObject> {
+ JS_OBJECT(DebuggerGlobalJSObject, JS::GlobalObject);
+
+public:
+ DebuggerGlobalJSObject();
+
+ JS::Value get(const JS::PropertyName& name, JS::Value receiver, bool without_side_effects) const override;
+ bool put(const JS::PropertyName& name, JS::Value value, JS::Value receiver) override;
+
+private:
+ Optional<JS::Value> debugger_to_js(const Debug::DebugInfo::VariableInfo&) const;
+ Optional<u32> js_to_debugger(JS::Value value, const Debug::DebugInfo::VariableInfo&) const;
+
+private:
+ NonnullOwnPtrVector<Debug::DebugInfo::VariableInfo> m_variables;
+};
+
+}
diff --git a/Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.cpp b/Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.cpp
new file mode 100644
index 0000000000..b0ec0e7f33
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.cpp
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2021, Hunter Salyer <thefalsehonesty@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "EvaluateExpressionDialog.h"
+#include "DebuggerGlobalJSObject.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibJS/Interpreter.h>
+#include <LibJS/MarkupGenerator.h>
+#include <LibJS/Parser.h>
+#include <LibJS/SyntaxHighlighter.h>
+#include <LibWeb/DOM/DocumentType.h>
+
+namespace HackStudio {
+
+static JS::VM& global_vm()
+{
+ static RefPtr<JS::VM> vm;
+ if (!vm)
+ vm = JS::VM::create();
+ return *vm;
+}
+
+EvaluateExpressionDialog::EvaluateExpressionDialog(Window* parent_window)
+ : Dialog(parent_window)
+ , m_interpreter(JS::Interpreter::create<DebuggerGlobalJSObject>(global_vm()))
+{
+ set_title("Evaluate Expression");
+ set_icon(parent_window->icon());
+ build(parent_window);
+}
+
+void EvaluateExpressionDialog::build(Window* parent_window)
+{
+ auto& widget = set_main_widget<GUI::Widget>();
+
+ int width = max(parent_window->width() / 2, 150);
+ int height = max(parent_window->height() * (2 / 3), 350);
+
+ set_rect(x(), y(), width, height);
+
+ widget.set_layout<GUI::VerticalBoxLayout>();
+ widget.set_fill_with_background_color(true);
+
+ widget.layout()->set_margins({ 6, 6, 6, 6 });
+ widget.layout()->set_spacing(6);
+
+ m_text_editor = widget.add<GUI::TextBox>();
+ m_text_editor->set_fixed_height(19);
+ m_text_editor->set_syntax_highlighter(make<JS::SyntaxHighlighter>());
+ m_text_editor->set_font(Gfx::FontDatabase::default_fixed_width_font());
+ m_text_editor->set_history_enabled(true);
+
+ auto base_document = Web::DOM::Document::create();
+ base_document->append_child(adopt_ref(*new Web::DOM::DocumentType(base_document)));
+ auto html_element = base_document->create_element("html");
+ base_document->append_child(html_element);
+ auto head_element = base_document->create_element("head");
+ html_element->append_child(head_element);
+ auto body_element = base_document->create_element("body");
+ html_element->append_child(body_element);
+ m_output_container = body_element;
+
+ m_output_view = widget.add<Web::InProcessWebView>();
+ m_output_view->set_document(base_document);
+
+ auto& button_container_outer = widget.add<GUI::Widget>();
+ button_container_outer.set_fixed_height(20);
+ button_container_outer.set_layout<GUI::VerticalBoxLayout>();
+
+ auto& button_container_inner = button_container_outer.add<GUI::Widget>();
+ button_container_inner.set_layout<GUI::HorizontalBoxLayout>();
+ button_container_inner.layout()->set_spacing(6);
+ button_container_inner.layout()->set_margins({ 4, 4, 0, 4 });
+ button_container_inner.layout()->add_spacer();
+
+ m_evaluate_button = button_container_inner.add<GUI::Button>();
+ m_evaluate_button->set_fixed_height(20);
+ m_evaluate_button->set_text("Evaluate");
+ m_evaluate_button->on_click = [this](auto) {
+ handle_evaluation(m_text_editor->text());
+ };
+
+ m_close_button = button_container_inner.add<GUI::Button>();
+ m_close_button->set_fixed_height(20);
+ m_close_button->set_text("Close");
+ m_close_button->on_click = [this](auto) {
+ done(ExecOK);
+ };
+
+ m_text_editor->on_return_pressed = [this] {
+ m_evaluate_button->click();
+ };
+ m_text_editor->on_escape_pressed = [this] {
+ m_close_button->click();
+ };
+ m_text_editor->set_focus(true);
+}
+
+void EvaluateExpressionDialog::handle_evaluation(const String& expression)
+{
+ m_output_container->remove_all_children();
+ m_output_view->update();
+
+ auto parser = JS::Parser(JS::Lexer(expression));
+ auto program = parser.parse_program();
+
+ StringBuilder output_html;
+ if (parser.has_errors()) {
+ auto error = parser.errors()[0];
+ auto hint = error.source_location_hint(expression);
+ if (!hint.is_empty())
+ output_html.append(String::formatted("<pre>{}</pre>", escape_html_entities(hint)));
+ m_interpreter->vm().throw_exception<JS::SyntaxError>(m_interpreter->global_object(), error.to_string());
+ } else {
+ m_interpreter->run(m_interpreter->global_object(), *program);
+ }
+
+ if (m_interpreter->exception()) {
+ auto* exception = m_interpreter->exception();
+ m_interpreter->vm().clear_exception();
+ output_html.append("Uncaught exception: ");
+ auto error = exception->value();
+ if (error.is_object())
+ output_html.append(JS::MarkupGenerator::html_from_error(error.as_object()));
+ else
+ output_html.append(JS::MarkupGenerator::html_from_value(error));
+ set_output(output_html.string_view());
+ return;
+ }
+
+ set_output(JS::MarkupGenerator::html_from_value(m_interpreter->vm().last_value()));
+}
+
+void EvaluateExpressionDialog::set_output(const StringView& html)
+{
+ auto paragraph = m_output_container->document().create_element("p");
+ paragraph->set_inner_html(html);
+
+ m_output_container->append_child(paragraph);
+ m_output_container->document().invalidate_layout();
+ m_output_container->document().update_layout();
+}
+
+}
diff --git a/Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.h b/Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.h
new file mode 100644
index 0000000000..d67d984b5d
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Debugger/EvaluateExpressionDialog.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2021, Hunter Salyer <thefalsehonesty@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibGUI/Dialog.h>
+#include <LibWeb/InProcessWebView.h>
+
+namespace HackStudio {
+
+class EvaluateExpressionDialog : public GUI::Dialog {
+ C_OBJECT(EvaluateExpressionDialog);
+
+public:
+ explicit EvaluateExpressionDialog(Window* parent_window);
+
+private:
+ void build(Window* parent_window);
+ void handle_evaluation(const String& expression);
+ void set_output(const StringView& html);
+
+ NonnullOwnPtr<JS::Interpreter> m_interpreter;
+ RefPtr<GUI::TextBox> m_text_editor;
+ RefPtr<Web::InProcessWebView> m_output_view;
+ RefPtr<Web::DOM::Element> m_output_container;
+ RefPtr<GUI::Button> m_evaluate_button;
+ RefPtr<GUI::Button> m_close_button;
+};
+
+}
diff --git a/Userland/DevTools/HackStudio/Editor.cpp b/Userland/DevTools/HackStudio/Editor.cpp
index 998470d2ff..d7703b1d2b 100644
--- a/Userland/DevTools/HackStudio/Editor.cpp
+++ b/Userland/DevTools/HackStudio/Editor.cpp
@@ -7,6 +7,7 @@
#include "Editor.h"
#include "Debugger/Debugger.h"
+#include "Debugger/EvaluateExpressionDialog.h"
#include "EditorWrapper.h"
#include "HackStudio.h"
#include "Language.h"
@@ -16,6 +17,7 @@
#include <LibCore/DirIterator.h>
#include <LibCore/File.h>
#include <LibCpp/SyntaxHighlighter.h>
+#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/GMLSyntaxHighlighter.h>
#include <LibGUI/INISyntaxHighlighter.h>
@@ -42,6 +44,15 @@ Editor::Editor()
m_documentation_tooltip_window->set_rect(0, 0, 500, 400);
m_documentation_tooltip_window->set_window_type(GUI::WindowType::Tooltip);
m_documentation_page_view = m_documentation_tooltip_window->set_main_widget<Web::OutOfProcessWebView>();
+ m_evaluate_expression_action = GUI::Action::create("Evaluate expression", { Mod_Ctrl, Key_E }, [this](auto&) {
+ if (!execution_position().has_value()) {
+ GUI::MessageBox::show(window(), "Program is not running", "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+ auto dialog = EvaluateExpressionDialog::construct(window());
+ dialog->exec();
+ });
+ add_custom_context_menu_action(*m_evaluate_expression_action);
}
Editor::~Editor()
diff --git a/Userland/DevTools/HackStudio/Editor.h b/Userland/DevTools/HackStudio/Editor.h
index 0439ae888a..78ba87e928 100644
--- a/Userland/DevTools/HackStudio/Editor.h
+++ b/Userland/DevTools/HackStudio/Editor.h
@@ -103,6 +103,7 @@ private:
bool m_hovering_editor { false };
bool m_hovering_clickable { false };
bool m_autocomplete_in_focus { false };
+ RefPtr<GUI::Action> m_evaluate_expression_action;
OwnPtr<LanguageClient> m_language_client;
};