diff options
Diffstat (limited to 'Userland/DevTools/HackStudio')
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; }; |