GDScript: Fix autocompletion issues with nested types

This commit is contained in:
HolonProduction 2024-07-31 23:44:48 +02:00
parent 3978628c6c
commit 5e5a4aa93d
19 changed files with 291 additions and 35 deletions

View File

@ -976,6 +976,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
@ -987,24 +988,26 @@ static void _list_available_types(bool p_inherit_only, GDScriptParser::Completio
}
}
// 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;
@ -1012,6 +1015,7 @@ static void _list_available_types(bool p_inherit_only, GDScriptParser::Completio
break;
}
}
location_offset += 1;
current = current->outer;
}
}
@ -1076,7 +1080,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)) {
@ -1308,6 +1312,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;
StringName type = type_str.get_slicec('.', 0);
StringName type_enum = base_type.enum_type;
@ -2489,6 +2497,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: {
@ -3313,44 +3331,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, options, 0);
}
}
if (found) {
_find_identifiers_in_base(base, false, true, options, 0);
}
r_forced = true;
} break;
case GDScriptParser::COMPLETION_RESOURCE_PATH: {

View File

@ -1310,7 +1310,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;

View File

@ -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

View File

@ -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

View File

@ -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"},
]

View File

@ -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

View File

@ -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"},
]

View File

@ -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 {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"},
]

View File

@ -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.

View File

@ -160,6 +160,7 @@ static void test_directory(const String &p_dir) {
owner = scene->get_node(conf.get_value("input", "node_path", "."));
}
ERR_PRINT_OFF
if (owner != nullptr) {
// Remove the line which contains the sentinel char, to get a valid script.
Ref<GDScript> scr;
@ -181,8 +182,9 @@ static void test_directory(const String &p_dir) {
scr->set_path(res_path);
owner->set_script(scr);
}
GDScriptLanguage::get_singleton()->complete_code(code, res_path, owner, &options, forced, call_hint);
ERR_PRINT_ON
String contains_excluded;
for (ScriptLanguage::CodeCompletionOption &option : options) {
for (const Dictionary &E : exclude) {
@ -219,13 +221,50 @@ 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;
String source_file = path.path_join(next);
String class_name = GDScriptLanguage::get_singleton()->get_global_class_name(source_file, &base_type);
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);
}
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