Ver código fonte

Code Editor: Add documentation tooltips

Danil Alexeev 7 meses atrás
pai
commit
80d11500b5

+ 0 - 33
core/doc_data.cpp

@@ -105,28 +105,6 @@ void DocData::argument_doc_from_arginfo(DocData::ArgumentDoc &p_argument, const
 	}
 }
 
-void DocData::property_doc_from_scriptmemberinfo(DocData::PropertyDoc &p_property, const ScriptMemberInfo &p_memberinfo) {
-	p_property.name = p_memberinfo.propinfo.name;
-	p_property.description = p_memberinfo.doc_string;
-
-	if (p_memberinfo.propinfo.type == Variant::OBJECT) {
-		p_property.type = p_memberinfo.propinfo.class_name;
-	} else if (p_memberinfo.propinfo.type == Variant::NIL && p_memberinfo.propinfo.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
-		p_property.type = "Variant";
-	} else {
-		p_property.type = Variant::get_type_name(p_memberinfo.propinfo.type);
-	}
-
-	p_property.setter = p_memberinfo.setter;
-	p_property.getter = p_memberinfo.getter;
-
-	if (p_memberinfo.has_default_value && p_memberinfo.default_value.get_type() != Variant::OBJECT) {
-		p_property.default_value = get_default_value_string(p_memberinfo.default_value);
-	}
-
-	p_property.overridden = false;
-}
-
 void DocData::method_doc_from_methodinfo(DocData::MethodDoc &p_method, const MethodInfo &p_methodinfo, const String &p_desc) {
 	p_method.name = p_methodinfo.name;
 	p_method.description = p_desc;
@@ -170,14 +148,3 @@ void DocData::method_doc_from_methodinfo(DocData::MethodDoc &p_method, const Met
 		p_method.arguments.push_back(argument);
 	}
 }
-
-void DocData::constant_doc_from_variant(DocData::ConstantDoc &p_const, const StringName &p_name, const Variant &p_value, const String &p_desc) {
-	p_const.name = p_name;
-	p_const.value = p_value;
-	p_const.is_value_valid = (p_value.get_type() != Variant::OBJECT);
-	p_const.description = p_desc;
-}
-
-void DocData::signal_doc_from_methodinfo(DocData::MethodDoc &p_signal, const MethodInfo &p_methodinfo, const String &p_desc) {
-	return method_doc_from_methodinfo(p_signal, p_methodinfo, p_desc);
-}

+ 7 - 13
core/doc_data.h

@@ -34,16 +34,6 @@
 #include "core/io/xml_parser.h"
 #include "core/variant/variant.h"
 
