From c6cd08e37a31a89b87867ca6d3dc84a71919fce7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 12 Feb 2026 09:36:48 +0200 Subject: [PATCH] Use document symbols' ranges to derive their outline labels (#48978) --- .../collab/tests/integration/editor_tests.rs | 15 ++-- crates/editor/src/document_symbols.rs | 6 +- crates/outline/src/outline.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 2 +- .../project/src/lsp_store/document_symbols.rs | 70 ++++++++++++++++--- 5 files changed, 73 insertions(+), 22 deletions(-) diff --git a/crates/collab/tests/integration/editor_tests.rs b/crates/collab/tests/integration/editor_tests.rs index 596d857729da4c7d37881567f5e90013e403d5ca..a48e43741641b92cedaf4e6d4d6bd80ad6f68c19 100644 --- a/crates/collab/tests/integration/editor_tests.rs +++ b/crates/collab/tests/integration/editor_tests.rs @@ -5649,7 +5649,7 @@ async fn test_document_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let texts: Vec<_> = breadcrumbs.iter().map(|b| b.text.as_str()).collect(); assert_eq!( texts, - vec!["main.rs", "Foo"], + vec!["main.rs", "struct Foo"], "Host should see file path and LSP symbol 'Foo' in breadcrumbs" ); }); @@ -5675,13 +5675,14 @@ async fn test_document_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont executor.run_until_parked(); editor_b.update(cx_b, |editor, cx| { - let breadcrumbs = editor - .breadcrumbs(cx) - .expect("Client B should have breadcrumbs"); - let texts: Vec<_> = breadcrumbs.iter().map(|b| b.text.as_str()).collect(); assert_eq!( - texts, - vec!["main.rs", "Foo"], + editor + .breadcrumbs(cx) + .expect("Client B should have breadcrumbs") + .iter() + .map(|b| b.text.as_str()) + .collect::>(), + vec!["main.rs", "struct Foo"], "Client B should see file path and LSP symbol 'Foo' via remote project" ); }); diff --git a/crates/editor/src/document_symbols.rs b/crates/editor/src/document_symbols.rs index 7e1586baffb78f3814f46de6f0b739f965c9fcc3..3d26a15800505cca4beff337e425803f8d0b567e 100644 --- a/crates/editor/src/document_symbols.rs +++ b/crates/editor/src/document_symbols.rs @@ -476,7 +476,7 @@ mod tests { cx.run_until_parked(); cx.update_editor(|editor, _window, _cx| { - assert_eq!(outline_symbol_names(editor), vec!["main"]); + assert_eq!(outline_symbol_names(editor), vec!["fn main"]); }); } @@ -533,7 +533,7 @@ mod tests { cx.update_editor(|editor, _window, _cx| { assert_eq!( outline_symbol_names(editor), - vec!["Foo", "bar"], + vec!["struct Foo", "bar"], "cursor is inside Foo > bar, so we expect the containing chain" ); }); @@ -762,7 +762,7 @@ mod tests { cx.update_editor(|editor, _window, _cx| { assert_eq!( outline_symbol_names(editor), - vec!["MyModule", "my_function"] + vec!["mod MyModule", "fn my_function"] ); }); } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 6ffb381148edaafcc375c26e6257106246ee58ae..bfe62863fbc2a0d5fb7d3974c241f0ad5d934ec1 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -788,7 +788,7 @@ mod tests { let lsp_names = outline_names(&outline_view, cx); assert_eq!( lsp_names, - vec!["Foo", "bar", "lsp_only_field"], + vec!["struct Foo", "bar", "lsp_only_field"], "Step 2: LSP-provided symbols should be displayed" ); assert_eq!( diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index aa4b5b2acec9fdafe79fef970547191ae5c17036..a6cce4e6845548c2615a755ad3c8e6e226be1110 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -8062,7 +8062,7 @@ outline: struct Foo <==== selected ), indoc!( " -outline: Foo <==== selected +outline: struct Foo <==== selected outline: bar outline: lsp_only_field" ), diff --git a/crates/project/src/lsp_store/document_symbols.rs b/crates/project/src/lsp_store/document_symbols.rs index b18d27a889554c19e0cbeb7c1ae863656dbe3efe..cfac24fd1511bf0ada1c6a59ade0017282b3568d 100644 --- a/crates/project/src/lsp_store/document_symbols.rs +++ b/crates/project/src/lsp_store/document_symbols.rs @@ -1,3 +1,4 @@ +use std::ops::Range; use std::sync::Arc; use std::time::Duration; @@ -11,7 +12,7 @@ use itertools::Itertools; use language::{Buffer, BufferSnapshot, OutlineItem}; use lsp::LanguageServerId; use settings::Settings as _; -use text::{Anchor, Bias}; +use text::{Anchor, Bias, PointUtf16}; use util::ResultExt; use crate::DocumentSymbol; @@ -255,13 +256,23 @@ fn flatten_document_symbols( let selection_range = snapshot.anchor_after(selection_start)..snapshot.anchor_before(selection_end); - let text = symbol.name.clone(); - let name_ranges = vec![0..text.len()]; + let (text, name_ranges, source_range_for_text) = enriched_symbol_text( + &symbol.name, + start, + selection_start, + selection_end, + snapshot, + ) + .unwrap_or_else(|| { + let name = symbol.name.clone(); + let name_len = name.len(); + (name, vec![0..name_len], selection_range.clone()) + }); output.push(OutlineItem { depth, range, - source_range_for_text: selection_range, + source_range_for_text, text, highlight_ranges: Vec::new(), name_ranges, @@ -275,6 +286,45 @@ fn flatten_document_symbols( } } +/// Tries to build an enriched label by including buffer text from the symbol +/// range start to the selection range end (e.g., "struct Foo" instead of just "Foo"). +/// Only uses same-line prefix to avoid pulling in attributes/decorators. +fn enriched_symbol_text( + name: &str, + range_start: PointUtf16, + selection_start: PointUtf16, + selection_end: PointUtf16, + snapshot: &BufferSnapshot, +) -> Option<(String, Vec>, Range)> { + let text_start = if range_start.row == selection_start.row { + range_start + } else { + PointUtf16::new(selection_start.row, 0) + }; + + let start_offset = snapshot.point_utf16_to_offset(text_start); + let end_offset = snapshot.point_utf16_to_offset(selection_end); + if start_offset >= end_offset { + return None; + } + + let raw: String = snapshot.text_for_range(start_offset..end_offset).collect(); + let trimmed = raw.trim_start(); + if trimmed.len() <= name.len() || !trimmed.ends_with(name) { + return None; + } + + let name_start = trimmed.len() - name.len(); + let leading_ws = raw.len() - trimmed.len(); + let adjusted_start = start_offset + leading_ws; + + Some(( + trimmed.to_string(), + vec![name_start..trimmed.len()], + snapshot.anchor_after(adjusted_start)..snapshot.anchor_before(end_offset), + )) +} + #[cfg(test)] mod tests { use super::*; @@ -372,8 +422,8 @@ mod tests { assert_eq!(items.len(), 5); assert_eq!(items[0].depth, 0); - assert_eq!(items[0].text, "Foo"); - assert_eq!(items[0].name_ranges, vec![0..3]); + assert_eq!(items[0].text, "struct Foo"); + assert_eq!(items[0].name_ranges, vec![7..10]); assert_eq!(items[1].depth, 1); assert_eq!(items[1].text, "bar"); @@ -384,12 +434,12 @@ mod tests { assert_eq!(items[2].name_ranges, vec![0..3]); assert_eq!(items[3].depth, 0); - assert_eq!(items[3].text, "Foo"); - assert_eq!(items[3].name_ranges, vec![0..3]); + assert_eq!(items[3].text, "impl Foo"); + assert_eq!(items[3].name_ranges, vec![5..8]); assert_eq!(items[4].depth, 1); - assert_eq!(items[4].text, "new"); - assert_eq!(items[4].name_ranges, vec![0..3]); + assert_eq!(items[4].text, "fn new"); + assert_eq!(items[4].name_ranges, vec![3..6]); } #[gpui::test]