Make language server initialization asynchronous

This commit is contained in:
Antonio Scandurra 2022-02-21 09:39:28 +01:00
parent b8523509da
commit 1ca50d0134
4 changed files with 153 additions and 148 deletions

View file

@ -8,7 +8,7 @@ mod tests;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::HashSet; use collections::HashSet;
use gpui::AppContext; use gpui::{AppContext, Task};
use highlight_map::HighlightMap; use highlight_map::HighlightMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -229,9 +229,9 @@ impl Language {
pub fn start_server( pub fn start_server(
&self, &self,
root_path: &Path, root_path: Arc<Path>,
cx: &AppContext, cx: &AppContext,
) -> Result<Option<Arc<lsp::LanguageServer>>> { ) -> Task<Result<Option<Arc<lsp::LanguageServer>>>> {
if let Some(config) = &self.config.language_server { if let Some(config) = &self.config.language_server {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
if let Some(fake_config) = &config.fake_config { if let Some(fake_config) = &config.fake_config {
@ -255,19 +255,19 @@ impl Language {
}) })
.detach(); .detach();
return Ok(Some(server.clone())); return Task::ready(Ok(Some(server.clone())));
} }
const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE"); let background = cx.background().clone();
let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? { let server = lsp::LanguageServer::new(
cx.platform() Path::new(&config.binary),
.path_for_resource(Some(&config.binary), None)? &root_path,
} else { cx.background().clone(),
Path::new(&config.binary).to_path_buf() )
}; .map(Some);
lsp::LanguageServer::new(&binary_path, root_path, cx.background().clone()).map(Some) cx.background().spawn(async move { server })
} else { } else {
Ok(None) Task::ready(Ok(None))
} }
} }

View file

