Add initial support for defining language server adapters in WebAssembly-based extensions (#8645)

This PR adds **internal** ability to run arbitrary language servers via
WebAssembly extensions. The functionality isn't exposed yet - we're just
landing this in this early state because there have been a lot of
changes to the `LspAdapter` trait, and other language server logic.

## Next steps

* Currently, wasm extensions can only define how to *install* and run a
language server, they can't yet implement the other LSP adapter methods,
such as formatting completion labels and workspace symbols.
* We don't have an automatic way to install or develop these types of
extensions
* We don't have a way to package these types of extensions in our
extensions repo, to make them available via our extensions API.
* The Rust extension API crate, `zed-extension-api` has not yet been
published to crates.io, because we still consider the API a work in
progress.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Max Brunsfeld 2024-03-01 16:00:55 -08:00 committed by GitHub
parent f3f2225a8e
commit 268fa1cbaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 3714 additions and 1973 deletions

View file

@ -1472,6 +1472,12 @@ impl LspCommand for GetCompletions {
Default::default()
};
let language_server_adapter = project
.update(&mut cx, |project, _cx| {
project.language_server_adapter_for_id(server_id)
})?
.ok_or_else(|| anyhow!("no such language server"))?;
let completions = buffer.update(&mut cx, |buffer, cx| {
let language_registry = project.read(cx).languages().clone();
let language = buffer.language().cloned();
@ -1559,12 +1565,17 @@ impl LspCommand for GetCompletions {
let language_registry = language_registry.clone();
let language = language.clone();
let language_server_adapter = language_server_adapter.clone();
LineEnding::normalize(&mut new_text);
Some(async move {
let mut label = None;
if let Some(language) = language.as_ref() {
language.process_completion(&mut lsp_completion).await;
label = language.label_for_completion(&lsp_completion).await;
if let Some(language) = &language {
language_server_adapter
.process_completion(&mut lsp_completion)
.await;
label = language_server_adapter
.label_for_completion(&lsp_completion, language)
.await;
}
let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
@ -1651,7 +1662,7 @@ impl LspCommand for GetCompletions {
async fn response_from_proto(
self,
message: proto::GetCompletionsResponse,
_: Model<Project>,
project: Model<Project>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> Result<Vec<Completion>> {
@ -1662,8 +1673,13 @@ impl LspCommand for GetCompletions {
.await?;
let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?;
let completions = message.completions.into_iter().map(|completion| {
language::proto::deserialize_completion(completion, language.clone())
language::proto::deserialize_completion(
completion,
language.clone(),
&language_registry,
)
});
future::try_join_all(completions).await
}

View file

@ -14,7 +14,7 @@ use futures::{
use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
use language::{
language_settings::{Formatter, LanguageSettings},
Buffer, Language, LanguageServerName, LocalFile,
Buffer, Language, LanguageRegistry, LanguageServerName, LocalFile,
};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
@ -26,7 +26,8 @@ use crate::{
};
pub fn prettier_plugins_for_language(
language: &Language,
language_registry: &Arc<LanguageRegistry>,
language: &Arc<Language>,
language_settings: &LanguageSettings,
) -> Option<HashSet<&'static str>> {
match &language_settings.formatter {
@ -38,8 +39,8 @@ pub fn prettier_plugins_for_language(
prettier_plugins
.get_or_insert_with(|| HashSet::default())
.extend(
language
.lsp_adapters()
language_registry
.lsp_adapters(language)
.iter()
.flat_map(|adapter| adapter.prettier_plugins()),
)
@ -303,15 +304,20 @@ fn start_prettier(
) -> PrettierTask {
cx.spawn(|project, mut cx| async move {
log::info!("Starting prettier at path {prettier_dir:?}");
let new_server_id = project.update(&mut cx, |project, _| {
project.languages.next_language_server_id()
})?;
let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?;
let new_server_id = language_registry.next_language_server_id();
let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
.await
.context("default prettier spawn")
.map(Arc::new)
.map_err(Arc::new)?;
let new_prettier = Prettier::start(
new_server_id,
prettier_dir,
node,
language_registry,
cx.clone(),
)
.await
.context("default prettier spawn")
.map(Arc::new)
.map_err(Arc::new)?;
register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
Ok(new_prettier)
})

View file

@ -10,6 +10,7 @@ pub mod terminals;
mod project_tests;
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
@ -847,10 +848,12 @@ impl Project {
let current_lsp_settings = &self.current_lsp_settings;
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
let language = languages.iter().find_map(|l| {
let adapter = l
.lsp_adapters()
let adapter = self
.languages
.lsp_adapters(l)
.iter()
.find(|adapter| &adapter.name == started_lsp_name)?;
.find(|adapter| &adapter.name == started_lsp_name)?
.clone();
Some((l, adapter))
});
if let Some((language, adapter)) = language {
@ -889,9 +892,11 @@ impl Project {
let mut prettier_plugins_by_worktree = HashMap::default();
for (worktree, language, settings) in language_formatters_to_check {
if let Some(plugins) =
prettier_support::prettier_plugins_for_language(&language, &settings)
{
if let Some(plugins) = prettier_support::prettier_plugins_for_language(
&self.languages,
&language,
&settings,
) {
prettier_plugins_by_worktree
.entry(worktree)
.or_insert_with(|| HashSet::default())
@ -2047,7 +2052,7 @@ impl Project {
}
if let Some(language) = language {
for adapter in language.lsp_adapters() {
for adapter in self.languages.lsp_adapters(&language) {
let language_id = adapter.language_ids.get(language.name().as_ref()).cloned();
let server = self
.language_server_ids
@ -2118,10 +2123,12 @@ impl Project {
let worktree_id = old_file.worktree_id(cx);
let ids = &self.language_server_ids;
let language = buffer.language().cloned();
let adapters = language.iter().flat_map(|language| language.lsp_adapters());
for &server_id in adapters.flat_map(|a| ids.get(&(worktree_id, a.name.clone()))) {
buffer.update_diagnostics(server_id, Default::default(), cx);
if let Some(language) = buffer.language().cloned() {
for adapter in self.languages.lsp_adapters(&language) {
if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) {
buffer.update_diagnostics(*server_id, Default::default(), cx);
}
}
}
self.buffer_snapshots.remove(&buffer.remote_id());
@ -2701,9 +2708,11 @@ impl Project {
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
if let Some(prettier_plugins) =
prettier_support::prettier_plugins_for_language(&new_language, &settings)
{
if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language(
&self.languages,
&new_language,
&settings,
) {
self.install_default_prettier(worktree, prettier_plugins, cx);
};
if let Some(file) = buffer_file {
@ -2726,7 +2735,7 @@ impl Project {
return;
}
for adapter in language.lsp_adapters() {
for adapter in self.languages.clone().lsp_adapters(&language) {
self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
}
}
@ -3240,7 +3249,11 @@ impl Project {
};
if file.worktree.read(cx).id() != key.0
|| !language.lsp_adapters().iter().any(|a| a.name == key.1)
|| !self
.languages
.lsp_adapters(&language)
.iter()
.any(|a| a.name == key.1)
{
continue;
}
@ -3433,8 +3446,10 @@ impl Project {
) {
let worktree_id = worktree.read(cx).id();
let stop_tasks = language
.lsp_adapters()
let stop_tasks = self
.languages
.clone()
.lsp_adapters(&language)
.iter()
.map(|adapter| {
let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
@ -4785,14 +4800,15 @@ impl Project {
.languages
.language_for_file(&project_path.path, None)
.unwrap_or_else(move |_| adapter_language);
let language_server_name = adapter.name.clone();
let adapter = adapter.clone();
Some(async move {
let language = language.await;
let label =
language.label_for_symbol(&symbol_name, symbol_kind).await;
let label = adapter
.label_for_symbol(&symbol_name, symbol_kind, &language)
.await;
Symbol {
language_server_name,
language_server_name: adapter.name.clone(),
source_worktree_id,
path: project_path,
label: label.unwrap_or_else(|| {
@ -7972,6 +7988,7 @@ impl Project {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::ApplyCompletionAdditionalEditsResponse> {
let languages = this.update(&mut cx, |this, _| this.languages.clone())?;
let (buffer, completion) = this.update(&mut cx, |this, cx| {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let buffer = this
@ -7986,6 +8003,7 @@ impl Project {
.completion
.ok_or_else(|| anyhow!("invalid completion"))?,
language.cloned(),
&languages,
);
Ok::<_, anyhow::Error>((buffer, completion))
})??;
@ -8713,6 +8731,9 @@ impl Project {
.language_for_file(&path.path, None)
.await
.log_err();
let adapter = language
.as_ref()
.and_then(|language| languages.lsp_adapters(language).first().cloned());
Ok(Symbol {
language_server_name: LanguageServerName(
serialized_symbol.language_server_name.into(),
@ -8720,10 +8741,10 @@ impl Project {
source_worktree_id,
path,
label: {
match language {
Some(language) => {
language
.label_for_symbol(&serialized_symbol.name, kind)
match language.as_ref().zip(adapter.as_ref()) {
Some((language, adapter)) => {
adapter
.label_for_symbol(&serialized_symbol.name, kind, language)
.await
}
None => None,
@ -8975,6 +8996,17 @@ impl Project {
self.supplementary_language_servers.iter()
}
pub fn language_server_adapter_for_id(
&self,
id: LanguageServerId,
) -> Option<Arc<CachedLspAdapter>> {
if let Some(LanguageServerState::Running { adapter, .. }) = self.language_servers.get(&id) {
Some(adapter.clone())
} else {
None
}
}
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
Some(server.clone())
@ -9025,8 +9057,8 @@ impl Project {
) -> Vec<LanguageServerId> {
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
let worktree_id = file.worktree_id(cx);
language
.lsp_adapters()
self.languages
.lsp_adapters(&language)
.iter()
.flat_map(|adapter| {
let key = (worktree_id, adapter.name.clone());
@ -9190,20 +9222,25 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
struct ProjectLspAdapterDelegate {
project: Model<Project>,
worktree: Model<Worktree>,
worktree: worktree::Snapshot,
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
}
impl ProjectLspAdapterDelegate {
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
Arc::new(Self {
project: cx.handle(),
worktree: worktree.clone(),
worktree: worktree.read(cx).snapshot(),
fs: project.fs.clone(),
http_client: project.client.http_client(),
language_registry: project.languages.clone(),
})
}
}
#[async_trait]
impl LspAdapterDelegate for ProjectLspAdapterDelegate {
fn show_notification(&self, message: &str, cx: &mut AppContext) {
self.project
@ -9214,41 +9251,50 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
self.http_client.clone()
}
fn which_command(
&self,
command: OsString,
cx: &AppContext,
) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
let worktree_abs_path = self.worktree.read(cx).abs_path();
let command = command.to_owned();
async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)> {
let worktree_abs_path = self.worktree.abs_path();
cx.background_executor().spawn(async move {
let shell_env = load_shell_environment(&worktree_abs_path)
.await
.with_context(|| {
format!(
"failed to determine load login shell environment in {worktree_abs_path:?}"
)
})
.log_err();
let shell_env = load_shell_environment(&worktree_abs_path)
.await
.with_context(|| {
format!("failed to determine load login shell environment in {worktree_abs_path:?}")
})
.log_err();
if let Some(shell_env) = shell_env {
let shell_path = shell_env.get("PATH");
match which::which_in(&command, shell_path, &worktree_abs_path) {
Ok(command_path) => Some((command_path, shell_env)),
Err(error) => {
log::warn!(
"failed to determine path for command {:?} in shell PATH {:?}: {error}",
command.to_string_lossy(),
shell_path.map(String::as_str).unwrap_or("")
);
None
}
if let Some(shell_env) = shell_env {
let shell_path = shell_env.get("PATH");
match which::which_in(&command, shell_path, &worktree_abs_path) {
Ok(command_path) => Some((command_path, shell_env)),
Err(error) => {
log::warn!(
"failed to determine path for command {:?} in shell PATH {:?}: {error}",
command.to_string_lossy(),
shell_path.map(String::as_str).unwrap_or("")
);
None
}
} else {
None
}
})
} else {
None
}
}
fn update_status(
&self,
server_name: LanguageServerName,
status: language::LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status);
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
if self.worktree.entry_for_path(&path).is_none() {
return Err(anyhow!("no such path {path:?}"));
}
let path = self.worktree.absolutize(path.as_ref())?;
let content = self.fs.load(&path).await?;
Ok(content)
}
}

View file

@ -189,55 +189,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut rust_language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut json_language = Language::new(
LanguageConfig {
name: "JSON".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["json".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_rust_servers = rust_language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-rust-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
}))
.await;
let mut fake_json_servers = json_language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-json-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/the-root",
@ -251,6 +202,36 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "the-rust-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
);
let mut fake_json_servers = language_registry.register_fake_lsp_adapter(
"JSON",
FakeLspAdapter {
name: "the-json-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
);
// Open a buffer without an associated language server.
let toml_buffer = project
@ -273,10 +254,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
// Now we add the languages to the project, and ensure they get assigned to all
// the relevant open buffers.
project.update(cx, |project, _| {
project.languages.add(Arc::new(json_language));
project.languages.add(Arc::new(rust_language));
});
language_registry.add(json_lang());
language_registry.add(rust_lang());
cx.executor().run_until_parked();
rust_buffer.update(cx, |buffer, _| {
assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
@ -581,24 +560,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-language-server",
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/the-root",
@ -630,9 +591,16 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
.await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages.add(Arc::new(language));
});
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "the-language-server",
..Default::default()
},
);
cx.executor().run_until_parked();
// Start the language server by opening a buffer with a compatible file extension.
@ -1019,24 +987,6 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
init_test(cx);
let progress_token = "the-progress-token";
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_progress_token: Some(progress_token.into()),
disk_based_diagnostics_sources: vec!["disk".into()],
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
@ -1049,7 +999,18 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
disk_based_diagnostics_progress_token: Some(progress_token.into()),
disk_based_diagnostics_sources: vec!["disk".into()],
..Default::default()
},
);
let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
// Cause worktree to start the fake language server
@ -1155,29 +1116,23 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
init_test(cx);
let progress_token = "the-progress-token";
let mut language = Language::new(
LanguageConfig {
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_sources: vec!["disk".into()],
disk_based_diagnostics_progress_token: Some(progress_token.into()),
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "the-language-server",
disk_based_diagnostics_sources: vec!["disk".into()],
disk_based_diagnostics_progress_token: Some(progress_token.into()),
..Default::default()
},
);
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@ -1239,27 +1194,15 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers =
language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@ -1331,28 +1274,15 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-lsp",
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers =
language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@ -1383,50 +1313,29 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut rust = Language::new(
LanguageConfig {
name: Arc::from("Rust"),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_rust_servers = rust
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "rust-lsp",
..Default::default()
}))
.await;
let mut js = Language::new(
LanguageConfig {
name: Arc::from("JavaScript"),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_js_servers = js
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "js-lsp",
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages.add(Arc::new(rust));
project.languages.add(Arc::new(js));
});
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-lsp",
..Default::default()
},
);
let mut fake_js_servers = language_registry.register_fake_lsp_adapter(
"JavaScript",
FakeLspAdapter {
name: "js-lsp",
..Default::default()
},
);
language_registry.add(rust_lang());
language_registry.add(js_lang());
let _rs_buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@ -1518,24 +1427,6 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_sources: vec!["disk".into()],
..Default::default()
}))
.await;
let text = "
fn a() { A }
fn b() { BB }
@ -1547,7 +1438,16 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
disk_based_diagnostics_sources: vec!["disk".into()],
..Default::default()
},
);
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@ -1932,19 +1832,6 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
let text = "
fn a() {
f1();
@ -1968,7 +1855,12 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers =
language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
@ -2322,19 +2214,6 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
async fn test_definition(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2346,7 +2225,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers =
language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
@ -2426,30 +2309,6 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2460,7 +2319,23 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(typescript_lang());
let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
"TypeScript",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@ -2526,30 +2401,6 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2560,7 +2411,23 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(typescript_lang());
let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
"TypeScript",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@ -2595,19 +2462,6 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
None,
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -2618,7 +2472,12 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(typescript_lang());
let mut fake_language_servers =
language_registry.register_fake_lsp_adapter("TypeScript", Default::default());
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
@ -2904,16 +2763,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let languages = project.update(cx, |project, _| project.languages().clone());
languages.register_native_grammars([("rust", tree_sitter_rust::language())]);
languages.register_test_language(LanguageConfig {
name: "Rust".into(),
grammar: Some("rust".into()),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".into()],
..Default::default()
},
..Default::default()
});
languages.add(rust_lang());
let buffer = project.update(cx, |project, cx| {
project.create_buffer("", None, cx).unwrap()
@ -3733,30 +3583,6 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
async fn test_rename(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
@ -3768,7 +3594,23 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
},
..Default::default()
},
);
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/one.rs", cx)
@ -4475,3 +4317,59 @@ fn init_test(cx: &mut gpui::TestAppContext) {
Project::init_settings(cx);
});
}
fn json_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "JSON".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["json".to_string()],
..Default::default()
},
..Default::default()
},
None,
))
}
fn js_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: Arc::from("JavaScript"),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
None,
))
}
fn rust_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
))
}
fn typescript_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
))
}

View file

@ -219,7 +219,7 @@ impl Inventory {
}
}
#[cfg(feature = "test-support")]
#[cfg(any(test, feature = "test-support"))]
pub mod test_inventory {
use std::{
path::{Path, PathBuf},