فهرست منبع

GDScript: Fix autocompletion issues with nested types

HolonProduction 1 سال پیش
والد
کامیت
d4abc211f1
23فایلهای تغییر یافته به همراه302 افزوده شده و 44 حذف شده
  1. 36 34
      modules/gdscript/gdscript_editor.cpp
  2. 4 1
      modules/gdscript/gdscript_parser.h
  3. 22 0
      modules/gdscript/tests/scripts/completion/class_a.notest.gd
  4. 31 0
      modules/gdscript/tests/scripts/completion/class_b.notest.gd
  5. 2 2
      modules/gdscript/tests/scripts/completion/common/infer_return_type_without_value.gd
  6. 1 1
      modules/gdscript/tests/scripts/completion/enum_values_in_dictionary/lua_key_1.cfg
  7. 0 6
      modules/gdscript/tests/scripts/completion/enum_values_in_dictionary/lua_key_1.gd
  8. 7 0
      modules/gdscript/tests/scripts/completion/enum_values_in_dictionary/lua_key_1.notest.gd
  9. 21 0
      modules/gdscript/tests/scripts/completion/types/hints/index_0.cfg
  10. 11 0
      modules/gdscript/tests/scripts/completion/types/hints/index_0.gd
  11. 19 0
      modules/gdscript/tests/scripts/completion/types/hints/index_0_inner_class.cfg
  12. 10 0
      modules/gdscript/tests/scripts/completion/types/hints/index_0_inner_class.gd
  13. 12 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_global_class.cfg
  14. 1 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_global_class.gd
  15. 12 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_local_class.cfg
  16. 8 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_local_class.gd
  17. 11 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_local_enum.cfg
  18. 7 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_local_enum.gd
  19. 12 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_preload.cfg
  20. 3 0
      modules/gdscript/tests/scripts/completion/types/hints/index_1_preload.gd
  21. 19 0
      modules/gdscript/tests/scripts/completion/types/hints/index_2.cfg
  22. 14 0
      modules/gdscript/tests/scripts/completion/types/hints/index_2.gd
  23. 39 0
      modules/gdscript/tests/test_completion.h

+ 36 - 34
modules/gdscript/gdscript_editor.cpp

@@ -1057,6 +1057,7 @@ static void _list_available_types(bool p_inherit_only, GDScriptParser::Completio
 		}
 	}
 