-struct ScriptMemberInfo {
-	PropertyInfo propinfo;
-	String doc_string;
-	StringName setter;
-	StringName getter;
-
-	bool has_default_value = false;
-	Variant default_value;
-};
-
 class DocData {
 public:
 	struct ArgumentDoc {
@@ -276,6 +266,7 @@ public:
 		String name;
 		String value;
 		bool is_value_valid = false;
+		String type;
 		String enumeration;
 		bool is_bitfield = false;
 		String description;
@@ -302,6 +293,10 @@ public:
 				doc.is_value_valid = p_dict["is_value_valid"];
 			}
 
+			if (p_dict.has("type")) {
+				doc.type = p_dict["type"];
+			}
+
 			if (p_dict.has("enumeration")) {
 				doc.enumeration = p_dict["enumeration"];
 				if (p_dict.has("is_bitfield")) {
@@ -352,6 +347,8 @@ public:
 
 			dict["is_value_valid"] = p_doc.is_value_valid;
 
+			dict["type"] = p_doc.type;
+
 			if (!p_doc.enumeration.is_empty()) {
 				dict["enumeration"] = p_doc.enumeration;
 				dict["is_bitfield"] = p_doc.is_bitfield;
@@ -981,10 +978,7 @@ public:
 
 	static void return_doc_from_retinfo(DocData::MethodDoc &p_method, const PropertyInfo &p_retinfo);
 	static void argument_doc_from_arginfo(DocData::ArgumentDoc &p_argument, const PropertyInfo &p_arginfo);
-	static void property_doc_from_scriptmemberinfo(DocData::PropertyDoc &p_property, const ScriptMemberInfo &p_memberinfo);
 	static void method_doc_from_methodinfo(DocData::MethodDoc &p_method, const MethodInfo &p_methodinfo, const String &p_desc);
-	static void constant_doc_from_variant(DocData::ConstantDoc &p_const, const StringName &p_name, const Variant &p_value, const String &p_desc);
-	static void signal_doc_from_methodinfo(DocData::MethodDoc &p_signal, const MethodInfo &p_methodinfo, const String &p_desc);
 };
 
 #endif // DOC_DATA_H

+ 29 - 7
core/object/script_language.h

@@ -150,6 +150,7 @@ public:
 	virtual Error reload(bool p_keep_state = false) = 0;
 
 #ifdef TOOLS_ENABLED
+	virtual StringName get_doc_class_name() const = 0;
 	virtual Vector<DocData::ClassDoc> get_documentation() const = 0;
 	virtual String get_class_icon_path() const = 0;
 	virtual PropertyInfo get_class_category() const;
@@ -181,7 +182,7 @@ public:
 	virtual int get_member_line(const StringName &p_member) const { return -1; }
 
 	virtual void get_constants(HashMap<StringName, Variant> *p_constants) {}
-	virtual void get_members(HashSet<StringName> *p_constants) {}
+	virtual void get_members(HashSet<StringName> *p_members) {}
 
 	virtual bool is_placeholder_fallback_enabled() const { return false; }
 
@@ -340,25 +341,46 @@ public:
 	virtual Error complete_code(const String &p_code, const String &p_path, Object *p_owner, List<CodeCompletionOption> *r_options, bool &r_force, String &r_call_hint) { return ERR_UNAVAILABLE; }
 
 	enum LookupResultType {
-		LOOKUP_RESULT_SCRIPT_LOCATION,
+		LOOKUP_RESULT_SCRIPT_LOCATION, // Use if none of the options below apply.
 		LOOKUP_RESULT_CLASS,
 		LOOKUP_RESULT_CLASS_CONSTANT,
 		LOOKUP_RESULT_CLASS_PROPERTY,
 		LOOKUP_RESULT_CLASS_METHOD,
 		LOOKUP_RESULT_CLASS_SIGNAL,
 		LOOKUP_RESULT_CLASS_ENUM,
-		LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE,
+		LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE, // Deprecated.
 		LOOKUP_RESULT_CLASS_ANNOTATION,
-		LOOKUP_RESULT_MAX
+		LOOKUP_RESULT_LOCAL_CONSTANT,
+		LOOKUP_RESULT_LOCAL_VARIABLE,
+		LOOKUP_RESULT_MAX,
 	};
 
 	struct LookupResult {
 		LookupResultType type;
-		Ref<Script> script;
+
+		// For `CLASS_*`.
 		String class_name;
 		String class_member;
-		String class_path;
-		int location;
+
+		// For `LOCAL_*`.
+		String description;
+		bool is_deprecated = false;
+		String deprecated_message;
+		bool is_experimental = false;
+		String experimental_message;
+
+		// For `LOCAL_*`.
+		String doc_type;
+		String enumeration;
+		bool is_bitfield = false;
+
+		// For `LOCAL_*`.
+		String value;
+
+		// `SCRIPT_LOCATION` and `LOCAL_*` must have, `CLASS_*` can have.
+		Ref<Script> script;
+		String script_path;
+		int location = -1;
 	};
 
 	virtual Error lookup_code(const String &p_code, const String &p_symbol, const String &p_path, Object *p_owner, LookupResult &r_result) { return ERR_UNAVAILABLE; }

+ 4 - 1
core/object/script_language_extension.cpp

@@ -51,6 +51,7 @@ void ScriptExtension::_bind_methods() {
 	GDVIRTUAL_BIND(_set_source_code, "code");
 	GDVIRTUAL_BIND(_reload, "keep_state");
 
+	GDVIRTUAL_BIND(_get_doc_class_name);
 	GDVIRTUAL_BIND(_get_documentation);
 	GDVIRTUAL_BIND(_get_class_icon_path);
 
@@ -169,8 +170,10 @@ void ScriptLanguageExtension::_bind_methods() {
 	BIND_ENUM_CONSTANT(LOOKUP_RESULT_CLASS_METHOD);
 	BIND_ENUM_CONSTANT(LOOKUP_RESULT_CLASS_SIGNAL);
 	BIND_ENUM_CONSTANT(LOOKUP_RESULT_CLASS_ENUM);
-	BIND_ENUM_CONSTANT(LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE);
+	BIND_ENUM_CONSTANT(LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE); // Deprecated.
 	BIND_ENUM_CONSTANT(LOOKUP_RESULT_CLASS_ANNOTATION);
+	BIND_ENUM_CONSTANT(LOOKUP_RESULT_LOCAL_CONSTANT);
+	BIND_ENUM_CONSTANT(LOOKUP_RESULT_LOCAL_VARIABLE);
 	BIND_ENUM_CONSTANT(LOOKUP_RESULT_MAX);
 
 	BIND_ENUM_CONSTANT(LOCATION_LOCAL);

+ 28 - 12
core/object/script_language_extension.h

@@ -76,9 +76,16 @@ public:
 	EXBIND1(set_source_code, const String &)
 	EXBIND1R(Error, reload, bool)
 
+	GDVIRTUAL0RC_REQUIRED(StringName, _get_doc_class_name)
 	GDVIRTUAL0RC_REQUIRED(TypedArray<Dictionary>, _get_documentation)
 	GDVIRTUAL0RC(String, _get_class_icon_path)
 #ifdef TOOLS_ENABLED
+	virtual StringName get_doc_class_name() const override {
+		StringName ret;
+		GDVIRTUAL_CALL(_get_doc_class_name, ret);
+		return ret;
+	}
+
 	virtual Vector<DocData::ClassDoc> get_documentation() const override {
 		TypedArray<Dictionary> doc;
 		GDVIRTUAL_CALL(_get_documentation, doc);
@@ -454,22 +461,31 @@ public:
 	virtual Error lookup_code(const String &p_code, const String &p_symbol, const String &p_path, Object *p_owner, LookupResult &r_result) override {
 		Dictionary ret;
 		GDVIRTUAL_CALL(_lookup_code, p_code, p_symbol, p_path, p_owner, ret);
-		if (!ret.has("result")) {
-			return ERR_UNAVAILABLE;
-		}
+
+		ERR_FAIL_COND_V(!ret.has("result"), ERR_UNAVAILABLE);
+		const Error result = Error(int(ret["result"]));
 
 		ERR_FAIL_COND_V(!ret.has("type"), ERR_UNAVAILABLE);
 		r_result.type = LookupResultType(int(ret["type"]));
-		ERR_FAIL_COND_V(!ret.has("script"), ERR_UNAVAILABLE);
-		r_result.script = ret["script"];
-		ERR_FAIL_COND_V(!ret.has("class_name"), ERR_UNAVAILABLE);
-		r_result.class_name = ret["class_name"];
-		ERR_FAIL_COND_V(!ret.has("class_path"), ERR_UNAVAILABLE);
-		r_result.class_path = ret["class_path"];
-		ERR_FAIL_COND_V(!ret.has("location"), ERR_UNAVAILABLE);
-		r_result.location = ret["location"];
 
-		Error result = Error(int(ret["result"]));
+		r_result.class_name = ret.get("class_name", "");
+		r_result.class_member = ret.get("class_member", "");
+
+		r_result.description = ret.get("description", "");
+		r_result.is_deprecated = ret.get("is_deprecated", false);
+		r_result.deprecated_message = ret.get("deprecated_message", "");
+		r_result.is_deprecated = ret.get("is_deprecated", false);
+		r_result.experimental_message = ret.get("experimental_message", "");
+
+		r_result.doc_type = ret.get("doc_type", "");
+		r_result.enumeration = ret.get("enumeration", "");
+		r_result.is_bitfield = ret.get("is_bitfield", false);
+
+		r_result.value = ret.get("value", "");
+
+		r_result.script = ret.get("script", Ref<Script>());
+		r_result.script_path = ret.get("script_path", "");
+		r_result.location = ret.get("location", -1);
 
 		return result;
 	}

+ 11 - 0
doc/classes/@GlobalScope.xml

@@ -844,6 +844,7 @@
 			</description>
 		</method>
 		<method name="print" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Converts one or more arguments of any type to string in the best way possible and prints them to the console.
 				[codeblocks]
@@ -860,6 +861,7 @@
 			</description>
 		</method>
 		<method name="print_rich" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Converts one or more arguments of any type to string in the best way possible and prints them to the console.
 				The following BBCode tags are supported: [code]b[/code], [code]i[/code], [code]u[/code], [code]s[/code], [code]indent[/code], [code]code[/code], [code]url[/code], [code]center[/code], [code]right[/code], [code]color[/code], [code]bgcolor[/code], [code]fgcolor[/code].
@@ -879,11 +881,13 @@
 			</description>
 		</method>
 		<method name="print_verbose" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				If verbose mode is enabled ([method OS.is_stdout_verbose] returning [code]true[/code]), converts one or more arguments of any type to string in the best way possible and prints them to the console.
 			</description>
 		</method>
 		<method name="printerr" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Prints one or more arguments to strings in the best way possible to standard error line.
 				[codeblocks]
@@ -897,6 +901,7 @@
 			</description>
 		</method>
 		<method name="printraw" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Prints one or more arguments to strings in the best way possible to the OS terminal. Unlike [method print], no newline is automatically added at the end.
 				[b]Note:[/b] The OS terminal is [i]not[/i] the same as the editor's Output dock. The output sent to the OS terminal can be seen when running Godot from a terminal. On Windows, this requires using the [code]console.exe[/code] executable.
@@ -917,6 +922,7 @@
 			</description>
 		</method>
 		<method name="prints" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Prints one or more arguments to the console with a space between each argument.
 				[codeblocks]
@@ -930,6 +936,7 @@
 			</description>
 		</method>
 		<method name="printt" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Prints one or more arguments to the console with a tab between each argument.
 				[codeblocks]
@@ -943,6 +950,7 @@
 			</description>
 		</method>
 		<method name="push_error" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Pushes an error message to Godot's built-in debugger and to the OS terminal.
 				[codeblocks]
@@ -957,6 +965,7 @@
 			</description>
 		</method>
 		<method name="push_warning" qualifiers="vararg">
+			<return type="void" />
 			<description>
 				Pushes a warning message to Godot's built-in debugger and to the OS terminal.
 				[codeblocks]
@@ -1075,6 +1084,7 @@
 			</description>
 		</method>
 		<method name="randomize">
+			<return type="void" />
 			<description>
 				Randomizes the seed (or the internal state) of the random number generator. The current implementation uses a number based on the device's time.
 				[b]Note:[/b] This function is called automatically when the project is run. If you need to fix the seed to have consistent, reproducible results, use [method seed] to initialize the random number generator.
@@ -1151,6 +1161,7 @@
 			</description>
 		</method>
 		<method name="seed">
+			<return type="void" />
 			<param index="0" name="base" type="int" />
 			<description>
 				Sets the seed for the random number generator to [param base]. Setting the seed manually can ensure consistent, repeatable results for most random functions.

+ 13 - 0
doc/classes/CodeEdit.xml

@@ -587,6 +587,9 @@
 		<member name="symbol_lookup_on_click" type="bool" setter="set_symbol_lookup_on_click_enabled" getter="is_symbol_lookup_on_click_enabled" default="false">
 			Set when a validated word from [signal symbol_validate] is clicked, the [signal symbol_lookup] should be emitted.
 		</member>
+		<member name="symbol_tooltip_on_hover" type="bool" setter="set_symbol_tooltip_on_hover_enabled" getter="is_symbol_tooltip_on_hover_enabled" default="false">
+			Set when a word is hovered, the [signal symbol_hovered] should be emitted.
+		</member>
 		<member name="text_direction" type="int" setter="set_text_direction" getter="get_text_direction" overrides="TextEdit" enum="Control.TextDirection" default="1" />
 	</members>
 	<signals>
@@ -601,6 +604,15 @@
 				Emitted when the user requests code completion. This signal will not be sent if [method _request_code_completion] is overridden or [member code_completion_enabled] is [code]false[/code].
 			</description>
 		</signal>
+		<signal name="symbol_hovered">
+			<param index="0" name="symbol" type="String" />
+			<param index="1" name="line" type="int" />
+			<param index="2" name="column" type="int" />
+			<description>
+				Emitted when the user hovers over a symbol. Unlike [signal Control.mouse_entered], this signal is not emitted immediately, but when the cursor is over the symbol for [member ProjectSettings.gui/timers/tooltip_delay_sec] seconds.
+				[b]Note:[/b] [member symbol_tooltip_on_hover] must be [code]true[/code] for this signal to be emitted.
+			</description>
+		</signal>
 		<signal name="symbol_lookup">
 			<param index="0" name="symbol" type="String" />
 			<param index="1" name="line" type="int" />
@@ -613,6 +625,7 @@
 			<param index="0" name="symbol" type="String" />
 			<description>
 				Emitted when the user hovers over a symbol. The symbol should be validated and responded to, by calling [method set_symbol_lookup_word_as_valid].
+				[b]Note:[/b] [member symbol_lookup_on_click] must be [code]true[/code] for this signal to be emitted.
 			</description>
 		</signal>
 	</signals>

+ 5 - 0
doc/classes/ScriptExtension.xml

@@ -32,6 +32,11 @@
 			<description>
 			</description>
 		</method>
+		<method name="_get_doc_class_name" qualifiers="virtual const">
+			<return type="StringName" />
+			<description>
+			</description>
+		</method>
 		<method name="_get_documentation" qualifiers="virtual const">
 			<return type="Dictionary[]" />
 			<description>

+ 6 - 2
doc/classes/ScriptLanguageExtension.xml

@@ -387,11 +387,15 @@
 		</constant>
 		<constant name="LOOKUP_RESULT_CLASS_ENUM" value="6" enum="LookupResultType">
 		</constant>
-		<constant name="LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE" value="7" enum="LookupResultType">
+		<constant name="LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE" value="7" enum="LookupResultType" deprecated="">
 		</constant>
 		<constant name="LOOKUP_RESULT_CLASS_ANNOTATION" value="8" enum="LookupResultType">
 		</constant>
-		<constant name="LOOKUP_RESULT_MAX" value="9" enum="LookupResultType">
+		<constant name="LOOKUP_RESULT_LOCAL_CONSTANT" value="9" enum="LookupResultType">
+		</constant>
+		<constant name="LOOKUP_RESULT_LOCAL_VARIABLE" value="10" enum="LookupResultType">
+		</constant>
+		<constant name="LOOKUP_RESULT_MAX" value="11" enum="LookupResultType">
 		</constant>
 		<constant name="LOCATION_LOCAL" value="0" enum="CodeCompletionLocation">
 			The option is local to the location of the code completion query - e.g. a local variable. Subsequent value of location represent options from the outer class, the exact value represent how far they are (in terms of inner classes).

+ 1 - 1
editor/create_dialog.cpp

@@ -831,7 +831,7 @@ CreateDialog::CreateDialog() {
 	vbc->add_margin_child(TTR("Matches:"), search_options, true);
 
 	help_bit = memnew(EditorHelpBit);
-	help_bit->set_content_height_limits(64 * EDSCALE, 64 * EDSCALE);
+	help_bit->set_content_height_limits(80 * EDSCALE, 80 * EDSCALE);
 	help_bit->connect("request_hide", callable_mp(this, &CreateDialog::_hide_requested));
 	vbc->add_margin_child(TTR("Description:"), help_bit);
 

+ 8 - 1
editor/doc_tools.cpp

@@ -674,6 +674,7 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
 				constant.name = E;
 				constant.value = itos(ClassDB::get_integer_constant(name, E));
 				constant.is_value_valid = true;
+				constant.type = "int";
 				constant.enumeration = ClassDB::get_integer_constant_enum(name, E);
 				constant.is_bitfield = ClassDB::is_enum_bitfield(name, constant.enumeration);
 				c.constants.push_back(constant);
@@ -920,6 +921,7 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
 				constant.name = F;
 				constant.value = itos(Variant::get_enum_value(Variant::Type(i), E, F));
 				constant.is_value_valid = true;
+				constant.type = "int";
 				constant.enumeration = E;
 				c.constants.push_back(constant);
 			}
@@ -934,6 +936,7 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
 			Variant value = Variant::get_constant_value(Variant::Type(i), E);
 			constant.value = value.get_type() == Variant::INT ? itos(value) : value.get_construct_string().replace("\n", " ");
 			constant.is_value_valid = true;
+			constant.type = Variant::get_type_name(value.get_type());
 			c.constants.push_back(constant);
 		}
 	}
@@ -951,6 +954,8 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
 		for (int i = 0; i < CoreConstants::get_global_constant_count(); i++) {
 			DocData::ConstantDoc cd;
 			cd.name = CoreConstants::get_global_constant_name(i);
+			cd.type = "int";
+			cd.enumeration = CoreConstants::get_global_constant_enum(i);
 			cd.is_bitfield = CoreConstants::is_global_constant_bitfield(i);
 			if (!CoreConstants::get_ignore_value_in_docs(i)) {
 				cd.value = itos(CoreConstants::get_global_constant_value(i));
@@ -958,7 +963,6 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
 			} else {
 				cd.is_value_valid = false;
 			}
-			cd.enumeration = CoreConstants::get_global_constant_enum(i);
 			c.constants.push_back(cd);
 		}
 
@@ -998,6 +1002,8 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
 				DocData::ArgumentDoc ad;
 				DocData::argument_doc_from_arginfo(ad, pi);
 				md.return_type = ad.type;
+			} else {
+				md.return_type = "void";
 			}
 
 			// Utility function's arguments.
@@ -1077,6 +1083,7 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
 				cd.name = E.first;
 				cd.value = E.second;
 				cd.is_value_valid = true;
+				cd.type = Variant::get_type_name(E.second.get_type());
 				c.constants.push_back(cd);
 			}
 

Diferenças do arquivo suprimidas por serem muito extensas
+ 451 - 173
editor/editor_help.cpp


+ 25 - 8
editor/editor_help.h

@@ -188,7 +188,6 @@ class EditorHelp : public VBoxContainer {
 	void _request_help(const String &p_string);
 	void _search(bool p_search_previous = false);
 
-	String _fix_constant(const String &p_constant) const;
 	void _toggle_scripts_pressed();
 
 	static int doc_generation_count;
@@ -254,6 +253,13 @@ public:
 class EditorHelpBit : public VBoxContainer {
 	GDCLASS(EditorHelpBit, VBoxContainer);
 
+	enum SymbolHint {
+		SYMBOL_HINT_NONE,
+		SYMBOL_HINT_INHERITANCE, // [ < ParentClass[ < ...]]
+		SYMBOL_HINT_ASSIGNABLE, // [: Type][ = value]
+		SYMBOL_HINT_SIGNATURE, // (arguments)[ -> Type][ qualifiers]
+	};
+
 	struct DocType {
 		String type;
 		String enumeration;
@@ -270,23 +276,31 @@ class EditorHelpBit : public VBoxContainer {
 		String description;
 		String deprecated_message;
 		String experimental_message;
-		DocType doc_type; // For method return type.
-		Vector<ArgumentData> arguments; // For methods and signals.
+		DocType doc_type;
+		String value;
+		Vector<ArgumentData> arguments;
+		String qualifiers;
 	};
 
 	inline static HashMap<StringName, HelpData> doc_class_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_enum_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_constant_cache;
 	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_property_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_theme_item_cache;
 	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_method_cache;
 	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_signal_cache;
-	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_theme_item_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_annotation_cache;
 
 	RichTextLabel *title = nullptr;
 	RichTextLabel *content = nullptr;
 
+	bool use_class_prefix = false;
+
+	String symbol_doc_link;
 	String symbol_class_name;
 	String symbol_type;
-	String symbol_visible_type;
 	String symbol_name;
+	SymbolHint symbol_hint = SYMBOL_HINT_NONE;
 
 	HelpData help_data;
 
@@ -294,10 +308,13 @@ class EditorHelpBit : public VBoxContainer {
 	float content_max_height = 0.0;
 
 	static HelpData _get_class_help_data(const StringName &p_class_name);
+	static HelpData _get_enum_help_data(const StringName &p_class_name, const StringName &p_enum_name);
+	static HelpData _get_constant_help_data(const StringName &p_class_name, const StringName &p_constant_name);
 	static HelpData _get_property_help_data(const StringName &p_class_name, const StringName &p_property_name);
+	static HelpData _get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name);
 	static HelpData _get_method_help_data(const StringName &p_class_name, const StringName &p_method_name);
 	static HelpData _get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name);
-	static HelpData _get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name);
+	static HelpData _get_annotation_help_data(const StringName &p_class_name, const StringName &p_annotation_name);
 
 	void _add_type_to_title(const DocType &p_doc_type);
 	void _update_labels();
@@ -315,7 +332,7 @@ public:
 	void set_content_height_limits(float p_min, float p_max);
 	void update_content_height();
 
-	EditorHelpBit(const String &p_symbol = String(), const String &p_prologue = String(), bool p_allow_selection = true);
+	EditorHelpBit(const String &p_symbol = String(), const String &p_prologue = String(), bool p_use_class_prefix = false, bool p_allow_selection = true);
 };
 
 // Standard tooltips do not allow you to hover over them.
@@ -338,7 +355,7 @@ protected:
 	void _notification(int p_what);
 
 public:
-	static Control *show_tooltip(Control *p_target, const String &p_symbol, const String &p_prologue = String());
+	static Control *show_tooltip(Control *p_target, const String &p_symbol, const String &p_prologue = String(), bool p_use_class_prefix = false);
 
 	void popup_under_cursor();
 

+ 3 - 13
editor/editor_inspector.cpp

@@ -3018,24 +3018,14 @@ void EditorInspector::update_tree() {
 				category_label = p.name;
 
 				// Use category's owner script to update some of its information.
-				if (!EditorNode::get_editor_data().is_type_recognized(p.name) && ResourceLoader::exists(p.hint_string)) {
+				if (!EditorNode::get_editor_data().is_type_recognized(p.name) && ResourceLoader::exists(p.hint_string, "Script")) {
 					Ref<Script> scr = ResourceLoader::load(p.hint_string, "Script");
 					if (scr.is_valid()) {
-						StringName script_name = EditorNode::get_editor_data().script_class_get_name(scr->get_path());
+						doc_name = scr->get_doc_class_name();
 
-						// Update the docs reference and the label based on the script.
-						Vector<DocData::ClassDoc> docs = scr->get_documentation();
-						if (!docs.is_empty()) {
-							// The documentation of a GDScript's main class is at the end of the array.
-							// Hacky because this isn't necessarily always guaranteed.
-							doc_name = docs[docs.size() - 1].name;
-						}
+						StringName script_name = EditorNode::get_editor_data().script_class_get_name(scr->get_path());
 						if (script_name != StringName()) {
 							category_label = script_name;
-						}
-
-						// Find the icon corresponding to the script.
-						if (script_name != StringName()) {
 							category_icon = EditorNode::get_singleton()->get_class_icon(script_name);
 						} else {
 							category_icon = EditorNode::get_singleton()->get_object_icon(scr.ptr(), "Object");

+ 178 - 78
editor/plugins/script_text_editor.cpp

@@ -31,10 +31,12 @@
 #include "script_text_editor.h"
 
 #include "core/config/project_settings.h"
+#include "core/io/json.h"
 #include "core/math/expression.h"
 #include "core/os/keyboard.h"
 #include "editor/debugger/editor_debugger_node.h"
 #include "editor/editor_command_palette.h"
+#include "editor/editor_help.h"
 #include "editor/editor_node.h"
 #include "editor/editor_settings.h"
 #include "editor/editor_string_names.h"
@@ -963,95 +965,88 @@ void ScriptTextEditor::_lookup_symbol(const String &p_symbol, int p_row, int p_c
 		} else {
 			EditorNode::get_singleton()->load_resource(symbol);
 		}
-
 	} else if (lc_error == OK) {
 		_goto_line(p_row);
 
-		switch (result.type) {
-			case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION: {
-				if (result.script.is_valid()) {
-					emit_signal(SNAME("request_open_script_at_line"), result.script, result.location - 1);
-				} else {
-					emit_signal(SNAME("request_save_history"));
-					goto_line_centered(result.location - 1);
-				}
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS: {
-				emit_signal(SNAME("go_to_help"), "class_name:" + result.class_name);
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
-				StringName cname = result.class_name;
-				bool success;
-				while (true) {
-					ClassDB::get_integer_constant(cname, result.class_member, &success);
-					if (success) {
-						result.class_name = cname;
+		if (!result.class_name.is_empty() && EditorHelp::get_doc_data()->class_list.has(result.class_name)) {
+			switch (result.type) {
+				case ScriptLanguage::LOOKUP_RESULT_CLASS: {
+					emit_signal(SNAME("go_to_help"), "class_name:" + result.class_name);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
+					StringName cname = result.class_name;
+					while (ClassDB::class_exists(cname)) {
+						if (ClassDB::has_integer_constant(cname, result.class_member, true)) {
+							result.class_name = cname;
+							break;
+						}
 						cname = ClassDB::get_parent_class(cname);
-					} else {
-						break;
 					}
-				}
-
-				emit_signal(SNAME("go_to_help"), "class_constant:" + result.class_name + ":" + result.class_member);
-
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
-				emit_signal(SNAME("go_to_help"), "class_property:" + result.class_name + ":" + result.class_member);
-
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
-				StringName cname = result.class_name;
-
-				while (true) {
-					if (ClassDB::has_method(cname, result.class_member)) {
-						result.class_name = cname;
+					emit_signal(SNAME("go_to_help"), "class_constant:" + result.class_name + ":" + result.class_member);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
+					StringName cname = result.class_name;
+					while (ClassDB::class_exists(cname)) {
+						if (ClassDB::has_property(cname, result.class_member, true)) {
+							result.class_name = cname;
+							break;
+						}
 						cname = ClassDB::get_parent_class(cname);
-					} else {
-						break;
 					}
-				}
-
-				emit_signal(SNAME("go_to_help"), "class_method:" + result.class_name + ":" + result.class_member);
-
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
-				StringName cname = result.class_name;
-
-				while (true) {
-					if (ClassDB::has_signal(cname, result.class_member)) {
-						result.class_name = cname;
+					emit_signal(SNAME("go_to_help"), "class_property:" + result.class_name + ":" + result.class_member);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
+					StringName cname = result.class_name;
+					while (ClassDB::class_exists(cname)) {
+						if (ClassDB::has_method(cname, result.class_member, true)) {
+							result.class_name = cname;
+							break;
+						}
 						cname = ClassDB::get_parent_class(cname);
-					} else {
-						break;
 					}
-				}
-
-				emit_signal(SNAME("go_to_help"), "class_signal:" + result.class_name + ":" + result.class_member);
-
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
-				StringName cname = result.class_name;
-				StringName success;
-				while (true) {
-					success = ClassDB::get_integer_constant_enum(cname, result.class_member, true);
-					if (success != StringName()) {
-						result.class_name = cname;
+					emit_signal(SNAME("go_to_help"), "class_method:" + result.class_name + ":" + result.class_member);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
+					StringName cname = result.class_name;
+					while (ClassDB::class_exists(cname)) {
+						if (ClassDB::has_signal(cname, result.class_member, true)) {
+							result.class_name = cname;
+							break;
+						}
 						cname = ClassDB::get_parent_class(cname);
-					} else {
-						break;
 					}
-				}
-
-				emit_signal(SNAME("go_to_help"), "class_enum:" + result.class_name + ":" + result.class_member);
-
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
-				emit_signal(SNAME("go_to_help"), "class_annotation:" + result.class_name + ":" + result.class_member);
-			} break;
-			case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: {
-				emit_signal(SNAME("go_to_help"), "class_global:" + result.class_name + ":" + result.class_member);
-			} break;
-			default: {
+					emit_signal(SNAME("go_to_help"), "class_signal:" + result.class_name + ":" + result.class_member);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
+					StringName cname = result.class_name;
+					while (ClassDB::class_exists(cname)) {
+						if (ClassDB::has_enum(cname, result.class_member, true)) {
+							result.class_name = cname;
+							break;
+						}
+						cname = ClassDB::get_parent_class(cname);
+					}
+					emit_signal(SNAME("go_to_help"), "class_enum:" + result.class_name + ":" + result.class_member);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
+					emit_signal(SNAME("go_to_help"), "class_annotation:" + result.class_name + ":" + result.class_member);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: { // Deprecated.
+					emit_signal(SNAME("go_to_help"), "class_global:" + result.class_name + ":" + result.class_member);
+				} break;
+				case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:
+				case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:
+				case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE:
+				case ScriptLanguage::LOOKUP_RESULT_MAX: {
+					// Nothing to do.
+				} break;
+			}
+		} else if (result.location >= 0) {
+			if (result.script.is_valid()) {
+				emit_signal(SNAME("request_open_script_at_line"), result.script, result.location - 1);
+			} else {
+				emit_signal(SNAME("request_save_history"));
+				goto_line_centered(result.location - 1);
 			}
 		}
 	} else if (ProjectSettings::get_singleton()->has_autoload(p_symbol)) {
@@ -1102,6 +1097,109 @@ void ScriptTextEditor::_validate_symbol(const String &p_symbol) {
 	}
 }
 
+void ScriptTextEditor::_show_symbol_tooltip(const String &p_symbol, int p_row, int p_column) {
+	Node *base = get_tree()->get_edited_scene_root();
+	if (base) {
+		base = _find_node_for_script(base, base, script);
+	}
+
+	ScriptLanguage::LookupResult result;
+	const String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);
+	const Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);
+	if (lc_error != OK) {
+		return;
+	}
+
+	String doc_symbol;
+	switch (result.type) {
+		case ScriptLanguage::LOOKUP_RESULT_CLASS: {
+			doc_symbol = "class|" + result.class_name + "|";
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
+			StringName cname = result.class_name;
+			while (ClassDB::class_exists(cname)) {
+				if (ClassDB::has_integer_constant(cname, result.class_member, true)) {
+					result.class_name = cname;
+					break;
+				}
+				cname = ClassDB::get_parent_class(cname);
+			}
+			doc_symbol = "constant|" + result.class_name + "|" + result.class_member;
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
+			StringName cname = result.class_name;
+			while (ClassDB::class_exists(cname)) {
+				if (ClassDB::has_property(cname, result.class_member, true)) {
+					result.class_name = cname;
+					break;
+				}
+				cname = ClassDB::get_parent_class(cname);
+			}
+			doc_symbol = "property|" + result.class_name + "|" + result.class_member;
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
+			StringName cname = result.class_name;
+			while (ClassDB::class_exists(cname)) {
+				if (ClassDB::has_method(cname, result.class_member, true)) {
+					result.class_name = cname;
+					break;
+				}
+				cname = ClassDB::get_parent_class(cname);
+			}
+			doc_symbol = "method|" + result.class_name + "|" + result.class_member;
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
+			StringName cname = result.class_name;
+			while (ClassDB::class_exists(cname)) {
+				if (ClassDB::has_signal(cname, result.class_member, true)) {
+					result.class_name = cname;
+					break;
+				}
+				cname = ClassDB::get_parent_class(cname);
+			}
+			doc_symbol = "signal|" + result.class_name + "|" + result.class_member;
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
+			StringName cname = result.class_name;
+			while (ClassDB::class_exists(cname)) {
+				if (ClassDB::has_enum(cname, result.class_member, true)) {
+					result.class_name = cname;
+					break;
+				}
+				cname = ClassDB::get_parent_class(cname);
+			}
+			doc_symbol = "enum|" + result.class_name + "|" + result.class_member;
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
+			doc_symbol = "annotation|" + result.class_name + "|" + result.class_member;
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:
+		case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE: {
+			const String item_type = (result.type == ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT) ? "local_constant" : "local_variable";
+			Dictionary item_data;
+			item_data["description"] = result.description;
+			item_data["is_deprecated"] = result.is_deprecated;
+			item_data["deprecated_message"] = result.deprecated_message;
+			item_data["is_experimental"] = result.is_experimental;
+			item_data["experimental_message"] = result.experimental_message;
+			item_data["doc_type"] = result.doc_type;
+			item_data["enumeration"] = result.enumeration;
+			item_data["is_bitfield"] = result.is_bitfield;
+			item_data["value"] = result.value;
+			doc_symbol = item_type + "||" + p_symbol + "|" + JSON::stringify(item_data);
+		} break;
+		case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:
+		case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: // Deprecated.
+		case ScriptLanguage::LOOKUP_RESULT_MAX: {
+			// Nothing to do.
+		} break;
+	}
+
+	if (!doc_symbol.is_empty()) {
+		EditorHelpBitTooltip::show_tooltip(code_editor->get_text_editor(), doc_symbol, String(), true);
+	}
+}
+
 String ScriptTextEditor::_get_absolute_path(const String &rel_path) {
 	String base_path = script->get_path().get_base_dir();
 	String path = base_path.path_join(rel_path);
@@ -2235,6 +2333,7 @@ void ScriptTextEditor::_enable_code_editor() {
 	code_editor->connect("validate_script", callable_mp(this, &ScriptTextEditor::_validate_script));
 	code_editor->connect("load_theme_settings", callable_mp(this, &ScriptTextEditor::_load_theme_settings));
 	code_editor->get_text_editor()->connect("symbol_lookup", callable_mp(this, &ScriptTextEditor::_lookup_symbol));
+	code_editor->get_text_editor()->connect("symbol_hovered", callable_mp(this, &ScriptTextEditor::_show_symbol_tooltip));
 	code_editor->get_text_editor()->connect("symbol_validate", callable_mp(this, &ScriptTextEditor::_validate_symbol));
 	code_editor->get_text_editor()->connect("gutter_added", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
 	code_editor->get_text_editor()->connect("gutter_removed", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
@@ -2411,6 +2510,7 @@ ScriptTextEditor::ScriptTextEditor() {
 	update_settings();
 
 	code_editor->get_text_editor()->set_symbol_lookup_on_click_enabled(true);
+	code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true);
 	code_editor->get_text_editor()->set_context_menu_enabled(false);
 
 	context_menu = memnew(PopupMenu);

+ 2 - 0
editor/plugins/script_text_editor.h

@@ -200,6 +200,8 @@ protected:
 	void _lookup_symbol(const String &p_symbol, int p_row, int p_column);
 	void _validate_symbol(const String &p_symbol);
 
+	void _show_symbol_tooltip(const String &p_symbol, int p_row, int p_column);
+
 	void _convert_case(CodeTextEditor::CaseStyle p_case);
 
 	Variant get_drag_data_fw(const Point2 &p_point, Control *p_from);

+ 17 - 3
modules/gdscript/editor/gdscript_docgen.cpp

@@ -289,7 +289,7 @@ String GDScriptDocGen::_docvalue_from_variant(const Variant &p_variant, int p_re
 	}
 }
 
-String GDScriptDocGen::_docvalue_from_expression(const GDP::ExpressionNode *p_expression) {
+String GDScriptDocGen::docvalue_from_expression(const GDP::ExpressionNode *p_expression) {
 	ERR_FAIL_NULL_V(p_expression, String());
 
 	if (p_expression->is_constant) {
@@ -385,6 +385,7 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
 				const_doc.name = const_name;
 				const_doc.value = _docvalue_from_variant(m_const->initializer->reduced_value);
 				const_doc.is_value_valid = true;
+				_doctype_from_gdtype(m_const->get_datatype(), const_doc.type, const_doc.enumeration);
 				const_doc.description = m_const->doc_data.description;
 				const_doc.is_deprecated = m_const->doc_data.is_deprecated;
 				const_doc.deprecated_message = m_const->doc_data.deprecated_message;
@@ -425,7 +426,7 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
 					arg_doc.name = p->identifier->name;
 					_doctype_from_gdtype(p->get_datatype(), arg_doc.type, arg_doc.enumeration);
 					if (p->initializer != nullptr) {
-						arg_doc.default_value = _docvalue_from_expression(p->initializer);
+						arg_doc.default_value = docvalue_from_expression(p->initializer);
 					}
 					method_doc.arguments.push_back(arg_doc);
 				}
@@ -494,7 +495,7 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
 				}
 
 				if (m_var->initializer != nullptr) {
-					prop_doc.default_value = _docvalue_from_expression(m_var->initializer);
+					prop_doc.default_value = docvalue_from_expression(m_var->initializer);
 				}
 
 				prop_doc.overridden = false;
@@ -521,6 +522,7 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
 					const_doc.name = val.identifier->name;
 					const_doc.value = _docvalue_from_variant(val.value);
 					const_doc.is_value_valid = true;
+					const_doc.type = "int";
 					const_doc.enumeration = name;
 					const_doc.description = val.doc_data.description;
 					const_doc.is_deprecated = val.doc_data.is_deprecated;
@@ -543,6 +545,7 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
 				const_doc.name = name;
 				const_doc.value = _docvalue_from_variant(m_enum_val.value);
 				const_doc.is_value_valid = true;
+				const_doc.type = "int";
 				const_doc.enumeration = "@unnamed_enums";
 				const_doc.description = m_enum_val.doc_data.description;
 				const_doc.is_deprecated = m_enum_val.doc_data.is_deprecated;
@@ -570,3 +573,14 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c
 	_generate_docs(p_script, p_class);
 	singletons.clear();
 }
+
+// This method is needed for the editor, since during autocompletion the script is not compiled, only analyzed.
+void GDScriptDocGen::doctype_from_gdtype(const GDType &p_gdtype, String &r_type, String &r_enum, bool p_is_return) {
+	for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
+		if (E.value.is_singleton) {
+			singletons[E.value.path] = E.key;
+		}
+	}
+	_doctype_from_gdtype(p_gdtype, r_type, r_enum, p_is_return);
+	singletons.clear();
+}

+ 2 - 1
modules/gdscript/editor/gdscript_docgen.h

@@ -45,11 +45,12 @@ class GDScriptDocGen {
 	static String _get_class_name(const GDP::ClassNode &p_class);
 	static void _doctype_from_gdtype(const GDType &p_gdtype, String &r_type, String &r_enum, bool p_is_return = false);
 	static String _docvalue_from_variant(const Variant &p_variant, int p_recursion_level = 1);
-	static String _docvalue_from_expression(const GDP::ExpressionNode *p_expression);
 	static void _generate_docs(GDScript *p_script, const GDP::ClassNode *p_class);
 
 public:
 	static void generate_docs(GDScript *p_script, const GDP::ClassNode *p_class);
+	static void doctype_from_gdtype(const GDType &p_gdtype, String &r_type, String &r_enum, bool p_is_return = false);
+	static String docvalue_from_expression(const GDP::ExpressionNode *p_expression);
 };
 
 #endif // GDSCRIPT_DOCGEN_H

+ 9 - 7
modules/gdscript/gdscript.cpp

@@ -479,23 +479,25 @@ void GDScript::_update_exports_values(HashMap<StringName, Variant> &values, List
 	}
 }
 
-void GDScript::_add_doc(const DocData::ClassDoc &p_inner_class) {
-	if (_owner) { // Only the top-level class stores doc info
-		_owner->_add_doc(p_inner_class);
-	} else { // Remove old docs, add new
+void GDScript::_add_doc(const DocData::ClassDoc &p_doc) {
+	doc_class_name = p_doc.name;
+	if (_owner) { // Only the top-level class stores doc info.
+		_owner->_add_doc(p_doc);
+	} else { // Remove old docs, add new.
 		for (int i = 0; i < docs.size(); i++) {
-			if (docs[i].name == p_inner_class.name) {
+			if (docs[i].name == p_doc.name) {
 				docs.remove_at(i);
 				break;
 			}
 		}
-		docs.append(p_inner_class);
+		docs.append(p_doc);
 	}
 }
 
 void GDScript::_clear_doc() {
-	docs.clear();
+	doc_class_name = StringName();
 	doc = DocData::ClassDoc();
+	docs.clear();
 }
 
 String GDScript::get_class_icon_path() const {

+ 4 - 4
modules/gdscript/gdscript.h

@@ -157,10 +157,11 @@ private:
 	bool placeholder_fallback_enabled = false;
 	void _update_exports_values(HashMap<StringName, Variant> &values, List<PropertyInfo> &propnames);
 
+	StringName doc_class_name;
 	DocData::ClassDoc doc;
 	Vector<DocData::ClassDoc> docs;
+	void _add_doc(const DocData::ClassDoc &p_doc);
 	void _clear_doc();
-	void _add_doc(const DocData::ClassDoc &p_inner_class);
 #endif
 
 	GDScriptFunction *implicit_initializer = nullptr;
@@ -292,9 +293,8 @@ public:
 	virtual void update_exports() override;
 
 #ifdef TOOLS_ENABLED
-	virtual Vector<DocData::ClassDoc> get_documentation() const override {
-		return docs;
-	}
+	virtual StringName get_doc_class_name() const override { return doc_class_name; }
+	virtual Vector<DocData::ClassDoc> get_documentation() const override { return docs; }
 	virtual String get_class_icon_path() const override;
 #endif // TOOLS_ENABLED
 

+ 2 - 1
modules/gdscript/gdscript_analyzer.h

@@ -127,7 +127,6 @@ class GDScriptAnalyzer {
 	Array make_array_from_element_datatype(const GDScriptParser::DataType &p_element_datatype, const GDScriptParser::Node *p_source_node = nullptr);
 	Dictionary make_dictionary_from_element_datatype(const GDScriptParser::DataType &p_key_element_datatype, const GDScriptParser::DataType &p_value_element_datatype, const GDScriptParser::Node *p_source_node = nullptr);
 	GDScriptParser::DataType type_from_variant(const Variant &p_value, const GDScriptParser::Node *p_source);
-	static GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type);
 	GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false, bool p_is_readonly = false) const;
 	GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name, const GDScriptParser::Node *p_source);
 	bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, BitField<MethodFlags> &r_method_flags, StringName *r_native_class = nullptr);
@@ -163,7 +162,9 @@ public:
 	Error analyze();
 
 	Variant make_variable_default_value(GDScriptParser::VariableNode *p_variable);
+
 	static bool check_type_compatibility(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr);
+	static GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type);
 
 	GDScriptAnalyzer(GDScriptParser *p_parser);
 };

+ 323 - 168
modules/gdscript/gdscript_editor.cpp

@@ -31,12 +31,12 @@
 #include "gdscript.h"
 
 #include "gdscript_analyzer.h"
-#include "gdscript_compiler.h"
 #include "gdscript_parser.h"
 #include "gdscript_tokenizer.h"
 #include "gdscript_utility_functions.h"
 
 #ifdef TOOLS_ENABLED
+#include "editor/gdscript_docgen.h"
 #include "editor/script_templates/templates.gen.h"
 #endif
 
@@ -3659,60 +3659,174 @@ void GDScriptLanguage::auto_indent_code(String &p_code, int p_from_line, int p_t
 
 #ifdef TOOLS_ENABLED
 
-static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, const String &p_symbol, bool p_is_function, GDScriptLanguage::LookupResult &r_result) {
+static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, const String &p_symbol, GDScriptLanguage::LookupResult &r_result) {
 	GDScriptParser::DataType base_type = p_base;
 
-	while (base_type.is_set()) {
+	while (true) {
 		switch (base_type.kind) {
 			case GDScriptParser::DataType::CLASS: {
-				if (base_type.class_type) {
-					String name = p_symbol;
-					if (name == "new") {
-						name = "_init";
-					}
-					if (base_type.class_type->has_member(name)) {
-						r_result.type = ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION;
-						r_result.location = base_type.class_type->get_member(name).get_line();
-						r_result.class_path = base_type.script_path;
-						Error err = OK;
-						r_result.script = GDScriptCache::get_shallow_script(r_result.class_path, err);
-						return err;
-					}
+				ERR_FAIL_NULL_V(base_type.class_type, ERR_BUG);
+
+				String name = p_symbol;
+				if (name == "new") {
+					name = "_init";
+				}
+
+				if (!base_type.class_type->has_member(name)) {
 					base_type = base_type.class_type->base_type;
+					break;
+				}
+
+				const GDScriptParser::ClassNode::Member &member = base_type.class_type->get_member(name);
+
+				switch (member.type) {
+					case GDScriptParser::ClassNode::Member::UNDEFINED:
+					case GDScriptParser::ClassNode::Member::GROUP:
+						return ERR_BUG;
+					case GDScriptParser::ClassNode::Member::CLASS: {
+						String type_name;
+						String enum_name;
+						GDScriptDocGen::doctype_from_gdtype(GDScriptAnalyzer::type_from_metatype(member.get_datatype()), type_name, enum_name);
+
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS;
+						r_result.class_name = type_name;
+					} break;
+					case GDScriptParser::ClassNode::Member::CONSTANT:
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
+						break;
+					case GDScriptParser::ClassNode::Member::FUNCTION:
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
+						break;
+					case GDScriptParser::ClassNode::Member::SIGNAL:
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL;
+						break;
+					case GDScriptParser::ClassNode::Member::VARIABLE:
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY;
+						break;
+					case GDScriptParser::ClassNode::Member::ENUM:
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM;
+						break;
+					case GDScriptParser::ClassNode::Member::ENUM_VALUE:
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
+						break;
+				}
+
+				if (member.type != GDScriptParser::ClassNode::Member::CLASS) {
+					String type_name;
+					String enum_name;
+					GDScriptDocGen::doctype_from_gdtype(GDScriptAnalyzer::type_from_metatype(base_type), type_name, enum_name);
+
+					r_result.class_name = type_name;
+					r_result.class_member = name;
 				}
+
+				Error err = OK;
+				r_result.script = GDScriptCache::get_shallow_script(base_type.script_path, err);
+				r_result.script_path = base_type.script_path;
+				r_result.location = member.get_line();
+				return err;
 			} break;
 			case GDScriptParser::DataType::SCRIPT: {
-				Ref<Script> scr = base_type.script_type;
-				if (scr.is_valid()) {
-					int line = scr->get_member_line(p_symbol);
-					if (line >= 0) {
-						r_result.type = ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION;
-						r_result.location = line;
-						r_result.script = scr;
-						return OK;
+				const Ref<Script> scr = base_type.script_type;
+
+				if (scr.is_null()) {
+					return ERR_CANT_RESOLVE;
+				}
+
+				String name = p_symbol;
+				if (name == "new") {
+					name = "_init";
+				}
+
+				const int line = scr->get_member_line(name);
+				if (line >= 0) {
+					bool found_type = false;
+					r_result.type = ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION;
+					{
+						List<PropertyInfo> properties;
+						scr->get_script_property_list(&properties);
+						for (const PropertyInfo &property : properties) {
+							if (property.name == name && (property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE)) {
+								found_type = true;
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY;
+								r_result.class_name = scr->get_doc_class_name();
+								r_result.class_member = name;
+								break;
+							}
+						}
 					}
-					Ref<Script> base_script = scr->get_base_script();
-					if (base_script.is_valid()) {
-						base_type.script_type = base_script;
-					} else {
-						base_type.kind = GDScriptParser::DataType::NATIVE;
-						base_type.builtin_type = Variant::OBJECT;
-						base_type.native_type = scr->get_instance_base_type();
+					if (!found_type) {
+						List<MethodInfo> methods;
+						scr->get_script_method_list(&methods);
+						for (const MethodInfo &method : methods) {
+							if (method.name == name) {
+								found_type = true;
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
+								r_result.class_name = scr->get_doc_class_name();
+								r_result.class_member = name;
+								break;
+							}
+						}
+					}
+					if (!found_type) {
+						List<MethodInfo> signals;
+						scr->get_script_method_list(&signals);
+						for (const MethodInfo &signal : signals) {
+							if (signal.name == name) {
+								found_type = true;
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL;
+								r_result.class_name = scr->get_doc_class_name();
+								r_result.class_member = name;
+								break;
+							}
+						}
 					}
+					if (!found_type) {
+						const Ref<GDScript> gds = scr;
+						if (gds.is_valid()) {
+							const Ref<GDScript> *subclass = gds->get_subclasses().getptr(name);
+							if (subclass != nullptr) {
+								found_type = true;
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS;
+								r_result.class_name = subclass->ptr()->get_doc_class_name();
+							}
+							// TODO: enums.
+						}
+					}
+					if (!found_type) {
+						HashMap<StringName, Variant> constants;
+						scr->get_constants(&constants);
+						if (constants.has(name)) {
+							found_type = true;
+							r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
+							r_result.class_name = scr->get_doc_class_name();
+							r_result.class_member = name;
+						}
+					}
+
+					r_result.script = scr;
+					r_result.script_path = base_type.script_path;
+					r_result.location = line;
+					return OK;
+				}
+
+				const Ref<Script> base_script = scr->get_base_script();
+				if (base_script.is_valid()) {
+					base_type.script_type = base_script;
 				} else {
-					base_type.kind = GDScriptParser::DataType::UNRESOLVED;
+					base_type.kind = GDScriptParser::DataType::NATIVE;
+					base_type.builtin_type = Variant::OBJECT;
+					base_type.native_type = scr->get_instance_base_type();
 				}
 			} break;
 			case GDScriptParser::DataType::NATIVE: {
-				StringName class_name = base_type.native_type;
-				if (!ClassDB::class_exists(class_name)) {
-					base_type.kind = GDScriptParser::DataType::UNRESOLVED;
-					break;
-				}
+				const StringName &class_name = base_type.native_type;
+
+				ERR_FAIL_COND_V(!ClassDB::class_exists(class_name), ERR_BUG);
 
 				if (ClassDB::has_method(class_name, p_symbol, true)) {
 					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
-					r_result.class_name = base_type.native_type;
+					r_result.class_name = class_name;
 					r_result.class_member = p_symbol;
 					return OK;
 				}
@@ -3722,7 +3836,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 				for (const MethodInfo &E : virtual_methods) {
 					if (E.name == p_symbol) {
 						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
-						r_result.class_name = base_type.native_type;
+						r_result.class_name = class_name;
 						r_result.class_member = p_symbol;
 						return OK;
 					}
@@ -3730,7 +3844,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 
 				if (ClassDB::has_signal(class_name, p_symbol, true)) {
 					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL;
-					r_result.class_name = base_type.native_type;
+					r_result.class_name = class_name;
 					r_result.class_member = p_symbol;
 					return OK;
 				}
@@ -3740,7 +3854,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 				for (const StringName &E : enums) {
 					if (E == p_symbol) {
 						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM;
-						r_result.class_name = base_type.native_type;
+						r_result.class_name = class_name;
 						r_result.class_member = p_symbol;
 						return OK;
 					}
@@ -3748,7 +3862,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 
 				if (!String(ClassDB::get_integer_constant_enum(class_name, p_symbol, true)).is_empty()) {
 					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
-					r_result.class_name = base_type.native_type;
+					r_result.class_name = class_name;
 					r_result.class_member = p_symbol;
 					return OK;
 				}
@@ -3758,7 +3872,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 				for (const String &E : constants) {
 					if (E == p_symbol) {
 						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
-						r_result.class_name = base_type.native_type;
+						r_result.class_name = class_name;
 						r_result.class_member = p_symbol;
 						return OK;
 					}
@@ -3772,80 +3886,98 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 					}
 
 					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY;
-					r_result.class_name = base_type.native_type;
+					r_result.class_name = class_name;
 					r_result.class_member = p_symbol;
 					return OK;
 				}
 
-				StringName parent = ClassDB::get_parent_class(class_name);
-				if (parent != StringName()) {
-					base_type.native_type = parent;
+				const StringName parent_class = ClassDB::get_parent_class(class_name);
+				if (parent_class != StringName()) {
+					base_type.native_type = parent_class;
 				} else {
-					base_type.kind = GDScriptParser::DataType::UNRESOLVED;
+					return ERR_CANT_RESOLVE;
 				}
 			} break;
 			case GDScriptParser::DataType::BUILTIN: {
-				base_type.kind = GDScriptParser::DataType::UNRESOLVED;
-
-				if (Variant::has_constant(base_type.builtin_type, p_symbol)) {
-					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
-					r_result.class_name = Variant::get_type_name(base_type.builtin_type);
-					r_result.class_member = p_symbol;
-					return OK;
-				}
+				if (base_type.is_meta_type) {
+					if (Variant::has_enum(base_type.builtin_type, p_symbol)) {
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM;
+						r_result.class_name = Variant::get_type_name(base_type.builtin_type);
+						r_result.class_member = p_symbol;
+						return OK;
+					}
 
-				Variant v;
-				Ref<RefCounted> v_ref;
-				if (base_type.builtin_type == Variant::OBJECT) {
-					v_ref.instantiate();
-					v = v_ref;
+					if (Variant::has_constant(base_type.builtin_type, p_symbol)) {
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
+						r_result.class_name = Variant::get_type_name(base_type.builtin_type);
+						r_result.class_member = p_symbol;
+						return OK;
+					}
 				} else {
-					Callable::CallError err;
-					Variant::construct(base_type.builtin_type, v, nullptr, 0, err);
-					if (err.error != Callable::CallError::CALL_OK) {
-						break;
+					if (Variant::has_member(base_type.builtin_type, p_symbol)) {
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY;
+						r_result.class_name = Variant::get_type_name(base_type.builtin_type);
+						r_result.class_member = p_symbol;
+						return OK;
 					}
 				}
 
-				if (v.has_method(p_symbol)) {
+				if (Variant::has_builtin_method(base_type.builtin_type, p_symbol)) {
 					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
 					r_result.class_name = Variant::get_type_name(base_type.builtin_type);
 					r_result.class_member = p_symbol;
 					return OK;
 				}
 
-				bool valid = false;
-				v.get(p_symbol, &valid);
-				if (valid) {
-					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY;
-					r_result.class_name = Variant::get_type_name(base_type.builtin_type);
-					r_result.class_member = p_symbol;
-					return OK;
-				}
+				return ERR_CANT_RESOLVE;
 			} break;
 			case GDScriptParser::DataType::ENUM: {
-				if (base_type.class_type && base_type.class_type->has_member(base_type.enum_type)) {
-					GDScriptParser::EnumNode *base_enum = base_type.class_type->get_member(base_type.enum_type).m_enum;
-					for (const GDScriptParser::EnumNode::Value &value : base_enum->values) {
-						if (value.identifier && value.identifier->name == p_symbol) {
-							r_result.type = ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION;
-							r_result.class_path = base_type.script_path;
-							r_result.location = value.line;
-							Error err = OK;
-							r_result.script = GDScriptCache::get_shallow_script(r_result.class_path, err);
-							return err;
+				if (base_type.is_meta_type) {
+					if (base_type.enum_values.has(p_symbol)) {
+						String type_name;
+						String enum_name;
+						GDScriptDocGen::doctype_from_gdtype(GDScriptAnalyzer::type_from_metatype(base_type), type_name, enum_name);
+
+						if (CoreConstants::is_global_enum(enum_name)) {
+							r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
+							r_result.class_name = "@GlobalScope";
+							r_result.class_member = p_symbol;
+							return OK;
+						} else {
+							const int dot_pos = enum_name.rfind_char('.');
+							if (dot_pos >= 0) {
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
+								r_result.class_name = enum_name.left(dot_pos);
+								r_result.class_member = p_symbol;
+								return OK;
+							}
 						}
+					} else if (Variant::has_builtin_method(Variant::DICTIONARY, p_symbol)) {
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
+						r_result.class_name = "Dictionary";
+						r_result.class_member = p_symbol;
+						return OK;
 					}
-				} else if (base_type.enum_values.has(p_symbol)) {
-					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
-					r_result.class_name = String(base_type.native_type).get_slicec('.', 0);
-					r_result.class_member = p_symbol;
-					return OK;
 				}
-				base_type.kind = GDScriptParser::DataType::UNRESOLVED;
+
+				return ERR_CANT_RESOLVE;
 			} break;
-			default: {
-				base_type.kind = GDScriptParser::DataType::UNRESOLVED;
+			case GDScriptParser::DataType::VARIANT: {
+				if (base_type.is_meta_type) {
+					const String enum_name = "Variant." + p_symbol;
+					if (CoreConstants::is_global_enum(enum_name)) {
+						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM;
+						r_result.class_name = "@GlobalScope";
+						r_result.class_member = enum_name;
+						return OK;
+					}
+				}
+
+				return ERR_CANT_RESOLVE;
+			} break;
+			case GDScriptParser::DataType::RESOLVING:
+			case GDScriptParser::DataType::UNRESOLVED: {
+				return ERR_CANT_RESOLVE;
 			} break;
 		}
 	}
@@ -3867,13 +3999,13 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 		return OK;
 	}
 
-	if ("Variant" == p_symbol) {
+	if (p_symbol == "Variant") {
 		r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS;
 		r_result.class_name = "Variant";
 		return OK;
 	}
 
-	if ("PI" == p_symbol || "TAU" == p_symbol || "INF" == p_symbol || "NAN" == p_symbol) {
+	if (p_symbol == "PI" || p_symbol == "TAU" || p_symbol == "INF" || p_symbol == "NAN") {
 		r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
 		r_result.class_name = "@GDScript";
 		r_result.class_member = p_symbol;
@@ -3888,9 +4020,9 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 
 	// Allows class functions with the names like built-ins to be handled properly.
 	if (context.type != GDScriptParser::COMPLETION_ATTRIBUTE) {
-		// Need special checks for assert and preload as they are technically
-		// keywords, so are not registered in GDScriptUtilityFunctions.
-		if (GDScriptUtilityFunctions::function_exists(p_symbol) || "assert" == p_symbol || "preload" == p_symbol) {
+		// Need special checks for `assert` and `preload` as they are technically
+		// keywords, so are not registered in `GDScriptUtilityFunctions`.
+		if (GDScriptUtilityFunctions::function_exists(p_symbol) || p_symbol == "assert" || p_symbol == "preload") {
 			r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
 			r_result.class_name = "@GDScript";
 			r_result.class_member = p_symbol;
@@ -3952,26 +4084,16 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 
 	switch (context.type) {
 		case GDScriptParser::COMPLETION_BUILT_IN_TYPE_CONSTANT_OR_STATIC_METHOD: {
-			if (!Variant::has_builtin_method(context.builtin_type, StringName(p_symbol))) {
-				// A constant.
-				r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
-				r_result.class_name = Variant::get_type_name(context.builtin_type);
-				r_result.class_member = p_symbol;
-				return OK;
-			}
-			// A method.
 			GDScriptParser::DataType base_type;
 			base_type.kind = GDScriptParser::DataType::BUILTIN;
 			base_type.builtin_type = context.builtin_type;
-			if (_lookup_symbol_from_base(base_type, p_symbol, true, r_result) == OK) {
+			base_type.is_meta_type = true;
+			if (_lookup_symbol_from_base(base_type, p_symbol, r_result) == OK) {
 				return OK;
 			}
 		} break;
 		case GDScriptParser::COMPLETION_SUPER_METHOD:
-		case GDScriptParser::COMPLETION_METHOD: {
-			is_function = true;
-			[[fallthrough]];
-		}
+		case GDScriptParser::COMPLETION_METHOD:
 		case GDScriptParser::COMPLETION_ASSIGN:
 		case GDScriptParser::COMPLETION_CALL_ARGUMENTS:
 		case GDScriptParser::COMPLETION_IDENTIFIER:
@@ -3993,45 +4115,96 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 				const GDScriptParser::SuiteNode *suite = context.current_suite;
 				while (suite) {
 					if (suite->has_local(p_symbol)) {
-						r_result.type = ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION;
-						r_result.location = suite->get_local(p_symbol).start_line;
-						return OK;
+						const GDScriptParser::SuiteNode::Local &local = suite->get_local(p_symbol);
+
+						switch (local.type) {
+							case GDScriptParser::SuiteNode::Local::UNDEFINED:
+								return ERR_BUG;
+							case GDScriptParser::SuiteNode::Local::CONSTANT:
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT;
+								r_result.description = local.constant->doc_data.description;
+								r_result.is_deprecated = local.constant->doc_data.is_deprecated;
+								r_result.deprecated_message = local.constant->doc_data.deprecated_message;
+								r_result.is_experimental = local.constant->doc_data.is_experimental;
+								r_result.experimental_message = local.constant->doc_data.experimental_message;
+								if (local.constant->initializer != nullptr) {
+									r_result.value = GDScriptDocGen::docvalue_from_expression(local.constant->initializer);
+								}
+								break;
+							case GDScriptParser::SuiteNode::Local::VARIABLE:
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE;
+								r_result.description = local.variable->doc_data.description;
+								r_result.is_deprecated = local.variable->doc_data.is_deprecated;
+								r_result.deprecated_message = local.variable->doc_data.deprecated_message;
+								r_result.is_experimental = local.variable->doc_data.is_experimental;
+								r_result.experimental_message = local.variable->doc_data.experimental_message;
+								if (local.variable->initializer != nullptr) {
+									r_result.value = GDScriptDocGen::docvalue_from_expression(local.variable->initializer);
+								}
+								break;
+							case GDScriptParser::SuiteNode::Local::PARAMETER:
+							case GDScriptParser::SuiteNode::Local::FOR_VARIABLE:
+							case GDScriptParser::SuiteNode::Local::PATTERN_BIND:
+								r_result.type = ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE;
+								break;
+						}
+
+						GDScriptDocGen::doctype_from_gdtype(local.get_datatype(), r_result.doc_type, r_result.enumeration);
+
+						Error err = OK;
+						r_result.script = GDScriptCache::get_shallow_script(base_type.script_path, err);
+						r_result.script_path = base_type.script_path;
+						r_result.location = local.start_line;
+						return err;
 					}
 					suite = suite->parent_block;
 				}
 			}
 
-			if (_lookup_symbol_from_base(base_type, p_symbol, is_function, r_result) == OK) {
+			if (_lookup_symbol_from_base(base_type, p_symbol, r_result) == OK) {
 				return OK;
 			}
 
 			if (!is_function) {
-				// Guess in autoloads as singletons.
 				if (ProjectSettings::get_singleton()->has_autoload(p_symbol)) {
 					const ProjectSettings::AutoloadInfo &autoload = ProjectSettings::get_singleton()->get_autoload(p_symbol);
 					if (autoload.is_singleton) {
 						String scr_path = autoload.path;
 						if (!scr_path.ends_with(".gd")) {
-							// Not a script, try find the script anyway,
-							// may have some success.
+							// Not a script, try find the script anyway, may have some success.
 							scr_path = scr_path.get_basename() + ".gd";
 						}
 
 						if (FileAccess::exists(scr_path)) {
-							r_result.type = ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION;
-							r_result.location = 0;
+							r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS;
+							r_result.class_name = p_symbol;
 							r_result.script = ResourceLoader::load(scr_path);
+							r_result.script_path = scr_path;
+							r_result.location = 0;
 							return OK;
 						}
 					}
 				}
 
-				// Global.
-				HashMap<StringName, int> classes = GDScriptLanguage::get_singleton()->get_global_map();
-				if (classes.has(p_symbol)) {
-					Variant value = GDScriptLanguage::get_singleton()->get_global_array()[classes[p_symbol]];
+				if (ScriptServer::is_global_class(p_symbol)) {
+					const String scr_path = ScriptServer::get_global_class_path(p_symbol);
+					const Ref<Script> scr = ResourceLoader::load(scr_path);
+					if (scr.is_null()) {
+						return ERR_BUG;
+					}
+					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS;
+					r_result.class_name = scr->get_doc_class_name();
+					r_result.script = scr;
+					r_result.script_path = scr_path;
+					r_result.location = 0;
+					return OK;
+				}
+
+				const HashMap<StringName, int> &global_map = GDScriptLanguage::get_singleton()->get_global_map();
+				if (global_map.has(p_symbol)) {
+					Variant value = GDScriptLanguage::get_singleton()->get_global_array()[global_map[p_symbol]];
 					if (value.get_type() == Variant::OBJECT) {
-						Object *obj = value;
+						const Object *obj = value;
 						if (obj) {
 							if (Object::cast_to<GDScriptNativeClass>(obj)) {
 								r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS;
@@ -4040,52 +4213,34 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 								r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS;
 								r_result.class_name = obj->get_class();
 							}
-
-							// proxy class remove the underscore.
-							if (r_result.class_name.begins_with("_")) {
-								r_result.class_name = r_result.class_name.substr(1);
-							}
 							return OK;
 						}
-					} else {
-						/*
-						// Because get_integer_constant_enum and get_integer_constant don't work on @GlobalScope
-						// We cannot determine the exact nature of the identifier here
-						// Otherwise these codes would work
-						StringName enumName = ClassDB::get_integer_constant_enum("@GlobalScope", p_symbol, true);
-						if (enumName != nullptr) {
-							r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM;
-							r_result.class_name = "@GlobalScope";
-							r_result.class_member = enumName;
-							return OK;
-						}
-						else {
-							r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
-							r_result.class_name = "@GlobalScope";
-							r_result.class_member = p_symbol;
-							return OK;
-						}*/
-						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE;
-						r_result.class_name = "@GlobalScope";
-						r_result.class_member = p_symbol;
-						return OK;
-					}
-				} else {
-					List<StringName> utility_functions;
-					Variant::get_utility_function_list(&utility_functions);
-					if (utility_functions.find(p_symbol) != nullptr) {
-						r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE;
-						r_result.class_name = "@GlobalScope";
-						r_result.class_member = p_symbol;
-						return OK;
 					}
 				}
+
+				if (CoreConstants::is_global_enum(p_symbol)) {
+					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM;
+					r_result.class_name = "@GlobalScope";
+					r_result.class_member = p_symbol;
+					return OK;
+				}
+
+				if (CoreConstants::is_global_constant(p_symbol)) {
+					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT;
+					r_result.class_name = "@GlobalScope";
+					r_result.class_member = p_symbol;
+					return OK;
+				}
+
+				if (Variant::has_utility_function(p_symbol)) {
+					r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD;
+					r_result.class_name = "@GlobalScope";
+					r_result.class_member = p_symbol;
+					return OK;
+				}
 			}
 		} break;
-		case GDScriptParser::COMPLETION_ATTRIBUTE_METHOD: {
-			is_function = true;
-			[[fallthrough]];
-		}
+		case GDScriptParser::COMPLETION_ATTRIBUTE_METHOD:
 		case GDScriptParser::COMPLETION_ATTRIBUTE: {
 			if (context.node->type != GDScriptParser::Node::SUBSCRIPT) {
 				break;
@@ -4101,7 +4256,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 				break;
 			}
 
-			if (_lookup_symbol_from_base(base.type, p_symbol, is_function, r_result) == OK) {
+			if (_lookup_symbol_from_base(base.type, p_symbol, r_result) == OK) {
 				return OK;
 			}
 		} break;
@@ -4128,14 +4283,14 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 				base_type = base.type;
 			}
 
-			if (_lookup_symbol_from_base(base_type, p_symbol, is_function, r_result) == OK) {
+			if (_lookup_symbol_from_base(base_type, p_symbol, r_result) == OK) {
 				return OK;
 			}
 		} break;
 		case GDScriptParser::COMPLETION_OVERRIDE_METHOD: {
 			GDScriptParser::DataType base_type = context.current_class->base_type;
 
-			if (_lookup_symbol_from_base(base_type, p_symbol, true, r_result) == OK) {
+			if (_lookup_symbol_from_base(base_type, p_symbol, r_result) == OK) {
 				return OK;
 			}
 		} break;
@@ -4144,7 +4299,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
 		case GDScriptParser::COMPLETION_TYPE_NAME: {
 			GDScriptParser::DataType base_type = context.current_class->get_datatype();
 
-			if (_lookup_symbol_from_base(base_type, p_symbol, false, r_result) == OK) {
+			if (_lookup_symbol_from_base(base_type, p_symbol, r_result) == OK) {
 				return OK;
 			}
 		} break;

+ 59 - 4
modules/gdscript/gdscript_parser.cpp

@@ -736,19 +736,35 @@ void GDScriptParser::parse_program() {
 #undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
 
 	parse_class_body(true);
+
+	head->end_line = current.end_line;
+	head->end_column = current.end_column;
+	head->leftmost_column = MIN(head->leftmost_column, current.leftmost_column);
+	head->rightmost_column = MAX(head->rightmost_column, current.rightmost_column);
+
 	complete_extents(head);
 
 #ifdef TOOLS_ENABLED
 	const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer->get_comments();
-	int line = MIN(max_script_doc_line, head->end_line);
-	while (line > 0) {
+
+	int max_line = head->end_line;
+	if (!head->members.is_empty()) {
+		max_line = MIN(max_script_doc_line, head->members[0].get_line() - 1);
+	}
+
+	int line = 0;
+	while (line <= max_line) {
+		// Find the start.
 		if (comments.has(line) && comments[line].new_line && comments[line].comment.begins_with("##")) {
+			// Find the end.
+			while (line + 1 <= max_line && comments.has(line + 1) && comments[line + 1].new_line && comments[line + 1].comment.begins_with("##")) {
+				line++;
+			}
 			head->doc_data = parse_class_doc_comment(line);
 			break;
 		}
-		line--;
+		line++;
 	}
-
 #endif // TOOLS_ENABLED
 
 	if (!check(GDScriptTokenizer::Token::TK_EOF)) {
@@ -1612,6 +1628,7 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
 	function->is_static = p_is_static;
 
 	SuiteNode *body = alloc_node<SuiteNode>();
+
 	SuiteNode *previous_suite = current_suite;
 	current_suite = body;
 
@@ -1620,6 +1637,11 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
 	parse_function_signature(function, body, "function");
 
 	current_suite = previous_suite;
+
+#ifdef TOOLS_ENABLED
+	function->min_local_doc_line = previous.end_line + 1;
+#endif
+
 	function->body = parse_suite("function declaration", body);
 
 	current_function = previous_function;
@@ -1988,11 +2010,44 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
 		}
 	}
 
+#ifdef TOOLS_ENABLED
+	int doc_comment_line = 0;
+	if (result != nullptr) {
+		doc_comment_line = result->start_line - 1;
+	}
+#endif // TOOLS_ENABLED
+
 	if (result != nullptr && !annotations.is_empty()) {
 		for (AnnotationNode *&annotation : annotations) {
 			result->annotations.push_back(annotation);
+#ifdef TOOLS_ENABLED
+			if (annotation->start_line <= doc_comment_line) {
+				doc_comment_line = annotation->start_line - 1;
+			}
+#endif // TOOLS_ENABLED
+		}
+	}
+
+#ifdef TOOLS_ENABLED
+	if (result != nullptr) {
+		MemberDocData doc_data;
+		if (has_comment(result->start_line, true)) {
+			// Inline doc comment.
+			doc_data = parse_doc_comment(result->start_line, true);
+		} else if (doc_comment_line >= current_function->min_local_doc_line && has_comment(doc_comment_line, true) && tokenizer->get_comments()[doc_comment_line].new_line) {
+			// Normal doc comment.
+			doc_data = parse_doc_comment(doc_comment_line);
 		}
+
+		if (result->type == Node::CONSTANT) {
+			static_cast<ConstantNode *>(result)->doc_data = doc_data;
+		} else if (result->type == Node::VARIABLE) {
+			static_cast<VariableNode *>(result)->doc_data = doc_data;
+		}
+
+		current_function->min_local_doc_line = result->end_line + 1; // Prevent multiple locals from using the same doc comment.
 	}
+#endif // TOOLS_ENABLED
 
 #ifdef DEBUG_ENABLED
 	if (unreachable && result != nullptr) {

+ 1 - 0
modules/gdscript/gdscript_parser.h

@@ -862,6 +862,7 @@ public:
 		Vector<Variant> default_arg_values;
 #ifdef TOOLS_ENABLED
 		MemberDocData doc_data;
+		int min_local_doc_line = 0;
 #endif // TOOLS_ENABLED
 
 		bool resolved_signature = false;

+ 4 - 5
modules/gdscript/language_server/gdscript_workspace.cpp

@@ -699,12 +699,12 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu
 					symbol_identifier = "_init";
 				}
 				if (OK == GDScriptLanguage::get_singleton()->lookup_code(parser->get_text_for_lookup_symbol(pos, symbol_identifier, p_func_required), symbol_identifier, path, nullptr, ret)) {
-					if (ret.type == ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION) {
+					if (ret.location >= 0) {
 						String target_script_path = path;
-						if (!ret.script.is_null()) {
+						if (ret.script.is_valid()) {
 							target_script_path = ret.script->get_path();
-						} else if (!ret.class_path.is_empty()) {
-							target_script_path = ret.class_path;
+						} else if (!ret.script_path.is_empty()) {
+							target_script_path = ret.script_path;
 						}
 
 						if (const ExtendGDScriptParser *target_parser = get_parse_result(target_script_path)) {
@@ -720,7 +720,6 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu
 								}
 							}
 						}
-
 					} else {
 						String member = ret.class_member;
 						if (member.is_empty() && symbol_identifier != ret.class_name) {

+ 1 - 0
modules/mono/csharp_script.h

@@ -243,6 +243,7 @@ public:
 	void set_source_code(const String &p_code) override;
 
 #ifdef TOOLS_ENABLED
+	virtual StringName get_doc_class_name() const override { return StringName(); } // TODO
 	virtual Vector<DocData::ClassDoc> get_documentation() const override {
 		// TODO
 		Vector<DocData::ClassDoc> docs;

+ 56 - 0
scene/gui/code_edit.cpp

@@ -31,13 +31,27 @@
 #include "code_edit.h"
 #include "code_edit.compat.inc"
 
+#include "core/config/project_settings.h"
 #include "core/os/keyboard.h"
 #include "core/string/string_builder.h"
 #include "core/string/ustring.h"
 #include "scene/theme/theme_db.h"
 
+void CodeEdit::_apply_project_settings() {
+	symbol_tooltip_timer->set_wait_time(GLOBAL_GET("gui/timers/tooltip_delay_sec"));
+}
+
 void CodeEdit::_notification(int p_what) {
 	switch (p_what) {
+		case NOTIFICATION_READY: {
+			_apply_project_settings();
+#ifdef TOOLS_ENABLED
+			if (Engine::get_singleton()->is_editor_hint()) {
+				ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &CodeEdit::_apply_project_settings));
+			}
+#endif // TOOLS_ENABLED
+		} break;
+
 		case NOTIFICATION_THEME_CHANGED: {
 			set_gutter_width(main_gutter, get_line_height());
 			_update_line_number_gutter_width();
@@ -262,6 +276,7 @@ void CodeEdit::_notification(int p_what) {
 
 		case NOTIFICATION_MOUSE_EXIT: {
 			queue_redraw();
+			symbol_tooltip_timer->stop();
 		} break;
 	}
 }
@@ -438,6 +453,12 @@ void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
 			}
 		}
 
+		if (symbol_tooltip_on_hover_enabled) {
+			symbol_tooltip_pos = get_line_column_at_pos(mpos, false);
+			symbol_tooltip_word = get_word_at_pos(mpos);
+			symbol_tooltip_timer->start();
+		}
+
 		bool scroll_hovered = code_completion_scroll_rect.has_point(mpos);
 		if (is_code_completion_scroll_hovered != scroll_hovered) {
 			is_code_completion_scroll_hovered = scroll_hovered;
@@ -2408,6 +2429,26 @@ void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) {
 	}
 }
 
+/* Symbol tooltip */
+void CodeEdit::set_symbol_tooltip_on_hover_enabled(bool p_enabled) {
+	symbol_tooltip_on_hover_enabled = p_enabled;
+	if (!p_enabled) {
+		symbol_tooltip_timer->stop();
+	}
+}
+
+bool CodeEdit::is_symbol_tooltip_on_hover_enabled() const {
+	return symbol_tooltip_on_hover_enabled;
+}
+
+void CodeEdit::_on_symbol_tooltip_timer_timeout() {
+	const int line = symbol_tooltip_pos.y;
+	const int column = symbol_tooltip_pos.x;
+	if (line >= 0 && column >= 0 && !symbol_tooltip_word.is_empty() && !has_selection() && !Input::get_singleton()->is_anything_pressed()) {
+		emit_signal(SNAME("symbol_hovered"), symbol_tooltip_word, line, column);
+	}
+}
+
 /* Text manipulation */
 void CodeEdit::move_lines_up() {
 	begin_complex_operation();
@@ -2771,6 +2812,10 @@ void CodeEdit::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid);
 
+	/* Symbol tooltip */
+	ClassDB::bind_method(D_METHOD("set_symbol_tooltip_on_hover_enabled", "enable"), &CodeEdit::set_symbol_tooltip_on_hover_enabled);
+	ClassDB::bind_method(D_METHOD("is_symbol_tooltip_on_hover_enabled"), &CodeEdit::is_symbol_tooltip_on_hover_enabled);
+
 	/* Text manipulation */
 	ClassDB::bind_method(D_METHOD("move_lines_up"), &CodeEdit::move_lines_up);
 	ClassDB::bind_method(D_METHOD("move_lines_down"), &CodeEdit::move_lines_down);
@@ -2780,6 +2825,7 @@ void CodeEdit::_bind_methods() {
 
 	/* Inspector */
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "symbol_lookup_on_click"), "set_symbol_lookup_on_click_enabled", "is_symbol_lookup_on_click_enabled");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "symbol_tooltip_on_hover"), "set_symbol_tooltip_on_hover_enabled", "is_symbol_tooltip_on_hover_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "line_folding"), "set_line_folding_enabled", "is_line_folding_enabled");
 
 	ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "line_length_guidelines"), "set_line_length_guidelines", "get_line_length_guidelines");
@@ -2826,6 +2872,9 @@ void CodeEdit::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("symbol_lookup", PropertyInfo(Variant::STRING, "symbol"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "column")));
 	ADD_SIGNAL(MethodInfo("symbol_validate", PropertyInfo(Variant::STRING, "symbol")));
 
+	/* Symbol tooltip */
+	ADD_SIGNAL(MethodInfo("symbol_hovered", PropertyInfo(Variant::STRING, "symbol"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "column")));
+
 	/* Theme items */
 	/* Gutters */
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, code_folding_color);
@@ -3749,6 +3798,13 @@ CodeEdit::CodeEdit() {
 	set_gutter_custom_draw(gutter_idx, callable_mp(this, &CodeEdit::_fold_gutter_draw_callback));
 	gutter_idx++;
 
+	/* Symbol tooltip */
+	symbol_tooltip_timer = memnew(Timer);
+	symbol_tooltip_timer->set_wait_time(0.5); // See `_apply_project_settings()`.
+	symbol_tooltip_timer->set_one_shot(true);
+	symbol_tooltip_timer->connect("timeout", callable_mp(this, &CodeEdit::_on_symbol_tooltip_timer_timeout));
+	add_child(symbol_tooltip_timer, false, INTERNAL_MODE_FRONT);
+
 	connect("lines_edited_from", callable_mp(this, &CodeEdit::_lines_edited_from));
 	connect("text_set", callable_mp(this, &CodeEdit::_text_set));
 	connect(SceneStringName(text_changed), callable_mp(this, &CodeEdit::_text_changed));

+ 15 - 3
scene/gui/code_edit.h

@@ -233,10 +233,16 @@ private:
 
 	/* Symbol lookup */
 	bool symbol_lookup_on_click_enabled = false;
+	Point2i symbol_lookup_pos; // Column and line.
+	String symbol_lookup_new_word;
+	String symbol_lookup_word;
 
-	String symbol_lookup_new_word = "";
-	String symbol_lookup_word = "";
-	Point2i symbol_lookup_pos;
+	/* Symbol tooltip */
+	bool symbol_tooltip_on_hover_enabled = false;
+	Point2i symbol_tooltip_pos; // Column and line.
+	String symbol_tooltip_word;
+	Timer *symbol_tooltip_timer = nullptr;
+	void _on_symbol_tooltip_timer_timeout();
 
 	/* Visual */
 	struct ThemeCache {
@@ -304,6 +310,8 @@ private:
 	void _text_set();
 	void _text_changed();
 
+	void _apply_project_settings();
+
 protected:
 	void _notification(int p_what);
 	static void _bind_methods();
@@ -497,6 +505,10 @@ public:
 
 	void set_symbol_lookup_word_as_valid(bool p_valid);
 
+	/* Symbol tooltip */
+	void set_symbol_tooltip_on_hover_enabled(bool p_enabled);
+	bool is_symbol_tooltip_on_hover_enabled() const;
+
 	/* Text manipulation */
 	void move_lines_up();
 	void move_lines_down();

+ 5 - 0
scene/gui/rich_text_label.cpp

@@ -5882,6 +5882,11 @@ String RichTextLabel::get_selected_text() const {
 	for (int i = 0; i < to_line; i++) {
 		txt += _get_line_text(main, i, selection);
 	}
+
+	if (selection_modifier.is_valid()) {
+		txt = selection_modifier.call(txt);
+	}
+
 	return txt;
 }
 

+ 5 - 0
scene/gui/rich_text_label.h

@@ -539,6 +539,7 @@ private:
 	};
 
 	Selection selection;
+	Callable selection_modifier;
 	bool deselect_on_focus_loss_enabled = true;
 	bool drag_and_drop_selection_enabled = true;
 
@@ -791,6 +792,10 @@ public:
 	void select_all();
 	void selection_copy();
 
+	_FORCE_INLINE_ void set_selection_modifier(const Callable &p_modifier) {
+		selection_modifier = p_modifier;
+	}
+
 	void set_deselect_on_focus_loss_enabled(const bool p_enabled);
 	bool is_deselect_on_focus_loss_enabled() const;
 

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff