Browse Source

Support object inspection through DAP `variables` request

Ricardo Subtil 10 months ago
parent
commit
6aac039ee7

+ 14 - 6
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<int, Array>::Iterator E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id);
+	if (HashMap<int, Array>::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 {

+ 165 - 1
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<EncodedObjectAsID>(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<ObjectID, DAPVarID> &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);
 }
 

+ 13 - 3
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<DAP::Breakpoint> breakpoint_list;
 	HashMap<DAP::StackFrame, List<int>, DAP::StackFrame> stackframe_list;
-	HashMap<int, Array> variable_list;
+	HashMap<DAPVarID, Array> variable_list;
+
+	HashMap<ObjectID, DAPVarID> object_list;
+	HashSet<ObjectID> object_pending_set;
 
 public:
 	friend class DebugAdapterServer;