Browse Source

Merge pull request #106198 from SatLess/User-Func-Autocomplete

Add code completion for user-defined methods when overriding in GDScript
Rémi Verschelde 3 months ago
parent
commit
fb59a99244

+ 26 - 3
modules/gdscript/gdscript_editor.cpp

@@ -3519,9 +3519,29 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
 		} break;
 		case GDScriptParser::COMPLETION_OVERRIDE_METHOD: {
 			GDScriptParser::DataType native_type = completion_context.current_class->base_type;
+			GDScriptParser::FunctionNode *function_node = static_cast<GDScriptParser::FunctionNode *>(completion_context.node);
+			bool is_static = function_node != nullptr && function_node->is_static;
 			while (native_type.is_set() && native_type.kind != GDScriptParser::DataType::NATIVE) {
 				switch (native_type.kind) {
 					case GDScriptParser::DataType::CLASS: {
+						for (const GDScriptParser::ClassNode::Member &member : native_type.class_type->members) {
+							if (member.type != GDScriptParser::ClassNode::Member::FUNCTION) {
+								continue;
+							}
+
+							if (options.has(member.function->identifier->name) || completion_context.current_class->has_function(member.function->identifier->name)) {
+								continue;
+							}
+
+							if (is_static != member.function->is_static) {
+								continue;
+							}
+
+							String display_name = member.function->identifier->name;
+							display_name += member.function->signature + ":";
+							ScriptLanguage::CodeCompletionOption option(display_name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);
+							options.insert(member.function->identifier->name, option); // Insert name instead of display to track duplicates.
+						}
 						native_type = native_type.class_type->base_type;
 					} break;
 					default: {
@@ -3542,17 +3562,20 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
 			bool use_type_hint = EditorSettings::get_singleton()->get_setting("text_editor/completion/add_type_hints").operator bool();
 
 			List<MethodInfo> virtual_methods;
-			ClassDB::get_virtual_methods(class_name, &virtual_methods);
-
-			{
+			if (is_static) {
 				// Not truly a virtual method, but can also be "overridden".
 				MethodInfo static_init("_static_init");
 				static_init.return_val.type = Variant::NIL;
 				static_init.flags |= METHOD_FLAG_STATIC | METHOD_FLAG_VIRTUAL;
 				virtual_methods.push_back(static_init);
+			} else {
+				ClassDB::get_virtual_methods(class_name, &virtual_methods);
 			}
 
 			for (const MethodInfo &mi : virtual_methods) {
+				if (options.has(mi.name) || completion_context.current_class->has_function(mi.name)) {
+					continue;
+				}
 				String method_hint = mi.name;
 				if (method_hint.contains_char(':')) {
 					method_hint = method_hint.get_slicec(':', 0);

+ 24 - 5
modules/gdscript/gdscript_parser.cpp

@@ -1609,7 +1609,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_
 	return enum_node;
 }
 
-void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type) {
+void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start) {
 	if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) {
 		bool default_used = false;
 		do {
@@ -1660,15 +1660,30 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
 		current_class->has_static_data = true;
 	}
 
+#ifdef TOOLS_ENABLED
+	if (p_type == "function" && p_signature_start != -1) {
+		int signature_end_pos = tokenizer->get_current_position() - 1;
+		String source_code = tokenizer->get_source_code();
+		p_function->signature = source_code.substr(p_signature_start, signature_end_pos - p_signature_start);
+	}
+#endif // TOOLS_ENABLED
+
 	// TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens.
 	consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type));
 }
 
 GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, bool p_is_static) {
 	FunctionNode *function = alloc_node<FunctionNode>();
+	function->is_static = p_is_static;
 
 	make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
 
+#ifdef TOOLS_ENABLED
+	// The signature is something like `(a: int, b: int = 0) -> void`.
+	// We start one token earlier, since the parser looks one token ahead.
+	const int signature_start_pos = tokenizer->get_current_position();
+#endif // TOOLS_ENABLED
+
 	if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected function name after "func".)")) {
 		complete_extents(function);
 		return nullptr;
@@ -1678,7 +1693,6 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract,
 	current_function = function;
 
 	function->identifier = parse_identifier();
-	function->is_static = p_is_static;
 
 	SuiteNode *body = alloc_node<SuiteNode>();
 
@@ -1687,13 +1701,18 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract,
 
 	push_multiline(true);
 	consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected opening "(" after function name.)");
-	parse_function_signature(function, body, "function");
+
+#ifdef TOOLS_ENABLED
+	parse_function_signature(function, body, "function", signature_start_pos);
+#else // !TOOLS_ENABLED
+	parse_function_signature(function, body, "function", -1);
+#endif // TOOLS_ENABLED
 
 	current_suite = previous_suite;
 
 #ifdef TOOLS_ENABLED
 	function->min_local_doc_line = previous.end_line + 1;
-#endif
+#endif // TOOLS_ENABLED
 
 	function->body = parse_suite("function declaration", body);
 
@@ -3621,7 +3640,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p
 	SuiteNode *previous_suite = current_suite;
 	current_suite = body;
 
-	parse_function_signature(function, body, "lambda");
+	parse_function_signature(function, body, "lambda", -1);
 
 	current_suite = previous_suite;
 

+ 2 - 1
modules/gdscript/gdscript_parser.h

@@ -862,6 +862,7 @@ public:
 #ifdef TOOLS_ENABLED
 		MemberDocData doc_data;
 		int min_local_doc_line = 0;
+		String signature; // For autocompletion.
 #endif // TOOLS_ENABLED
 
 		bool resolved_signature = false;
@@ -1508,7 +1509,7 @@ private:
 	EnumNode *parse_enum(bool p_is_abstract, bool p_is_static);
 	ParameterNode *parse_parameter();
 	FunctionNode *parse_function(bool p_is_abstract, bool p_is_static);
-	void parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type);
+	void parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start);
 	SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
 	// Annotations
 	AnnotationNode *parse_annotation(uint32_t p_valid_targets);

