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:
parent
bee16bb83a
commit
60d329a186
7 changed files with 378 additions and 1 deletions
|
@ -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
|
||||
|
|
145
Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.cpp
Normal file
145
Userland/DevTools/HackStudio/Debugger/DebuggerGlobalJSObject.cpp
Normal 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 {};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue