Use document symbols' ranges to derive their outline labels (#48978)

Kirill Bulatov created

Change summary

crates/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 
crates/project/src/lsp_store/document_symbols.rs | 70 +++++++++++++++--
5 files changed, 73 insertions(+), 22 deletions(-)

Detailed changes

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<_>>(),
+            vec!["main.rs", "struct Foo"],
             "Client B should see file path and LSP symbol 'Foo' via remote project"
         );
     });

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"]
             );
         });
     }

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!(

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<usize>>, Range<Anchor>)> {
+    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]