+	// TODO: Unify with _find_identifiers_in_class.
 	if (p_context.current_class) {
 		if (!p_inherit_only && p_context.current_class->base_type.is_set()) {
 			// Native enums from base class
@@ -1067,25 +1068,27 @@ static void _list_available_types(bool p_inherit_only, GDScriptParser::Completio
 				r_result.insert(option.display, option);
 			}
 		}
-		// Check current class for potential types
+		// Check current class for potential types.
+		// TODO: Also check classes the current class inherits from.
 		const GDScriptParser::ClassNode *current = p_context.current_class;
+		int location_offset = 0;
 		while (current) {
 			for (int i = 0; i < current->members.size(); i++) {
 				const GDScriptParser::ClassNode::Member &member = current->members[i];
 				switch (member.type) {
 					case GDScriptParser::ClassNode::Member::CLASS: {
-						ScriptLanguage::CodeCompletionOption option(member.m_class->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_CLASS, ScriptLanguage::LOCATION_LOCAL);
+						ScriptLanguage::CodeCompletionOption option(member.m_class->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_CLASS, ScriptLanguage::LOCATION_LOCAL + location_offset);
 						r_result.insert(option.display, option);
 					} break;
 					case GDScriptParser::ClassNode::Member::ENUM: {
 						if (!p_inherit_only) {
-							ScriptLanguage::CodeCompletionOption option(member.m_enum->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_ENUM, ScriptLanguage::LOCATION_LOCAL);
+							ScriptLanguage::CodeCompletionOption option(member.m_enum->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_ENUM, ScriptLanguage::LOCATION_LOCAL + location_offset);
 							r_result.insert(option.display, option);
 						}
 					} break;
 					case GDScriptParser::ClassNode::Member::CONSTANT: {
-						if (member.constant->get_datatype().is_meta_type && p_context.current_class->outer != nullptr) {
-							ScriptLanguage::CodeCompletionOption option(member.constant->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_CLASS, ScriptLanguage::LOCATION_LOCAL);
+						if (member.constant->get_datatype().is_meta_type) {
+							ScriptLanguage::CodeCompletionOption option(member.constant->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_CLASS, ScriptLanguage::LOCATION_LOCAL + location_offset);
 							r_result.insert(option.display, option);
 						}
 					} break;
@@ -1093,6 +1096,7 @@ static void _list_available_types(bool p_inherit_only, GDScriptParser::Completio
 						break;
 				}
 			}
+			location_offset += 1;
 			current = current->outer;
 		}
 	}
@@ -1162,7 +1166,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class,
 						option = ScriptLanguage::CodeCompletionOption(member.variable->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location);
 						break;
 					case GDScriptParser::ClassNode::Member::CONSTANT:
-						if (p_types_only || p_only_functions) {
+						if ((p_types_only && !member.constant->datatype.is_meta_type) || p_only_functions) {
 							continue;
 						}
 						if (r_result.has(member.constant->identifier->name)) {
@@ -1409,6 +1413,10 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
 				return;
 			} break;
 			case GDScriptParser::DataType::ENUM: {
+				if (p_types_only) {
+					return;
+				}
+
 				String type_str = base_type.native_type;
 
 				if (type_str.contains_char('.')) {
@@ -2484,6 +2492,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
 			r_type.type.is_meta_type = true;
 			r_type.value = Variant();
 		}
+		return true;
 	}
 
 	return false;
@@ -2645,6 +2654,16 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
 					return true;
 				}
 
+				if (ClassDB::has_enum(class_name, p_identifier)) {
+					r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
+					r_type.type.kind = GDScriptParser::DataType::ENUM;
+					r_type.type.enum_type = p_identifier;
+					r_type.type.is_constant = true;
+					r_type.type.is_meta_type = true;
+					r_type.type.native_type = String(class_name) + "." + p_identifier;
+					return true;
+				}
+
 				return false;
 			} break;
 			case GDScriptParser::DataType::BUILTIN: {
@@ -3543,44 +3562,27 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
 			if (!completion_context.current_class) {
 				break;
 			}
+
 			const GDScriptParser::TypeNode *type = static_cast<const GDScriptParser::TypeNode *>(completion_context.node);
-			bool found = true;
+			ERR_FAIL_INDEX_V_MSG(completion_context.type_chain_index - 1, type->type_chain.size(), Error::ERR_BUG, "Could not complete type argument with out of bounds type chain index.");
 
 			GDScriptCompletionIdentifier base;
-			base.type.kind = GDScriptParser::DataType::CLASS;
-			base.type.type_source = GDScriptParser::DataType::INFERRED;
-			base.type.is_constant = true;
-
-			if (completion_context.current_argument == 1) {
-				StringName type_name = type->type_chain[0]->name;
-
-				if (ClassDB::class_exists(type_name)) {
-					base.type.kind = GDScriptParser::DataType::NATIVE;
-					base.type.native_type = type_name;
-				} else if (ScriptServer::is_global_class(type_name)) {
-					base.type.kind = GDScriptParser::DataType::SCRIPT;
-					String scr_path = ScriptServer::get_global_class_path(type_name);
-					base.type.script_type = ResourceLoader::load(scr_path);
-				}
-			}
-
-			if (base.type.kind == GDScriptParser::DataType::CLASS) {
-				base.type.class_type = completion_context.current_class;
-				base.value = completion_context.base;
 
-				for (int i = 0; i < completion_context.current_argument; i++) {
+			if (_guess_identifier_type(completion_context, type->type_chain[0], base)) {
+				bool found = true;
+				for (int i = 1; i < completion_context.type_chain_index; i++) {
 					GDScriptCompletionIdentifier ci;
-					if (!_guess_identifier_type_from_base(completion_context, base, type->type_chain[i]->name, ci)) {
-						found = false;
+					found = _guess_identifier_type_from_base(completion_context, base, type->type_chain[i]->name, ci);
+					base = ci;
+					if (!found) {
 						break;
 					}
-					base = ci;
+				}
+				if (found) {
+					_find_identifiers_in_base(base, false, true, true, options, 0);
 				}
 			}
 
-			if (found) {
-				_find_identifiers_in_base(base, false, true, true, options, 0);
-			}
 			r_forced = true;
 		} break;
 		case GDScriptParser::COMPLETION_RESOURCE_PATH: {

+ 4 - 1
modules/gdscript/gdscript_parser.h

@@ -1317,7 +1317,10 @@ public:
 		FunctionNode *current_function = nullptr;
 		SuiteNode *current_suite = nullptr;
 		int current_line = -1;
-		int current_argument = -1;
+		union {
+			int current_argument = -1;
+			int type_chain_index;
+		};
 		Variant::Type builtin_type = Variant::VARIANT_MAX;
 		Node *node = nullptr;
 		Object *base = nullptr;

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

@@ -1,5 +1,27 @@
 extends Node
 
+class InnerA:
+	class InnerInnerA:
+		enum EnumOfInnerInnerA {
+		ENUM_VALUE_1,
+		ENUM_VALUE_2,
+	}
+
+	enum EnumOfInnerA {
+		ENUM_VALUE_1,
+		ENUM_VALUE_2,
+	}
+
+	signal signal_of_inner_a
+	var property_of_inner_a
+	func func_of_inner_a():
+		pass
+
+enum EnumOfA {
+	ENUM_VALUE_1,
+	ENUM_VALUE_2,
+}
+
 signal signal_of_a
 
 var property_of_a

+ 31 - 0
modules/gdscript/tests/scripts/completion/class_b.notest.gd

@@ -0,0 +1,31 @@
+extends Node
+class_name B
+
+class InnerB:
+	class InnerInnerB:
+		enum EnumOfInnerInnerB {
+		ENUM_VALUE_1,
+		ENUM_VALUE_2,
+	}
+
+	enum EnumOfInnerB {
+		ENUM_VALUE_1,
+		ENUM_VALUE_2,
+	}
+
+	signal signal_of_inner_b
+	var property_of_inner_b
+	func func_of_inner_b():
+		pass
+
+enum EnumOfB {
+	ENUM_VALUE_1,
+	ENUM_VALUE_2,
+}
+
+signal signal_of_b
+
+var property_of_b
+
+func func_of_b():
+	pass

+ 2 - 2
modules/gdscript/tests/scripts/completion/common/infer_return_type_without_value.gd

@@ -1,8 +1,8 @@
-class B:
+class TestClass:
 	func to_str(b: int):
 		return str(b)
 
-var a: B
+var a: TestClass
 
 func _ready():
 	a.to_str(10).➡

+ 1 - 1
modules/gdscript/tests/scripts/completion/enum_values_in_dictionary/lua_key_1.cfg

@@ -1,4 +1,4 @@
 [output]
 exclude=[
-    {"display": "AUTO_TRANSLATE_MODE_INHERIT"},
+	{"display": "AUTO_TRANSLATE_MODE_INHERIT"},
 ]

+ 0 - 6
modules/gdscript/tests/scripts/completion/enum_values_in_dictionary/lua_key_1.gd

@@ -1,6 +0,0 @@
-extends Node
-
-var test = {
-    t = 1,
-    AutoTranslateMode.➡
-}

+ 7 - 0
modules/gdscript/tests/scripts/completion/enum_values_in_dictionary/lua_key_1.notest.gd

@@ -0,0 +1,7 @@
+# Disabled due to GH-105421
+extends Node
+
+var test = {
+	t = 1,
+	AutoTranslateMode.➡
+}

+ 21 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_0.cfg

@@ -0,0 +1,21 @@
+[output]
+include=[
+    {"display": "A"},
+    {"display": "B"},
+    {"display": "LocalInnerClass"},
+    {"display": "LocalInnerEnum"},
+    {"display": "ConnectFlags"},
+    {"display": "int"},
+    {"display": "String"},
+    {"display": "float"},
+    {"display": "Vector2"},
+    {"display": "Vector3"},
+    {"display": "Vector4"},
+    {"display": "Node"},
+    {"display": "Node2D"},
+]
+exclude=[
+    {"display": "AInner"},
+    {"display": "LocalInnerInnerEnum"},
+    {"display": "LocalInnerInnerClass"},
+]

+ 11 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_0.gd

@@ -0,0 +1,11 @@
+const A = preload("res://completion/class_a.notest.gd")
+
+class LocalInnerClass:
+    const AInner = preload("res://completion/class_a.notest.gd")
+    enum LocalInnerInnerEnum {}
+    class LocalInnerInnerClass:
+        pass
+
+enum LocalInnerEnum {}
+
+var test_var: A➡

+ 19 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_0_inner_class.cfg

@@ -0,0 +1,19 @@
+[output]
+include=[
+    {"display": "A"},
+    {"display": "AInner"},
+    {"display": "B"},
+    {"display": "LocalInnerClass"},
+    {"display": "LocalInnerInnerClass"},
+    {"display": "LocalInnerEnum"},
+    {"display": "LocalInnerInnerEnum"},
+    {"display": "ConnectFlags"},
+    {"display": "int"},
+    {"display": "String"},
+    {"display": "float"},
+    {"display": "Vector2"},
+    {"display": "Vector3"},
+    {"display": "Vector4"},
+    {"display": "Node"},
+    {"display": "Node2D"},
+]

+ 10 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_0_inner_class.gd

@@ -0,0 +1,10 @@
+const A = preload("res://completion/class_a.notest.gd")
+
+class LocalInnerClass:
+    const AInner = preload("res://completion/class_a.notest.gd")
+    enum LocalInnerInnerEnum {}
+    class LocalInnerInnerClass:
+        pass
+    var test_var: A➡
+
+enum LocalInnerEnum {}

+ 12 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_global_class.cfg

@@ -0,0 +1,12 @@
+[output]
+include=[
+    {"display": "InnerB"},
+    {"display": "EnumOfB"},
+    {"display": "ConnectFlags"},
+]
+exclude=[
+    {"display": "int"},
+    {"display": "String"},
+    {"display": "Node2D"},
+    {"display": "B"},
+]

+ 1 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_global_class.gd

@@ -0,0 +1 @@
+var test_var: B.➡

+ 12 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_local_class.cfg

@@ -0,0 +1,12 @@
+[output]
+include=[
+    {"display": "InnerInnerClass"},
+    {"display": "InnerInnerEnum"},
+    {"display": "ConnectFlags"},
+]
+exclude=[
+    {"display": "int"},
+    {"display": "String"},
+    {"display": "Node2D"},
+    {"display": "B"},
+]

+ 8 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_local_class.gd

@@ -0,0 +1,8 @@
+const A = preload("res://completion/class_a.notest.gd")
+
+class LocalInnerClass:
+    class InnerInnerClass:
+        pass
+    enum InnerInnerEnum {}
+
+var test_var: LocalInnerClass.➡

+ 11 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_local_enum.cfg

@@ -0,0 +1,11 @@
+[output]
+exclude=[
+    {"display": "TEST_LOCAL_VAL"},
+    {"display": "ConnectFlags"},
+]
+exclude=[
+    {"display": "int"},
+    {"display": "String"},
+    {"display": "Node2D"},
+    {"display": "B"},
+]

+ 7 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_local_enum.gd

@@ -0,0 +1,7 @@
+const A = preload("res://completion/class_a.notest.gd")
+
+enum LocalInnerEnum {
+    TEST_LOCAL_VAL,
+}
+
+var test_var: LocalInnerEnum.➡

+ 12 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_preload.cfg

@@ -0,0 +1,12 @@
+[output]
+include=[
+    {"display": "InnerA"},
+    {"display": "EnumOfA"},
+    {"display": "ConnectFlags"},
+]
+exclude=[
+    {"display": "int"},
+    {"display": "String"},
+    {"display": "Node2D"},
+    {"display": "B"},
+]

+ 3 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_1_preload.gd

@@ -0,0 +1,3 @@
+const A = preload("res://completion/class_a.notest.gd")
+
+var test_var: A.➡

+ 19 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_2.cfg

@@ -0,0 +1,19 @@
+[output]
+include=[
+    {"display": "AInnerInner"},
+    {"display": "InnerInnerInnerEnum"},
+    {"display": "InnerInnerInnerClass"},
+    {"display": "ConnectFlags"},
+]
+exclude=[
+    {"display": "A"},
+    {"display": "AInner"},
+    {"display": "TestEnum"},
+    {"display": "InnerInnerEnum"},
+    {"display": "InnerInnerClass"},
+    {"display": "LocalInnerClass"},
+    {"display": "int"},
+    {"display": "String"},
+    {"display": "Node2D"},
+    {"display": "B"},
+]

+ 14 - 0
modules/gdscript/tests/scripts/completion/types/hints/index_2.gd

@@ -0,0 +1,14 @@
+const A = preload("res://completion/class_a.notest.gd")
+
+class LocalInnerClass:
+    const AInner = preload("res://completion/class_a.notest.gd")
+    class InnerInnerClass:
+        const AInnerInner = preload("res://completion/class_a.notest.gd")
+        enum InnerInnerInnerEnum {}
+        class InnerInnerInnerClass:
+            pass
+    enum InnerInnerEnum {}
+
+enum TestEnum {}
+
+var test_var: LocalInnerClass.InnerInnerClass.➡

+ 39 - 0
modules/gdscript/tests/test_completion.h

@@ -223,13 +223,52 @@ static void test_directory(const String &p_dir) {
 	}
 }
 
+static void setup_global_classes(const String &p_dir) {
+	Error err = OK;
+	Ref<DirAccess> dir = DirAccess::open(p_dir, &err);
+
+	if (err != OK) {
+		FAIL("Invalid test directory.");
+		return;
+	}
+
+	String path = dir->get_current_dir();
+
+	dir->list_dir_begin();
+	String next = dir->get_next();
+
+	while (!next.is_empty()) {
+		if (dir->current_is_dir() && next != "." && next != "..") {
+			setup_global_classes(path.path_join(next));
+		} else if (next.ends_with(".gd")) {
+			String base_type;
+			bool is_abstract;
+			bool is_tool;
+			String source_file = path.path_join(next);
+			String class_name = GDScriptLanguage::get_singleton()->get_global_class_name(source_file, &base_type, nullptr, &is_abstract, &is_tool);
+			if (class_name.is_empty()) {
+				next = dir->get_next();
+				continue;
+			}
+			ERR_FAIL_COND_MSG(ScriptServer::is_global_class(class_name),
+					"Class name \"" + class_name + "\" from \"" + source_file + "\" is already used in \"" + ScriptServer::get_global_class_path(class_name) + "\".");
+
+			ScriptServer::add_global_class(class_name, base_type, GDScriptLanguage::get_singleton()->get_name(), source_file, is_abstract, is_tool);
+		}
+		next = dir->get_next();
+	}
+}
+
 TEST_SUITE("[Modules][GDScript][Completion]") {
 	TEST_CASE("[Editor] Check suggestion list") {
 		// Set all editor settings that code completion relies on.
 		EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false);
 		init_language("modules/gdscript/tests/scripts");
 
+		setup_global_classes("modules/gdscript/tests/scripts/completion");
 		test_directory("modules/gdscript/tests/scripts/completion");
+
+		finish_language();
 	}
 }
 } // namespace GDScriptTests