ZIm/crates/extension_host/src/headless_host.rs
Piotr Osiewicz b8a106632f
lsp: Identify language servers by their configuration (#35270)
- **WIP: reorganize dispositions**
- **Introduce a LocalToolchainStore trait and use it for LspAdapter
methods**

Closes #35782
Closes #27331

Release Notes:

- Python: Improved propagation of a selected virtual environment into
the LSP configuration. This should the make all language-related
features such as Go to definition or Find all references more reliable.

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-18 11:43:52 +02:00

353 lines
12 KiB
Rust

use std::{path::PathBuf, sync::Arc};
use anyhow::{Context as _, Result};
use client::{
TypedEnvelope,
proto::{self, FromProto},
};
use collections::{HashMap, HashSet};
use extension::{
Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
ExtensionLanguageServerProxy, ExtensionManifest,
};
use fs::{Fs, RemoveOptions, RenameOptions};
use futures::future::join_all;
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
use http_client::HttpClient;
use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use crate::wasm_host::{WasmExtension, WasmHost};
#[derive(Clone, Debug)]
pub struct ExtensionVersion {
pub id: String,
pub version: String,
pub dev: bool,
}
pub struct HeadlessExtensionStore {
pub fs: Arc<dyn Fs>,
pub extension_dir: PathBuf,
pub proxy: Arc<ExtensionHostProxy>,
pub wasm_host: Arc<WasmHost>,
pub loaded_extensions: HashMap<Arc<str>, Arc<str>>,
pub loaded_languages: HashMap<Arc<str>, Vec<LanguageName>>,
pub loaded_language_servers: HashMap<Arc<str>, Vec<(LanguageServerName, LanguageName)>>,
}
impl HeadlessExtensionStore {
pub fn new(
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
extension_dir: PathBuf,
extension_host_proxy: Arc<ExtensionHostProxy>,
node_runtime: NodeRuntime,
cx: &mut App,
) -> Entity<Self> {
cx.new(|cx| Self {
fs: fs.clone(),
wasm_host: WasmHost::new(
fs.clone(),
http_client.clone(),
node_runtime,
extension_host_proxy.clone(),
extension_dir.join("work"),
cx,
),
extension_dir,
proxy: extension_host_proxy,
loaded_extensions: Default::default(),
loaded_languages: Default::default(),
loaded_language_servers: Default::default(),
})
}
pub fn sync_extensions(
&mut self,
extensions: Vec<ExtensionVersion>,
cx: &Context<Self>,
) -> Task<Result<Vec<ExtensionVersion>>> {
let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str()));
let to_remove: Vec<Arc<str>> = self
.loaded_extensions
.keys()
.filter(|id| !on_client.contains(id.as_ref()))
.cloned()
.collect();
let to_load: Vec<ExtensionVersion> = extensions
.into_iter()
.filter(|e| {
if e.dev {
return true;
}
self.loaded_extensions
.get(e.id.as_str())
.is_none_or(|loaded| loaded.as_ref() != e.version.as_str())
})
.collect();
cx.spawn(async move |this, cx| {
let mut missing = Vec::new();
for extension_id in to_remove {
log::info!("removing extension: {}", extension_id);
this.update(cx, |this, cx| this.uninstall_extension(&extension_id, cx))?
.await?;
}
for extension in to_load {
if let Err(e) = Self::load_extension(this.clone(), extension.clone(), cx).await {
log::info!("failed to load extension: {}, {:?}", extension.id, e);
missing.push(extension)
} else if extension.dev {
missing.push(extension)
}
}
Ok(missing)
})
}
pub async fn load_extension(
this: WeakEntity<Self>,
extension: ExtensionVersion,
cx: &mut AsyncApp,
) -> Result<()> {
let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| {
this.loaded_extensions.insert(
extension.id.clone().into(),
extension.version.clone().into(),
);
(
this.fs.clone(),
this.wasm_host.clone(),
this.extension_dir.join(&extension.id),
)
})?;
let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load());
if manifest.version.as_ref() != extension.version.as_str() {
anyhow::bail!(
"mismatched versions: ({}) != ({})",
manifest.version,
extension.version
)
}
for language_path in &manifest.languages {
let language_path = extension_dir.join(language_path);
let config = fs.load(&language_path.join("config.toml")).await?;
let mut config = ::toml::from_str::<LanguageConfig>(&config)?;
this.update(cx, |this, _cx| {
this.loaded_languages
.entry(manifest.id.clone())
.or_default()
.push(config.name.clone());
config.grammar = None;
this.proxy.register_language(
config.name.clone(),
None,
config.matcher.clone(),
config.hidden,
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: LanguageQueries::default(),
context_provider: None,
toolchain_provider: None,
manifest_name: None,
})
}),
);
})?;
}
if !manifest.allow_remote_load() {
return Ok(());
}
let wasm_extension: Arc<dyn Extension> =
Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), &cx).await?);
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
this.update(cx, |this, _cx| {
this.loaded_language_servers
.entry(manifest.id.clone())
.or_default()
.push((language_server_id.clone(), language.clone()));
this.proxy.register_language_server(
wasm_extension.clone(),
language_server_id.clone(),
language.clone(),
);
})?;
}
log::info!("Loaded language server: {}", language_server_id);
}
for (debug_adapter, meta) in &manifest.debug_adapters {
let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta);
this.update(cx, |this, _cx| {
this.proxy.register_debug_adapter(
wasm_extension.clone(),
debug_adapter.clone(),
&extension_dir.join(schema_path),
);
})?;
log::info!("Loaded debug adapter: {}", debug_adapter);
}
for debug_locator in manifest.debug_locators.keys() {
this.update(cx, |this, _cx| {
this.proxy
.register_debug_locator(wasm_extension.clone(), debug_locator.clone());
})?;
log::info!("Loaded debug locator: {}", debug_locator);
}
Ok(())
}
fn uninstall_extension(
&mut self,
extension_id: &Arc<str>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.loaded_extensions.remove(extension_id);
let languages_to_remove = self
.loaded_languages
.remove(extension_id)
.unwrap_or_default();
self.proxy.remove_languages(&languages_to_remove, &[]);
let servers_to_remove = self
.loaded_language_servers
.remove(extension_id)
.unwrap_or_default();
let proxy = self.proxy.clone();
let path = self.extension_dir.join(&extension_id.to_string());
let fs = self.fs.clone();
cx.spawn(async move |_, cx| {
let mut removal_tasks = Vec::with_capacity(servers_to_remove.len());
cx.update(|cx| {
for (language_server_name, language) in servers_to_remove {
removal_tasks.push(proxy.remove_language_server(
&language,
&language_server_name,
cx,
));
}
})
.ok();
let _ = join_all(removal_tasks).await;
fs.remove_dir(
&path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await
.with_context(|| format!("Removing directory {path:?}"))
})
}
pub fn install_extension(
&mut self,
extension: ExtensionVersion,
tmp_path: PathBuf,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let path = self.extension_dir.join(&extension.id);
let fs = self.fs.clone();
cx.spawn(async move |this, cx| {
if fs.is_dir(&path).await {
this.update(cx, |this, cx| {
this.uninstall_extension(&extension.id.clone().into(), cx)
})?
.await?;
}
fs.rename(&tmp_path, &path, RenameOptions::default())
.await?;
Self::load_extension(this, extension, cx).await
})
}
pub async fn handle_sync_extensions(
extension_store: Entity<HeadlessExtensionStore>,
envelope: TypedEnvelope<proto::SyncExtensions>,
mut cx: AsyncApp,
) -> Result<proto::SyncExtensionsResponse> {
let requested_extensions =
envelope
.payload
.extensions
.into_iter()
.map(|p| ExtensionVersion {
id: p.id,
version: p.version,
dev: p.dev,
});
let missing_extensions = extension_store
.update(&mut cx, |extension_store, cx| {
extension_store.sync_extensions(requested_extensions.collect(), cx)
})?
.await?;
Ok(proto::SyncExtensionsResponse {
missing_extensions: missing_extensions
.into_iter()
.map(|e| proto::Extension {
id: e.id,
version: e.version,
dev: e.dev,
})
.collect(),
tmp_dir: paths::remote_extensions_uploads_dir()
.to_string_lossy()
.to_string(),
})
}
pub async fn handle_install_extension(
extensions: Entity<HeadlessExtensionStore>,
envelope: TypedEnvelope<proto::InstallExtension>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let extension = envelope
.payload
.extension
.context("Invalid InstallExtension request")?;
extensions
.update(&mut cx, |extensions, cx| {
extensions.install_extension(
ExtensionVersion {
id: extension.id,
version: extension.version,
dev: extension.dev,
},
PathBuf::from_proto(envelope.payload.tmp_dir),
cx,
)
})?
.await?;
Ok(proto::Ack {})
}
}