Godot 4 源码分析 - 代码自动补全流程

使用Godot 4过程中,有一点比较吸引我:代码自动补全

用RAD开发时,代码自动补全功能一直被吐槽,主要是速度慢

但我看Godot 4中的Script编写过程中,代码补全很快,这个可以研究一下。

研究代码可找到,代码补全触发过程

1) CodeTextEditor中创建时钟code_complete_timer,其timeout超时信号绑定_code_complete_timer_timeout函数。超时量缺省值为0.3,单位应该是秒。

	code_complete_timer = memnew(Timer);add_child(code_complete_timer);code_complete_timer->set_one_shot(true);code_complete_timer->set_wait_time(EDITOR_GET("text_editor/completion/code_complete_delay"));...code_complete_timer->connect("timeout", callable_mp(this, &CodeTextEditor::_code_complete_timer_timeout));

2) 编辑器中文本变化时,触发CodeTextEditor::_text_changed,时钟code_complete_timer开始计时。

void CodeTextEditor::_text_changed() {if (text_editor->is_insert_text_operation()) {code_complete_timer_line = text_editor->get_caret_line();code_complete_timer->start();}idle->start();if (find_replace_bar) {find_replace_bar->needs_to_count_results = true;}
}

而_line_col_changed函数(绑定caret_changed事件)会停止时钟。

void CodeTextEditor::_line_col_changed() {if (!code_complete_timer->is_stopped() && code_complete_timer_line != text_editor->get_caret_line()) {code_complete_timer->stop();}String line = text_editor->get_line(text_editor->get_caret_line());int positional_column = 0;for (int i = 0; i < text_editor->get_caret_column(); i++) {if (line[i] == '\t') {positional_column += text_editor->get_indent_size(); //tab size} else {positional_column += 1;}}StringBuilder sb;sb.append(itos(text_editor->get_caret_line() + 1).lpad(4));sb.append(" : ");sb.append(itos(positional_column + 1).lpad(3));line_and_col_txt->set_text(sb.as_string());if (find_replace_bar) {if (!find_replace_bar->line_col_changed_for_result) {find_replace_bar->needs_to_count_results = true;}find_replace_bar->line_col_changed_for_result = false;}
}

3)时钟启动后在超时期内未被中止,则会触发timeout信号,调用_code_complete_timer_timeout

void CodeTextEditor::_code_complete_timer_timeout() {if (!is_visible_in_tree()) {return;}text_editor->request_code_completion();
}

在CodeEdit::request_code_completion函数中发出信号code_completion_requested

void CodeEdit::request_code_completion(bool p_force) {if (GDVIRTUAL_CALL(_request_code_completion, p_force)) {return;}/* Don't re-query if all existing options are quoted types, eg path, signal. */bool ignored = code_completion_active && !code_completion_options.is_empty();if (ignored) {ScriptLanguage::CodeCompletionKind kind = ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT;const ScriptLanguage::CodeCompletionOption *previous_option = nullptr;for (int i = 0; i < code_completion_options.size(); i++) {const ScriptLanguage::CodeCompletionOption &current_option = code_completion_options[i];if (!previous_option) {previous_option = &current_option;kind = current_option.kind;}if (previous_option->kind != current_option.kind) {ignored = false;break;}}ignored = ignored && (kind == ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH || kind == ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH || kind == ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL);}if (ignored) {return;}if (p_force) {emit_signal(SNAME("code_completion_requested"));return;}String line = get_line(get_caret_line());int ofs = CLAMP(get_caret_column(), 0, line.length());if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || !is_symbol(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) {emit_signal(SNAME("code_completion_requested"));} else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(line[ofs - 2])) {emit_signal(SNAME("code_completion_requested"));}
}

信号code_completion_requested绑定函数_complete_request,其中调用代理函数code_complete_func进行处理

