Allow opening of buffers associated with a project symbol

Antonio Scandurra created

Change summary

Cargo.lock                                    |   2 
crates/project/src/lsp_command.rs             |   2 
crates/project/src/project.rs                 | 180 +++++++++++++-------
crates/project_symbols/Cargo.toml             |   2 
crates/project_symbols/src/project_symbols.rs |  48 ++++
crates/rpc/proto/zed.proto                    | 113 +++++++-----
crates/rpc/src/proto.rs                       |   6 
crates/server/src/rpc.rs                      |  15 +
8 files changed, 242 insertions(+), 126 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3563,9 +3563,11 @@ dependencies = [
 name = "project_symbols"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "editor",
  "fuzzy",
  "gpui",
+ "language",
  "ordered-float",
  "postage",
  "project",

crates/project/src/lsp_command.rs 🔗

@@ -342,7 +342,7 @@ impl LspCommand for GetDefinition {
             for (target_uri, target_range) in unresolved_locations {
                 let target_buffer_handle = project
                     .update(&mut cx, |this, cx| {
-                        this.open_local_buffer_from_lsp_path(
+                        this.open_local_buffer_via_lsp(
                             target_uri,
                             language.name().to_string(),
                             language_server.clone(),

crates/project/src/project.rs 🔗

@@ -118,8 +118,10 @@ pub struct Definition {
     pub target_range: Range<language::Anchor>,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Symbol {
+    pub worktree_id: WorktreeId,
+    pub language_name: String,
     pub label: CodeLabel,
     pub lsp_symbol: lsp::SymbolInformation,
 }
@@ -193,6 +195,7 @@ impl Project {
         client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
         client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
         client.add_entity_request_handler(Self::handle_get_project_symbols);
+        client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
         client.add_entity_request_handler(Self::handle_open_buffer);
         client.add_entity_request_handler(Self::handle_save_buffer);
     }
@@ -653,7 +656,7 @@ impl Project {
         })
     }
 
-    fn open_local_buffer_from_lsp_path(
+    fn open_local_buffer_via_lsp(
         &mut self,
         abs_path: lsp::Url,
         lang_name: String,
@@ -1223,22 +1226,18 @@ impl Project {
         self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
     }
 
-    pub fn symbols(
-        &self,
-        query: &str,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<Result<HashMap<String, Vec<Symbol>>>> {
+    pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
         if self.is_local() {
             let mut language_servers = HashMap::default();
-            for ((_, language_name), language_server) in self.language_servers.iter() {
+            for ((worktree_id, language_name), language_server) in self.language_servers.iter() {
                 let language = self.languages.get_language(language_name).unwrap();
                 language_servers
                     .entry(Arc::as_ptr(language_server))
-                    .or_insert((language_server.clone(), language.clone()));
+                    .or_insert((language_server.clone(), *worktree_id, language.clone()));
             }
 
             let mut requests = Vec::new();
-            for (language_server, _) in language_servers.values() {
+            for (language_server, _, _) in language_servers.values() {
                 requests.push(language_server.request::<lsp::request::WorkspaceSymbol>(
                     lsp::WorkspaceSymbolParams {
                         query: query.to_string(),
@@ -1249,17 +1248,21 @@ impl Project {
 
             cx.foreground().spawn(async move {
                 let responses = futures::future::try_join_all(requests).await?;
-                let mut symbols = HashMap::default();
-                for ((_, language), lsp_symbols) in language_servers.into_values().zip(responses) {
-                    let language_symbols = symbols
-                        .entry(language.name().to_string())
-                        .or_insert(Vec::new());
-                    for lsp_symbol in lsp_symbols.into_iter().flatten() {
+                let mut symbols = Vec::new();
+                for ((_, worktree_id, language), lsp_symbols) in
+                    language_servers.into_values().zip(responses)
+                {
+                    symbols.extend(lsp_symbols.into_iter().flatten().map(|lsp_symbol| {
                         let label = language
                             .label_for_symbol(&lsp_symbol)
                             .unwrap_or_else(|| CodeLabel::plain(lsp_symbol.name.clone(), None));
-                        language_symbols.push(Symbol { label, lsp_symbol });
-                    }
+                        Symbol {
+                            worktree_id,
+                            language_name: language.name().to_string(),
+                            label,
+                            lsp_symbol,
+                        }
+                    }));
                 }
                 Ok(symbols)
             })
@@ -1270,40 +1273,15 @@ impl Project {
             });
             cx.spawn_weak(|this, cx| async move {
                 let response = request.await?;
-                let mut symbols = HashMap::default();
+                let mut symbols = Vec::new();
                 if let Some(this) = this.upgrade(&cx) {
                     this.read_with(&cx, |this, _| {
-                        let mut serialized_symbols = response.symbols.into_iter();
-                        for (language_name, symbol_count) in response
-                            .languages
-                            .into_iter()
-                            .zip(response.symbol_counts_per_language)
-                        {
-                            let language = this.languages.get_language(&language_name);
-                            let language_symbols =
-                                symbols.entry(language_name).or_insert(Vec::new());
-                            language_symbols.extend(
-                                serialized_symbols
-                                    .by_ref()
-                                    .take(symbol_count as usize)
-                                    .filter_map(|serialized_symbol| {
-                                        let lsp_symbol =
-                                            serde_json::from_slice(&serialized_symbol.lsp_symbol)
-                                                .log_err()?;
-                                        Some(Symbol {
-                                            label: language
-                                                .and_then(|language| {
-                                                    language.label_for_symbol(&lsp_symbol)
-                                                })
-                                                .unwrap_or(CodeLabel::plain(
-                                                    lsp_symbol.name.clone(),
-                                                    None,
-                                                )),
-                                            lsp_symbol,
-                                        })
-                                    }),
-                            );
-                        }
+                        symbols.extend(
+                            response
+                                .symbols
+                                .into_iter()
+                                .filter_map(|symbol| this.deserialize_symbol(symbol).log_err()),
+                        );
                     })
                 }
                 Ok(symbols)
@@ -1313,6 +1291,45 @@ impl Project {
         }
     }
 
+    pub fn open_buffer_for_symbol(
+        &mut self,
+        symbol: &Symbol,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        if self.is_local() {
+            let language_server = if let Some(server) = self
+                .language_servers
+                .get(&(symbol.worktree_id, symbol.language_name.clone()))
+            {
+                server.clone()
+            } else {
+                return Task::ready(Err(anyhow!(
+                    "language server for worktree and language not found"
+                )));
+            };
+
+            self.open_local_buffer_via_lsp(
+                symbol.lsp_symbol.location.uri.clone(),
+                symbol.language_name.clone(),
+                language_server,
+                cx,
+            )
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self.client.request(proto::OpenBufferForSymbol {
+                project_id,
+                symbol: Some(serialize_symbol(symbol)),
+            });
+            cx.spawn(|this, mut cx| async move {
+                let response = request.await?;
+                let buffer = response.buffer.ok_or_else(|| anyhow!("invalid buffer"))?;
+                this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+                    .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
     pub fn completions<T: ToPointUtf16>(
         &self,
         source_buffer_handle: &ModelHandle<Buffer>,
@@ -1785,7 +1802,7 @@ impl Project {
                 lsp::DocumentChangeOperation::Edit(op) => {
                     let buffer_to_edit = this
                         .update(cx, |this, cx| {
-                            this.open_local_buffer_from_lsp_path(
+                            this.open_local_buffer_via_lsp(
                                 op.text_document.uri,
                                 language_name.clone(),
                                 language_server.clone(),
@@ -2634,21 +2651,31 @@ impl Project {
             })
             .await?;
 
-        let mut languages = Vec::new();
-        let mut symbol_counts_per_language = Vec::new();
-        let mut serialized_symbols = Vec::new();
-        for (language_name, language_symbols) in symbols {
-            languages.push(language_name);
-            symbol_counts_per_language.push(language_symbols.len() as u64);
-            serialized_symbols.extend(language_symbols.into_iter().map(|symbol| proto::Symbol {
-                lsp_symbol: serde_json::to_vec(&symbol.lsp_symbol).unwrap(),
-            }));
-        }
-
         Ok(proto::GetProjectSymbolsResponse {
-            languages,
-            symbol_counts_per_language,
-            symbols: serialized_symbols,
+            symbols: symbols.iter().map(serialize_symbol).collect(),
+        })
+    }
+
+    async fn handle_open_buffer_for_symbol(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::OpenBufferForSymbol>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OpenBufferForSymbolResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let symbol = envelope
+            .payload
+            .symbol
+            .ok_or_else(|| anyhow!("invalid symbol"))?;
+        let symbol = this.read_with(&cx, |this, _| this.deserialize_symbol(symbol))?;
+        let buffer = this
+            .update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))
+            .await?;
+
+        Ok(proto::OpenBufferForSymbolResponse {
+            buffer: Some(this.update(&mut cx, |this, cx| {
+                this.serialize_buffer_for_peer(&buffer, peer_id, cx)
+            })),
         })
     }
 
@@ -2813,6 +2840,21 @@ impl Project {
         })
     }
 
+    fn deserialize_symbol(&self, serialized_symbol: proto::Symbol) -> Result<Symbol> {
+        let language = self
+            .languages
+            .get_language(&serialized_symbol.language_name);
+        let lsp_symbol = serde_json::from_slice(&serialized_symbol.lsp_symbol)?;
+        Ok(Symbol {
+            worktree_id: WorktreeId::from_proto(serialized_symbol.worktree_id),
+            language_name: serialized_symbol.language_name.clone(),
+            label: language
+                .and_then(|language| language.label_for_symbol(&lsp_symbol))
+                .unwrap_or(CodeLabel::plain(lsp_symbol.name.clone(), None)),
+            lsp_symbol,
+        })
+    }
+
     async fn handle_close_buffer(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::CloseBuffer>,
@@ -3094,6 +3136,14 @@ impl From<lsp::DeleteFileOptions> for fs::RemoveOptions {
     }
 }
 
+fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
+    proto::Symbol {
+        worktree_id: symbol.worktree_id.to_proto(),
+        language_name: symbol.language_name.clone(),
+        lsp_symbol: serde_json::to_vec(&symbol.lsp_symbol).unwrap(),
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::{Event, *};

crates/project_symbols/Cargo.toml 🔗

@@ -10,10 +10,12 @@ path = "src/project_symbols.rs"
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+language = { path = "../language" }
 project = { path = "../project" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 util = { path = "../util" }
+anyhow = "1.0.38"
 ordered-float = "2.1.1"
 postage = { version = "0.4", features = ["futures-traits"] }
 smol = "1.2"

crates/project_symbols/src/project_symbols.rs 🔗

@@ -1,5 +1,6 @@
 use editor::{
-    combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Editor, EditorSettings,
+    combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label,
+    Autoscroll, Bias, Editor, EditorSettings,
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -9,6 +10,7 @@ use gpui::{
     AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
 };
+use language::range_from_lsp;
 use ordered_float::OrderedFloat;
 use postage::watch;
 use project::{Project, Symbol};
@@ -52,6 +54,7 @@ pub struct ProjectSymbolsView {
 
 pub enum Event {
     Dismissed,
+    Selected(Symbol),
 }
 
 impl Entity for ProjectSymbolsView {
@@ -170,7 +173,13 @@ impl ProjectSymbolsView {
     }
 
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Dismissed);
+        if let Some(symbol) = self
+            .matches
+            .get(self.selected_match_index)
+            .map(|mat| self.symbols[mat.candidate_id].clone())
+        {
+            cx.emit(Event::Selected(symbol));
+        }
     }
 
     fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
@@ -180,12 +189,7 @@ impl ProjectSymbolsView {
             .project
             .update(cx, |project, cx| project.symbols(&query, cx));
         self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move {
-            let symbols = symbols
-                .await
-                .log_err()?
-                .into_values()
-                .flatten()
-                .collect::<Vec<_>>();
+            let symbols = symbols.await.log_err()?;
             if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
                     this.match_candidates = symbols
@@ -336,6 +340,34 @@ impl ProjectSymbolsView {
     ) {
         match event {
             Event::Dismissed => workspace.dismiss_modal(cx),
+            Event::Selected(symbol) => {
+                let buffer = workspace
+                    .project()
+                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
+                let symbol = symbol.clone();
+                cx.spawn(|workspace, mut cx| async move {
+                    let buffer = buffer.await?;
+                    let range = range_from_lsp(symbol.lsp_symbol.location.range);
+                    workspace.update(&mut cx, |workspace, cx| {
+                        let start;
+                        let end;
+                        {
+                            let buffer = buffer.read(cx);
+                            start = buffer.clip_point_utf16(range.start, Bias::Left);
+                            end = buffer.clip_point_utf16(range.end, Bias::Left);
+                        }
+
+                        let editor = workspace.open_item(BufferItemHandle(buffer), cx);
+                        let editor = editor.downcast::<Editor>().unwrap();
+                        editor.update(cx, |editor, cx| {
+                            editor.select_ranges([start..end], Some(Autoscroll::Center), cx);
+                        });
+                    });
+                    Ok::<_, anyhow::Error>(())
+                })
+                .detach_and_log_err(cx);
+                workspace.dismiss_modal(cx);
+            }
         }
     }
 }

crates/rpc/proto/zed.proto 🔗

@@ -25,53 +25,55 @@ message Envelope {
         GetDefinitionResponse get_definition_response = 19;
         GetProjectSymbols get_project_symbols = 20;
         GetProjectSymbolsResponse get_project_symbols_response = 21;
-
-        RegisterWorktree register_worktree = 22;
-        UnregisterWorktree unregister_worktree = 23;
-        ShareWorktree share_worktree = 24;
-        UpdateWorktree update_worktree = 25;
-        UpdateDiagnosticSummary update_diagnostic_summary = 26;
-        DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 27;
-        DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 28;
-
-        OpenBuffer open_buffer = 29;
-        OpenBufferResponse open_buffer_response = 30;
-        CloseBuffer close_buffer = 31;
-        UpdateBuffer update_buffer = 32;
-        UpdateBufferFile update_buffer_file = 33;
-        SaveBuffer save_buffer = 34;
-        BufferSaved buffer_saved = 35;
-        BufferReloaded buffer_reloaded = 36;
-        FormatBuffers format_buffers = 37;
-        FormatBuffersResponse format_buffers_response = 38;
-        GetCompletions get_completions = 39;
-        GetCompletionsResponse get_completions_response = 40;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 41;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 42;
-        GetCodeActions get_code_actions = 43;
-        GetCodeActionsResponse get_code_actions_response = 44;
-        ApplyCodeAction apply_code_action = 45;
-        ApplyCodeActionResponse apply_code_action_response = 46;
-        PrepareRename prepare_rename = 47;
-        PrepareRenameResponse prepare_rename_response = 48;
-        PerformRename perform_rename = 49;
-        PerformRenameResponse perform_rename_response = 50;
-
-        GetChannels get_channels = 51;
-        GetChannelsResponse get_channels_response = 52;
-        JoinChannel join_channel = 53;
-        JoinChannelResponse join_channel_response = 54;
-        LeaveChannel leave_channel = 55;
-        SendChannelMessage send_channel_message = 56;
-        SendChannelMessageResponse send_channel_message_response = 57;
-        ChannelMessageSent channel_message_sent = 58;
-        GetChannelMessages get_channel_messages = 59;
-        GetChannelMessagesResponse get_channel_messages_response = 60;
-
-        UpdateContacts update_contacts = 61;
-
-        GetUsers get_users = 62;
-        GetUsersResponse get_users_response = 63;
+        OpenBufferForSymbol open_buffer_for_symbol = 22;
+        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 23;
+
+        RegisterWorktree register_worktree = 24;
+        UnregisterWorktree unregister_worktree = 25;
+        ShareWorktree share_worktree = 26;
+        UpdateWorktree update_worktree = 27;
+        UpdateDiagnosticSummary update_diagnostic_summary = 28;
+        DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 29;
+        DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 30;
+
+        OpenBuffer open_buffer = 31;
+        OpenBufferResponse open_buffer_response = 32;
+        CloseBuffer close_buffer = 33;
+        UpdateBuffer update_buffer = 34;
+        UpdateBufferFile update_buffer_file = 35;
+        SaveBuffer save_buffer = 36;
+        BufferSaved buffer_saved = 37;
+        BufferReloaded buffer_reloaded = 38;
+        FormatBuffers format_buffers = 39;
+        FormatBuffersResponse format_buffers_response = 40;
+        GetCompletions get_completions = 41;
+        GetCompletionsResponse get_completions_response = 42;
+        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 43;
+        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 44;
+        GetCodeActions get_code_actions = 45;
+        GetCodeActionsResponse get_code_actions_response = 46;
+        ApplyCodeAction apply_code_action = 47;
+        ApplyCodeActionResponse apply_code_action_response = 48;
+        PrepareRename prepare_rename = 49;
+        PrepareRenameResponse prepare_rename_response = 50;
+        PerformRename perform_rename = 51;
+        PerformRenameResponse perform_rename_response = 52;
+
+        GetChannels get_channels = 53;
+        GetChannelsResponse get_channels_response = 54;
+        JoinChannel join_channel = 55;
+        JoinChannelResponse join_channel_response = 56;
+        LeaveChannel leave_channel = 57;
+        SendChannelMessage send_channel_message = 58;
+        SendChannelMessageResponse send_channel_message_response = 59;
+        ChannelMessageSent channel_message_sent = 60;
+        GetChannelMessages get_channel_messages = 61;
+        GetChannelMessagesResponse get_channel_messages_response = 62;
+
+        UpdateContacts update_contacts = 63;
+
+        GetUsers get_users = 64;
+        GetUsersResponse get_users_response = 65;
     }
 }
 
@@ -179,13 +181,22 @@ message GetProjectSymbols {
 }
 
 message GetProjectSymbolsResponse {
-    repeated string languages = 1;
-    repeated uint64 symbol_counts_per_language = 2;
-    repeated Symbol symbols = 3;
+    repeated Symbol symbols = 4;
 }
 
 message Symbol {
-    bytes lsp_symbol = 1;
+    uint64 worktree_id = 1;
+    string language_name = 2;
+    bytes lsp_symbol = 3;
+}
+
+message OpenBufferForSymbol {
+    uint64 project_id = 1;
+    Symbol symbol = 2;
+}
+
+message OpenBufferForSymbolResponse {
+    Buffer buffer = 1;
 }
 
 message OpenBuffer {

crates/rpc/src/proto.rs 🔗

@@ -158,7 +158,7 @@ messages!(
     (GetDefinition, Foreground),
     (GetDefinitionResponse, Foreground),
     (GetProjectSymbols, Background),
-    (GetProjectSymbolsResponse, Foreground),
+    (GetProjectSymbolsResponse, Background),
     (GetUsers, Foreground),
     (GetUsersResponse, Foreground),
     (JoinChannel, Foreground),
@@ -168,6 +168,8 @@ messages!(
     (LeaveChannel, Foreground),
     (LeaveProject, Foreground),
     (OpenBuffer, Foreground),
+    (OpenBufferForSymbol, Foreground),
+    (OpenBufferForSymbolResponse, Foreground),
     (OpenBufferResponse, Foreground),
     (PerformRename, Background),
     (PerformRenameResponse, Background),
@@ -211,6 +213,7 @@ request_messages!(
     (JoinChannel, JoinChannelResponse),
     (JoinProject, JoinProjectResponse),
     (OpenBuffer, OpenBufferResponse),
+    (OpenBufferForSymbol, OpenBufferForSymbolResponse),
     (Ping, Ack),
     (PerformRename, PerformRenameResponse),
     (PrepareRename, PrepareRenameResponse),
@@ -243,6 +246,7 @@ entity_messages!(
     JoinProject,
     LeaveProject,
     OpenBuffer,
+    OpenBufferForSymbol,
     PerformRename,
     PrepareRename,
     RemoveProjectCollaborator,

crates/server/src/rpc.rs 🔗

@@ -80,6 +80,7 @@ impl Server {
             .add_message_handler(Server::disk_based_diagnostics_updated)
             .add_request_handler(Server::get_definition)
             .add_request_handler(Server::get_project_symbols)
+            .add_request_handler(Server::open_buffer_for_symbol)
             .add_request_handler(Server::open_buffer)
             .add_message_handler(Server::close_buffer)
             .add_request_handler(Server::update_buffer)
@@ -602,6 +603,20 @@ impl Server {
             .await?)
     }
 
+    async fn open_buffer_for_symbol(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::OpenBufferForSymbol>,
+    ) -> tide::Result<proto::OpenBufferForSymbolResponse> {
+        let host_connection_id = self
+            .state()
+            .read_project(request.payload.project_id, request.sender_id)?
+            .host_connection_id;
+        Ok(self
+            .peer
+            .forward_request(request.sender_id, host_connection_id, request.payload)
+            .await?)
+    }
+
     async fn open_buffer(
         self: Arc<Server>,
         request: TypedEnvelope<proto::OpenBuffer>,