Use type hints to improve completion

- Allow type hints to be completed.
- Use type information to infer completion candidates.
- Show typed function signature in tooltip.
- Add type hints when completing declaration from virtual functions
(optional).
This commit is contained in:
George Marques 2018-05-29 23:16:59 -03:00
parent 95351ac867
commit 75f395c2a0
No known key found for this signature in database
GPG key ID: 046BD46A3201E43D
3 changed files with 188 additions and 56 deletions

View file

@ -820,7 +820,9 @@ void ConnectionsDock::update_tree() {
if (i > 0)
signaldesc += ", ";
String tname = "var";
if (pi.type != Variant::NIL) {
if (pi.type == Variant::OBJECT && pi.class_name != StringName()) {
tname = pi.class_name.operator String();
} else if (pi.type != Variant::NIL) {
tname = Variant::get_type_name(pi.type);
}
signaldesc += tname + " " + (pi.name == "" ? String("arg " + itos(i)) : pi.name);

View file

@ -404,6 +404,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
_initial_set("text_editor/completion/callhint_tooltip_offset", Vector2());
_initial_set("text_editor/files/restore_scripts_on_load", true);
_initial_set("text_editor/completion/complete_file_paths", true);
_initial_set("text_editor/completion/add_type_hints", false);
_initial_set("docks/scene_tree/start_create_dialog_fully_expanded", false);
_initial_set("docks/scene_tree/draw_relationship_lines", false);

View file

@ -53,21 +53,45 @@ void GDScriptLanguage::get_string_delimiters(List<String> *p_delimiters) const {
p_delimiters->push_back("' '");
}
Ref<Script> GDScriptLanguage::get_template(const String &p_class_name, const String &p_base_class_name) const {
#ifdef TOOLS_ENABLED
bool th = EDITOR_DEF("text_editor/completion/add_type_hints", false);
#else
bool th = false;
#endif
String _template = "extends %BASE%\n"
"\n"
"# Declare member variables here. Examples:\n"
"# var a = 2\n"
"# var b = \"text\"\n"
"# var a %INT_TYPE%= 2\n"
"# var b %STRING_TYPE%= \"text\"\n"
"\n"
"# Called when the node enters the scene tree for the first time.\n"
"func _ready():\n"
"func _ready()%VOID_RETURN%:\n"
"%TS%pass # Replace with function body.\n"
"\n"
"# Called every frame. 'delta' is the elapsed time since the previous frame.\n"
"#func _process(delta):\n"
"#func _process(delta%FLOAT_TYPE%)%VOID_RETURN%:\n"
"#%TS%pass\n";
#ifdef TOOLS_ENABLED
if (EDITOR_DEF("text_editor/completion/add_type_hints", false)) {
_template = _template.replace("%INT_TYPE%", ": int ");
_template = _template.replace("%STRING_TYPE%", ": String ");
_template = _template.replace("%FLOAT_TYPE%", " : float");
_template = _template.replace("%VOID_RETURN%", " -> void");
} else {
_template = _template.replace("%INT_TYPE%", "");
_template = _template.replace("%STRING_TYPE%", "");
_template = _template.replace("%FLOAT_TYPE%", "");
_template = _template.replace("%VOID_RETURN%", "");
}
#else
_template = _template.replace("%INT_TYPE%", "");
_template = _template.replace("%STRING_TYPE%", "");
_template = _template.replace("%FLOAT_TYPE%", "");
_template = _template.replace("%VOID_RETURN%", "");
#endif
_template = _template.replace("%BASE%", p_base_class_name);
_template = _template.replace("%TS%", _get_indentation());
@ -415,15 +439,27 @@ void GDScriptLanguage::get_public_constants(List<Pair<String, Variant> > *p_cons
String GDScriptLanguage::make_function(const String &p_class, const String &p_name, const PoolStringArray &p_args) const {
#ifdef TOOLS_ENABLED
bool th = EditorSettings::get_singleton()->get_setting("text_editor/completion/add_type_hints");
#else
bool th = false;
#endif
String s = "func " + p_name + "(";
if (p_args.size()) {
for (int i = 0; i < p_args.size(); i++) {
if (i > 0)
s += ", ";
s += p_args[i].get_slice(":", 0);
if (th) {
String type = p_args[i].get_slice(":", 1);
if (!type.empty() && type != "var") {
s += " : " + type;
}
}
}
}
s += "):\n" + _get_indentation() + "pass # Replace with function body.\n";
s += String(")") + (th ? " -> void" : "") + ":\n" + _get_indentation() + "pass # Replace with function body.\n";
return s;
}
@ -473,6 +509,30 @@ static GDScriptCompletionIdentifier _get_type_from_pinfo(const PropertyInfo &p_i
return t;
}
static bool _get_type_from_parser_type(const GDScriptParser::DataType &p_datatype, GDScriptCompletionIdentifier &r_type) {
if (p_datatype.has_type && p_datatype.kind != GDScriptParser::DataType::UNRESOLVED) {
switch (p_datatype.kind) {
case GDScriptParser::DataType::BUILTIN: {
r_type.type = p_datatype.builtin_type;
return true;
} break;
case GDScriptParser::DataType::NATIVE: {
r_type.type = Variant::OBJECT;
r_type.obj_type = p_datatype.native_type;
return true;
} break;
case GDScriptParser::DataType::SCRIPT:
case GDScriptParser::DataType::GDSCRIPT: {
r_type.type = Variant::OBJECT;
r_type.script = p_datatype.script_type;
r_type.obj_type = r_type.script->get_instance_base_type();
return true;
} break;
}
}
return false;
}
struct GDScriptCompletionContext {
const GDScriptParser::ClassNode *_class;
@ -592,6 +652,11 @@ static bool _guess_identifier_type(GDScriptCompletionContext &context, int p_lin
static bool _guess_expression_type(GDScriptCompletionContext &context, const GDScriptParser::Node *p_node, int p_line, GDScriptCompletionIdentifier &r_type, bool p_for_indexing = false) {
GDScriptParser::DataType datatype = p_node->get_datatype();
if (_get_type_from_parser_type(datatype, r_type)) {
return true;
}
if (p_node->type == GDScriptParser::Node::TYPE_CONSTANT) {
const GDScriptParser::ConstantNode *cn = static_cast<const GDScriptParser::ConstantNode *>(p_node);
@ -1060,6 +1125,18 @@ static bool _guess_identifier_type_in_block(GDScriptCompletionContext &context,
}
}
// Check type hint first
const GDScriptParser::BlockNode *blk = context.block;
while (blk) {
if (blk->variables.has(p_identifier)) {
GDScriptParser::DataType var_type = blk->variables[p_identifier]->get_datatype();
if (_get_type_from_parser_type(var_type, r_type)) {
return true;
}
}
blk = blk->parent_block;
}
GDScriptCompletionIdentifier gdi = _get_native_class(context);
if (gdi.obj_type != StringName()) {
bool valid;
@ -1198,6 +1275,9 @@ static bool _guess_identifier_type(GDScriptCompletionContext &context, int p_lin
if (context.function->arguments[i] == p_identifier) {
argindex = i;
if (_get_type_from_parser_type(context.function->argument_types[i], r_type)) {
return true;
}
break;
}
}
@ -1507,6 +1587,9 @@ static void _find_identifiers(GDScriptCompletionContext &context, int p_line, bo
c.block = NULL;
c.function = NULL;
_find_identifiers_in_class(c, _static, p_only_functions, result);
if (!p_only_functions && clss->name != StringName()) {
result.insert(clss->name.operator String());
}
clss = clss->owner;
}
@ -1533,19 +1616,8 @@ static void _find_identifiers(GDScriptCompletionContext &context, int p_line, bo
}
//autoload singletons
List<PropertyInfo> props;
ProjectSettings::get_singleton()->get_property_list(&props);
for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
String s = E->get().name;
if (!s.begins_with("autoload/"))
continue;
String name = s.get_slice("/", 1);
String path = ProjectSettings::get_singleton()->get(s);
if (path.begins_with("*")) {
result.insert(name);
}
for (const Map<StringName, Variant>::Element *E = GDScriptLanguage::get_singleton()->get_named_globals_map().front(); E; E = E->next()) {
result.insert(E->key().operator String());
}
for (const Map<StringName, int>::Element *E = GDScriptLanguage::get_singleton()->get_global_map().front(); E; E = E->next()) {
@ -1553,6 +1625,45 @@ static void _find_identifiers(GDScriptCompletionContext &context, int p_line, bo
}
}
static void _find_types(GDScriptCompletionContext &p_context, Set<String> &r_result, bool p_is_function) {
const GDScriptParser::ClassNode *clss = p_context._class;
while (clss) {
for (Map<StringName, GDScriptParser::ClassNode::Constant>::Element *E = clss->constant_expressions.front(); E; E = E->next()) {
r_result.insert(E->key().operator String());
}
for (int i = 0; i < clss->subclasses.size(); i++) {
if (clss->subclasses[i]->name != StringName()) {
r_result.insert(clss->subclasses[i]->name.operator String());
}
}
clss = clss->owner;
}
for (int i = 0; i < Variant::VARIANT_MAX; i++) {
r_result.insert(Variant::get_type_name((Variant::Type)i));
}
List<StringName> native_classes;
ClassDB::get_class_list(&native_classes);
for (List<StringName>::Element *E = native_classes.front(); E; E = E->next()) {
String class_name = E->get().operator String();
if (class_name.begins_with("_")) {
class_name = class_name.right(1);
}
if (Engine::get_singleton()->has_singleton(class_name)) {
continue;
}
r_result.insert(class_name);
}
if (p_is_function) {
r_result.insert("void");
}
}
static String _get_visual_datatype(const PropertyInfo &p_info, bool p_isarg = true) {
String n = p_info.name;
@ -1563,6 +1674,9 @@ static String _get_visual_datatype(const PropertyInfo &p_info, bool p_isarg = tr
if (p_info.type == Variant::OBJECT && p_info.hint == PROPERTY_HINT_RESOURCE_TYPE)
return p_info.hint_string;
if (p_info.type == Variant::OBJECT && p_info.class_name != StringName())
return p_info.class_name.operator String();
if (p_info.type == Variant::NIL) {
if (p_isarg)
return "var";
@ -1575,7 +1689,16 @@ static String _get_visual_datatype(const PropertyInfo &p_info, bool p_isarg = tr
static void _make_function_hint(const GDScriptParser::FunctionNode *p_func, int p_argidx, String &arghint) {
arghint = "func " + p_func->name + "(";
GDScriptParser::DataType rettype = p_func->return_type;
if (rettype.has_type && rettype.kind == GDScriptParser::DataType::UNRESOLVED) {
// Can be unresolved due to incomplete parse, use the unresolved name instead
arghint = rettype.native_type.operator String();
} else {
arghint = rettype.to_string();
}
arghint += " " + p_func->name + "(";
for (int i = 0; i < p_func->arguments.size(); i++) {
if (i > 0)
arghint += ", ";
@ -1585,7 +1708,17 @@ static void _make_function_hint(const GDScriptParser::FunctionNode *p_func, int
if (i == p_argidx) {
arghint += String::chr(0xFFFF);
}
arghint += p_func->arguments[i].operator String();
GDScriptParser::DataType argtype = p_func->argument_types[i];
if (argtype.has_type && argtype.kind == GDScriptParser::DataType::UNRESOLVED) {
// Can be unresolved due to incomplete parse, use the unresolved name instead
arghint += argtype.native_type.operator String();
} else {
arghint += argtype.to_string();
}
arghint += " " + p_func->arguments[i].operator String();
int deffrom = p_func->arguments.size() - p_func->default_values.size();
if (i >= deffrom) {
@ -1598,9 +1731,8 @@ static void _make_function_hint(const GDScriptParser::FunctionNode *p_func, int
const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(p_func->default_values[defidx]);
if (op->op == GDScriptParser::OperatorNode::OP_ASSIGN) {
const GDScriptParser::ConstantNode *cn = static_cast<const GDScriptParser::ConstantNode *>(op->arguments[1]);
arghint += "=" + cn->value.get_construct_string();
arghint += " = " + cn->value.get_construct_string();
}
} else {
}
}
}
@ -1736,37 +1868,7 @@ static void _find_type_arguments(GDScriptCompletionContext &context, const GDScr
if (func) {
arghint = "func " + String(p_method) + "(";
if (st)
arghint = "static " + arghint;
for (int i = 0; i < func->arguments.size(); i++) {
if (i > 0)
arghint += ", ";
else
arghint += " ";
if (i == p_argidx) {
arghint += String::chr(0xFFFF);
}
arghint += "var " + String(func->arguments[i]);
int deffrom = func->arguments.size() - func->default_values.size();
if (i >= deffrom) {
int defidx = deffrom - i;
if (defidx >= 0 && defidx < func->default_values.size() && func->default_values[defidx]->type == GDScriptParser::Node::TYPE_OPERATOR) {
const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(func->default_values[defidx]);
if (op->op == GDScriptParser::OperatorNode::OP_ASSIGN) {
const GDScriptParser::ConstantNode *cn = static_cast<const GDScriptParser::ConstantNode *>(op->arguments[1]);
arghint += "=" + cn->value.get_construct_string();
}
}
}
if (i == p_argidx) {
arghint += String::chr(0xFFFF);
}
}
arghint += " )";
_make_function_hint(func, p_argidx, arghint);
return;
}
} else {
@ -1816,7 +1918,7 @@ static void _find_type_arguments(GDScriptCompletionContext &context, const GDScr
} else {
//regular method
//regular method
#if defined(DEBUG_METHODS_ENABLED) && defined(TOOLS_ENABLED)
if (p_argidx < m->get_argument_count()) {
PropertyInfo pi = m->get_argument_info(p_argidx);
@ -2423,6 +2525,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_base
if (cid.obj_type != StringName()) {
List<MethodInfo> vm;
ClassDB::get_virtual_methods(cid.obj_type, &vm);
bool type_hint = EditorSettings::get_singleton()->get_setting("text_editor/completion/add_type_hints").operator bool();
for (List<MethodInfo>::Element *E = vm.front(); E; E = E->next()) {
MethodInfo &mi = E->get();
@ -2439,9 +2542,28 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_base
if (n.find(":") != -1)
n = n.substr(0, n.find(":"));
m += n;
if (type_hint && mi.arguments[i].type != Variant::NIL) {
m += " : ";
if (mi.arguments[i].type == Variant::OBJECT && mi.arguments[i].class_name != StringName()) {
m += mi.arguments[i].class_name.operator String();
} else {
m += Variant::get_type_name(mi.arguments[i].type);
}
}
}
}
m += "):";
m += ")";
if (type_hint && (mi.return_val.type != Variant::NIL || !(mi.return_val.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) {
m += " -> ";
if (mi.return_val.type == Variant::NIL) {
m += "void";
} else if (mi.return_val.type == Variant::OBJECT && mi.return_val.class_name != StringName()) {
m += mi.return_val.class_name.operator String();
} else {
m += Variant::get_type_name(mi.return_val.type);
}
}
m += ":";
options.insert(m);
}
@ -2509,6 +2631,13 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_base
}
#endif
} break;
case GDScriptParser::COMPLETION_TYPE_HINT: {
_find_types(context, options, p.get_completion_identifier_is_function());
} break;
case GDScriptParser::COMPLETION_TYPE_HINT_INDEX: {
options.insert("SomeIndexedTypeHere");
options.insert(p.get_completion_cursor().operator String());
} break;
}
for (Set<String>::Element *E = options.front(); E; E = E->next()) {