diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 19cd920021ce3aab0ba033df8925085c001484f4..a3256907ac410b1bfe9c40006f76b07e9a8390e6 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1829,6 +1829,40 @@ impl RenderedLine { mapping.source_index + (rendered_index - mapping.rendered_index) } + /// Returns the source index for use as an exclusive range end at a word/selection boundary. + /// When the rendered index is exactly at the start of a segment with a gap from the previous + /// segment (e.g., after stripped markdown syntax like backticks), this returns the end of the + /// previous segment rather than the start of the current one. + fn source_index_for_exclusive_rendered_end(&self, rendered_index: usize) -> usize { + if rendered_index >= self.layout.len() { + return self.source_end; + } + + let ix = match self + .source_mappings + .binary_search_by_key(&rendered_index, |probe| probe.rendered_index) + { + Ok(ix) => ix, + Err(ix) => { + return self.source_mappings[ix - 1].source_index + + (rendered_index - self.source_mappings[ix - 1].rendered_index); + } + }; + + // Exact match at the start of a segment. Check if there's a gap from the previous segment. + if ix > 0 { + let prev_mapping = &self.source_mappings[ix - 1]; + let mapping = &self.source_mappings[ix]; + let prev_segment_len = mapping.rendered_index - prev_mapping.rendered_index; + let prev_source_end = prev_mapping.source_index + prev_segment_len; + if prev_source_end < mapping.source_index { + return prev_source_end; + } + } + + self.source_mappings[ix].source_index + } + fn source_index_for_position(&self, position: Point) -> Result { let line_rendered_index; let out_of_bounds; @@ -1935,7 +1969,7 @@ impl RenderedText { }; return line.source_index_for_rendered_index(line_rendered_start + previous_space) - ..line.source_index_for_rendered_index(line_rendered_start + next_space); + ..line.source_index_for_exclusive_rendered_end(line_rendered_start + next_space); } source_index..source_index @@ -2208,6 +2242,31 @@ mod tests { assert_eq!(selected_text, "code"); } + #[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, + // not the backticks. This verifies the fix for the bug where selecting + // inline code would include the trailing backtick. + let rendered = render_markdown("use `blah` here", cx); + + // Source layout: "use `blah` here" + // 0123456789... + // The inline code "blah" is at source positions 5-8 (content range 5..9) + + // Click inside "blah" - should select just "blah", not "blah`" + let word_range = rendered.surrounding_word_range(6); // 'l' in "blah" + + // text_for_range extracts from the rendered text (without backticks), so it + // would return "blah" even with a wrong source range. We check it anyway. + let selected_text = rendered.text_for_range(word_range.clone()); + assert_eq!(selected_text, "blah"); + + // The source range is what matters for copy_as_markdown and selected_text, + // which extract directly from the source. With the bug, this would be 5..10 + // which includes the closing backtick at position 9. + assert_eq!(word_range, 5..9); + } + #[gpui::test] fn test_all_selection(cx: &mut TestAppContext) { let rendered = render_markdown("Hello world\n\nThis is a test\n\nwith multiple lines", cx);