1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 16:57:35 +00:00

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.
This commit is contained in:
FalseHonesty 2021-04-12 21:02:47 -04:00 committed by Linus Groh
parent bee16bb83a
commit 60d329a186
7 changed files with 378 additions and 1 deletions

View file

@ -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

View file

@ -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 {};
}
}

View file

@ -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;
};
}

View file

@ -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();
}
}

View file

@ -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;
};
}

View file

@ -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()

View file

@ -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;
};