From 72318df4b579cf56dbc97feb583cebeeb4283d00 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 26 Mar 2025 12:38:22 +0100 Subject: [PATCH] lsp: Add support for `textDocument/documentSymbol` (#27488) 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, pub responses: ipc::IpcReceiver, } #[derive(Debug, Serialize, Deserialize)] pub enum CliRequest { Open { paths: Vec, urls: Vec, wait: bool, open_new_workspace: Option, env: Option>, }, } #[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 --- 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(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 96e3694eaf..d0f99dc50b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -304,6 +304,7 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_find_search_candidates_request) .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 486b431547..69cdf1fc7c 100644 --- a/crates/lsp/src/lsp.rs +++ b/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!({ diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ee2afe2f54..3a018d4f01 100644 --- a/crates/project/src/lsp_command.rs +++ b/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; + 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, + _: &App, + ) -> Result { + 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, + _: Entity, + _: Entity, + _: LanguageServerId, + _: AsyncApp, + ) -> Result> { + 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::>() + }) + .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, + buffer: Entity, + mut cx: AsyncApp, + ) -> Result { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + })? + .await?; + Ok(Self) + } + + fn response_to_proto( + response: Vec, + _: &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::(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::>(); + + proto::GetDocumentSymbolsResponse { symbols } + } + + async fn response_from_proto( + self, + message: proto::GetDocumentSymbolsResponse, + _: Entity, + _: Entity, + _: AsyncApp, + ) -> Result> { + 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 { + let kind = + unsafe { mem::transmute::(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::>(), + }) + } + + symbols.push(deserialize_symbol_with_children(serialized_symbol)?); + } + + Ok(symbols) + } + + fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result { + BufferId::new(message.buffer_id) + } +} + #[async_trait(?Send)] impl LspCommand for GetSignatureHelp { type Response = Option; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e835f9e311..9f466b3cae 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3432,6 +3432,7 @@ impl LspStore { client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ea987a4fff..1f6f771831 100644 --- a/crates/project/src/project.rs +++ b/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>, + pub selection_range: Range>, + pub children: Vec, +} + #[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, + cx: &mut Context, + ) -> Task>> { + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::FirstCapable, + GetDocumentSymbols, + cx, + ) + } + pub fn symbols(&self, query: &str, cx: &mut Context) -> Task>> { self.lsp_store .update(cx, |lsp_store, cx| lsp_store.symbols(query, cx)) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 276a637868..0e6cb76e59 100644 --- a/crates/proto/proto/zed.proto +++ b/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; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 72e476cd95..f9d3baa3df 100644 --- a/crates/proto/src/proto.rs +++ b/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,