Repository: godotengine/godot (language: cpp)

Issue:
Documentation tooltip trigger area includes end-of-line and whitespace surrounding a symbol
### Tested versions

v4.4.dev7.official [46c8f8c5c]

### System information

Godot v4.4.dev7 - Windows 11 (build 22631) - Multi-window, 1 monitor - OpenGL 3 (Compatibility) - Intel(R) Iris(R) Xe Graphics (Intel Corporation; 32.0.101.5768) - 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz (8 threads)

### Issue description

The symbol info / documentation tooltips (#91060) are triggered by hovering in the empty space at the end of a line or the whitespace surrounding a symbol.

https://github.com/user-attachments/assets/d15df433-edad-4044-8bb5-b02c89c869ec


### Steps to reproduce

Stop the mouse cursor in the space surrounding a hoverable symbol.

### Minimal reproduction project (MRP)

N/A


Discussion / hints:

Fix patch:
diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml
index 89e2a2017af1..826a491888df 100644
--- a/doc/classes/TextEdit.xml
+++ b/doc/classes/TextEdit.xml
@@ -349,9 +349,12 @@
 		<method name="get_line_column_at_pos" qualifiers="const">
 			<return type="Vector2i" />
 			<param index="0" name="position" type="Vector2i" />
-			<param index="1" name="allow_out_of_bounds" type="bool" default="true" />
+			<param index="1" name="clamp_line" type="bool" default="true" />
+			<param index="2" name="clamp_column" type="bool" default="true" />
 			<description>
-				Returns the line and column at the given position. In the returned vector, [code]x[/code] is the column, [code]y[/code] is the line. If [param allow_out_of_bounds] is [code]false[/code] and the position is not over the text, both vector values will be set to [code]-1[/code].
+				Returns the line and column at the given position. In the returned vector, [code]x[/code] is the column and [code]y[/code] is the line.
+				If [param clamp_line] is [code]false[/code] and [param position] is below the last line, [code]Vector2i(-1, -1)[/code] is returned.
+				If [param clamp_column] is [code]false[/code] and [param position] is outside the column range of the line, [code]Vector2i(-1, -1)[/code] is returned.
 			</description>
 		</method>
 		<method name="get_line_count" qualifiers="const">
diff --git a/misc/extension_api_validation/4.3-stable.expected b/misc/extension_api_validation/4.3-stable.expected
index 7b5889f464b9..9c96811f875f 100644
--- a/misc/extension_api_validation/4.3-stable.expected
+++ b/misc/extension_api_validation/4.3-stable.expected
@@ -309,3 +309,10 @@ Validate extension JSON: API was removed: classes/EditorSceneFormatImporter/meth
 
 This virtual method, and the internal public `get_import_flags`, were never used by the engine, since it was open sourced.
 So we're removing it despite the compat breakage as there's no way for users to rely on this affecting engine behavior.
+
+
+GH-100913
+---------
+Validate extension JSON: Error: Field 'classes/TextEdit/methods/get_line_column_at_pos/arguments': size changed value in new API, from 2 to 3.
+
+Added optional argument to disallow positions that are outside the column range of the line. Compatibility method registered.
diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp
index 7f8b4dca7862..be8b6f075d9c 100644
--- a/scene/gui/code_edit.cpp
+++ b/scene/gui/code_edit.cpp
@@ -421,7 +421,7 @@ void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
 						mpos.x = get_size().x - mpos.x;
 					}
 
-					Point2i pos = get_line_column_at_pos(mpos, false);
+					Point2i pos = get_line_column_at_pos(mpos, false, false);
 					int line = pos.y;
 					int col = pos.x;
 
@@ -443,18 +443,18 @@ void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
 
 		if (symbol_lookup_on_click_enabled) {
 			if (mm->is_command_or_control_pressed() && mm->get_button_mask().is_empty()) {
-				symbol_lookup_pos = get_line_column_at_pos(mpos);
+				symbol_lookup_pos = get_line_column_at_pos(mpos, false, false);
 				symbol_lookup_new_word = get_word_at_pos(mpos);
 				if (symbol_lookup_new_word != symbol_lookup_word) {
 					emit_signal(SNAME("symbol_validate"), symbol_lookup_new_word);
 				}
-			} else if (!mm->is_command_or_control_pressed() || (!mm->get_button_mask().is_empty() && symbol_lookup_pos != get_line_column_at_pos(mpos))) {
+			} else if (!mm->is_command_or_control_pressed() || (!mm->get_button_mask().is_empty() && symbol_lookup_pos != get_line_column_at_pos(mpos, false, false))) {
 				set_symbol_lookup_word_as_valid(false);
 			}
 		}
 
 		if (symbol_tooltip_on_hover_enabled) {
-			symbol_tooltip_pos = get_line_column_at_pos(mpos, false);
+			symbol_tooltip_pos = get_line_column_at_pos(mpos, false, false);
 			symbol_tooltip_word = get_word_at_pos(mpos);
 			symbol_tooltip_timer->start();
 		}
