Merge pull request #479 from zed-industries/project-symbols

Max Brunsfeld created

Project symbols

Change summary

Cargo.lock                                    | 102 ++
crates/editor/src/display_map.rs              |   4 
crates/editor/src/editor.rs                   |   8 
crates/language/src/buffer.rs                 |   4 
crates/language/src/language.rs               |  92 +-
crates/language/src/proto.rs                  |   7 
crates/language/src/tests.rs                  |  14 
crates/project/Cargo.toml                     |   3 
crates/project/src/fs.rs                      |  78 ++
crates/project/src/lsp_command.rs             |   2 
crates/project/src/project.rs                 | 366 ++++++++++++
crates/project/src/worktree.rs                |   7 
crates/project_symbols/Cargo.toml             |  20 
crates/project_symbols/src/project_symbols.rs | 401 ++++++++++++++
crates/rpc/proto/zed.proto                    | 133 +++-
crates/rpc/src/proto.rs                       |   8 
crates/server/src/rpc.rs                      | 601 ++++++++++++++------
crates/workspace/src/lsp_status.rs            |   2 
crates/workspace/src/settings.rs              |  12 
crates/zed/Cargo.toml                         |   1 
crates/zed/src/language.rs                    | 119 +++
crates/zed/src/main.rs                        |   1 
crates/zed/src/test.rs                        |  21 
crates/zed/src/zed.rs                         |   4 
24 files changed, 1,625 insertions(+), 385 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -409,7 +409,7 @@ dependencies = [
  "rand 0.7.3",
  "serde",
  "serde_json",
- "sha2",
+ "sha2 0.9.5",
 ]
 
 [[package]]
@@ -686,6 +686,15 @@ dependencies = [
  "generic-array 0.14.4",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
 [[package]]
 name = "block-padding"
 version = "0.1.5"
@@ -1124,7 +1133,7 @@ dependencies = [
  "hmac 0.10.1",
  "percent-encoding",
  "rand 0.8.3",
- "sha2",
+ "sha2 0.9.5",
  "time 0.2.25",
  "version_check",
 ]
@@ -1187,6 +1196,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "cpuid-bool"
 version = "0.2.0"
@@ -1280,6 +1298,16 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "crypto-common"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
+dependencies = [
+ "generic-array 0.14.4",
+ "typenum",
+]
+
 [[package]]
 name = "crypto-mac"
 version = "0.8.0"
@@ -1447,6 +1475,16 @@ dependencies = [
  "generic-array 0.14.4",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+dependencies = [
+ "block-buffer 0.10.2",
+ "crypto-common",
+]
+
 [[package]]
 name = "dirs"
 version = "3.0.1"
@@ -2604,7 +2642,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "ecdsa",
  "elliptic-curve",
- "sha2",
+ "sha2 0.9.5",
 ]
 
 [[package]]
@@ -2691,9 +2729,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.98"
+version = "0.2.119"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
+checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
 
 [[package]]
 name = "libloading"
@@ -3065,7 +3103,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_path_to_error",
- "sha2",
+ "sha2 0.9.5",
  "thiserror",
  "url",
 ]
@@ -3183,7 +3221,7 @@ checksum = "d053368e1bae4c8a672953397bd1bd7183dde1c72b0b7612a15719173148d186"
 dependencies = [
  "ecdsa",
  "elliptic-curve",
- "sha2",
+ "sha2 0.9.5",
 ]
 
 [[package]]