@ -39,6 +39,8 @@ pub struct Project {
active_entry: Option<ProjectEntry>, active_entry: Option<ProjectEntry>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>, language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>,
loading_language_servers:
HashMap<(WorktreeId, String), watch::Receiver<Option<Arc<LanguageServer>>>>,
client: Arc<client::Client>, client: Arc<client::Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -258,6 +260,7 @@ impl Project {
fs, fs,
language_servers_with_diagnostics_running: 0, language_servers_with_diagnostics_running: 0,
language_servers: Default::default(), language_servers: Default::default(),
loading_language_servers: Default::default(),
} }
}) })
} }
@ -309,6 +312,7 @@ impl Project {
}, },
language_servers_with_diagnostics_running: 0, language_servers_with_diagnostics_running: 0,
language_servers: Default::default(), language_servers: Default::default(),
loading_language_servers: Default::default(),
}; };
for worktree in worktrees { for worktree in worktrees {
this.add_worktree(&worktree, cx); this.add_worktree(&worktree, cx);
@ -776,7 +780,7 @@ impl Project {
}; };
// If the buffer has a language, set it and start/assign the language server // If the buffer has a language, set it and start/assign the language server
if let Some(language) = self.languages.select_language(&full_path) { if let Some(language) = self.languages.select_language(&full_path).cloned() {
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language.clone()), cx); buffer.set_language(Some(language.clone()), cx);
}); });
@ -786,24 +790,20 @@ impl Project {
if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) { if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) {
let worktree_id = local_worktree.id(); let worktree_id = local_worktree.id();
let worktree_abs_path = local_worktree.abs_path().clone(); let worktree_abs_path = local_worktree.abs_path().clone();
let buffer = buffer.downgrade();
let language_server =
self.start_language_server(worktree_id, worktree_abs_path, language, cx);
let language_server = match self cx.spawn_weak(|_, mut cx| async move {
.language_servers if let Some(language_server) = language_server.await {
.entry((worktree_id, language.name().to_string())) if let Some(buffer) = buffer.upgrade(&cx) {
{ buffer.update(&mut cx, |buffer, cx| {
hash_map::Entry::Occupied(e) => Some(e.get().clone()), buffer.set_language_server(Some(language_server), cx);
hash_map::Entry::Vacant(e) => Self::start_language_server( });
self.client.clone(), }
language.clone(), }
&worktree_abs_path, })
cx, .detach();
)
.map(|server| e.insert(server).clone()),
};
buffer.update(cx, |buffer, cx| {
buffer.set_language_server(language_server, cx);
});
} }
} }
@ -819,116 +819,144 @@ impl Project {
} }
fn start_language_server( fn start_language_server(
rpc: Arc<Client>, &mut self,
worktree_id: WorktreeId,
worktree_path: Arc<Path>,
language: Arc<Language>, language: Arc<Language>,
worktree_path: &Path,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<Arc<LanguageServer>> { ) -> Task<Option<Arc<LanguageServer>>> {
enum LspEvent { enum LspEvent {
DiagnosticsStart, DiagnosticsStart,
DiagnosticsUpdate(lsp::PublishDiagnosticsParams), DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
DiagnosticsFinish, DiagnosticsFinish,
} }
let language_server = language let key = (worktree_id, language.name().to_string());
.start_server(worktree_path, cx) if let Some(language_server) = self.language_servers.get(&key) {
.log_err() return Task::ready(Some(language_server.clone()));
.flatten()?; } else if let Some(mut language_server) = self.loading_language_servers.get(&key).cloned() {
let disk_based_sources = language return cx
.disk_based_diagnostic_sources() .foreground()
.cloned() .spawn(async move { language_server.recv().await.flatten() });
.unwrap_or_default(); }
let disk_based_diagnostics_progress_token =
language.disk_based_diagnostics_progress_token().cloned();
let has_disk_based_diagnostic_progress_token =
disk_based_diagnostics_progress_token.is_some();
let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
// Listen for `PublishDiagnostics` notifications. let (mut language_server_tx, language_server_rx) = watch::channel();
language_server self.loading_language_servers
.on_notification::<lsp::notification::PublishDiagnostics, _>({ .insert(key.clone(), language_server_rx);
let diagnostics_tx = diagnostics_tx.clone(); let language_server = language.start_server(worktree_path, cx);
move |params| { let rpc = self.client.clone();
if !has_disk_based_diagnostic_progress_token {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
}
block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))).ok();
if !has_disk_based_diagnostic_progress_token {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
}
}
})
.detach();
// Listen for `Progress` notifications. Send an event when the language server
// transitions between running jobs and not running any jobs.
let mut running_jobs_for_this_server: i32 = 0;
language_server
.on_notification::<lsp::notification::Progress, _>(move |params| {
let token = match params.token {
lsp::NumberOrString::Number(_) => None,
lsp::NumberOrString::String(token) => Some(token),
};
if token == disk_based_diagnostics_progress_token {
match params.value {
lsp::ProgressParamsValue::WorkDone(progress) => match progress {
lsp::WorkDoneProgress::Begin(_) => {
running_jobs_for_this_server += 1;
if running_jobs_for_this_server == 1 {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
}
}
lsp::WorkDoneProgress::End(_) => {
running_jobs_for_this_server -= 1;
if running_jobs_for_this_server == 0 {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
}
}
_ => {}
},
}
}
})
.detach();
// Process all the LSP events.
cx.spawn_weak(|this, mut cx| async move { cx.spawn_weak(|this, mut cx| async move {
while let Ok(message) = diagnostics_rx.recv().await { let language_server = language_server.await.log_err().flatten();
let this = this.upgrade(&cx)?; if let Some(this) = this.upgrade(&cx) {
match message { this.update(&mut cx, |this, _| {
LspEvent::DiagnosticsStart => { this.loading_language_servers.remove(&key);
this.update(&mut cx, |this, cx| { if let Some(language_server) = language_server.clone() {
this.disk_based_diagnostics_started(cx); this.language_servers.insert(key, language_server);
if let Some(project_id) = this.remote_id() {
rpc.send(proto::DiskBasedDiagnosticsUpdating { project_id })
.log_err();
}
});
} }
LspEvent::DiagnosticsUpdate(mut params) => { });
language.process_diagnostics(&mut params); }
this.update(&mut cx, |this, cx| {
this.update_diagnostics(params, &disk_based_sources, cx) let language_server = language_server?;
.log_err(); *language_server_tx.borrow_mut() = Some(language_server.clone());
});
let disk_based_sources = language
.disk_based_diagnostic_sources()
.cloned()
.unwrap_or_default();
let disk_based_diagnostics_progress_token =
language.disk_based_diagnostics_progress_token().cloned();
let has_disk_based_diagnostic_progress_token =
disk_based_diagnostics_progress_token.is_some();
let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
// Listen for `PublishDiagnostics` notifications.
language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({
let diagnostics_tx = diagnostics_tx.clone();
move |params| {
if !has_disk_based_diagnostic_progress_token {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
}
block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))).ok();
if !has_disk_based_diagnostic_progress_token {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
}
} }
LspEvent::DiagnosticsFinish => { })
this.update(&mut cx, |this, cx| { .detach();
this.disk_based_diagnostics_finished(cx);
if let Some(project_id) = this.remote_id() { // Listen for `Progress` notifications. Send an event when the language server
rpc.send(proto::DiskBasedDiagnosticsUpdated { project_id }) // transitions between running jobs and not running any jobs.
let mut running_jobs_for_this_server: i32 = 0;
language_server
.on_notification::<lsp::notification::Progress, _>(move |params| {
let token = match params.token {
lsp::NumberOrString::Number(_) => None,
lsp::NumberOrString::String(token) => Some(token),
};
if token == disk_based_diagnostics_progress_token {
match params.value {
lsp::ProgressParamsValue::WorkDone(progress) => match progress {
lsp::WorkDoneProgress::Begin(_) => {
running_jobs_for_this_server += 1;
if running_jobs_for_this_server == 1 {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart))
.ok();
}
}
lsp::WorkDoneProgress::End(_) => {
running_jobs_for_this_server -= 1;
if running_jobs_for_this_server == 0 {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish))
.ok();
}
}
_ => {}
},
}
}
})
.detach();
// Process all the LSP events.
cx.spawn(|mut cx| async move {
while let Ok(message) = diagnostics_rx.recv().await {
let this = this.upgrade(&cx)?;
match message {
LspEvent::DiagnosticsStart => {
this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_started(cx);
if let Some(project_id) = this.remote_id() {
rpc.send(proto::DiskBasedDiagnosticsUpdating { project_id })
.log_err();
}
});
}
LspEvent::DiagnosticsUpdate(mut params) => {
language.process_diagnostics(&mut params);
this.update(&mut cx, |this, cx| {
this.update_diagnostics(params, &disk_based_sources, cx)
.log_err(); .log_err();
} });
}); }
LspEvent::DiagnosticsFinish => {
this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_finished(cx);
if let Some(project_id) = this.remote_id() {
rpc.send(proto::DiskBasedDiagnosticsUpdated { project_id })
.log_err();
}
});
}
} }
} }
} Some(())
Some(()) })
}) .detach();
.detach();
Some(language_server) Some(language_server)
})
} }
pub fn update_diagnostics( pub fn update_diagnostics(

View file

@ -21,9 +21,6 @@ cargo build --release --target aarch64-apple-darwin
# Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries # Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries
lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
# Bundle rust-analyzer
cp vendor/bin/rust-analyzer target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/
# Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now. # Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now.
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
echo "Signing bundle with Apple-issued certificate" echo "Signing bundle with Apple-issued certificate"
@ -34,7 +31,6 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
security import /tmp/zed-certificate.p12 -k zed.keychain -P $MACOS_CERTIFICATE_PASSWORD -T /usr/bin/codesign security import /tmp/zed-certificate.p12 -k zed.keychain -P $MACOS_CERTIFICATE_PASSWORD -T /usr/bin/codesign
rm /tmp/zed-certificate.p12 rm /tmp/zed-certificate.p12
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CERTIFICATE_PASSWORD zed.keychain security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CERTIFICATE_PASSWORD zed.keychain
/usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/rust-analyzer -v
/usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v /usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v
security default-keychain -s login.keychain security default-keychain -s login.keychain
else else

View file

@ -1,19 +0,0 @@
#!/bin/bash
set -e
export RUST_ANALYZER_URL="https://github.com/rust-analyzer/rust-analyzer/releases/download/2022-01-24/"
function download {
local filename="rust-analyzer-$1"
curl -L $RUST_ANALYZER_URL/$filename.gz | gunzip > vendor/bin/$filename
chmod +x vendor/bin/$filename
}
mkdir -p vendor/bin
download "x86_64-apple-darwin"
download "aarch64-apple-darwin"
cd vendor/bin
lipo -create rust-analyzer-* -output rust-analyzer
rm rust-analyzer-*