@@ -2388,7 +2388,7 @@ bool CodeEdit::is_symbol_lookup_on_click_enabled() const {
 
 String CodeEdit::get_text_for_symbol_lookup() const {
 	Point2i mp = get_local_mouse_pos();
-	Point2i pos = get_line_column_at_pos(mp, false);
+	Point2i pos = get_line_column_at_pos(mp, false, false);
 	int line = pos.y;
 	int col = pos.x;
 
diff --git a/scene/gui/text_edit.compat.inc b/scene/gui/text_edit.compat.inc
index bf73229868ef..d15740390e9d 100644
--- a/scene/gui/text_edit.compat.inc
+++ b/scene/gui/text_edit.compat.inc
@@ -34,8 +34,13 @@ void TextEdit::_set_selection_mode_compat_86978(SelectionMode p_mode, int p_line
 	set_selection_mode(p_mode);
 }
 
+Point2i TextEdit::_get_line_column_at_pos_bind_compat_100913(const Point2i &p_pos, bool p_allow_out_of_bounds) const {
+	return get_line_column_at_pos(p_pos, p_allow_out_of_bounds, true);
+}
+
 void TextEdit::_bind_compatibility_methods() {
 	ClassDB::bind_compatibility_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::_set_selection_mode_compat_86978, DEFVAL(-1), DEFVAL(-1), DEFVAL(0));
+	ClassDB::bind_compatibility_method(D_METHOD("get_line_column_at_pos", "position", "allow_out_of_bounds"), &TextEdit::_get_line_column_at_pos_bind_compat_100913, DEFVAL(true));
 }
 
 #endif
diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp
index addbff1e979a..b707a5a93342 100644
--- a/scene/gui/text_edit.cpp
+++ b/scene/gui/text_edit.cpp
@@ -4397,9 +4397,12 @@ Point2 TextEdit::get_local_mouse_pos() const {
 }
 
 String TextEdit::get_word_at_pos(const Vector2 &p_pos) const {
-	Point2i pos = get_line_column_at_pos(p_pos);
+	Point2i pos = get_line_column_at_pos(p_pos, false, false);
 	int row = pos.y;
 	int col = pos.x;
+	if (row < 0 || col < 0) {
+		return "";
+	}
 
 	String s = text[row];
 	if (s.length() == 0) {
@@ -4435,7 +4438,7 @@ String TextEdit::get_word_at_pos(const Vector2 &p_pos) const {
 	return String();
 }
 
-Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_of_bounds) const {
+Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_clamp_line, bool p_clamp_column) const {
 	float rows = p_pos.y - theme_cache.style_normal->get_margin(SIDE_TOP);
 	if (!editable) {
 		rows -= theme_cache.style_readonly->get_offset().y / 2;
@@ -4462,10 +4465,10 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_
 
 	int visible_lines = get_visible_line_count_in_range(first_vis_line, row);
 	if (rows > visible_lines) {
-		if (!p_allow_out_of_bounds) {
-			return Point2i(-1, -1);
+		if (p_clamp_line) {
+			return Point2i(text[row].length(), row);
 		}
-		return Point2i(text[row].length(), row);
+		return Point2i(-1, -1);
 	}
 
 	int colx = p_pos.x - (theme_cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding);
@@ -4482,6 +4485,11 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_
 	} else {
 		colx -= wrap_indent;
 	}
+
+	if (!p_clamp_column && (colx < 0 || colx > TS->shaped_text_get_size(text_rid).x)) {
+		return Point2i(-1, -1);
+	}
+
 	int col = TS->shaped_text_hit_test_position(text_rid, colx);
 	if (!caret_mid_grapheme_enabled) {
 		col = TS->shaped_text_closest_character_pos(text_rid, col);
@@ -6740,7 +6748,7 @@ void TextEdit::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("get_word_at_pos", "position"), &TextEdit::get_word_at_pos);
 
-	ClassDB::bind_method(D_METHOD("get_line_column_at_pos", "position", "allow_out_of_bounds"), &TextEdit::get_line_column_at_pos, DEFVAL(true));
+	ClassDB::bind_method(D_METHOD("get_line_column_at_pos", "position", "clamp_line", "clamp_column"), &TextEdit::get_line_column_at_pos, DEFVAL(true), DEFVAL(true));
 	ClassDB::bind_method(D_METHOD("get_pos_at_line_column", "line", "column"), &TextEdit::get_pos_at_line_column);
 	ClassDB::bind_method(D_METHOD("get_rect_at_line_column", "line", "column"), &TextEdit::get_rect_at_line_column);
 
diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h
index ef5110963023..8609b86eccf0 100644
--- a/scene/gui/text_edit.h
+++ b/scene/gui/text_edit.h
@@ -658,6 +658,7 @@ class TextEdit : public Control {
 
 #ifndef DISABLE_DEPRECATED
 	void _set_selection_mode_compat_86978(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0);
+	Point2i _get_line_column_at_pos_bind_compat_100913(const Point2i &p_pos, bool p_allow_out_of_bounds = true) const;
 	static void _bind_compatibility_methods();
 #endif // DISABLE_DEPRECATED
 
@@ -860,7 +861,7 @@ class TextEdit : public Control {
 
 	String get_word_at_pos(const Vector2 &p_pos) const;
 
-	Point2i get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_of_bounds = true) const;
+	Point2i get_line_column_at_pos(const Point2i &p_pos, bool p_clamp_line = true, bool p_clamp_column = true) const;
 	Point2i get_pos_at_line_column(int p_line, int p_column) const;
 	Rect2i get_rect_at_line_column(int p_line, int p_column) const;
 


Test patch: