Godot 4 ソース コード分析 - コードの自動補完プロセス

Godot 4 を使用する過程で、私を魅了する点が 1 つあります。それは、コードの自動補完です。

RAD で開発する場合、主に速度が遅いため、コードの自動補完機能について苦情が寄せられています。

しかし、Godot 4 のスクリプト作成プロセスでは、コードの完成が非常に高速であることが分かりました。これは研究の余地があります。

研究コードが見つかり、コード補完によってプロセスがトリガーされます

1) CodeTextEditor でクロック code_complete_timer を作成し、そのタイムアウト タイムアウト信号が _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) クロックが開始されてからタイムアウト期間内にクロックが一時停止されなかった場合、タイムアウト信号がトリガーされ、_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);
}

スクリプト エディターの場合、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 コード補完ロジックは、主にその処理フロー、コード分析、意味分析、候補オプションの処理、および表示に焦点を当てています。

おすすめ

転載: blog.csdn.net/drgraph/article/details/131339725