Merge pull request #479 from zed-industries/project-symbols
Project symbols
This commit is contained in:
Generated
+85
-17
@@ -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"
|
||||
|
||||
+59
-21
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
-42
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
+104
-15
@@ -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);
|
||||
|
||||
+9
-12
@@ -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")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user