markdown: Fix markdown table selection hit testing (#47720)

Xiaobo Liu created

Release Notes:

- Fixed markdown table selection hit testing

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>

Change summary

crates/markdown/src/markdown.rs | 53 +++++++++++++++++++++++++++++++++-
1 file changed, 51 insertions(+), 2 deletions(-)

Detailed changes

crates/markdown/src/markdown.rs 🔗

@@ -2044,19 +2044,33 @@ struct RenderedLink {
 impl RenderedText {
     fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
         let mut lines = self.lines.iter().peekable();
+        let mut fallback_line: Option<&RenderedLine> = None;
 
         while let Some(line) = lines.next() {
             let line_bounds = line.layout.bounds();
+
+            // Exact match: position is within bounds (handles overlapping bounds like table columns)
+            if line_bounds.contains(&position) {
+                return line.source_index_for_position(position);
+            }
+
+            // Track fallback for Y-coordinate based matching
+            if position.y <= line_bounds.bottom() && fallback_line.is_none() {
+                fallback_line = Some(line);
+            }
+
+            // Handle gap between lines
             if position.y > line_bounds.bottom() {
                 if let Some(next_line) = lines.peek()
                     && position.y < next_line.layout.bounds().top()
                 {
                     return Err(line.source_end);
                 }
-
-                continue;
             }
+        }
 
+        // Fall back to Y-coordinate matched line
+        if let Some(line) = fallback_line {
             return line.source_index_for_position(position);
         }
 
@@ -2188,6 +2202,17 @@ mod tests {
     use language::{Language, LanguageConfig, LanguageMatcher};
     use std::sync::Arc;
 
+    fn ensure_theme_initialized(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            if !cx.has_global::<settings::SettingsStore>() {
+                settings::init(cx);
+            }
+            if !cx.has_global::<theme::GlobalTheme>() {
+                theme::init(theme::LoadThemes::JustBase, cx);
+            }
+        });
+    }
+
     #[gpui::test]
     fn test_mappings(cx: &mut TestAppContext) {
         // Formatting.
@@ -2256,6 +2281,8 @@ mod tests {
             }
         }
 
+        ensure_theme_initialized(cx);
+
         let (_, cx) = cx.add_window_view(|_, _| TestWindow);
         let markdown =
             cx.new(|cx| Markdown::new(markdown.to_string().into(), language_registry, None, cx));
@@ -2413,6 +2440,28 @@ mod tests {
         assert_eq!(selected_text, "code");
     }
 
+    #[gpui::test]
+    fn test_table_column_selection(cx: &mut TestAppContext) {
+        let rendered = render_markdown("| a | b |\n|---|---|\n| c | d |", cx);
+
+        assert!(rendered.lines.len() >= 2);
+        let first_bounds = rendered.lines[0].layout.bounds();
+        let second_bounds = rendered.lines[1].layout.bounds();
+
+        let first_index = match rendered.source_index_for_position(first_bounds.center()) {
+            Ok(index) | Err(index) => index,
+        };
+        let second_index = match rendered.source_index_for_position(second_bounds.center()) {
+            Ok(index) | Err(index) => index,
+        };
+
+        let first_word = rendered.text_for_range(rendered.surrounding_word_range(first_index));
+        let second_word = rendered.text_for_range(rendered.surrounding_word_range(second_index));
+
+        assert_eq!(first_word, "a");
+        assert_eq!(second_word, "b");
+    }
+
     #[gpui::test]
     fn test_inline_code_word_selection_excludes_backticks(cx: &mut TestAppContext) {
         // Test that double-clicking on inline code selects just the code content,