From 91cedc1e09fe4165c4f613ebecd0cb69f0de73cd Mon Sep 17 00:00:00 2001 From: Ricardo Subtil Date: Tue, 24 Sep 2024 21:33:29 +0100 Subject: [PATCH] Support object inspection through DAP `variables` request --- .../debug_adapter/debug_adapter_parser.cpp | 20 ++- .../debug_adapter/debug_adapter_protocol.cpp | 166 +++++++++++++++++- .../debug_adapter/debug_adapter_protocol.h | 16 +- 3 files changed, 192 insertions(+), 10 deletions(-) diff --git a/editor/debugger/debug_adapter/debug_adapter_parser.cpp b/editor/debugger/debug_adapter/debug_adapter_parser.cpp index 4210baeed2d..8e2f037a1c3 100644 --- a/editor/debugger/debug_adapter/debug_adapter_parser.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_parser.cpp @@ -442,26 +442,34 @@ Dictionary DebugAdapterParser::req_variables(const Dictionary &p_params) const { return Dictionary(); } - Dictionary response = prepare_success_response(p_params), body; - response["body"] = body; - Dictionary args = p_params["arguments"]; int variable_id = args["variablesReference"]; - HashMap::Iterator E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id); + if (HashMap::Iterator E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id); E) { + Dictionary response = prepare_success_response(p_params); + Dictionary body; + response["body"] = body; - if (E) { if (!DebugAdapterProtocol::get_singleton()->get_current_peer()->supportsVariableType) { for (int i = 0; i < E->value.size(); i++) { Dictionary variable = E->value[i]; variable.erase("type"); } } + body["variables"] = E ? E->value : Array(); return response; } else { - return Dictionary(); + // If the requested variable is an object, it needs to be requested from the debuggee. + ObjectID object_id = DebugAdapterProtocol::get_singleton()->search_object_id(variable_id); + + if (object_id.is_null()) { + return prepare_error_response(p_params, DAP::ErrorType::UNKNOWN); + } + + DebugAdapterProtocol::get_singleton()->request_remote_object(object_id); } + return Dictionary(); } Dictionary DebugAdapterParser::req_next(const Dictionary &p_params) const { diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp index 4febb8bf047..f8f5584ba6d 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp @@ -33,8 +33,8 @@ #include "core/config/project_settings.h" #include "core/debugger/debugger_marshalls.h" #include "core/io/json.h" +#include "core/io/marshalls.h" #include "editor/debugger/script_editor_debugger.h" -#include "editor/doc_tools.h" #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" @@ -186,6 +186,8 @@ void DebugAdapterProtocol::reset_stack_info() { stackframe_list.clear(); variable_list.clear(); + object_list.clear(); + object_pending_set.clear(); } int DebugAdapterProtocol::parse_variant(const Variant &p_var) { @@ -671,12 +673,166 @@ int DebugAdapterProtocol::parse_variant(const Variant &p_var) { variable_list.insert(id, arr); return id; } + case Variant::OBJECT: { + // Objects have to be requested from the debuggee. This has do be done + // in a lazy way, as retrieving object properties takes time. + EncodedObjectAsID *encoded_obj = Object::cast_to(p_var); + + // Object may be null; in that case, return early. + if (!encoded_obj) { + return 0; + } + + // Object may have been already requested. + ObjectID object_id = encoded_obj->get_object_id(); + if (object_list.has(object_id)) { + return object_list[object_id]; + } + + // Queue requesting the object. + int id = variable_id++; + object_list.insert(object_id, id); + return id; + } default: // Simple atomic stuff, or too complex to be manipulated return 0; } } +void DebugAdapterProtocol::parse_object(SceneDebuggerObject &p_obj) { + // If the object is not on the pending list, we weren't expecting it. Ignore it. + ObjectID object_id = p_obj.id; + if (!object_pending_set.erase(object_id)) { + return; + } + + // Populate DAP::Variable's with the object's properties. These properties will be divided by categories. + Array properties; + Array script_members; + Array script_constants; + Array script_node; + DAP::Variable node_type; + Array node_properties; + + for (SceneDebuggerObject::SceneDebuggerProperty &property : p_obj.properties) { + PropertyInfo &info = property.first; + + // Script members ("Members/" prefix) + if (info.name.begins_with("Members/")) { + info.name = info.name.trim_prefix("Members/"); + script_members.push_back(parse_object_variable(property)); + } + + // Script constants ("Constants/" prefix) + else if (info.name.begins_with("Constants/")) { + info.name = info.name.trim_prefix("Constants/"); + script_constants.push_back(parse_object_variable(property)); + } + + // Script node ("Node/" prefix) + else if (info.name.begins_with("Node/")) { + info.name = info.name.trim_prefix("Node/"); + script_node.push_back(parse_object_variable(property)); + } + + // Regular categories (with type Variant::NIL) + else if (info.type == Variant::NIL) { + if (!node_properties.is_empty()) { + node_type.value = itos(node_properties.size()); + variable_list.insert(node_type.variablesReference, node_properties.duplicate()); + properties.push_back(node_type.to_json()); + } + + node_type.name = info.name; + node_type.type = "Category"; + node_type.variablesReference = variable_id++; + node_properties.clear(); + } + + // Regular properties. + else { + node_properties.push_back(parse_object_variable(property)); + } + } + + // Add the last category. + if (!node_properties.is_empty()) { + node_type.value = itos(node_properties.size()); + variable_list.insert(node_type.variablesReference, node_properties.duplicate()); + properties.push_back(node_type.to_json()); + } + + // Add the script categories, in reverse order to be at the front of the array: + // ( [members; constants; node; category1; category2; ...] ) + if (!script_node.is_empty()) { + DAP::Variable node; + node.name = "Node"; + node.type = "Category"; + node.value = itos(script_node.size()); + node.variablesReference = variable_id++; + variable_list.insert(node.variablesReference, script_node); + properties.push_front(node.to_json()); + } + + if (!script_constants.is_empty()) { + DAP::Variable constants; + constants.name = "Constants"; + constants.type = "Category"; + constants.value = itos(script_constants.size()); + constants.variablesReference = variable_id++; + variable_list.insert(constants.variablesReference, script_constants); + properties.push_front(constants.to_json()); + } + + if (!script_members.is_empty()) { + DAP::Variable members; + members.name = "Members"; + members.type = "Category"; + members.value = itos(script_members.size()); + members.variablesReference = variable_id++; + variable_list.insert(members.variablesReference, script_members); + properties.push_front(members.to_json()); + } + + ERR_FAIL_COND(!object_list.has(object_id)); + variable_list.insert(object_list[object_id], properties); +} + +const Variant DebugAdapterProtocol::parse_object_variable(const SceneDebuggerObject::SceneDebuggerProperty &p_property) { + const PropertyInfo &info = p_property.first; + const Variant &value = p_property.second; + + DAP::Variable var; + var.name = info.name; + var.type = Variant::get_type_name(info.type); + var.value = value; + var.variablesReference = parse_variant(value); + + return var.to_json(); +} + +ObjectID DebugAdapterProtocol::search_object_id(DAPVarID p_var_id) { + for (const KeyValue &E : object_list) { + if (E.value == p_var_id) { + return E.key; + } + } + return ObjectID(); +} + +bool DebugAdapterProtocol::request_remote_object(const ObjectID &p_object_id) { + // If the object is already on the pending list, we don't need to request it again. + if (object_pending_set.has(p_object_id)) { + return false; + } + + EditorDebuggerNode::get_singleton()->get_default_debugger()->request_remote_object(p_object_id); + object_pending_set.insert(p_object_id); + + return true; +} + bool DebugAdapterProtocol::process_message(const String &p_text) { JSON json; ERR_FAIL_COND_V_MSG(json.parse(p_text) != OK, true, "Malformed message!"); @@ -986,6 +1142,14 @@ void DebugAdapterProtocol::on_debug_data(const String &p_msg, const Array &p_dat return; } + if (p_msg == "scene:inspect_object") { + // An object was requested from the debuggee; parse it. + SceneDebuggerObject remote_obj; + remote_obj.deserialize(p_data); + + parse_object(remote_obj); + } + notify_custom_data(p_msg, p_data); } diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.h b/editor/debugger/debug_adapter/debug_adapter_protocol.h index caff0f9c7ff..29d7457ee76 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.h +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.h @@ -31,12 +31,12 @@ #ifndef DEBUG_ADAPTER_PROTOCOL_H #define DEBUG_ADAPTER_PROTOCOL_H -#include "core/io/stream_peer.h" #include "core/io/stream_peer_tcp.h" #include "core/io/tcp_server.h" #include "debug_adapter_parser.h" #include "debug_adapter_types.h" +#include "scene/debugger/scene_debugger.h" #define DAP_MAX_BUFFER_SIZE 4194304 // 4MB #define DAP_MAX_CLIENTS 8 @@ -75,6 +75,8 @@ class DebugAdapterProtocol : public Object { friend class DebugAdapterParser; + using DAPVarID = int; + private: static DebugAdapterProtocol *singleton; DebugAdapterParser *parser = nullptr; @@ -99,6 +101,11 @@ private: void reset_stack_info(); int parse_variant(const Variant &p_var); + void parse_object(SceneDebuggerObject &p_obj); + const Variant parse_object_variable(const SceneDebuggerObject::SceneDebuggerProperty &p_property); + + ObjectID search_object_id(DAPVarID p_var_id); + bool request_remote_object(const ObjectID &p_object_id); bool _initialized = false; bool _processing_breakpoint = false; @@ -114,10 +121,13 @@ private: int breakpoint_id = 0; int stackframe_id = 0; - int variable_id = 0; + DAPVarID variable_id = 0; List breakpoint_list; HashMap, DAP::StackFrame> stackframe_list; - HashMap variable_list; + HashMap variable_list; + + HashMap object_list; + HashSet object_pending_set; public: friend class DebugAdapterServer;