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

Project symbols
This commit is contained in:
Max Brunsfeld
2022-02-22 14:25:46 -08:00
committed by GitHub
24 changed files with 1624 additions and 384 deletions
Generated
+85 -17
View File
@@ -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",
+2 -2
View File
@@ -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()
},
+4 -4
View File
@@ -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)> {
+2 -2
View File
@@ -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,
}
+54 -38
View File
@@ -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();
}
}
+5 -2
View File
@@ -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,
})
}
+7 -7
View File
@@ -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()
+2 -1
View File
@@ -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"
+59 -21
View File
@@ -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 mut state = self.state.lock().await;
state.validate_path(source)?;
state.validate_path(target)?;
let source = normalize_path(source);
let target = normalize_path(target);
if !options.overwrite && state.entries.contains_key(target) {
let mut state = self.state.lock().await;
state.validate_path(&source)?;
state.validate_path(&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
}
+1 -1
View File
@@ -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(),
+353 -13
View File
@@ -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()
+5 -2
View File
@@ -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 {
+20
View File
@@ -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);
}
}
}
}
+81 -42
View File
@@ -23,53 +23,57 @@ message Envelope {
RemoveProjectCollaborator remove_project_collaborator = 17;
GetDefinition get_definition = 18;
GetDefinitionResponse get_definition_response = 19;
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 = 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;
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 = 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;
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 = 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;
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 = 55;
UpdateContacts update_contacts = 63;
GetUsers get_users = 56;
GetUsersResponse get_users_response = 57;
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;
+8
View File
@@ -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,
+409 -198
View File
@@ -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();
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;
// 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()
}]))
});
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(),
))
}
});
}
});
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.find_or_create_local_worktree(path, false, cx)
})
.await
.unwrap();
}
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: find/create local worktree {:?}", path);
project
.update(&mut cx, |project, cx| {
project.find_or_create_local_worktree(path, false, cx)
})
.await
.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");
}
},
}
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
.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.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");
}
},
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)
});
}
}
+1 -1
View File
@@ -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 {
+8 -4
View File
@@ -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)
}
+1
View File
@@ -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" }
+104 -15
View File
@@ -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)],
})
);
}
+1
View File
@@ -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);
+9 -12
View File
@@ -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,
+2 -2
View File
@@ -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")
});
}