mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
extension_host: Sync language extensions to remotes (#56487)
Resolves #56068 Remote extension sync previously only sent extensions that can run remotely: language servers, debug adapters, and debug locators. That excluded language-only extensions such as TOML, so a remote session could have Tombi synced and loaded while the remote language registry still lacked TOML's language definition. This diff keeps the remote sync set narrow: it starts from extensions that can run remotely, then adds the extension that defines each language referenced by those extensions' language servers. This installs the TOML language config alongside Tombi without syncing unrelated language-only extensions, allowing project settings schemas to recognize language names like TOML and allowing remote language servers to attach to buffers whose language is provided by a separate extension. Release Notes: - Fixed remote sessions failing to recognize languages provided by language-only extensions. --------- Co-authored-by: Finn Evers <finn.evers@outlook.de>
This commit is contained in:
parent
ee571d3c69
commit
0b458e53a5
3 changed files with 257 additions and 16 deletions
|
|
@ -181,9 +181,27 @@ impl ExtensionManifest {
|
|||
}
|
||||
|
||||
pub fn allow_remote_load(&self) -> bool {
|
||||
!self.language_servers.is_empty()
|
||||
self.remote_load().is_some()
|
||||
}
|
||||
|
||||
pub fn remote_load(&self) -> Option<RemoteLoad<'_>> {
|
||||
(!self.language_servers.is_empty()
|
||||
|| !self.debug_adapters.is_empty()
|
||||
|| !self.debug_locators.is_empty()
|
||||
|| !self.debug_locators.is_empty())
|
||||
.then_some(RemoteLoad { manifest: self })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteLoad<'a> {
|
||||
manifest: &'a ExtensionManifest,
|
||||
}
|
||||
|
||||
impl RemoteLoad<'_> {
|
||||
pub fn language_dependencies(&self) -> impl Iterator<Item = LanguageName> + '_ {
|
||||
self.manifest
|
||||
.language_servers
|
||||
.values()
|
||||
.flat_map(|language_server_config| language_server_config.languages())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -345,7 +363,7 @@ pub struct SlashCommandManifestEntry {
|
|||
pub requires_argument: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct DebugAdapterManifestEntry {
|
||||
pub schema_path: Option<RelPathBuf>,
|
||||
}
|
||||
|
|
@ -484,7 +502,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_build_adapter_schema_path_without_schema_path() {
|
||||
let adapter_name = Arc::from("my_adapter");
|
||||
let entry = DebugAdapterManifestEntry { schema_path: None };
|
||||
let entry = DebugAdapterManifestEntry::default();
|
||||
|
||||
let path = build_debug_adapter_schema_path(&adapter_name, &entry).unwrap();
|
||||
assert_eq!(path, rel_path_buf("debug_adapter_schemas/my_adapter.json"));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder;
|
|||
use async_tar::Archive;
|
||||
use client::{Client, proto, telemetry::Telemetry};
|
||||
use cloud_api_types::{ExtensionMetadata, ExtensionProvides, GetExtensionsResponse};
|
||||
use collections::{BTreeMap, BTreeSet, FxHashSet, HashSet, btree_map};
|
||||
use collections::{BTreeMap, BTreeSet, FxHashSet, HashMap, HashSet, btree_map};
|
||||
pub use extension::ExtensionManifest;
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::{
|
||||
|
|
@ -178,6 +178,55 @@ pub struct ExtensionIndex {
|
|||
pub languages: BTreeMap<LanguageName, ExtensionIndexLanguageEntry>,
|
||||
}
|
||||
|
||||
impl ExtensionIndex {
|
||||
fn extensions_to_sync_to_remote(&self) -> RemoteSyncExtensions {
|
||||
let mut extensions = RemoteSyncExtensions::default();
|
||||
|
||||
for (id, entry) in &self.extensions {
|
||||
if entry.manifest.remote_load().is_some() {
|
||||
extensions.insert_extension_and_language_dependencies(self, id);
|
||||
}
|
||||
}
|
||||
|
||||
extensions
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RemoteSyncExtensions(HashMap<Arc<str>, ExtensionIndexEntry>);
|
||||
|
||||
impl RemoteSyncExtensions {
|
||||
fn insert_extension_and_language_dependencies(
|
||||
&mut self,
|
||||
index: &ExtensionIndex,
|
||||
id: &Arc<str>,
|
||||
) {
|
||||
if self.0.contains_key(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(entry) = index.extensions.get(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.0.insert(id.clone(), entry.clone());
|
||||
|
||||
let Some(remote_load) = entry.manifest.remote_load() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for language in remote_load.language_dependencies() {
|
||||
if let Some(language_entry) = index.languages.get(&language) {
|
||||
self.insert_extension_and_language_dependencies(index, &language_entry.extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_entries(self) -> impl Iterator<Item = (Arc<str>, ExtensionIndexEntry)> {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct ExtensionIndexEntry {
|
||||
pub manifest: Arc<ExtensionManifest>,
|
||||
|
|
@ -1812,17 +1861,12 @@ impl ExtensionStore {
|
|||
) -> Result<()> {
|
||||
let extensions = this.update(cx, |this, _cx| {
|
||||
this.extension_index
|
||||
.extensions
|
||||
.iter()
|
||||
.filter_map(|(id, entry)| {
|
||||
if !entry.manifest.allow_remote_load() {
|
||||
return None;
|
||||
}
|
||||
Some(proto::Extension {
|
||||
id: id.to_string(),
|
||||
version: entry.manifest.version.to_string(),
|
||||
dev: entry.dev,
|
||||
})
|
||||
.extensions_to_sync_to_remote()
|
||||
.into_entries()
|
||||
.map(|(id, entry)| proto::Extension {
|
||||
id: id.to_string(),
|
||||
version: entry.manifest.version.to_string(),
|
||||
dev: entry.dev,
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,185 @@ fn init_logger() {
|
|||
zlog::init_test();
|
||||
}
|
||||
|
||||
fn remote_sync_entry(id: &str, manifest_body: &str) -> ExtensionIndexEntry {
|
||||
let manifest = format!(
|
||||
r#"
|
||||
id = "{id}"
|
||||
name = "{id}"
|
||||
version = "1.0.0"
|
||||
schema_version = 0
|
||||
|
||||
{manifest_body}
|
||||
"#
|
||||
);
|
||||
|
||||
ExtensionIndexEntry {
|
||||
manifest: Arc::new(toml::from_str(&manifest).unwrap()),
|
||||
dev: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_sync_language_entry(extension: &str, path: &str) -> ExtensionIndexLanguageEntry {
|
||||
ExtensionIndexLanguageEntry {
|
||||
extension: extension.into(),
|
||||
path: path.into(),
|
||||
matcher: LanguageMatcher::default(),
|
||||
hidden: false,
|
||||
grammar: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_sync_extension_ids(index: &ExtensionIndex) -> Vec<String> {
|
||||
let mut extensions = index
|
||||
.extensions_to_sync_to_remote()
|
||||
.into_entries()
|
||||
.map(|(id, _)| id.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
extensions.sort();
|
||||
|
||||
extensions
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_sync_includes_language_dependencies() {
|
||||
let index = ExtensionIndex {
|
||||
extensions: [
|
||||
(
|
||||
"bar-language".into(),
|
||||
remote_sync_entry("bar-language", r#"languages = ["languages/bar"]"#),
|
||||
),
|
||||
(
|
||||
"foo-lsp".into(),
|
||||
remote_sync_entry(
|
||||
"foo-lsp",
|
||||
r#"
|
||||
[language_servers.foo]
|
||||
language = "Foo"
|
||||
"#,
|
||||
),
|
||||
),
|
||||
(
|
||||
"foo-language".into(),
|
||||
remote_sync_entry("foo-language", r#"languages = ["languages/foo"]"#),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
languages: [
|
||||
(
|
||||
"Bar".into(),
|
||||
remote_sync_language_entry("bar-language", "languages/bar"),
|
||||
),
|
||||
(
|
||||
"Foo".into(),
|
||||
remote_sync_language_entry("foo-language", "languages/foo"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
themes: BTreeMap::default(),
|
||||
icon_themes: BTreeMap::default(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
remote_sync_extension_ids(&index),
|
||||
["foo-language", "foo-lsp"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_sync_keeps_shared_language_dependency_once() {
|
||||
let index = ExtensionIndex {
|
||||
extensions: [
|
||||
(
|
||||
"aaa-lsp".into(),
|
||||
remote_sync_entry(
|
||||
"aaa-lsp",
|
||||
r#"
|
||||
[language_servers.aaa]
|
||||
language = "Foo"
|
||||
"#,
|
||||
),
|
||||
),
|
||||
(
|
||||
"bbb-lsp".into(),
|
||||
remote_sync_entry(
|
||||
"bbb-lsp",
|
||||
r#"
|
||||
[language_servers.bbb]
|
||||
language = "Foo"
|
||||
"#,
|
||||
),
|
||||
),
|
||||
(
|
||||
"zzz-language".into(),
|
||||
remote_sync_entry("zzz-language", r#"languages = ["languages/foo"]"#),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
languages: [(
|
||||
"Foo".into(),
|
||||
remote_sync_language_entry("zzz-language", "languages/foo"),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
themes: BTreeMap::default(),
|
||||
icon_themes: BTreeMap::default(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
remote_sync_extension_ids(&index),
|
||||
["aaa-lsp", "bbb-lsp", "zzz-language"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_sync_keeps_remote_loadable_extensions_without_language_dependency() {
|
||||
let index = ExtensionIndex {
|
||||
extensions: [(
|
||||
"foo".into(),
|
||||
remote_sync_entry(
|
||||
"foo",
|
||||
r#"
|
||||
[language_servers.foo]
|
||||
language = "Foo"
|
||||
"#,
|
||||
),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
languages: BTreeMap::default(),
|
||||
themes: BTreeMap::default(),
|
||||
icon_themes: BTreeMap::default(),
|
||||
};
|
||||
|
||||
assert_eq!(remote_sync_extension_ids(&index), ["foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_sync_keeps_debug_adapters() {
|
||||
let index = ExtensionIndex {
|
||||
extensions: [(
|
||||
"foo".into(),
|
||||
remote_sync_entry(
|
||||
"foo",
|
||||
r#"
|
||||
[debug_adapters.foo]
|
||||
"#,
|
||||
),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
languages: BTreeMap::default(),
|
||||
themes: BTreeMap::default(),
|
||||
icon_themes: BTreeMap::default(),
|
||||
};
|
||||
|
||||
assert_eq!(remote_sync_extension_ids(&index), ["foo"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue