diff --git a/Cargo.lock b/Cargo.lock index 31bd5f2993d40baee623a7bb25ca334404fcf6aa..93674db8b94f27ef6877768f20334fb1cabcbce8 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 24fd53155886d5d980af6bec1639ed74113131e3..dd23799175d4b7b04415df07ae99a0921b678e4f 100644 --- a/crates/editor/src/display_map.rs +++ b/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() }, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ebe24da4e286282d7ae90d7e12690c977c791931..72bb002ed7dd397fdf1a1e9b3df4e85fdf187a8b 100644 --- a/crates/editor/src/editor.rs +++ b/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, HighlightStyle)> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b4543b02b02d98cfb06ecf75bbc66391b0447d47..336ad737c36da5298e635cd15fd5a045f5e7a129 100644 --- a/crates/language/src/buffer.rs +++ b/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, pub new_text: String, - pub label: CompletionLabel, + pub label: CodeLabel, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 9f685befffa99f6f9271639b33148db166a8e802..57a4fe001a7228767e29f261b064451bcc6f8ceb 100644 --- a/crates/language/src/language.rs +++ b/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 = 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>; fn cached_server_binary(&self, download_dir: Arc) -> BoxFuture<'static, Option>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); - fn label_for_completion( - &self, - _: &lsp::CompletionItem, - _: &Language, - ) -> Option { + fn label_for_completion(&self, _: &lsp::CompletionItem, _: &Language) -> Option { + None + } + fn label_for_symbol(&self, _: &str, _: lsp::SymbolKind, _: &Language) -> Option { None } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct CompletionLabel { +pub struct CodeLabel { pub text: String, pub runs: Vec<(Range, HighlightId)>, pub filter_range: Range, - pub left_aligned_len: usize, } -#[derive(Default, Deserialize)] +#[derive(Deserialize)] pub struct LanguageConfig { - pub name: String, + pub name: Arc, pub path_suffixes: Vec, pub brackets: Vec, pub line_comment: Option, pub language_server: Option, } +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, @@ -153,7 +163,7 @@ pub enum LanguageServerBinaryStatus { } pub struct LanguageRegistry { - languages: Vec>, + languages: RwLock>>, language_server_download_dir: Option>, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, @@ -170,12 +180,12 @@ impl LanguageRegistry { } } - pub fn add(&mut self, language: Arc) { - self.languages.push(language.clone()); + pub fn add(&self, language: Arc) { + 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> { + pub fn get_language(&self, name: &str) -> Option> { 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) -> Option<&Arc> { + pub fn select_language(&self, path: impl AsRef) -> Option> { 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 { + 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 { + pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { self.lsp_ext .as_ref()? .label_for_completion(completion, self) } + pub fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option { + 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(); } } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index a4267cfda2f37bba51829222c2f9f4be121bfe54..80c990455cf7ec9d50f177490f326d59cced44fb 100644 --- a/crates/language/src/proto.rs +++ b/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, }) } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 1adcca91a5c3cb6b8276d34e1e369c494359f715..9fd5d693ff65de0de342d6c2b3ac86d70d5d188c 100644 --- a/crates/language/src/tests.rs +++ b/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() diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 1130282e0e9257195995818229922894c3da1455..f72ba133c32df4135cbd4e0f68c6eac9e300252d 100644 --- a/crates/project/Cargo.toml +++ b/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" diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 8fbdb3220e0575a2c4e3977a0a5c2198921e9e35..7f89c29c8384e33d3817e180c42c288cdbda22a2 100644 --- a/crates/project/src/fs.rs +++ b/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 { + 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 { 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> { 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 +} diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 432963a57f012214138490f5f2196babd7180de9..14f0c2817a8fa4437b09c353b31ac02d02c68d1c 100644 --- a/crates/project/src/lsp_command.rs +++ b/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(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c1ef8fdfcdfb7cf38c97839a42782c37bb7a84f9..19500f322969f06bb13e60e1c04fdea1a73b772e 100644 --- a/crates/project/src/project.rs +++ b/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, Arc>>>, >, shared_buffers: HashMap>>, + nonce: u128, } enum OpenBuffer { @@ -118,6 +123,19 @@ pub struct Definition { pub target_range: Range, } +#[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, + pub signature: [u8; 32], +} + #[derive(Default)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); @@ -186,6 +204,8 @@ impl Project { client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_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 { + &self.languages + } + pub fn fs(&self) -> &Arc { &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> { + 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) -> Task> { + pub fn share(&self, cx: &mut ModelContext) -> Task> { 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) -> Task> { + pub fn unshare(&self, cx: &mut ModelContext) -> Task> { 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) -> Task>> { + 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::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, + ) -> Task>> { + 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( &self, source_buffer_handle: &ModelHandle, @@ -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, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + 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, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + 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, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, - ) -> anyhow::Result { + ) -> Result { 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 { + 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, envelope: TypedEnvelope, _: Arc, 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 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 = 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() diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 89b93a09961a5652a03fd4d634a622175b6ef556..84170115890e747dd270709d29848d6ba1663694 100644 --- a/crates/project/src/worktree.rs +++ b/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 { diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f467261986cedd92bf9590af1d8f909c2744b0f8 --- /dev/null +++ b/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" diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs new file mode 100644 index 0000000000000000000000000000000000000000..1fa8add221591e2acd7f9657fc74ef455e4fda64 --- /dev/null +++ b/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, + project: ModelHandle, + settings: watch::Receiver, + selected_match_index: usize, + list_state: UniformListState, + symbols: Vec, + match_candidates: Vec, + matches: Vec, + pending_symbols_task: Task>, + query_editor: ViewHandle, +} + +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) -> 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) { + cx.focus(&self.query_editor); + } +} + +impl ProjectSymbolsView { + fn new( + project: ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> 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.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) { + if self.selected_match_index > 0 { + self.select(self.selected_match_index - 1, cx); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + 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.select(0, cx); + } + + fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + self.select(self.matches.len().saturating_sub(1), cx); + } + + fn select(&mut self, index: usize, cx: &mut ViewContext) { + self.selected_match_index = index; + self.list_state.scroll_to(ScrollTarget::Show(index)); + cx.notify(); + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + 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.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) { + 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, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Blurred => cx.emit(Event::Dismissed), + editor::Event::Edited => self.update_matches(cx), + _ => {} + } + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + 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::() + .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); + } + } + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 65622e70d41cb13c21421ad5786db868b3d03ba3..664d103a158ee741acf4eb43db067d6b8c186a09 100644 --- a/crates/rpc/proto/zed.proto +++ b/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; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index fa8b2f692cf09d867dd96121b7bca16fdd25ef43..e11586e9ddb378b391a5cea61505217d419cc388 100644 --- a/crates/rpc/src/proto.rs +++ b/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, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0bfb918b45ad42e4d6498a58ff98ee7ed35cd2c6..00b4c61d23f1fcd5b92d059c6394db8ed289c848 100644 --- a/crates/server/src/rpc.rs +++ b/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, + request: TypedEnvelope, + ) -> tide::Result { + 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, + request: TypedEnvelope, + ) -> tide::Result { + 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, request: TypedEnvelope, @@ -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::(|_| { + #[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::(|_| { - 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::(|_| { - Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), - ..Default::default() - }, - )]) - }); - - fake_server.handle_request::(|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, + mut language_server_config: LanguageServerConfig, operations: Rc>, max_operations: usize, - rng: Rc>, + rng: Arc>, mut cx: TestAppContext, - ) -> (Self, TestAppContext) { - let fs = project.read_with(&cx, |project, _| project.fs().clone()); - let mut files: Vec = Default::default(); - while operations.get() < max_operations { - operations.set(operations.get() + 1); + ) -> impl Future { + let files: Arc>> = 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::(|_| { + 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::(|_| { + Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "the-code-action".to_string(), + ..Default::default() + }, + )]) + }); + + fake_server.handle_request::(|params| { + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + params.position, + params.position, + ))) + }); + + fake_server.handle_request::({ + let files = files.clone(); + let rng = rng.clone(); + move |_| { + let files = files.lock(); + let mut rng = rng.lock(); + let count = rng.gen_range::(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::(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::(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, operations: Rc>, max_operations: usize, - rng: Rc>, + rng: Arc>, 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) }); } } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index 093f10b1435fa0aa528c4d7cf2add2c0baa34089..ee61ecf24d9fc1e509377f460689dc68d9885563 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/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 { diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index a1598836a48c626591ced292a3bd53622437863f..7073c1144c7f4b7bd5bf432000bff2cba188469c 100644 --- a/crates/workspace/src/settings.rs +++ b/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, + overrides: HashMap, Override>, pub theme: Arc, } @@ -50,21 +50,25 @@ impl Settings { self } - pub fn with_overrides(mut self, language_name: impl Into, overrides: Override) -> Self { + pub fn with_overrides( + mut self, + language_name: impl Into>, + overrides: Override, + ) -> Self { self.overrides.insert(language_name.into(), overrides); self } pub fn soft_wrap(&self, language: Option<&Arc>) -> 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>) -> 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) } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3b3b0393f6a2ac6b6318a2a8281f1343afd2602d..02e0b70f8853987a16034afa739202a6de7451ea 100644 --- a/crates/zed/Cargo.toml +++ b/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" } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 748e82ffe877eeeabda918b8eec7ac1a97f3c41c..d7969259a4e3edde7c82b9b97249f699c3f66dcb 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -159,7 +159,7 @@ impl LspExt for RustLsp { &self, completion: &lsp::CompletionItem, language: &Language, - ) -> Option { + ) -> Option { 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 { + 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) -> Vec".to_string()), ..Default::default() }), - Some(CompletionLabel { + Some(CodeLabel { text: "hello(&mut Option) -> Vec".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) -> Vec".to_string()), ..Default::default() }), - Some(CompletionLabel { + Some(CodeLabel { text: "hello(&mut Option) -> Vec".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)], }) ); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b15424134399f0ec98e5606421ecff199abd55e4..a6dda2e27bf928e9557a233e3f0c3986ab5e35df 100644 --- a/crates/zed/src/main.rs +++ b/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); diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 8df47ee95e6b72cae941315ff5e00d3beef86c7c..77b092b0bb48157a61ccfce854cb33809223cda3 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -25,18 +25,15 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { 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, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cdc24e5a52e5d74381d28fd3e10e5d547ea41d12..fc1dcdff1db3d12d73dabeb62bebbcbb76e96f7e 100644 --- a/crates/zed/src/zed.rs +++ b/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") }); }