Detailed changes
@@ -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",
@@ -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()
},
@@ -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)> {
@@ -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,
}
@@ -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();
}
}
@@ -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,
})
}
@@ -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()
@@ -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"
@@ -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
+}
@@ -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(),
@@ -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()
@@ -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 {
@@ -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"
@@ -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);
+ }
+ }
+ }
+}
@@ -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;
@@ -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,
@@ -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)
});
}
}
@@ -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 {
@@ -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)
}
@@ -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" }
@@ -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)],
})
);
}
@@ -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);
@@ -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,
@@ -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")
});
}