@@ -3537,6 +3575,7 @@ dependencies = [
  "rpc",
  "serde",
  "serde_json",
+ "sha2 0.10.2",
  "smol",
  "sum_tree",
  "tempdir",
@@ -3559,6 +3598,23 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "project_symbols"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "editor",
+ "fuzzy",
+ "gpui",
+ "ordered-float",
+ "postage",
+ "project",
+ "smol",
+ "text",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "prost"
 version = "0.8.0"
@@ -3955,7 +4011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ad22c7226e4829104deab21df575e995bfbc4adfad13a595e387477f238c1aec"
 dependencies = [
  "globset",
- "sha2",
+ "sha2 0.9.5",
  "walkdir",
 ]
 
@@ -4094,7 +4150,7 @@ dependencies = [
  "password-hash",
  "pbkdf2",
  "salsa20",
- "sha2",
+ "sha2 0.9.5",
 ]
 
 [[package]]
@@ -4264,7 +4320,7 @@ checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16"
 dependencies = [
  "block-buffer 0.9.0",
  "cfg-if 1.0.0",
- "cpufeatures",
+ "cpufeatures 0.1.4",
  "digest 0.9.0",
  "opaque-debug 0.3.0",
 ]
@@ -4283,11 +4339,22 @@ checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
 dependencies = [
  "block-buffer 0.9.0",
  "cfg-if 1.0.0",
- "cpufeatures",
+ "cpufeatures 0.1.4",
  "digest 0.9.0",
  "opaque-debug 0.3.0",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures 0.2.1",
+ "digest 0.10.3",
+]
+
 [[package]]
 name = "shell-words"
 version = "1.0.0"
@@ -4556,7 +4623,7 @@ dependencies = [
  "serde",
  "serde_json",
  "sha-1 0.9.6",
- "sha2",
+ "sha2 0.9.5",
  "smallvec",
  "sqlformat",
  "sqlx-rt 0.2.0",
@@ -4605,7 +4672,7 @@ dependencies = [
  "serde",
  "serde_json",
  "sha-1 0.9.6",
- "sha2",
+ "sha2 0.9.5",
  "smallvec",
  "sqlformat",
  "sqlx-rt 0.5.5",
@@ -4634,7 +4701,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde_json",
- "sha2",
+ "sha2 0.9.5",
  "sqlx-core 0.4.2",
  "sqlx-rt 0.2.0",
  "syn",
@@ -4654,7 +4721,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "sha2",
+ "sha2 0.9.5",
  "sqlx-core 0.5.5",
  "sqlx-rt 0.5.5",
  "syn",
@@ -5276,9 +5343,9 @@ checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
 
 [[package]]
 name = "typenum"
-version = "1.13.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
 
 [[package]]
 name = "ucd-trie"
@@ -5794,6 +5861,7 @@ dependencies = [
  "postage",
  "project",
  "project_panel",
+ "project_symbols",
  "rand 0.8.3",
  "regex",
  "rpc",

crates/editor/src/display_map.rs 🔗

@@ -864,7 +864,7 @@ mod tests {
         let language = Arc::new(
             Language::new(
                 LanguageConfig {
-                    name: "Test".to_string(),
+                    name: "Test".into(),
                     path_suffixes: vec![".test".to_string()],
                     ..Default::default()
                 },
@@ -951,7 +951,7 @@ mod tests {
         let language = Arc::new(
             Language::new(
                 LanguageConfig {
-                    name: "Test".to_string(),
+                    name: "Test".into(),
                     path_suffixes: vec![".test".to_string()],
                     ..Default::default()
                 },

crates/editor/src/editor.rs 🔗

@@ -31,7 +31,7 @@ use gpui::{
 use items::{BufferItemHandle, MultiBufferItemHandle};
 use itertools::Itertools as _;
 use language::{
-    AnchorRangeExt as _, BracketPair, Buffer, CodeAction, Completion, CompletionLabel, Diagnostic,
+    AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
     DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId,
 };
 use multi_buffer::MultiBufferChunks;
@@ -600,7 +600,7 @@ impl CompletionsMenu {
                                 .with_highlights(combine_syntax_and_fuzzy_match_highlights(
                                     &completion.label.text,
                                     settings.style.text.color.into(),
-                                    styled_runs_for_completion_label(
+                                    styled_runs_for_code_label(
                                         &completion.label,
                                         settings.style.text.color,
                                         &settings.style.syntax,
@@ -5654,8 +5654,8 @@ pub fn combine_syntax_and_fuzzy_match_highlights(
     result
 }
 
-fn styled_runs_for_completion_label<'a>(
-    label: &'a CompletionLabel,
+pub fn styled_runs_for_code_label<'a>(
+    label: &'a CodeLabel,
     default_color: Color,
     syntax_theme: &'a theme::SyntaxTheme,
 ) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> {

crates/language/src/buffer.rs 🔗

@@ -7,7 +7,7 @@ pub use crate::{
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
     outline::OutlineItem,
-    range_from_lsp, CompletionLabel, Outline, ToLspPosition,
+    range_from_lsp, CodeLabel, Outline, ToLspPosition,
 };
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
@@ -117,7 +117,7 @@ pub struct Diagnostic {
 pub struct Completion {
     pub old_range: Range<Anchor>,
     pub new_text: String,
-    pub label: CompletionLabel,
+    pub label: CodeLabel,
     pub lsp_completion: lsp::CompletionItem,
 }
 

crates/language/src/language.rs 🔗

@@ -16,7 +16,7 @@ use futures::{
 use gpui::{AppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use serde::Deserialize;
 use std::{
     cell::RefCell,
@@ -45,7 +45,7 @@ thread_local! {
 lazy_static! {
     pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
         LanguageConfig {
-            name: "Plain Text".to_string(),
+            name: "Plain Text".into(),
             path_suffixes: Default::default(),
             brackets: Default::default(),
             line_comment: None,
@@ -77,32 +77,42 @@ pub trait LspExt: 'static + Send + Sync {
     ) -> BoxFuture<'static, Result<PathBuf>>;
     fn cached_server_binary(&self, download_dir: Arc<Path>) -> BoxFuture<'static, Option<PathBuf>>;
     fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams);
-    fn label_for_completion(
-        &self,
-        _: &lsp::CompletionItem,
-        _: &Language,
-    ) -> Option<CompletionLabel> {
+    fn label_for_completion(&self, _: &lsp::CompletionItem, _: &Language) -> Option<CodeLabel> {
+        None
+    }
+    fn label_for_symbol(&self, _: &str, _: lsp::SymbolKind, _: &Language) -> Option<CodeLabel> {
         None
     }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub struct CompletionLabel {
+pub struct CodeLabel {
     pub text: String,
     pub runs: Vec<(Range<usize>, HighlightId)>,
     pub filter_range: Range<usize>,
-    pub left_aligned_len: usize,
 }
 
-#[derive(Default, Deserialize)]
+#[derive(Deserialize)]
 pub struct LanguageConfig {
-    pub name: String,
+    pub name: Arc<str>,
     pub path_suffixes: Vec<String>,
     pub brackets: Vec<BracketPair>,
     pub line_comment: Option<String>,
     pub language_server: Option<LanguageServerConfig>,
 }
 
+impl Default for LanguageConfig {
+    fn default() -> Self {
+        Self {
+            name: "".into(),
+            path_suffixes: Default::default(),
+            brackets: Default::default(),
+            line_comment: Default::default(),
+            language_server: Default::default(),
+        }
+    }
+}
+
 #[derive(Default, Deserialize)]
 pub struct LanguageServerConfig {
     pub disk_based_diagnostic_sources: HashSet<String>,
@@ -153,7 +163,7 @@ pub enum LanguageServerBinaryStatus {
 }
 
 pub struct LanguageRegistry {
-    languages: Vec<Arc<Language>>,
+    languages: RwLock<Vec<Arc<Language>>>,
     language_server_download_dir: Option<Arc<Path>>,
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
     lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
@@ -170,12 +180,12 @@ impl LanguageRegistry {
         }
     }
 
-    pub fn add(&mut self, language: Arc<Language>) {
-        self.languages.push(language.clone());
+    pub fn add(&self, language: Arc<Language>) {
+        self.languages.write().push(language.clone());
     }
 
     pub fn set_theme(&self, theme: &SyntaxTheme) {
-        for language in &self.languages {
+        for language in self.languages.read().iter() {
             language.set_theme(theme);
         }
     }
@@ -184,24 +194,30 @@ impl LanguageRegistry {
         self.language_server_download_dir = Some(path.into());
     }
 
-    pub fn get_language(&self, name: &str) -> Option<&Arc<Language>> {
+    pub fn get_language(&self, name: &str) -> Option<Arc<Language>> {
         self.languages
+            .read()
             .iter()
-            .find(|language| language.name() == name)
+            .find(|language| language.name().as_ref() == name)
+            .cloned()
     }
 
-    pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
+    pub fn select_language(&self, path: impl AsRef<Path>) -> Option<Arc<Language>> {
         let path = path.as_ref();
         let filename = path.file_name().and_then(|name| name.to_str());
         let extension = path.extension().and_then(|name| name.to_str());
         let path_suffixes = [extension, filename];
-        self.languages.iter().find(|language| {
-            language
-                .config
-                .path_suffixes
-                .iter()
-                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
-        })
+        self.languages
+            .read()
+            .iter()
+            .find(|language| {
+                language
+                    .config
+                    .path_suffixes
+                    .iter()
+                    .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
+            })
+            .cloned()
     }
 
     pub fn start_language_server(
@@ -403,8 +419,8 @@ impl Language {
         self
     }
 
-    pub fn name(&self) -> &str {
-        self.config.name.as_str()
+    pub fn name(&self) -> Arc<str> {
+        self.config.name.clone()
     }
 
     pub fn line_comment_prefix(&self) -> Option<&str> {
@@ -431,15 +447,16 @@ impl Language {
         }
     }
 
-    pub fn label_for_completion(
-        &self,
-        completion: &lsp::CompletionItem,
-    ) -> Option<CompletionLabel> {
+    pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option<CodeLabel> {
         self.lsp_ext
             .as_ref()?
             .label_for_completion(completion, self)
     }
 
+    pub fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option<CodeLabel> {
+        self.lsp_ext.as_ref()?.label_for_symbol(name, kind, self)
+    }
+
     pub fn highlight_text<'a>(
         &'a self,
         text: &'a Rope,
@@ -507,16 +524,15 @@ impl Grammar {
     }
 }
 
-impl CompletionLabel {
-    pub fn plain(completion: &lsp::CompletionItem) -> Self {
+impl CodeLabel {
+    pub fn plain(text: String, filter_text: Option<&str>) -> Self {
         let mut result = Self {
-            text: completion.label.clone(),
             runs: Vec::new(),
-            left_aligned_len: completion.label.len(),
-            filter_range: 0..completion.label.len(),
+            filter_range: 0..text.len(),
+            text,
         };
-        if let Some(filter_text) = &completion.filter_text {
-            if let Some(ix) = completion.label.find(filter_text) {
+        if let Some(filter_text) = filter_text {
+            if let Some(ix) = result.text.find(filter_text) {
                 result.filter_range = ix..ix + filter_text.len();
             }
         }

crates/language/src/proto.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    diagnostic_set::DiagnosticEntry, CodeAction, Completion, CompletionLabel, Diagnostic, Language,
+    diagnostic_set::DiagnosticEntry, CodeAction, CodeLabel, Completion, Diagnostic, Language,
     Operation,
 };
 use anyhow::{anyhow, Result};
@@ -421,7 +421,10 @@ pub fn deserialize_completion(
         new_text: completion.new_text,
         label: language
             .and_then(|l| l.label_for_completion(&lsp_completion))
-            .unwrap_or(CompletionLabel::plain(&lsp_completion)),
+            .unwrap_or(CodeLabel::plain(
+                lsp_completion.label.clone(),
+                lsp_completion.filter_text.as_deref(),
+            )),
         lsp_completion,
     })
 }

crates/language/src/tests.rs 🔗

@@ -24,10 +24,10 @@ fn init_logger() {
 
 #[gpui::test]
 fn test_select_language() {
-    let mut registry = LanguageRegistry::new();
+    let registry = LanguageRegistry::new();
     registry.add(Arc::new(Language::new(
         LanguageConfig {
-            name: "Rust".to_string(),
+            name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
             ..Default::default()
         },
@@ -35,7 +35,7 @@ fn test_select_language() {
     )));
     registry.add(Arc::new(Language::new(
         LanguageConfig {
-            name: "Make".to_string(),
+            name: "Make".into(),
             path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
             ..Default::default()
         },
@@ -45,17 +45,17 @@ fn test_select_language() {
     // matching file extension
     assert_eq!(
         registry.select_language("zed/lib.rs").map(|l| l.name()),
-        Some("Rust")
+        Some("Rust".into())
     );
     assert_eq!(
         registry.select_language("zed/lib.mk").map(|l| l.name()),
-        Some("Make")
+        Some("Make".into())
     );
 
     // matching filename
     assert_eq!(
         registry.select_language("zed/Makefile").map(|l| l.name()),
-        Some("Make")
+        Some("Make".into())
     );
 
     // matching suffix that is not the full file extension or filename
@@ -1354,7 +1354,7 @@ impl Buffer {
 fn rust_lang() -> Language {
     Language::new(
         LanguageConfig {
-            name: "Rust".to_string(),
+            name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
             language_server: None,
             ..Default::default()

crates/project/Cargo.toml 🔗

@@ -35,8 +35,10 @@ libc = "0.2"
 log = "0.4"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
+rand = "0.8.3"
 serde = { version = "1", features = ["derive"] }
 serde_json = { version = "1.0.64", features = ["preserve_order"] }
+sha2 = "0.10"
 smol = "1.2.5"
 toml = "0.5"
 
@@ -48,6 +50,5 @@ language = { path = "../language", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
-rand = "0.8.3"
 tempdir = { version = "0.3.7" }
 unindent = "0.1.7"

crates/project/src/fs.rs 🔗

@@ -5,7 +5,7 @@ use smol::io::{AsyncReadExt, AsyncWriteExt};
 use std::{
     io,
     os::unix::fs::MetadataExt,
-    path::{Path, PathBuf},
+    path::{Component, Path, PathBuf},
     pin::Pin,
     time::{Duration, SystemTime},
 };
@@ -379,6 +379,7 @@ impl Fs for FakeFs {
     async fn create_dir(&self, path: &Path) -> Result<()> {
         self.executor.simulate_random_delay().await;
         let state = &mut *self.state.lock().await;
+        let path = normalize_path(path);
         let mut ancestor_path = PathBuf::new();
         let mut created_dir_paths = Vec::new();
         for component in path.components() {
@@ -415,8 +416,9 @@ impl Fs for FakeFs {
     async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
         self.executor.simulate_random_delay().await;
         let mut state = self.state.lock().await;
-        state.validate_path(path)?;
-        if let Some(entry) = state.entries.get_mut(path) {
+        let path = normalize_path(path);
+        state.validate_path(&path)?;
+        if let Some(entry) = state.entries.get_mut(&path) {
             if entry.metadata.is_dir || entry.metadata.is_symlink {
                 return Err(anyhow!(
                     "cannot create file because {:?} is a dir or a symlink",
@@ -430,7 +432,7 @@ impl Fs for FakeFs {
             } else if !options.ignore_if_exists {
                 return Err(anyhow!(
                     "cannot create file because {:?} already exists",
-                    path
+                    &path
                 ));
             }
         } else {
@@ -453,11 +455,14 @@ impl Fs for FakeFs {
     }
 
     async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
+        let source = normalize_path(source);
+        let target = normalize_path(target);
+
         let mut state = self.state.lock().await;
-        state.validate_path(source)?;
-        state.validate_path(target)?;
+        state.validate_path(&source)?;
+        state.validate_path(&target)?;
 
-        if !options.overwrite && state.entries.contains_key(target) {
+        if !options.overwrite && state.entries.contains_key(&target) {
             if options.ignore_if_exists {
                 return Ok(());
             } else {
@@ -467,7 +472,7 @@ impl Fs for FakeFs {
 
         let mut removed = Vec::new();
         state.entries.retain(|path, entry| {
-            if let Ok(relative_path) = path.strip_prefix(source) {
+            if let Ok(relative_path) = path.strip_prefix(&source) {
                 removed.push((relative_path.to_path_buf(), entry.clone()));
                 false
             } else {
@@ -485,9 +490,10 @@ impl Fs for FakeFs {
     }
 
     async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        let path = normalize_path(path);
         let mut state = self.state.lock().await;
-        state.validate_path(path)?;
-        if let Some(entry) = state.entries.get(path) {
+        state.validate_path(&path)?;
+        if let Some(entry) = state.entries.get(&path) {
             if !entry.metadata.is_dir {
                 return Err(anyhow!("cannot remove {path:?} because it is not a dir"));
             }
@@ -513,14 +519,15 @@ impl Fs for FakeFs {
     }
 
     async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        let path = normalize_path(path);
         let mut state = self.state.lock().await;
-        state.validate_path(path)?;
-        if let Some(entry) = state.entries.get(path) {
+        state.validate_path(&path)?;
+        if let Some(entry) = state.entries.get(&path) {
             if entry.metadata.is_dir {
                 return Err(anyhow!("cannot remove {path:?} because it is not a file"));
             }
 
-            state.entries.remove(path);
+            state.entries.remove(&path);
             state.emit_event(&[path]).await;
         } else if !options.ignore_if_not_exists {
             return Err(anyhow!("{path:?} does not exist"));
@@ -529,11 +536,12 @@ impl Fs for FakeFs {
     }
 
     async fn load(&self, path: &Path) -> Result<String> {
+        let path = normalize_path(path);
         self.executor.simulate_random_delay().await;
         let state = self.state.lock().await;
         let text = state
             .entries
-            .get(path)
+            .get(&path)
             .and_then(|e| e.content.as_ref())
             .ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
         Ok(text.clone())
@@ -542,8 +550,9 @@ impl Fs for FakeFs {
     async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
         self.executor.simulate_random_delay().await;
         let mut state = self.state.lock().await;
-        state.validate_path(path)?;
-        if let Some(entry) = state.entries.get_mut(path) {
+        let path = normalize_path(path);
+        state.validate_path(&path)?;
+        if let Some(entry) = state.entries.get_mut(&path) {
             if entry.metadata.is_dir {
                 Err(anyhow!("cannot overwrite a directory with a file"))
             } else {
@@ -572,22 +581,24 @@ impl Fs for FakeFs {
 
     async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
         self.executor.simulate_random_delay().await;
-        Ok(path.to_path_buf())
+        Ok(normalize_path(path))
     }
 
     async fn is_file(&self, path: &Path) -> bool {
+        let path = normalize_path(path);
         self.executor.simulate_random_delay().await;
         let state = self.state.lock().await;
         state
             .entries
-            .get(path)
+            .get(&path)
             .map_or(false, |entry| !entry.metadata.is_dir)
     }
 
     async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
         self.executor.simulate_random_delay().await;
         let state = self.state.lock().await;
-        Ok(state.entries.get(path).map(|entry| entry.metadata.clone()))
+        let path = normalize_path(path);
+        Ok(state.entries.get(&path).map(|entry| entry.metadata.clone()))
     }
 
     async fn read_dir(
@@ -597,7 +608,7 @@ impl Fs for FakeFs {
         use futures::{future, stream};
         self.executor.simulate_random_delay().await;
         let state = self.state.lock().await;
-        let abs_path = abs_path.to_path_buf();
+        let abs_path = normalize_path(abs_path);
         Ok(Box::pin(stream::iter(state.entries.clone()).filter_map(
             move |(child_path, _)| {
                 future::ready(if child_path.parent() == Some(&abs_path) {
@@ -633,3 +644,30 @@ impl Fs for FakeFs {
         self
     }
 }
+
+pub fn normalize_path(path: &Path) -> PathBuf {
+    let mut components = path.components().peekable();
+    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
+        components.next();
+        PathBuf::from(c.as_os_str())
+    } else {
+        PathBuf::new()
+    };
+
+    for component in components {
+        match component {
+            Component::Prefix(..) => unreachable!(),
+            Component::RootDir => {
+                ret.push(component.as_os_str());
+            }
+            Component::CurDir => {}
+            Component::ParentDir => {
+                ret.pop();
+            }
+            Component::Normal(c) => {
+                ret.push(c);
+            }
+        }
+    }
+    ret
+}

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 🔗

@@ -14,18 +14,22 @@ use gpui::{
     UpgradeModelHandle, WeakModelHandle,
 };
 use language::{
-    range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel,
+    range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion,
     Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16,
     ToLspPosition, ToOffset, ToPointUtf16, Transaction,
 };
 use lsp::{DiagnosticSeverity, LanguageServer};
 use lsp_command::*;
 use postage::{broadcast, prelude::Stream, sink::Sink, watch};
+use rand::prelude::*;
+use sha2::{Digest, Sha256};
 use smol::block_on;
 use std::{
     convert::TryInto,
+    hash::Hash,
+    mem,
     ops::Range,
-    path::{Path, PathBuf},
+    path::{Component, Path, PathBuf},
     sync::{atomic::AtomicBool, Arc},
     time::Instant,
 };
@@ -55,6 +59,7 @@ pub struct Project {
         postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
     >,
     shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
+    nonce: u128,
 }
 
 enum OpenBuffer {
@@ -118,6 +123,19 @@ pub struct Definition {
     pub target_range: Range<language::Anchor>,
 }
 
+#[derive(Clone, Debug)]
+pub struct Symbol {
+    pub source_worktree_id: WorktreeId,
+    pub worktree_id: WorktreeId,
+    pub language_name: String,
+    pub path: PathBuf,
+    pub label: CodeLabel,
+    pub name: String,
+    pub kind: lsp::SymbolKind,
+    pub range: Range<PointUtf16>,
+    pub signature: [u8; 32],
+}
+
 #[derive(Default)]
 pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
 
@@ -186,6 +204,8 @@ impl Project {
         client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
         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);
     }
@@ -261,6 +281,7 @@ impl Project {
                 language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
                 started_language_servers: Default::default(),
+                nonce: StdRng::from_entropy().gen(),
             }
         })
     }
@@ -313,6 +334,7 @@ impl Project {
                 language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
                 started_language_servers: Default::default(),
+                nonce: StdRng::from_entropy().gen(),
             };
             for worktree in worktrees {
                 this.add_worktree(&worktree, cx);
@@ -365,6 +387,11 @@ impl Project {
             .any(|buffer| matches!(buffer, OpenBuffer::Loading(_)))
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn languages(&self) -> &Arc<LanguageRegistry> {
+        &self.languages
+    }
+
     pub fn fs(&self) -> &Arc<dyn Fs> {
         &self.fs
     }
@@ -431,6 +458,21 @@ impl Project {
             .filter_map(move |worktree| worktree.upgrade(cx))
     }
 
+    pub fn strong_worktrees<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+        self.worktrees.iter().filter_map(|worktree| {
+            worktree.upgrade(cx).and_then(|worktree| {
+                if worktree.read(cx).is_weak() {
+                    None
+                } else {
+                    Some(worktree)
+                }
+            })
+        })
+    }
+
     pub fn worktree_for_id(
         &self,
         id: WorktreeId,
@@ -440,7 +482,7 @@ impl Project {
             .find(|worktree| worktree.read(cx).id() == id)
     }
 
-    pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
+    pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         let rpc = self.client.clone();
         cx.spawn(|this, mut cx| async move {
             let project_id = this.update(&mut cx, |this, _| {
@@ -477,7 +519,7 @@ impl Project {
         })
     }
 
-    pub fn unshare(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
+    pub fn unshare(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         let rpc = self.client.clone();
         cx.spawn(|this, mut cx| async move {
             let project_id = this.update(&mut cx, |this, _| {
@@ -646,7 +688,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,
@@ -780,7 +822,7 @@ impl Project {
         };
 
         // If the buffer has a language, set it and start/assign the language server
-        if let Some(language) = self.languages.select_language(&full_path).cloned() {
+        if let Some(language) = self.languages.select_language(&full_path) {
             buffer.update(cx, |buffer, cx| {
                 buffer.set_language(Some(language.clone()), cx);
             });
@@ -1216,6 +1258,165 @@ impl Project {
         self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
     }
 
+    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 ((worktree_id, language_name), language_server) in self.language_servers.iter() {
+                if let Some((worktree, language)) = self
+                    .worktree_for_id(*worktree_id, cx)
+                    .and_then(|worktree| worktree.read(cx).as_local())
+                    .zip(self.languages.get_language(language_name))
+                {
+                    language_servers
+                        .entry(Arc::as_ptr(language_server))
+                        .or_insert((
+                            language_server.clone(),
+                            *worktree_id,
+                            worktree.abs_path().clone(),
+                            language.clone(),
+                        ));
+                }
+            }
+
+            let mut requests = Vec::new();
+            for (language_server, _, _, _) in language_servers.values() {
+                requests.push(language_server.request::<lsp::request::WorkspaceSymbol>(
+                    lsp::WorkspaceSymbolParams {
+                        query: query.to_string(),
+                        ..Default::default()
+                    },
+                ));
+            }
+
+            cx.spawn_weak(|this, cx| async move {
+                let responses = futures::future::try_join_all(requests).await?;
+
+                let mut symbols = Vec::new();
+                if let Some(this) = this.upgrade(&cx) {
+                    this.read_with(&cx, |this, cx| {
+                        for ((_, source_worktree_id, worktree_abs_path, language), lsp_symbols) in
+                            language_servers.into_values().zip(responses)
+                        {
+                            symbols.extend(lsp_symbols.into_iter().flatten().filter_map(
+                                |lsp_symbol| {
+                                    let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
+                                    let mut worktree_id = source_worktree_id;
+                                    let path;
+                                    if let Some((worktree, rel_path)) =
+                                        this.find_local_worktree(&abs_path, cx)
+                                    {
+                                        worktree_id = worktree.read(cx).id();
+                                        path = rel_path;
+                                    } else {
+                                        path = relativize_path(&worktree_abs_path, &abs_path);
+                                    }
+
+                                    let label = language
+                                        .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
+                                        .unwrap_or_else(|| {
+                                            CodeLabel::plain(lsp_symbol.name.clone(), None)
+                                        });
+                                    let signature = this.symbol_signature(worktree_id, &path);
+
+                                    Some(Symbol {
+                                        source_worktree_id,
+                                        worktree_id,
+                                        language_name: language.name().to_string(),
+                                        name: lsp_symbol.name,
+                                        kind: lsp_symbol.kind,
+                                        label,
+                                        path,
+                                        range: range_from_lsp(lsp_symbol.location.range),
+                                        signature,
+                                    })
+                                },
+                            ));
+                        }
+                    })
+                }
+
+                Ok(symbols)
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self.client.request(proto::GetProjectSymbols {
+                project_id,
+                query: query.to_string(),
+            });
+            cx.spawn_weak(|this, cx| async move {
+                let response = request.await?;
+                let mut symbols = Vec::new();
+                if let Some(this) = this.upgrade(&cx) {
+                    this.read_with(&cx, |this, _| {
+                        symbols.extend(
+                            response
+                                .symbols
+                                .into_iter()
+                                .filter_map(|symbol| this.deserialize_symbol(symbol).log_err()),
+                        );
+                    })
+                }
+                Ok(symbols)
+            })
+        } else {
+            Task::ready(Ok(Default::default()))
+        }
+    }
+
+    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.source_worktree_id, symbol.language_name.clone()))
+            {
+                server.clone()
+            } else {
+                return Task::ready(Err(anyhow!(
+                    "language server for worktree and language not found"
+                )));
+            };
+
+            let worktree_abs_path = if let Some(worktree_abs_path) = self
+                .worktree_for_id(symbol.worktree_id, cx)
+                .and_then(|worktree| worktree.read(cx).as_local())
+                .map(|local_worktree| local_worktree.abs_path())
+            {
+                worktree_abs_path
+            } else {
+                return Task::ready(Err(anyhow!("worktree not found for symbol")));
+            };
+            let symbol_abs_path = worktree_abs_path.join(&symbol.path);
+            let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) {
+                uri
+            } else {
+                return Task::ready(Err(anyhow!("invalid symbol path")));
+            };
+
+            self.open_local_buffer_via_lsp(
+                symbol_uri,
+                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>,
@@ -1295,7 +1496,12 @@ impl Project {
                                     label: language
                                         .as_ref()
                                         .and_then(|l| l.label_for_completion(&lsp_completion))
-                                        .unwrap_or_else(|| CompletionLabel::plain(&lsp_completion)),
+                                        .unwrap_or_else(|| {
+                                            CodeLabel::plain(
+                                                lsp_completion.label.clone(),
+                                                lsp_completion.filter_text.as_deref(),
+                                            )
+                                        }),
                                     lsp_completion,
                                 })
                             } else {
@@ -1683,7 +1889,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(),
@@ -2520,12 +2726,68 @@ impl Project {
         })
     }
 
+    async fn handle_get_project_symbols(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::GetProjectSymbols>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::GetProjectSymbolsResponse> {
+        let symbols = this
+            .update(&mut cx, |this, cx| {
+                this.symbols(&envelope.payload.query, cx)
+            })
+            .await?;
+
+        Ok(proto::GetProjectSymbolsResponse {
+            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, _| {
+            let symbol = this.deserialize_symbol(symbol)?;
+            let signature = this.symbol_signature(symbol.worktree_id, &symbol.path);
+            if signature == symbol.signature {
+                Ok(symbol)
+            } else {
+                Err(anyhow!("invalid symbol signature"))
+            }
+        })?;
+        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)
+            })),
+        })
+    }
+
+    fn symbol_signature(&self, worktree_id: WorktreeId, path: &Path) -> [u8; 32] {
+        let mut hasher = Sha256::new();
+        hasher.update(worktree_id.to_proto().to_be_bytes());
+        hasher.update(path.to_string_lossy().as_bytes());
+        hasher.update(self.nonce.to_be_bytes());
+        hasher.finalize().as_slice().try_into().unwrap()
+    }
+
     async fn handle_open_buffer(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::OpenBuffer>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
-    ) -> anyhow::Result<proto::OpenBufferResponse> {
+    ) -> Result<proto::OpenBufferResponse> {
         let peer_id = envelope.original_sender_id()?;
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
         let open_buffer = this.update(&mut cx, |this, cx| {
@@ -2681,12 +2943,41 @@ impl Project {
         })
     }
 
+    fn deserialize_symbol(&self, serialized_symbol: proto::Symbol) -> Result<Symbol> {
+        let language = self
+            .languages
+            .get_language(&serialized_symbol.language_name);
+        let start = serialized_symbol
+            .start
+            .ok_or_else(|| anyhow!("invalid start"))?;
+        let end = serialized_symbol
+            .end
+            .ok_or_else(|| anyhow!("invalid end"))?;
+        let kind = unsafe { mem::transmute(serialized_symbol.kind) };
+        Ok(Symbol {
+            source_worktree_id: WorktreeId::from_proto(serialized_symbol.source_worktree_id),
+            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(&serialized_symbol.name, kind))
+                .unwrap_or_else(|| CodeLabel::plain(serialized_symbol.name.clone(), None)),
+            name: serialized_symbol.name,
+            path: PathBuf::from(serialized_symbol.path),
+            range: PointUtf16::new(start.row, start.column)..PointUtf16::new(end.row, end.column),
+            kind,
+            signature: serialized_symbol
+                .signature
+                .try_into()
+                .map_err(|_| anyhow!("invalid signature"))?,
+        })
+    }
+
     async fn handle_close_buffer(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::CloseBuffer>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
-    ) -> anyhow::Result<()> {
+    ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
             if let Some(shared_buffers) =
                 this.shared_buffers.get_mut(&envelope.original_sender_id()?)
@@ -2962,6 +3253,55 @@ impl From<lsp::DeleteFileOptions> for fs::RemoveOptions {
     }
 }
 
+fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
+    proto::Symbol {
+        source_worktree_id: symbol.source_worktree_id.to_proto(),
+        worktree_id: symbol.worktree_id.to_proto(),
+        language_name: symbol.language_name.clone(),
+        name: symbol.name.clone(),
+        kind: unsafe { mem::transmute(symbol.kind) },
+        path: symbol.path.to_string_lossy().to_string(),
+        start: Some(proto::Point {
+            row: symbol.range.start.row,
+            column: symbol.range.start.column,
+        }),
+        end: Some(proto::Point {
+            row: symbol.range.end.row,
+            column: symbol.range.end.column,
+        }),
+        signature: symbol.signature.to_vec(),
+    }
+}
+
+fn relativize_path(base: &Path, path: &Path) -> PathBuf {
+    let mut path_components = path.components();
+    let mut base_components = base.components();
+    let mut components: Vec<Component> = Vec::new();
+    loop {
+        match (path_components.next(), base_components.next()) {
+            (None, None) => break,
+            (Some(a), None) => {
+                components.push(a);
+                components.extend(path_components.by_ref());
+                break;
+            }
+            (None, _) => components.push(Component::ParentDir),
+            (Some(a), Some(b)) if components.is_empty() && a == b => (),
+            (Some(a), Some(b)) if b == Component::CurDir => components.push(a),
+            (Some(a), Some(_)) => {
+                components.push(Component::ParentDir);
+                for _ in base_components {
+                    components.push(Component::ParentDir);
+                }
+                components.push(a);
+                components.extend(path_components.by_ref());
+                break;
+            }
+        }
+    }
+    components.iter().map(|c| c.as_os_str()).collect()
+}
+
 #[cfg(test)]
 mod tests {
     use super::{Event, *};
@@ -3051,7 +3391,7 @@ mod tests {
 
         let language = Arc::new(Language::new(
             LanguageConfig {
-                name: "Rust".to_string(),
+                name: "Rust".into(),
                 path_suffixes: vec!["rs".to_string()],
                 language_server: Some(language_server_config),
                 ..Default::default()
@@ -3197,7 +3537,7 @@ mod tests {
         let (language_server_config, mut fake_servers) = LanguageServerConfig::fake();
         let language = Arc::new(Language::new(
             LanguageConfig {
-                name: "Rust".to_string(),
+                name: "Rust".into(),
                 path_suffixes: vec!["rs".to_string()],
                 language_server: Some(language_server_config),
                 ..Default::default()
@@ -4090,7 +4430,7 @@ mod tests {
         let (language_server_config, mut fake_servers) = LanguageServerConfig::fake();
         let language = Arc::new(Language::new(
             LanguageConfig {
-                name: "Rust".to_string(),
+                name: "Rust".into(),
                 path_suffixes: vec!["rs".to_string()],
                 language_server: Some(language_server_config),
                 ..Default::default()

crates/project/src/worktree.rs 🔗

@@ -4,7 +4,7 @@ use super::{
     DiagnosticSummary,
 };
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use client::{proto, Client, TypedEnvelope};
 use clock::ReplicaId;
 use collections::{HashMap, VecDeque};
@@ -469,7 +469,10 @@ impl LocalWorktree {
             .file_name()
             .map_or(String::new(), |f| f.to_string_lossy().to_string());
         let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
-        let metadata = fs.metadata(&abs_path).await?;
+        let metadata = fs
+            .metadata(&abs_path)
+            .await
+            .context("failed to stat worktree path")?;
 
         let mut config = WorktreeConfig::default();
         if let Ok(zed_toml) = fs.load(&abs_path.join(".zed.toml")).await {

crates/project_symbols/Cargo.toml 🔗

@@ -0,0 +1,20 @@
+[package]
+name = "project_symbols"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/project_symbols.rs"
+
+[dependencies]
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+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 🔗

@@ -0,0 +1,401 @@
+use editor::{
+    combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label,
+    Autoscroll, Bias, Editor, EditorSettings,
+};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+    action,
+    elements::*,
+    keymap::{self, Binding},
+    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
+};
+use ordered_float::OrderedFloat;
+use postage::watch;
+use project::{Project, Symbol};
+use std::{
+    borrow::Cow,
+    cmp::{self, Reverse},
+    sync::Arc,
+};
+use util::ResultExt;
+use workspace::{
+    menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
+    Settings, Workspace,
+};
+
+action!(Toggle);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_bindings([
+        Binding::new("cmd-t", Toggle, None),
+        Binding::new("escape", Toggle, Some("ProjectSymbolsView")),
+    ]);
+    cx.add_action(ProjectSymbolsView::toggle);
+    cx.add_action(ProjectSymbolsView::confirm);
+    cx.add_action(ProjectSymbolsView::select_prev);
+    cx.add_action(ProjectSymbolsView::select_next);
+    cx.add_action(ProjectSymbolsView::select_first);
+    cx.add_action(ProjectSymbolsView::select_last);
+}
+
+pub struct ProjectSymbolsView {
+    handle: WeakViewHandle<Self>,
+    project: ModelHandle<Project>,
+    settings: watch::Receiver<Settings>,
+    selected_match_index: usize,
+    list_state: UniformListState,
+    symbols: Vec<Symbol>,
+    match_candidates: Vec<StringMatchCandidate>,
+    matches: Vec<StringMatch>,
+    pending_symbols_task: Task<Option<()>>,
+    query_editor: ViewHandle<Editor>,
+}
+
+pub enum Event {
+    Dismissed,
+    Selected(Symbol),
+}
+
+impl Entity for ProjectSymbolsView {
+    type Event = Event;
+}
+
+impl View for ProjectSymbolsView {
+    fn ui_name() -> &'static str {
+        "ProjectSymbolsView"
+    }
+
+    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+        let mut cx = Self::default_keymap_context();
+        cx.set.insert("menu".into());
+        cx
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        let settings = self.settings.borrow();
+
+        Flex::new(Axis::Vertical)
+            .with_child(
+                Container::new(ChildView::new(&self.query_editor).boxed())
+                    .with_style(settings.theme.selector.input_editor.container)
+                    .boxed(),
+            )
+            .with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
+            .contained()
+            .with_style(settings.theme.selector.container)
+            .constrained()
+            .with_max_width(500.0)
+            .with_max_height(420.0)
+            .aligned()
+            .top()
+            .named("project symbols view")
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.focus(&self.query_editor);
+    }
+}
+
+impl ProjectSymbolsView {
+    fn new(
+        project: ModelHandle<Project>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let query_editor = cx.add_view(|cx| {
+            Editor::single_line(
+                {
+                    let settings = settings.clone();
+                    Arc::new(move |_| {
+                        let settings = settings.borrow();
+                        EditorSettings {
+                            style: settings.theme.selector.input_editor.as_editor(),
+                            tab_size: settings.tab_size,
+                            soft_wrap: editor::SoftWrap::None,
+                        }
+                    })
+                },
+                cx,
+            )
+        });
+        cx.subscribe(&query_editor, Self::on_query_editor_event)
+            .detach();
+        let mut this = Self {
+            handle: cx.weak_handle(),
+            project,
+            settings,
+            selected_match_index: 0,
+            list_state: Default::default(),
+            symbols: Default::default(),
+            match_candidates: Default::default(),
+            matches: Default::default(),
+            pending_symbols_task: Task::ready(None),
+            query_editor,
+        };
+        this.update_matches(cx);
+        this
+    }
+
+    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+        workspace.toggle_modal(cx, |cx, workspace| {
+            let project = workspace.project().clone();
+            let symbols = cx.add_view(|cx| Self::new(project, workspace.settings.clone(), cx));
+            cx.subscribe(&symbols, Self::on_event).detach();
+            symbols
+        });
+    }
+
+    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+        if self.selected_match_index > 0 {
+            self.select(self.selected_match_index - 1, cx);
+        }
+    }
+
+    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+        if self.selected_match_index + 1 < self.matches.len() {
+            self.select(self.selected_match_index + 1, cx);
+        }
+    }
+
+    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
+        self.select(0, cx);
+    }
+
+    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
+        self.select(self.matches.len().saturating_sub(1), cx);
+    }
+
+    fn select(&mut self, index: usize, cx: &mut ViewContext<Self>) {
+        self.selected_match_index = index;
+        self.list_state.scroll_to(ScrollTarget::Show(index));
+        cx.notify();
+    }
+
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        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>) {
+        self.filter(cx);
+        let query = self.query_editor.read(cx).text(cx);
+        let symbols = self
+            .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()?;
+            if let Some(this) = this.upgrade(&cx) {
+                this.update(&mut cx, |this, cx| {
+                    this.match_candidates = symbols
+                        .iter()
+                        .enumerate()
+                        .map(|(id, symbol)| {
+                            StringMatchCandidate::new(
+                                id,
+                                symbol.label.text[symbol.label.filter_range.clone()].to_string(),
+                            )
+                        })
+                        .collect();
+                    this.symbols = symbols;
+                    this.filter(cx);
+                });
+            }
+            None
+        });
+    }
+
+    fn filter(&mut self, cx: &mut ViewContext<Self>) {
+        let query = self.query_editor.read(cx).text(cx);
+        let mut matches = if query.is_empty() {
+            self.match_candidates
+                .iter()
+                .enumerate()
+                .map(|(candidate_id, candidate)| StringMatch {
+                    candidate_id,
+                    score: Default::default(),
+                    positions: Default::default(),
+                    string: candidate.string.clone(),
+                })
+                .collect()
+        } else {
+            smol::block_on(fuzzy::match_strings(
+                &self.match_candidates,
+                &query,
+                false,
+                100,
+                &Default::default(),
+                cx.background().clone(),
+            ))
+        };
+
+        matches.sort_unstable_by_key(|mat| {
+            let label = &self.symbols[mat.candidate_id].label;
+            (
+                Reverse(OrderedFloat(mat.score)),
+                &label.text[label.filter_range.clone()],
+            )
+        });
+
+        for mat in &mut matches {
+            let filter_start = self.symbols[mat.candidate_id].label.filter_range.start;
+            for position in &mut mat.positions {
+                *position += filter_start;
+            }
+        }
+
+        self.matches = matches;
+        self.select_first(&SelectFirst, cx);
+        cx.notify();
+    }
+
+    fn render_matches(&self) -> ElementBox {
+        if self.matches.is_empty() {
+            let settings = self.settings.borrow();
+            return Container::new(
+                Label::new(
+                    "No matches".into(),
+                    settings.theme.selector.empty.label.clone(),
+                )
+                .boxed(),
+            )
+            .with_style(settings.theme.selector.empty.container)
+            .named("empty matches");
+        }
+
+        let handle = self.handle.clone();
+        let list = UniformList::new(
+            self.list_state.clone(),
+            self.matches.len(),
+            move |mut range, items, cx| {
+                let cx = cx.as_ref();
+                let view = handle.upgrade(cx).unwrap();
+                let view = view.read(cx);
+                let start = range.start;
+                range.end = cmp::min(range.end, view.matches.len());
+
+                let show_worktree_root_name =
+                    view.project.read(cx).strong_worktrees(cx).count() > 1;
+                items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| {
+                    view.render_match(m, start + ix, show_worktree_root_name, cx)
+                }));
+            },
+        );
+
+        Container::new(list.boxed())
+            .with_margin_top(6.0)
+            .named("matches")
+    }
+
+    fn render_match(
+        &self,
+        string_match: &StringMatch,
+        index: usize,
+        show_worktree_root_name: bool,
+        cx: &AppContext,
+    ) -> ElementBox {
+        let settings = self.settings.borrow();
+        let style = if index == self.selected_match_index {
+            &settings.theme.selector.active_item
+        } else {
+            &settings.theme.selector.item
+        };
+        let symbol = &self.symbols[string_match.candidate_id];
+        let syntax_runs = styled_runs_for_code_label(
+            &symbol.label,
+            style.label.text.color,
+            &settings.theme.editor.syntax,
+        );
+
+        let mut path = symbol.path.to_string_lossy();
+        if show_worktree_root_name {
+            let project = self.project.read(cx);
+            if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
+                path = Cow::Owned(format!(
+                    "{}{}{}",
+                    worktree.read(cx).root_name(),
+                    std::path::MAIN_SEPARATOR,
+                    path.as_ref()
+                ));
+            }
+        }
+
+        Flex::column()
+            .with_child(
+                Text::new(symbol.label.text.clone(), style.label.text.clone())
+                    .with_soft_wrap(false)
+                    .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+                        &symbol.label.text,
+                        style.label.text.clone().into(),
+                        syntax_runs,
+                        &string_match.positions,
+                    ))
+                    .boxed(),
+            )
+            .with_child(
+                // Avoid styling the path differently when it is selected, since
+                // the symbol's syntax highlighting doesn't change when selected.
+                Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(),
+            )
+            .contained()
+            .with_style(style.container)
+            .boxed()
+    }
+
+    fn on_query_editor_event(
+        &mut self,
+        _: ViewHandle<Editor>,
+        event: &editor::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            editor::Event::Blurred => cx.emit(Event::Dismissed),
+            editor::Event::Edited => self.update_matches(cx),
+            _ => {}
+        }
+    }
+
+    fn on_event(
+        workspace: &mut Workspace,
+        _: ViewHandle<Self>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        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?;
+                    workspace.update(&mut cx, |workspace, cx| {
+                        let position = buffer
+                            .read(cx)
+                            .clip_point_utf16(symbol.range.start, Bias::Left);
+                        let editor = workspace
+                            .open_item(BufferItemHandle(buffer), cx)
+                            .downcast::<Editor>()
+                            .unwrap();
+                        editor.update(cx, |editor, cx| {
+                            editor.select_ranges(
+                                [position..position],
+                                Some(Autoscroll::Center),
+                                cx,
+                            );
+                        });
+                    });
+                    Ok::<_, anyhow::Error>(())
+                })
+                .detach_and_log_err(cx);
+                workspace.dismiss_modal(cx);
+            }
+        }
+    }
+}

crates/rpc/proto/zed.proto 🔗

@@ -23,53 +23,57 @@ message Envelope {
         RemoveProjectCollaborator remove_project_collaborator = 17;
         GetDefinition get_definition = 18;
         GetDefinitionResponse get_definition_response = 19;
-
-        RegisterWorktree register_worktree = 20;
-        UnregisterWorktree unregister_worktree = 21;
-        ShareWorktree share_worktree = 22;
-        UpdateWorktree update_worktree = 23;
-        UpdateDiagnosticSummary update_diagnostic_summary = 24;
-        DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 25;
-        DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 26;
-
-        OpenBuffer open_buffer = 27;
-        OpenBufferResponse open_buffer_response = 28;
-        CloseBuffer close_buffer = 29;
-        UpdateBuffer update_buffer = 30;
-        UpdateBufferFile update_buffer_file = 31;
-        SaveBuffer save_buffer = 32;
-        BufferSaved buffer_saved = 33;
-        BufferReloaded buffer_reloaded = 34;
-        FormatBuffers format_buffers = 35;
-        FormatBuffersResponse format_buffers_response = 36;
-        GetCompletions get_completions = 37;
-        GetCompletionsResponse get_completions_response = 38;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 39;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 40;
-        GetCodeActions get_code_actions = 41;
-        GetCodeActionsResponse get_code_actions_response = 42;
-        ApplyCodeAction apply_code_action = 43;
-        ApplyCodeActionResponse apply_code_action_response = 44;
-        PrepareRename prepare_rename = 58;
-        PrepareRenameResponse prepare_rename_response = 59;
-        PerformRename perform_rename = 60;
-        PerformRenameResponse perform_rename_response = 61;
-
-        GetChannels get_channels = 45;
-        GetChannelsResponse get_channels_response = 46;
-        JoinChannel join_channel = 47;
-        JoinChannelResponse join_channel_response = 48;
-        LeaveChannel leave_channel = 49;
-        SendChannelMessage send_channel_message = 50;
-        SendChannelMessageResponse send_channel_message_response = 51;
-        ChannelMessageSent channel_message_sent = 52;
-        GetChannelMessages get_channel_messages = 53;
-        GetChannelMessagesResponse get_channel_messages_response = 54;
-
-        UpdateContacts update_contacts = 55;
-
-        GetUsers get_users = 56;
-        GetUsersResponse get_users_response = 57;
+        GetProjectSymbols get_project_symbols = 20;
+        GetProjectSymbolsResponse get_project_symbols_response = 21;
+        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;
     }
 }
 
@@ -171,6 +175,36 @@ message Definition {
     Anchor target_end = 3;
 }
 
+message GetProjectSymbols {
+    uint64 project_id = 1;
+    string query = 2;
+}
+
+message GetProjectSymbolsResponse {
+    repeated Symbol symbols = 4;
+}
+
+message Symbol {
+    uint64 source_worktree_id = 1;
+    uint64 worktree_id = 2;
+    string language_name = 3;
+    string name = 4;
+    int32 kind = 5;
+    string path = 6;
+    Point start = 7;
+    Point end = 8;
+    bytes signature = 9;
+}
+
+message OpenBufferForSymbol {
+    uint64 project_id = 1;
+    Symbol symbol = 2;
+}
+
+message OpenBufferForSymbolResponse {
+    Buffer buffer = 1;
+}
+
 message OpenBuffer {
     uint64 project_id = 1;
     uint64 worktree_id = 2;
@@ -590,6 +624,11 @@ message Range {
     uint64 end = 2;
 }
 
+message Point {
+    uint32 row = 1;
+    uint32 column = 2;
+}
+
 message Nonce {
     uint64 upper_half = 1;
     uint64 lower_half = 2;

crates/rpc/src/proto.rs 🔗

@@ -157,6 +157,8 @@ messages!(
     (GetCompletionsResponse, Foreground),
     (GetDefinition, Foreground),
     (GetDefinitionResponse, Foreground),
+    (GetProjectSymbols, Background),
+    (GetProjectSymbolsResponse, Background),
     (GetUsers, Foreground),
     (GetUsersResponse, Foreground),
     (JoinChannel, Foreground),
@@ -166,6 +168,8 @@ messages!(
     (LeaveChannel, Foreground),
     (LeaveProject, Foreground),
     (OpenBuffer, Foreground),
+    (OpenBufferForSymbol, Foreground),
+    (OpenBufferForSymbolResponse, Foreground),
     (OpenBufferResponse, Foreground),
     (PerformRename, Background),
     (PerformRenameResponse, Background),
@@ -204,10 +208,12 @@ request_messages!(
     (GetCodeActions, GetCodeActionsResponse),
     (GetCompletions, GetCompletionsResponse),
     (GetDefinition, GetDefinitionResponse),
+    (GetProjectSymbols, GetProjectSymbolsResponse),
     (GetUsers, GetUsersResponse),
     (JoinChannel, JoinChannelResponse),
     (JoinProject, JoinProjectResponse),
     (OpenBuffer, OpenBufferResponse),
+    (OpenBufferForSymbol, OpenBufferForSymbolResponse),
     (Ping, Ack),
     (PerformRename, PerformRenameResponse),
     (PrepareRename, PrepareRenameResponse),
@@ -236,9 +242,11 @@ entity_messages!(
     GetCodeActions,
     GetCompletions,
     GetDefinition,
+    GetProjectSymbols,
     JoinProject,
     LeaveProject,
     OpenBuffer,
+    OpenBufferForSymbol,
     PerformRename,
     PrepareRename,
     RemoveProjectCollaborator,

crates/server/src/rpc.rs 🔗

@@ -79,6 +79,8 @@ impl Server {
             .add_message_handler(Server::disk_based_diagnostics_updating)
             .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)
@@ -587,6 +589,34 @@ impl Server {
             .await?)
     }
 
+    async fn get_project_symbols(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::GetProjectSymbols>,
+    ) -> tide::Result<proto::GetProjectSymbolsResponse> {
+        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_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>,
@@ -1135,7 +1165,7 @@ mod tests {
     use serde_json::json;
     use sqlx::types::time::OffsetDateTime;
     use std::{
-        cell::{Cell, RefCell},
+        cell::Cell,
         env,
         ops::Deref,
         path::Path,
@@ -2001,18 +2031,17 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry).unwrap().add(
-            Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
                 LanguageConfig {
-                    name: "Rust".to_string(),
+                    name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
                     language_server: Some(language_server_config),
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )),
-            
-        );
+            )));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2233,18 +2262,17 @@ mod tests {
             }),
             ..Default::default()
         });
-        Arc::get_mut(&mut lang_registry).unwrap().add(
-            Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
                 LanguageConfig {
-                    name: "Rust".to_string(),
+                    name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
                     language_server: Some(language_server_config),
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )),
-            
-        );
+            )));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2436,18 +2464,17 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry).unwrap().add(
-            Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
                 LanguageConfig {
-                    name: "Rust".to_string(),
+                    name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
                     language_server: Some(language_server_config),
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )),
-            
-        );
+            )));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2554,18 +2581,17 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry).unwrap().add(
-            Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
                 LanguageConfig {
-                    name: "Rust".to_string(),
+                    name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
                     language_server: Some(language_server_config),
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )),
-            
-        );
+            )));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2681,6 +2707,142 @@ mod tests {
             .await;
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_project_symbols(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let mut lang_registry = Arc::new(LanguageRegistry::new());
+        let fs = FakeFs::new(cx_a.background());
+        fs.insert_tree(
+            "/code",
+            json!({
+                "crate-1": {
+                    ".zed.toml": r#"collaborators = ["user_b"]"#,
+                    "one.rs": "const ONE: usize = 1;",
+                },
+                "crate-2": {
+                    "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
+                },
+                "private": {
+                    "passwords.txt": "the-password",
+                }
+            }),
+        )
+        .await;
+
+        // Set up a fake language server.
+        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
+                LanguageConfig {
+                    name: "Rust".into(),
+                    path_suffixes: vec!["rs".to_string()],
+                    language_server: Some(language_server_config),
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )));
+
+        // Connect to a server as 2 clients.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let client_a = server.create_client(&mut cx_a, "user_a").await;
+        let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+        // Share a project as client A
+        let project_a = cx_a.update(|cx| {
+            Project::local(
+                client_a.clone(),
+                client_a.user_store.clone(),
+                lang_registry.clone(),
+                fs.clone(),
+                cx,
+            )
+        });
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_local_worktree("/code/crate-1", false, cx)
+            })
+            .await
+            .unwrap();
+        worktree_a
+            .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+            .await;
+        let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await;
+        let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id());
+        project_a
+            .update(&mut cx_a, |p, cx| p.share(cx))
+            .await
+            .unwrap();
+
+        // Join the worktree as client B.
+        let project_b = Project::remote(
+            project_id,
+            client_b.clone(),
+            client_b.user_store.clone(),
+            lang_registry.clone(),
+            fs.clone(),
+            &mut cx_b.to_async(),
+        )
+        .await
+        .unwrap();
+
+        // Cause the language server to start.
+        let _buffer = cx_b
+            .background()
+            .spawn(project_b.update(&mut cx_b, |p, cx| {
+                p.open_buffer((worktree_id, "one.rs"), cx)
+            }))
+            .await
+            .unwrap();
+
+        // Request the definition of a symbol as the guest.
+        let symbols = project_b.update(&mut cx_b, |p, cx| p.symbols("two", cx));
+        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _>(|_| {
+            #[allow(deprecated)]
+            Some(vec![lsp::SymbolInformation {
+                name: "TWO".into(),
+                location: lsp::Location {
+                    uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
+                    range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                },
+                kind: lsp::SymbolKind::CONSTANT,
+                tags: None,
+                container_name: None,
+                deprecated: None,
+            }])
+        });
+
+        let symbols = symbols.await.unwrap();
+        assert_eq!(symbols.len(), 1);
+        assert_eq!(symbols[0].name, "TWO");
+
+        // Open one of the returned symbols.
+        let buffer_b_2 = project_b
+            .update(&mut cx_b, |project, cx| {
+                project.open_buffer_for_symbol(&symbols[0], cx)
+            })
+            .await
+            .unwrap();
+        buffer_b_2.read_with(&cx_b, |buffer, _| {
+            assert_eq!(
+                buffer.file().unwrap().path().as_ref(),
+                Path::new("../crate-2/two.rs")
+            );
+        });
+
+        // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
+        let mut fake_symbol = symbols[0].clone();
+        fake_symbol.path = Path::new("/code/secrets").into();
+        let error = project_b
+            .update(&mut cx_b, |project, cx| {
+                project.open_buffer_for_symbol(&fake_symbol, cx)
+            })
+            .await
+            .unwrap_err();
+        assert!(error.to_string().contains("invalid symbol signature"));
+    }
+
     #[gpui::test(iterations = 10)]
     async fn test_open_buffer_while_getting_definition_pointing_to_it(
         mut cx_a: TestAppContext,
@@ -2703,18 +2865,17 @@ mod tests {
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
 
-        Arc::get_mut(&mut lang_registry).unwrap().add(
-            Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
                 LanguageConfig {
-                    name: "Rust".to_string(),
+                    name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
                     language_server: Some(language_server_config),
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )),
-            
-        );
+            )));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2805,18 +2966,17 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry).unwrap().add(
-            Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
                 LanguageConfig {
-                    name: "Rust".to_string(),
+                    name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
                     language_server: Some(language_server_config),
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )),
-            
-        );
+            )));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3045,18 +3205,17 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry).unwrap().add(
-            Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
                 LanguageConfig {
-                    name: "Rust".to_string(),
+                    name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
                     language_server: Some(language_server_config),
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )),
-            
-        );
+            )));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3817,53 +3976,10 @@ mod tests {
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
 
-        let rng = Rc::new(RefCell::new(rng));
+        let rng = Arc::new(Mutex::new(rng));
 
-        let mut host_lang_registry = Arc::new(LanguageRegistry::new());
         let guest_lang_registry = Arc::new(LanguageRegistry::new());
-
-        // Set up a fake language server.
-        let (mut language_server_config, _fake_language_servers) = LanguageServerConfig::fake();
-        language_server_config.set_fake_initializer(|fake_server| {
-            fake_server.handle_request::<lsp::request::Completion, _>(|_| {
-                Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem {
-                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
-                        new_text: "the-new-text".to_string(),
-                    })),
-                    ..Default::default()
-                }]))
-            });
-
-            fake_server.handle_request::<lsp::request::CodeActionRequest, _>(|_| {
-                Some(vec![lsp::CodeActionOrCommand::CodeAction(
-                    lsp::CodeAction {
-                        title: "the-code-action".to_string(),
-                        ..Default::default()
-                    },
-                )])
-            });
-
-            fake_server.handle_request::<lsp::request::PrepareRenameRequest, _>(|params| {
-                Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
-                    params.position,
-                    params.position,
-                )))
-            });
-        });
-
-        Arc::get_mut(&mut host_lang_registry).unwrap().add(
-            Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".to_string(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                None,
-            )),
-            
-        );
+        let (language_server_config, _fake_language_servers) = LanguageServerConfig::fake();
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -3892,7 +4008,7 @@ mod tests {
             Project::local(
                 host.client.clone(),
                 host.user_store.clone(),
-                host_lang_registry.clone(),
+                Arc::new(LanguageRegistry::new()),
                 fs.clone(),
                 cx,
             )
@@ -3917,6 +4033,7 @@ mod tests {
 
         clients.push(cx.foreground().spawn(host.simulate_host(
             host_project.clone(),
+            language_server_config,
             operations.clone(),
             max_operations,
             rng.clone(),
@@ -3925,7 +4042,7 @@ mod tests {
 
         while operations.get() < max_operations {
             cx.background().simulate_random_delay().await;
-            if clients.len() < max_peers && rng.borrow_mut().gen_bool(0.05) {
+            if clients.len() < max_peers && rng.lock().gen_bool(0.05) {
                 operations.set(operations.get() + 1);
 
                 let guest_id = clients.len();
@@ -4246,113 +4363,188 @@ mod tests {
             )
         }
 
-        async fn simulate_host(
+        fn simulate_host(
             mut self,
             project: ModelHandle<Project>,
+            mut language_server_config: LanguageServerConfig,
             operations: Rc<Cell<usize>>,
             max_operations: usize,
-            rng: Rc<RefCell<StdRng>>,
+            rng: Arc<Mutex<StdRng>>,
             mut cx: TestAppContext,
-        ) -> (Self, TestAppContext) {
-            let fs = project.read_with(&cx, |project, _| project.fs().clone());
-            let mut files: Vec<PathBuf> = Default::default();
-            while operations.get() < max_operations {
-                operations.set(operations.get() + 1);
+        ) -> impl Future<Output = (Self, TestAppContext)> {
+            let files: Arc<Mutex<Vec<PathBuf>>> = Default::default();
+
+            // Set up a fake language server.
+            language_server_config.set_fake_initializer({
+                let rng = rng.clone();
+                let files = files.clone();
+                move |fake_server| {
+                    fake_server.handle_request::<lsp::request::Completion, _>(|_| {
+                        Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem {
+                            text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 0),
+                                    lsp::Position::new(0, 0),
+                                ),
+                                new_text: "the-new-text".to_string(),
+                            })),
+                            ..Default::default()
+                        }]))
+                    });
 
-                let distribution = rng.borrow_mut().gen_range(0..100);
-                match distribution {
-                    0..=20 if !files.is_empty() => {
-                        let mut path = files.choose(&mut *rng.borrow_mut()).unwrap().as_path();
-                        while let Some(parent_path) = path.parent() {
-                            path = parent_path;
-                            if rng.borrow_mut().gen() {
-                                break;
-                            }
+                    fake_server.handle_request::<lsp::request::CodeActionRequest, _>(|_| {
+                        Some(vec![lsp::CodeActionOrCommand::CodeAction(
+                            lsp::CodeAction {
+                                title: "the-code-action".to_string(),
+                                ..Default::default()
+                            },
+                        )])
+                    });
+
+                    fake_server.handle_request::<lsp::request::PrepareRenameRequest, _>(|params| {
+                        Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                            params.position,
+                            params.position,
+                        )))
+                    });
+
+                    fake_server.handle_request::<lsp::request::GotoDefinition, _>({
+                        let files = files.clone();
+                        let rng = rng.clone();
+                        move |_| {
+                            let files = files.lock();
+                            let mut rng = rng.lock();
+                            let count = rng.gen_range::<usize, _>(1..3);
+                            Some(lsp::GotoDefinitionResponse::Array(
+                                (0..count)
+                                    .map(|_| {
+                                        let file = files.choose(&mut *rng).unwrap().as_path();
+                                        lsp::Location {
+                                            uri: lsp::Url::from_file_path(file).unwrap(),
+                                            range: Default::default(),
+                                        }
+                                    })
+                                    .collect(),
+                            ))
                         }
+                    });
+                }
+            });
 
-                        log::info!("Host: find/create local worktree {:?}", path);
-                        project
-                            .update(&mut cx, |project, cx| {
-                                project.find_or_create_local_worktree(path, false, cx)
-                            })
-                            .await
-                            .unwrap();
-                    }
-                    10..=80 if !files.is_empty() => {
-                        let buffer = if self.buffers.is_empty() || rng.borrow_mut().gen() {
-                            let file = files.choose(&mut *rng.borrow_mut()).unwrap();
-                            let (worktree, path) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.find_or_create_local_worktree(file, false, cx)
-                                })
-                                .await
-                                .unwrap();
-                            let project_path =
-                                worktree.read_with(&cx, |worktree, _| (worktree.id(), path));
-                            log::info!("Host: opening path {:?}", project_path);
-                            let buffer = project
+            project.update(&mut cx, |project, _| {
+                project.languages().add(Arc::new(Language::new(
+                    LanguageConfig {
+                        name: "Rust".into(),
+                        path_suffixes: vec!["rs".to_string()],
+                        language_server: Some(language_server_config),
+                        ..Default::default()
+                    },
+                    None,
+                )));
+            });
+
+            async move {
+                let fs = project.read_with(&cx, |project, _| project.fs().clone());
+                while operations.get() < max_operations {
+                    operations.set(operations.get() + 1);
+
+                    let distribution = rng.lock().gen_range::<usize, _>(0..100);
+                    match distribution {
+                        0..=20 if !files.lock().is_empty() => {
+                            let path = files.lock().choose(&mut *rng.lock()).unwrap().clone();
+                            let mut path = path.as_path();
+                            while let Some(parent_path) = path.parent() {
+                                path = parent_path;
+                                if rng.lock().gen() {
+                                    break;
+                                }
+                            }
+
+                            log::info!("Host: find/create local worktree {:?}", path);
+                            project
                                 .update(&mut cx, |project, cx| {
-                                    project.open_buffer(project_path, cx)
+                                    project.find_or_create_local_worktree(path, false, cx)
                                 })
                                 .await
                                 .unwrap();
-                            self.buffers.insert(buffer.clone());
-                            buffer
-                        } else {
-                            self.buffers
-                                .iter()
-                                .choose(&mut *rng.borrow_mut())
-                                .unwrap()
-                                .clone()
-                        };
-
-                        if rng.borrow_mut().gen_bool(0.1) {
-                            cx.update(|cx| {
-                                log::info!(
-                                    "Host: dropping buffer {:?}",
-                                    buffer.read(cx).file().unwrap().full_path(cx)
-                                );
-                                self.buffers.remove(&buffer);
-                                drop(buffer);
-                            });
-                        } else {
-                            buffer.update(&mut cx, |buffer, cx| {
-                                log::info!(
-                                    "Host: updating buffer {:?}",
-                                    buffer.file().unwrap().full_path(cx)
-                                );
-                                buffer.randomly_edit(&mut *rng.borrow_mut(), 5, cx)
-                            });
                         }
-                    }
-                    _ => loop {
-                        let path_component_count = rng.borrow_mut().gen_range(1..=5);
-                        let mut path = PathBuf::new();
-                        path.push("/");
-                        for _ in 0..path_component_count {
-                            let letter = rng.borrow_mut().gen_range(b'a'..=b'z');
-                            path.push(std::str::from_utf8(&[letter]).unwrap());
-                        }
-                        path.set_extension("rs");
-                        let parent_path = path.parent().unwrap();
-
-                        log::info!("Host: creating file {:?}", path);
-                        if fs.create_dir(&parent_path).await.is_ok()
-                            && fs.create_file(&path, Default::default()).await.is_ok()
-                        {
-                            files.push(path);
-                            break;
-                        } else {
-                            log::info!("Host: cannot create file");
+                        10..=80 if !files.lock().is_empty() => {
+                            let buffer = if self.buffers.is_empty() || rng.lock().gen() {
+                                let file = files.lock().choose(&mut *rng.lock()).unwrap().clone();
+                                let (worktree, path) = project
+                                    .update(&mut cx, |project, cx| {
+                                        project.find_or_create_local_worktree(file, false, cx)
+                                    })
+                                    .await
+                                    .unwrap();
+                                let project_path =
+                                    worktree.read_with(&cx, |worktree, _| (worktree.id(), path));
+                                log::info!("Host: opening path {:?}", project_path);
+                                let buffer = project
+                                    .update(&mut cx, |project, cx| {
+                                        project.open_buffer(project_path, cx)
+                                    })
+                                    .await
+                                    .unwrap();
+                                self.buffers.insert(buffer.clone());
+                                buffer
+                            } else {
+                                self.buffers
+                                    .iter()
+                                    .choose(&mut *rng.lock())
+                                    .unwrap()
+                                    .clone()
+                            };
+
+                            if rng.lock().gen_bool(0.1) {
+                                cx.update(|cx| {
+                                    log::info!(
+                                        "Host: dropping buffer {:?}",
+                                        buffer.read(cx).file().unwrap().full_path(cx)
+                                    );
+                                    self.buffers.remove(&buffer);
+                                    drop(buffer);
+                                });
+                            } else {
+                                buffer.update(&mut cx, |buffer, cx| {
+                                    log::info!(
+                                        "Host: updating buffer {:?}",
+                                        buffer.file().unwrap().full_path(cx)
+                                    );
+                                    buffer.randomly_edit(&mut *rng.lock(), 5, cx)
+                                });
+                            }
                         }
-                    },
+                        _ => loop {
+                            let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
+                            let mut path = PathBuf::new();
+                            path.push("/");
+                            for _ in 0..path_component_count {
+                                let letter = rng.lock().gen_range(b'a'..=b'z');
+                                path.push(std::str::from_utf8(&[letter]).unwrap());
+                            }
+                            path.set_extension("rs");
+                            let parent_path = path.parent().unwrap();
+
+                            log::info!("Host: creating file {:?}", path,);
+
+                            if fs.create_dir(&parent_path).await.is_ok()
+                                && fs.create_file(&path, Default::default()).await.is_ok()
+                            {
+                                files.lock().push(path);
+                                break;
+                            } else {
+                                log::info!("Host: cannot create file");
+                            }
+                        },
+                    }
+
+                    cx.background().simulate_random_delay().await;
                 }
 
-                cx.background().simulate_random_delay().await;
+                self.project = Some(project);
+                (self, cx)
             }
-
-            self.project = Some(project);
-            (self, cx)
         }
 
         pub async fn simulate_guest(
@@ -4361,18 +4553,18 @@ mod tests {
             project: ModelHandle<Project>,
             operations: Rc<Cell<usize>>,
             max_operations: usize,
-            rng: Rc<RefCell<StdRng>>,
+            rng: Arc<Mutex<StdRng>>,
             mut cx: TestAppContext,
         ) -> (Self, TestAppContext) {
             while operations.get() < max_operations {
-                let buffer = if self.buffers.is_empty() || rng.borrow_mut().gen() {
+                let buffer = if self.buffers.is_empty() || rng.lock().gen() {
                     let worktree = if let Some(worktree) = project.read_with(&cx, |project, cx| {
                         project
                             .worktrees(&cx)
                             .filter(|worktree| {
                                 worktree.read(cx).entries(false).any(|e| e.is_file())
                             })
-                            .choose(&mut *rng.borrow_mut())
+                            .choose(&mut *rng.lock())
                     }) {
                         worktree
                     } else {
@@ -4385,7 +4577,7 @@ mod tests {
                         let entry = worktree
                             .entries(false)
                             .filter(|e| e.is_file())
-                            .choose(&mut *rng.borrow_mut())
+                            .choose(&mut *rng.lock())
                             .unwrap();
                         (worktree.id(), entry.path.clone())
                     });
@@ -4401,12 +4593,12 @@ mod tests {
 
                     self.buffers
                         .iter()
-                        .choose(&mut *rng.borrow_mut())
+                        .choose(&mut *rng.lock())
                         .unwrap()
                         .clone()
                 };
 
-                let choice = rng.borrow_mut().gen_range(0..100);
+                let choice = rng.lock().gen_range(0..100);
                 match choice {
                     0..=9 => {
                         cx.update(|cx| {
@@ -4426,13 +4618,13 @@ mod tests {
                                 guest_id,
                                 buffer.read(cx).file().unwrap().full_path(cx)
                             );
-                            let offset = rng.borrow_mut().gen_range(0..=buffer.read(cx).len());
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
                             project.completions(&buffer, offset, cx)
                         });
                         let completions = cx.background().spawn(async move {
                             completions.await.expect("completions request failed");
                         });
-                        if rng.borrow_mut().gen_bool(0.3) {
+                        if rng.lock().gen_bool(0.3) {
                             log::info!("Guest {}: detaching completions request", guest_id);
                             completions.detach();
                         } else {
@@ -4446,14 +4638,13 @@ mod tests {
                                 guest_id,
                                 buffer.read(cx).file().unwrap().full_path(cx)
                             );
-                            let range =
-                                buffer.read(cx).random_byte_range(0, &mut *rng.borrow_mut());
+                            let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
                             project.code_actions(&buffer, range, cx)
                         });
                         let code_actions = cx.background().spawn(async move {
                             code_actions.await.expect("code actions request failed");
                         });
-                        if rng.borrow_mut().gen_bool(0.3) {
+                        if rng.lock().gen_bool(0.3) {
                             log::info!("Guest {}: detaching code actions request", guest_id);
                             code_actions.detach();
                         } else {
@@ -4476,7 +4667,7 @@ mod tests {
                                 assert!(saved_version.observed_all(&requested_version));
                             });
                         });
-                        if rng.borrow_mut().gen_bool(0.3) {
+                        if rng.lock().gen_bool(0.3) {
                             log::info!("Guest {}: detaching save request", guest_id);
                             save.detach();
                         } else {
@@ -4490,19 +4681,39 @@ mod tests {
                                 guest_id,
                                 buffer.read(cx).file().unwrap().full_path(cx)
                             );
-                            let offset = rng.borrow_mut().gen_range(0..=buffer.read(cx).len());
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
                             project.prepare_rename(buffer, offset, cx)
                         });
                         let prepare_rename = cx.background().spawn(async move {
                             prepare_rename.await.expect("prepare rename request failed");
                         });
-                        if rng.borrow_mut().gen_bool(0.3) {
+                        if rng.lock().gen_bool(0.3) {
                             log::info!("Guest {}: detaching prepare rename request", guest_id);
                             prepare_rename.detach();
                         } else {
                             prepare_rename.await;
                         }
                     }
+                    46..=49 => {
+                        let definitions = project.update(&mut cx, |project, cx| {
+                            log::info!(
+                                "Guest {}: requesting defintions for buffer {:?}",
+                                guest_id,
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.definition(&buffer, offset, cx)
+                        });
+                        let definitions = cx.background().spawn(async move {
+                            definitions.await.expect("definitions request failed");
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("Guest {}: detaching definitions request", guest_id);
+                            definitions.detach();
+                        } else {
+                            definitions.await;
+                        }
+                    }
                     _ => {
                         buffer.update(&mut cx, |buffer, cx| {
                             log::info!(
@@ -4510,7 +4721,7 @@ mod tests {
                                 guest_id,
                                 buffer.file().unwrap().full_path(cx)
                             );
-                            buffer.randomly_edit(&mut *rng.borrow_mut(), 5, cx)
+                            buffer.randomly_edit(&mut *rng.lock(), 5, cx)
                         });
                     }
                 }

crates/workspace/src/lsp_status.rs 🔗

@@ -37,7 +37,7 @@ impl LspStatus {
                             &mut this.downloading,
                             &mut this.failed,
                         ] {
-                            vector.retain(|name| name != language.name());
+                            vector.retain(|name| name != language.name().as_ref());
                         }
 
                         match event {

crates/workspace/src/settings.rs 🔗

@@ -11,7 +11,7 @@ pub struct Settings {
     pub tab_size: usize,
     soft_wrap: SoftWrap,
     preferred_line_length: u32,
-    overrides: HashMap<String, Override>,
+    overrides: HashMap<Arc<str>, Override>,
     pub theme: Arc<Theme>,
 }
 
@@ -50,21 +50,25 @@ impl Settings {
         self
     }
 
-    pub fn with_overrides(mut self, language_name: impl Into<String>, overrides: Override) -> Self {
+    pub fn with_overrides(
+        mut self,
+        language_name: impl Into<Arc<str>>,
+        overrides: Override,
+    ) -> Self {
         self.overrides.insert(language_name.into(), overrides);
         self
     }
 
     pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
         language
-            .and_then(|language| self.overrides.get(language.name()))
+            .and_then(|language| self.overrides.get(language.name().as_ref()))
             .and_then(|settings| settings.soft_wrap)
             .unwrap_or(self.soft_wrap)
     }
 
     pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
         language
-            .and_then(|language| self.overrides.get(language.name()))
+            .and_then(|language| self.overrides.get(language.name().as_ref()))
             .and_then(|settings| settings.preferred_line_length)
             .unwrap_or(self.preferred_line_length)
     }

crates/zed/Cargo.toml 🔗

@@ -47,6 +47,7 @@ lsp = { path = "../lsp" }
 outline = { path = "../outline" }
 project = { path = "../project" }
 project_panel = { path = "../project_panel" }
+project_symbols = { path = "../project_symbols" }
 rpc = { path = "../rpc" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }

crates/zed/src/language.rs 🔗

@@ -159,7 +159,7 @@ impl LspExt for RustLsp {
         &self,
         completion: &lsp::CompletionItem,
         language: &Language,
-    ) -> Option<CompletionLabel> {
+    ) -> Option<CodeLabel> {
         match completion.kind {
             Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
                 let detail = completion.detail.as_ref().unwrap();
@@ -167,11 +167,10 @@ impl LspExt for RustLsp {
                 let text = format!("{}: {}", name, detail);
                 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
                 let runs = language.highlight_text(&source, 11..11 + text.len());
-                return Some(CompletionLabel {
+                return Some(CodeLabel {
                     text,
                     runs,
                     filter_range: 0..name.len(),
-                    left_aligned_len: name.len(),
                 });
             }
             Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
@@ -182,11 +181,10 @@ impl LspExt for RustLsp {
                 let text = format!("{}: {}", name, detail);
                 let source = Rope::from(format!("let {} = ();", text).as_str());
                 let runs = language.highlight_text(&source, 4..4 + text.len());
-                return Some(CompletionLabel {
+                return Some(CodeLabel {
                     text,
                     runs,
                     filter_range: 0..name.len(),
-                    left_aligned_len: name.len(),
                 });
             }
             Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
@@ -201,8 +199,7 @@ impl LspExt for RustLsp {
                     let text = REGEX.replace(&completion.label, &detail[2..]).to_string();
                     let source = Rope::from(format!("fn {} {{}}", text).as_str());
                     let runs = language.highlight_text(&source, 3..3 + text.len());
-                    return Some(CompletionLabel {
-                        left_aligned_len: text.find("->").unwrap_or(text.len()),
+                    return Some(CodeLabel {
                         filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
                         text,
                         runs,
@@ -222,7 +219,7 @@ impl LspExt for RustLsp {
                     _ => None,
                 };
                 let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
-                let mut label = CompletionLabel::plain(&completion);
+                let mut label = CodeLabel::plain(completion.label.clone(), None);
                 label.runs.push((
                     0..label.text.rfind('(').unwrap_or(label.text.len()),
                     highlight_id,
@@ -233,6 +230,65 @@ impl LspExt for RustLsp {
         }
         None
     }
+
+    fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp::SymbolKind,
+        language: &Language,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
+                let text = format!("fn {} () {{}}", name);
+                let filter_range = 3..3 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::STRUCT => {
+                let text = format!("struct {} {{}}", name);
+                let filter_range = 7..7 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::ENUM => {
+                let text = format!("enum {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::INTERFACE => {
+                let text = format!("trait {} {{}}", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::CONSTANT => {
+                let text = format!("const {}: () = ();", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::MODULE => {
+                let text = format!("mod {} {{}}", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::TYPE_PARAMETER => {
+                let text = format!("type {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
 }
 
 pub fn build_language_registry() -> LanguageRegistry {
@@ -326,7 +382,7 @@ mod tests {
     }
 
     #[test]
-    fn test_process_rust_completions() {
+    fn test_rust_label_for_completion() {
         let language = rust();
         let grammar = language.grammar().unwrap();
         let theme = SyntaxTheme::new(vec![
@@ -350,7 +406,7 @@ mod tests {
                 detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
                 ..Default::default()
             }),
-            Some(CompletionLabel {
+            Some(CodeLabel {
                 text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
                 filter_range: 0..5,
                 runs: vec![
@@ -361,7 +417,6 @@ mod tests {
                     (25..28, highlight_type),
                     (29..30, highlight_type),
                 ],
-                left_aligned_len: 22,
             })
         );
 
@@ -372,11 +427,10 @@ mod tests {
                 detail: Some("usize".to_string()),
                 ..Default::default()
             }),
-            Some(CompletionLabel {
+            Some(CodeLabel {
                 text: "len: usize".to_string(),
                 filter_range: 0..3,
                 runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
-                left_aligned_len: 3,
             })
         );
 
@@ -387,7 +441,7 @@ mod tests {
                 detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
                 ..Default::default()
             }),
-            Some(CompletionLabel {
+            Some(CodeLabel {
                 text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
                 filter_range: 0..5,
                 runs: vec![
@@ -398,7 +452,42 @@ mod tests {
                     (25..28, highlight_type),
                     (29..30, highlight_type),
                 ],
-                left_aligned_len: 22,
+            })
+        );
+    }
+
+    #[test]
+    fn test_rust_label_for_symbol() {
+        let language = rust();
+        let grammar = language.grammar().unwrap();
+        let theme = SyntaxTheme::new(vec![
+            ("type".into(), Color::green().into()),
+            ("keyword".into(), Color::blue().into()),
+            ("function".into(), Color::red().into()),
+            ("property".into(), Color::white().into()),
+        ]);
+
+        language.set_theme(&theme);
+
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+
+        assert_eq!(
+            language.label_for_symbol("hello", lsp::SymbolKind::FUNCTION),
+            Some(CodeLabel {
+                text: "fn hello".to_string(),
+                filter_range: 3..8,
+                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
+            })
+        );
+
+        assert_eq!(
+            language.label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER),
+            Some(CodeLabel {
+                text: "type World".to_string(),
+                filter_range: 5..10,
+                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
             })
         );
     }

crates/zed/src/main.rs 🔗

@@ -57,6 +57,7 @@ fn main() {
         file_finder::init(cx);
         chat_panel::init(cx);
         outline::init(cx);
+        project_symbols::init(cx);
         project_panel::init(cx);
         diagnostics::init(cx);
         find::init(cx);

crates/zed/src/test.rs 🔗

@@ -25,18 +25,15 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     let http = FakeHttpClient::with_404_response();
     let client = Client::new(http.clone());
     let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-    let mut languages = LanguageRegistry::new();
-    languages.add(
-        Arc::new(language::Language::new(
-            language::LanguageConfig {
-                name: "Rust".to_string(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        )),
-        
-    );
+    let languages = LanguageRegistry::new();
+    languages.add(Arc::new(language::Language::new(
+        language::LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    )));
     Arc::new(AppState {
         settings_tx: Arc::new(Mutex::new(settings_tx)),
         settings,

crates/zed/src/zed.rs 🔗

@@ -534,7 +534,7 @@ mod tests {
         editor.read_with(&cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
             assert_eq!(editor.title(cx), "the-new-name.rs");
-            assert_eq!(editor.language(cx).unwrap().name(), "Rust");
+            assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust");
         });
 
         // Edit the file and save it again. This time, there is no filename prompt.
@@ -614,7 +614,7 @@ mod tests {
         // The buffer is not dirty anymore and the language is assigned based on the path.
         editor.read_with(&cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
-            assert_eq!(editor.language(cx).unwrap().name(), "Rust")
+            assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust")
         });
     }