Análisis de código fuente de Godot 4 - proceso de autocompletado de código

En el proceso de uso de Godot 4, hay una cosa que me atrae: el autocompletado de código

Al desarrollar con RAD, la función de autocompletado de código ha sido objeto de quejas, principalmente debido a la baja velocidad

Pero veo que en el proceso de escritura del script en Godot 4, la finalización del código es muy rápida, esto se puede estudiar.

Se puede encontrar el código de investigación y la finalización del código desencadena el proceso.

1) Cree un reloj code_complete_timer en CodeTextEditor, y su señal de tiempo de espera está vinculada a la función _code_complete_timer_timeout. El valor predeterminado de tiempo de espera es 0,3 y la unidad debe ser segundos.

	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) Cuando cambia el texto en el editor, CodeTextEditor::_text_changed se activa y el reloj code_complete_timer comienza a cronometrar.

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;
	}
}

Y la función _line_col_changed (evento caret_changed enlazado) detendrá el reloj.

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) Si el reloj no se suspende dentro del período de tiempo de espera después de que se inicia el reloj, se activará la señal de tiempo de espera y se llamará a _code_complete_timer_timeout

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

Señal code_completion_requested en la función CodeEdit::request_code_completion

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"));
	}
}

La señal code_completion_requested está vinculada a la función _complete_request, que llama a la función de proxy code_complete_func para su procesamiento.

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);
}

Para el editor de scripts, code_complete_func apunta a ScriptTextEditor::_code_complete_scripts, esencialmente llamando a la función 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);
	}
}

En GDScriptLanguage::complete_code, realice análisis de código y análisis semántico para obtener códigos candidatos que cumplan las condiciones

::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;
}

Después de clasificar

r_options->sort_custom_inplace<CodeCompletionOptionCompare>();

Luego agréguelo a la colección de opciones de finalización de código del editor.

	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);

De acuerdo con la lógica de visualización, aparece la interfaz de solicitud de código

 Por lo tanto, la lógica de finalización de código de Godot se enfoca principalmente en su flujo de procesamiento, análisis de código, análisis semántico, procesamiento de opciones candidatas y visualización.

Supongo que te gusta

Origin blog.csdn.net/drgraph/article/details/131339725
Recomendado
Clasificación