+ 11 - 0
modules/gdscript/gdscript_tokenizer.h

@@ -200,6 +200,12 @@ public:
 
 	static String get_token_name(Token::Type p_token_type);
 
+#ifdef TOOLS_ENABLED
+	// This is a temporary solution, as Tokens are not able to store their position, only lines and columns.
+	virtual int get_current_position() const { return 0; }
+	virtual String get_source_code() const { return ""; }
+#endif // TOOLS_ENABLED
+
 	virtual int get_cursor_line() const = 0;
 	virtual int get_cursor_column() const = 0;
 	virtual void set_cursor_position(int p_line, int p_column) = 0;
@@ -285,6 +291,11 @@ public:
 
 	const Vector<int> &get_continuation_lines() const { return continuation_lines; }
 
+#ifdef TOOLS_ENABLED
+	virtual int get_current_position() const override { return position; }
+	virtual String get_source_code() const override { return source; }
+#endif // TOOLS_ENABLED
+
 	virtual int get_cursor_line() const override;
 	virtual int get_cursor_column() const override;
 	virtual void set_cursor_position(int p_line, int p_column) override;

+ 15 - 0
modules/gdscript/tests/scripts/completion/class_a.notest.gd

@@ -6,3 +6,18 @@ var property_of_a
 
 func func_of_a():
 	pass
+
+
+func _func_of_a_underscore():
+	pass
+
+
+static func func_of_a_static():
+	pass
+
+func func_of_a_args(a: int):
+	pass
+
+func func_of_a_callable(call := func():
+	var x_of_a = 10):
+	pass

+ 13 - 0
modules/gdscript/tests/scripts/completion/common/override_function_no_underscore.cfg

@@ -0,0 +1,13 @@
+scene="res://completion/get_node/get_node.tscn"
+[output]
+include=[
+    ; GDScript: class_a.notest.gd
+    {"display": "func_of_a():"},
+    {"display": "func_of_a_args(a: int):"},
+    {"display": "func_of_a_callable(call := func():
+	var x_of_a = 10):"}
+]
+exclude=[
+    ; GDScript: class_a.notest.gd
+    {"display": "_static_init() -> void:"},
+]

+ 3 - 0
modules/gdscript/tests/scripts/completion/common/override_function_no_underscore.gd

@@ -0,0 +1,3 @@
+extends "res://completion/class_a.notest.gd"
+
+func ➡

+ 14 - 0
modules/gdscript/tests/scripts/completion/common/override_function_static.cfg

@@ -0,0 +1,14 @@
+scene="res://completion/get_node/get_node.tscn"
+[output]
+include=[
+    ; GDScript: class_a.notest.gd
+    {"display": "_static_init() -> void:"},
+    {"display": "func_of_a_static():"},
+]
+exclude=[
+    ; GDScript: class_a.notest.gd
+    {"display": "func_of_a():"},
+    {"display": "func_of_a_args(a: int):"},
+    {"display": "func_of_a_callable(call := func():
+	var x_of_a = 10):"},
+]

+ 3 - 0
modules/gdscript/tests/scripts/completion/common/override_function_static.gd

@@ -0,0 +1,3 @@
+extends "res://completion/class_a.notest.gd"
+
+static func ➡

+ 10 - 0
modules/gdscript/tests/scripts/completion/common/override_function_underscore.cfg

@@ -0,0 +1,10 @@
+scene="res://completion/get_node/get_node.tscn"
+[output]
+include=[
+    ; GDScript: class_a.notest.gd
+    {"display": "_func_of_a_underscore():"},
+]
+exclude=[
+    ; GDScript: class_a.notest.gd
+    {"display": "_static_init() -> void:"},
+]

+ 3 - 0
modules/gdscript/tests/scripts/completion/common/override_function_underscore.gd

@@ -0,0 +1,3 @@
+extends "res://completion/class_a.notest.gd"
+
+func _➡