void CodeTextEditor::_complete_request() {List<ScriptLanguage::CodeCompletionOption> entries;String ctext = text_editor->get_text_for_code_completion();_code_complete_script(ctext, &entries);bool forced = false;if (code_complete_func) {code_complete_func(code_complete_ud, ctext, &entries, forced);}if (entries.size() == 0) {return;}for (const ScriptLanguage::CodeCompletionOption &e : entries) {Color font_color = completion_font_color;if (e.insert_text.begins_with("\"") || e.insert_text.begins_with("\'")) {font_color = completion_string_color;} else if (e.insert_text.begins_with("#") || e.insert_text.begins_with("//")) {font_color = completion_comment_color;}text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value);}text_editor->update_code_completion_options(forced);
}

对于Script编辑器而言,code_complete_func指向ScriptTextEditor::_code_complete_scripts,本质上是调用GDScriptLanguage::complete_code函数

void ScriptTextEditor::_code_complete_script(const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {if (color_panel->is_visible()) {return;}Node *base = get_tree()->get_edited_scene_root();if (base) {base = _find_node_for_script(base, base, script);}String hint;Error err = script->get_language()->complete_code(p_code, script->get_path(), base, r_options, r_force, hint);r_options->sort_custom_inplace<CodeCompletionOptionCompare>();if (err == OK) {code_editor->get_text_editor()->set_code_hint(hint);}
}

在GDScriptLanguage::complete_code中,进行代码解析、语义分析,取得满足条件的候选代码

::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path, Object *p_owner, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_forced, String &r_call_hint) {const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\"";GDScriptParser parser;GDScriptAnalyzer analyzer(&parser);parser.parse(p_code, p_path, true);analyzer.analyze();r_forced = false;HashMap<String, ScriptLanguage::CodeCompletionOption> options;GDScriptParser::CompletionContext completion_context = parser.get_completion_context();completion_context.base = p_owner;bool is_function = false;switch (completion_context.type) {case GDScriptParser::COMPLETION_NONE:break;case GDScriptParser::COMPLETION_ANNOTATION: {List<MethodInfo> annotations;parser.get_annotation_list(&annotations);for (const MethodInfo &E : annotations) {ScriptLanguage::CodeCompletionOption option(E.name.substr(1), ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);if (E.arguments.size() > 0) {option.insert_text += "(";}options.insert(option.display, option);}r_forced = true;} break;case GDScriptParser::COMPLETION_ANNOTATION_ARGUMENTS: {if (completion_context.node == nullptr || completion_context.node->type != GDScriptParser::Node::ANNOTATION) {break;}const GDScriptParser::AnnotationNode *annotation = static_cast<const GDScriptParser::AnnotationNode *>(completion_context.node);_find_annotation_arguments(annotation, completion_context.current_argument, quote_style, options);r_forced = true;} break;case GDScriptParser::COMPLETION_BUILT_IN_TYPE_CONSTANT_OR_STATIC_METHOD: {// Constants.{List<StringName> constants;Variant::get_constants_for_type(completion_context.builtin_type, &constants);for (const StringName &E : constants) {ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT);bool valid = false;Variant default_value = Variant::get_constant_value(completion_context.builtin_type, E, &valid);if (valid) {option.default_value = default_value;}options.insert(option.display, option);}}// Methods.{List<StringName> methods;Variant::get_builtin_method_list(completion_context.builtin_type, &methods);for (const StringName &E : methods) {if (Variant::is_builtin_method_static(completion_context.builtin_type, E)) {ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);if (Variant::get_builtin_method_argument_count(completion_context.builtin_type, E) > 0 || Variant::is_builtin_method_vararg(completion_context.builtin_type, E)) {option.insert_text += "(";} else {option.insert_text += "()";}options.insert(option.display, option);}}}} break;case GDScriptParser::COMPLETION_INHERIT_TYPE: {_list_available_types(true, completion_context, options);r_forced = true;} break;case GDScriptParser::COMPLETION_TYPE_NAME_OR_VOID: {ScriptLanguage::CodeCompletionOption option("void", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);options.insert(option.display, option);}[[fallthrough]];case GDScriptParser::COMPLETION_TYPE_NAME: {_list_available_types(false, completion_context, options);r_forced = true;} break;case GDScriptParser::COMPLETION_PROPERTY_DECLARATION_OR_TYPE: {_list_available_types(false, completion_context, options);ScriptLanguage::CodeCompletionOption get("get", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);options.insert(get.display, get);ScriptLanguage::CodeCompletionOption set("set", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);options.insert(set.display, set);r_forced = true;} break;case GDScriptParser::COMPLETION_PROPERTY_DECLARATION: {ScriptLanguage::CodeCompletionOption get("get", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);options.insert(get.display, get);ScriptLanguage::CodeCompletionOption set("set", ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);options.insert(set.display, set);r_forced = true;} break;case GDScriptParser::COMPLETION_PROPERTY_METHOD: {if (!completion_context.current_class) {break;}for (int i = 0; i < completion_context.current_class->members.size(); i++) {const GDScriptParser::ClassNode::Member &member = completion_context.current_class->members[i];if (member.type != GDScriptParser::ClassNode::Member::FUNCTION) {continue;}if (member.function->is_static) {continue;}ScriptLanguage::CodeCompletionOption option(member.function->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);options.insert(option.display, option);}r_forced = true;} break;case GDScriptParser::COMPLETION_ASSIGN: {GDScriptCompletionIdentifier type;if (!completion_context.node || completion_context.node->type != GDScriptParser::Node::ASSIGNMENT) {break;}if (!_guess_expression_type(completion_context, static_cast<const GDScriptParser::AssignmentNode *>(completion_context.node)->assignee, type)) {_find_identifiers(completion_context, false, options, 0);r_forced = true;break;}if (!type.enumeration.is_empty()) {_find_enumeration_candidates(completion_context, type.enumeration, options);r_forced = options.size() > 0;} else {_find_identifiers(completion_context, false, options, 0);r_forced = true;}} break;case GDScriptParser::COMPLETION_METHOD:is_function = true;[[fallthrough]];case GDScriptParser::COMPLETION_IDENTIFIER: {_find_identifiers(completion_context, is_function, options, 0);} break;case GDScriptParser::COMPLETION_ATTRIBUTE_METHOD:is_function = true;[[fallthrough]];case GDScriptParser::COMPLETION_ATTRIBUTE: {r_forced = true;const GDScriptParser::SubscriptNode *attr = static_cast<const GDScriptParser::SubscriptNode *>(completion_context.node);if (attr->base) {GDScriptCompletionIdentifier base;bool found_type = _get_subscript_type(completion_context, attr, base.type);if (!found_type && !_guess_expression_type(completion_context, attr->base, base)) {break;}_find_identifiers_in_base(base, is_function, options, 0);}} break;case GDScriptParser::COMPLETION_SUBSCRIPT: {const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(completion_context.node);GDScriptCompletionIdentifier base;if (!_guess_expression_type(completion_context, subscript->base, base)) {break;}_find_identifiers_in_base(base, false, options, 0);} break;case GDScriptParser::COMPLETION_TYPE_ATTRIBUTE: {if (!completion_context.current_class) {break;}const GDScriptParser::TypeNode *type = static_cast<const GDScriptParser::TypeNode *>(completion_context.node);bool found = true;GDScriptCompletionIdentifier base;base.type.kind = GDScriptParser::DataType::CLASS;base.type.type_source = GDScriptParser::DataType::INFERRED;base.type.is_constant = true;base.type.class_type = completion_context.current_class;base.value = completion_context.base;for (int i = 0; i < completion_context.current_argument; i++) {GDScriptCompletionIdentifier ci;if (!_guess_identifier_type_from_base(completion_context, base, type->type_chain[i]->name, ci)) {found = false;break;}base = ci;}// TODO: Improve this to only list types.if (found) {_find_identifiers_in_base(base, false, options, 0);}r_forced = true;} break;case GDScriptParser::COMPLETION_RESOURCE_PATH: {if (EDITOR_GET("text_editor/completion/complete_file_paths")) {_get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), options);r_forced = true;}} break;case GDScriptParser::COMPLETION_CALL_ARGUMENTS: {if (!completion_context.node) {break;}_find_call_arguments(completion_context, completion_context.node, completion_context.current_argument, options, r_forced, r_call_hint);} break;case GDScriptParser::COMPLETION_OVERRIDE_METHOD: {GDScriptParser::DataType native_type = completion_context.current_class->base_type;while (native_type.is_set() && native_type.kind != GDScriptParser::DataType::NATIVE) {switch (native_type.kind) {case GDScriptParser::DataType::CLASS: {native_type = native_type.class_type->base_type;} break;default: {native_type.kind = GDScriptParser::DataType::UNRESOLVED;} break;}}if (!native_type.is_set()) {break;}StringName class_name = native_type.native_type;if (!ClassDB::class_exists(class_name)) {break;}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);for (const MethodInfo &mi : virtual_methods) {String method_hint = mi.name;if (method_hint.contains(":")) {method_hint = method_hint.get_slice(":", 0);}method_hint += "(";if (mi.arguments.size()) {for (int i = 0; i < mi.arguments.size(); i++) {if (i > 0) {method_hint += ", ";}String arg = mi.arguments[i].name;if (arg.contains(":")) {arg = arg.substr(0, arg.find(":"));}method_hint += arg;if (use_type_hint && mi.arguments[i].type != Variant::NIL) {method_hint += ": ";if (mi.arguments[i].type == Variant::OBJECT && mi.arguments[i].class_name != StringName()) {method_hint += mi.arguments[i].class_name.operator String();} else {method_hint += Variant::get_type_name(mi.arguments[i].type);}}}}method_hint += ")";if (use_type_hint && (mi.return_val.type != Variant::NIL || !(mi.return_val.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) {method_hint += " -> ";if (mi.return_val.type == Variant::NIL) {method_hint += "void";} else if (mi.return_val.type == Variant::OBJECT && mi.return_val.class_name != StringName()) {method_hint += mi.return_val.class_name.operator String();} else {method_hint += Variant::get_type_name(mi.return_val.type);}}method_hint += ":";ScriptLanguage::CodeCompletionOption option(method_hint, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);options.insert(option.display, option);}} break;case GDScriptParser::COMPLETION_GET_NODE: {// Handles the `$Node/Path` or `$"Some NodePath"` syntax specifically.if (p_owner) {List<String> opts;p_owner->get_argument_options("get_node", 0, &opts);for (const String &E : opts) {r_forced = true;String opt = E.strip_edges();if (opt.is_quoted()) {// Remove quotes so that we can handle user preferred quote style,// or handle NodePaths which are valid identifiers and don't need quotes.opt = opt.unquote();}// The path needs quotes if it's not a valid identifier (with an exception// for "/" as path separator, which also doesn't require quotes).if (!opt.replace("/", "_").is_valid_identifier()) {// Ignore quote_style and just use double quotes for paths with apostrophes.// Double quotes don't need to be checked because they're not valid in node and property names.opt = opt.quote(opt.contains("'") ? "\"" : quote_style); // Handle user preference.}ScriptLanguage::CodeCompletionOption option(opt, ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH);options.insert(option.display, option);}// Get autoloads.for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {String path = "/root/" + E.key;ScriptLanguage::CodeCompletionOption option(path.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH);options.insert(option.display, option);}}} break;case GDScriptParser::COMPLETION_SUPER_METHOD: {if (!completion_context.current_class) {break;}_find_identifiers_in_class(completion_context.current_class, true, false, true, options, 0);} break;}for (const KeyValue<String, ScriptLanguage::CodeCompletionOption> &E : options) {r_options->push_back(E.value);}return OK;
}

再排序后

r_options->sort_custom_inplace<CodeCompletionOptionCompare>();

之后加入到编辑器的代码补全选项集合中

	for (const ScriptLanguage::CodeCompletionOption &e : entries) {Color font_color = completion_font_color;if (e.insert_text.begins_with("\"") || e.insert_text.begins_with("\'")) {font_color = completion_string_color;} else if (e.insert_text.begins_with("#") || e.insert_text.begins_with("//")) {font_color = completion_comment_color;}text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value);}text_editor->update_code_completion_options(forced);

根据显示逻辑,出现代码提示界面

 所以,Godot代码补全逻辑主要关注其处理流程、代码解析、语义分析、候选选项处理、显示

本文链接:https://my.lmcjl.com/post/2244.html

展开阅读全文

4 评论

留下您的评论.