lsp: Add support for `textDocument/documentSymbol` (#27488)

Bennet Bo Fenner created

This PR adds support for retrieving the outline of a specific
buffer/document from the LSP.
E.g. for this code (`crates/cli/src/cli.rs`):
```rs
use collections::HashMap;
pub use ipc_channel::ipc;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct IpcHandshake {
    pub requests: ipc::IpcSender<CliRequest>,
    pub responses: ipc::IpcReceiver<CliResponse>,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest {
    Open {
        paths: Vec<String>,
        urls: Vec<String>,
        wait: bool,
        open_new_workspace: Option<bool>,
        env: Option<HashMap<String, String>>,
    },
}

#[derive(Debug, Serialize, Deserialize)]
pub enum CliResponse {
    Ping,
    Stdout { message: String },
    Stderr { message: String },
    Exit { status: i32 },
}

/// When Zed started not as an *.app but as a binary (e.g. local development),
/// there's a possibility to tell it to behave "regularly".
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";
```

Rust-analyzer responds with:
```
Symbol: 'IpcHandshake' - Struct - (4:0-8:1) (5:11-5:23)
  Symbol: 'requests' - Field - (6:4-6:44) (6:8-6:16)
  Symbol: 'responses' - Field - (7:4-7:48) (7:8-7:17)
Symbol: 'CliRequest' - Enum - (10:0-19:1) (11:9-11:19)
  Symbol: 'Open' - EnumMember - (12:4-18:5) (12:4-12:8)
    Symbol: 'paths' - Field - (13:8-13:26) (13:8-13:13)
    Symbol: 'urls' - Field - (14:8-14:25) (14:8-14:12)
    Symbol: 'wait' - Field - (15:8-15:18) (15:8-15:12)
    Symbol: 'open_new_workspace' - Field - (16:8-16:40) (16:8-16:26)
    Symbol: 'env' - Field - (17:8-17:44) (17:8-17:11)
Symbol: 'CliResponse' - Enum - (21:0-27:1) (22:9-22:20)
  Symbol: 'Ping' - EnumMember - (23:4-23:8) (23:4-23:8)
  Symbol: 'Stdout' - EnumMember - (24:4-24:30) (24:4-24:10)
    Symbol: 'message' - Field - (24:13-24:28) (24:13-24:20)
  Symbol: 'Stderr' - EnumMember - (25:4-25:30) (25:4-25:10)
    Symbol: 'message' - Field - (25:13-25:28) (25:13-25:20)
  Symbol: 'Exit' - EnumMember - (26:4-26:24) (26:4-26:8)
    Symbol: 'status' - Field - (26:11-26:22) (26:11-26:17)
Symbol: 'FORCE_CLI_MODE_ENV_VAR_NAME' - Constant - (29:0-31:67) (31:10-31:37)
```

We'll use this to reference specific symbols in assistant2

Release Notes:

- N/A

Change summary

crates/collab/src/rpc.rs          |   1 
crates/lsp/src/lsp.rs             |   4 
crates/project/src/lsp_command.rs | 212 ++++++++++++++++++++++++++++++++
crates/project/src/lsp_store.rs   |   1 
crates/project/src/project.rs     |  22 +++
crates/proto/proto/zed.proto      |  27 ++++
crates/proto/src/proto.rs         |   4 
7 files changed, 265 insertions(+), 6 deletions(-)

Detailed changes

crates/collab/src/rpc.rs 🔗

@@ -304,6 +304,7 @@ impl Server {
             .add_request_handler(forward_read_only_project_request::<proto::GetReferences>)
             .add_request_handler(forward_find_search_candidates_request)
             .add_request_handler(forward_read_only_project_request::<proto::GetDocumentHighlights>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetDocumentSymbols>)
             .add_request_handler(forward_read_only_project_request::<proto::GetProjectSymbols>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)

crates/lsp/src/lsp.rs 🔗

@@ -774,6 +774,10 @@ impl LanguageServer {
                     code_lens: Some(CodeLensClientCapabilities {
                         dynamic_registration: Some(false),
                     }),
+                    document_symbol: Some(DocumentSymbolClientCapabilities {
+                        hierarchical_document_symbol_support: Some(true),
+                        ..DocumentSymbolClientCapabilities::default()
+                    }),
                     ..TextDocumentClientCapabilities::default()
                 }),
                 experimental: Some(json!({

crates/project/src/lsp_command.rs 🔗

@@ -2,10 +2,10 @@ mod signature_help;
 
 use crate::{
     lsp_store::{LocalLspStore, LspStore},
-    CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, Hover, HoverBlock,
-    HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip,
-    InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent, PrepareRenameResponse,
-    ProjectTransaction, ResolveState,
+    CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, DocumentSymbol, Hover,
+    HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart,
+    InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent,
+    PrepareRenameResponse, ProjectTransaction, ResolveState,
 };
 use anyhow::{anyhow, Context as _, Result};
 use async_trait::async_trait;
@@ -28,7 +28,7 @@ use lsp::{
     ServerCapabilities,
 };
 use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
-use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
+use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
 use text::{BufferId, LineEnding};
 
 pub use signature_help::SignatureHelp;
@@ -199,6 +199,9 @@ pub(crate) struct GetDocumentHighlights {
     pub position: PointUtf16,
 }
 
+#[derive(Debug, Copy, Clone)]
+pub(crate) struct GetDocumentSymbols;
+
 #[derive(Clone, Debug)]
 pub(crate) struct GetSignatureHelp {
     pub position: PointUtf16,
@@ -1488,6 +1491,205 @@ impl LspCommand for GetDocumentHighlights {
     }
 }
 
+#[async_trait(?Send)]
+impl LspCommand for GetDocumentSymbols {
+    type Response = Vec<DocumentSymbol>;
+    type LspRequest = lsp::request::DocumentSymbolRequest;
+    type ProtoRequest = proto::GetDocumentSymbols;
+
+    fn display_name(&self) -> &str {
+        "Get document symbols"
+    }
+
+    fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
+        capabilities
+            .server_capabilities
+            .document_symbol_provider
+            .is_some()
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &App,
+    ) -> Result<lsp::DocumentSymbolParams> {
+        Ok(lsp::DocumentSymbolParams {
+            text_document: make_text_document_identifier(path)?,
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        })
+    }
+
+    async fn response_from_lsp(
+        self,
+        lsp_symbols: Option<lsp::DocumentSymbolResponse>,
+        _: Entity<LspStore>,
+        _: Entity<Buffer>,
+        _: LanguageServerId,
+        _: AsyncApp,
+    ) -> Result<Vec<DocumentSymbol>> {
+        let Some(lsp_symbols) = lsp_symbols else {
+            return Ok(Vec::new());
+        };
+
+        let symbols: Vec<_> = match lsp_symbols {
+            lsp::DocumentSymbolResponse::Flat(symbol_information) => symbol_information
+                .into_iter()
+                .map(|lsp_symbol| DocumentSymbol {
+                    name: lsp_symbol.name,
+                    kind: lsp_symbol.kind,
+                    range: range_from_lsp(lsp_symbol.location.range),
+                    selection_range: range_from_lsp(lsp_symbol.location.range),
+                    children: Vec::new(),
+                })
+                .collect(),
+            lsp::DocumentSymbolResponse::Nested(nested_responses) => {
+                fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol {
+                    DocumentSymbol {
+                        name: lsp_symbol.name,
+                        kind: lsp_symbol.kind,
+                        range: range_from_lsp(lsp_symbol.range),
+                        selection_range: range_from_lsp(lsp_symbol.selection_range),
+                        children: lsp_symbol
+                            .children
+                            .map(|children| {
+                                children.into_iter().map(convert_symbol).collect::<Vec<_>>()
+                            })
+                            .unwrap_or_default(),
+                    }
+                }
+                nested_responses.into_iter().map(convert_symbol).collect()
+            }
+        };
+        Ok(symbols)
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentSymbols {
+        proto::GetDocumentSymbols {
+            project_id,
+            buffer_id: buffer.remote_id().into(),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetDocumentSymbols,
+        _: Entity<LspStore>,
+        buffer: Entity<Buffer>,
+        mut cx: AsyncApp,
+    ) -> Result<Self> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self)
+    }
+
+    fn response_to_proto(
+        response: Vec<DocumentSymbol>,
+        _: &mut LspStore,
+        _: PeerId,
+        _: &clock::Global,
+        _: &mut App,
+    ) -> proto::GetDocumentSymbolsResponse {
+        let symbols = response
+            .into_iter()
+            .map(|symbol| {
+                fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol {
+                    proto::DocumentSymbol {
+                        name: symbol.name.clone(),
+                        kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
+                        start: Some(proto::PointUtf16 {
+                            row: symbol.range.start.0.row,
+                            column: symbol.range.start.0.column,
+                        }),
+                        end: Some(proto::PointUtf16 {
+                            row: symbol.range.end.0.row,
+                            column: symbol.range.end.0.column,
+                        }),
+                        selection_start: Some(proto::PointUtf16 {
+                            row: symbol.selection_range.start.0.row,
+                            column: symbol.selection_range.start.0.column,
+                        }),
+                        selection_end: Some(proto::PointUtf16 {
+                            row: symbol.selection_range.end.0.row,
+                            column: symbol.selection_range.end.0.column,
+                        }),
+                        children: symbol
+                            .children
+                            .into_iter()
+                            .map(convert_symbol_to_proto)
+                            .collect(),
+                    }
+                }
+                convert_symbol_to_proto(symbol)
+            })
+            .collect::<Vec<_>>();
+
+        proto::GetDocumentSymbolsResponse { symbols }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetDocumentSymbolsResponse,
+        _: Entity<LspStore>,
+        _: Entity<Buffer>,
+        _: AsyncApp,
+    ) -> Result<Vec<DocumentSymbol>> {
+        let mut symbols = Vec::with_capacity(message.symbols.len());
+        for serialized_symbol in message.symbols {
+            fn deserialize_symbol_with_children(
+                serialized_symbol: proto::DocumentSymbol,
+            ) -> Result<DocumentSymbol> {
+                let kind =
+                    unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
+
+                let start = serialized_symbol
+                    .start
+                    .ok_or_else(|| anyhow!("invalid start"))?;
+                let end = serialized_symbol
+                    .end
+                    .ok_or_else(|| anyhow!("invalid end"))?;
+
+                let selection_start = serialized_symbol
+                    .selection_start
+                    .ok_or_else(|| anyhow!("invalid selection start"))?;
+                let selection_end = serialized_symbol
+                    .selection_end
+                    .ok_or_else(|| anyhow!("invalid selection end"))?;
+
+                Ok(DocumentSymbol {
+                    name: serialized_symbol.name,
+                    kind,
+                    range: Unclipped(PointUtf16::new(start.row, start.column))
+                        ..Unclipped(PointUtf16::new(end.row, end.column)),
+                    selection_range: Unclipped(PointUtf16::new(
+                        selection_start.row,
+                        selection_start.column,
+                    ))
+                        ..Unclipped(PointUtf16::new(selection_end.row, selection_end.column)),
+                    children: serialized_symbol
+                        .children
+                        .into_iter()
+                        .filter_map(|symbol| deserialize_symbol_with_children(symbol).ok())
+                        .collect::<Vec<_>>(),
+                })
+            }
+
+            symbols.push(deserialize_symbol_with_children(serialized_symbol)?);
+        }
+
+        Ok(symbols)
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result<BufferId> {
+        BufferId::new(message.buffer_id)
+    }
+}
+
 #[async_trait(?Send)]
 impl LspCommand for GetSignatureHelp {
     type Response = Option<SignatureHelp>;

crates/project/src/lsp_store.rs 🔗

@@ -3432,6 +3432,7 @@ impl LspStore {
         client.add_entity_request_handler(Self::handle_lsp_command::<GetDeclaration>);
         client.add_entity_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
         client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
+        client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentSymbols>);
         client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
         client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
         client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);

crates/project/src/project.rs 🔗

@@ -659,6 +659,15 @@ pub struct Symbol {
     pub signature: [u8; 32],
 }
 
+#[derive(Clone, Debug)]
+pub struct DocumentSymbol {
+    pub name: String,
+    pub kind: lsp::SymbolKind,
+    pub range: Range<Unclipped<PointUtf16>>,
+    pub selection_range: Range<Unclipped<PointUtf16>>,
+    pub children: Vec<DocumentSymbol>,
+}
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct HoverBlock {
     pub text: String,
@@ -3222,6 +3231,19 @@ impl Project {
         self.document_highlights_impl(buffer, position, cx)
     }
 
+    pub fn document_symbols(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Vec<DocumentSymbol>>> {
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::FirstCapable,
+            GetDocumentSymbols,
+            cx,
+        )
+    }
+
     pub fn symbols(&self, query: &str, cx: &mut Context<Self>) -> Task<Result<Vec<Symbol>>> {
         self.lsp_store
             .update(cx, |lsp_store, cx| lsp_store.symbols(query, cx))

crates/proto/proto/zed.proto 🔗

@@ -358,7 +358,10 @@ message Envelope {
         BreakpointsForFile breakpoints_for_file = 327;
 
         UpdateRepository update_repository = 328;
-        RemoveRepository remove_repository = 329; // current max
+        RemoveRepository remove_repository = 329;
+
+        GetDocumentSymbols get_document_symbols = 330;
+        GetDocumentSymbolsResponse get_document_symbols_response = 331; // current max
     }
 
     reserved 87 to 88;
@@ -847,6 +850,28 @@ message Symbol {
     uint64 language_server_id = 10;
 }
 
+message GetDocumentSymbols {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    repeated VectorClockEntry version = 3;
+}
+
+message GetDocumentSymbolsResponse {
+    repeated DocumentSymbol symbols = 1;
+}
+
+message DocumentSymbol {
+    string name = 1;
+    int32 kind = 2;
+    // Cannot use generate anchors for unopened files,
+    // so we are forced to use point coords instead
+    PointUtf16 start = 3;
+    PointUtf16 end = 4;
+    PointUtf16 selection_start = 5;
+    PointUtf16 selection_end = 6;
+    repeated DocumentSymbol children = 7;
+}
+
 message OpenBufferForSymbol {
     uint64 project_id = 1;
     Symbol symbol = 2;

crates/proto/src/proto.rs 🔗

@@ -274,6 +274,8 @@ messages!(
     (GetDefinitionResponse, Background),
     (GetDocumentHighlights, Background),
     (GetDocumentHighlightsResponse, Background),
+    (GetDocumentSymbols, Background),
+    (GetDocumentSymbolsResponse, Background),
     (GetHover, Background),
     (GetHoverResponse, Background),
     (GetNotifications, Foreground),
@@ -504,6 +506,7 @@ request_messages!(
     (GetDeclaration, GetDeclarationResponse),
     (GetImplementation, GetImplementationResponse),
     (GetDocumentHighlights, GetDocumentHighlightsResponse),
+    (GetDocumentSymbols, GetDocumentSymbolsResponse),
     (GetHover, GetHoverResponse),
     (GetLlmToken, GetLlmTokenResponse),
     (GetNotifications, GetNotificationsResponse),
@@ -650,6 +653,7 @@ entity_messages!(
     GetDeclaration,
     GetImplementation,
     GetDocumentHighlights,
+    GetDocumentSymbols,
     GetHover,
     GetProjectSymbols,
     GetReferences,