使用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 ¤t_option = code_completion_options[i];
if (!previous_option) {
previous_option = ¤t_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代码补全逻辑主要关注其处理流程、代码解析、语义分析、候选选项处理、显示