Extension refactor (#20305)
This contains the main changes to the extensions crate from #20049. The primary goal here is removing dependencies that we can't include on the remote. Release Notes: - N/A --------- Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Marshall Bowers <elliott.codes@gmail.com> Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
parent
f22e56ff42
commit
608addf641
30 changed files with 675 additions and 236 deletions
|
@ -16,14 +16,18 @@ test-support = []
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension_host.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
num-format.workspace = true
|
||||
picker.workspace = true
|
||||
|
@ -33,12 +37,30 @@ semantic_version.workspace = true
|
|||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
theme.workspace = true
|
||||
theme_selector.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vim.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
79
crates/extensions_ui/src/extension_indexed_docs_provider.rs
Normal file
79
crates/extensions_ui/src/extension_indexed_docs_provider.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::FutureExt;
|
||||
use indexed_docs::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
|
||||
use wasmtime_wasi::WasiView;
|
||||
|
||||
use extension_host::wasm_host::{WasmExtension, WasmHost};
|
||||
|
||||
pub struct ExtensionIndexedDocsProvider {
|
||||
pub(crate) extension: WasmExtension,
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
pub(crate) id: ProviderId,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl IndexedDocsProvider for ExtensionIndexedDocsProvider {
|
||||
fn id(&self) -> ProviderId {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn database_path(&self) -> PathBuf {
|
||||
let mut database_path = self.host.work_dir.clone();
|
||||
database_path.push(self.extension.manifest.id.as_ref());
|
||||
database_path.push("docs");
|
||||
database_path.push(format!("{}.0.mdb", self.id));
|
||||
|
||||
database_path
|
||||
}
|
||||
|
||||
async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
|
||||
self.extension
|
||||
.call({
|
||||
let id = self.id.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let packages = extension
|
||||
.call_suggest_docs_packages(store, id.as_ref())
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err:?}"))?;
|
||||
|
||||
Ok(packages
|
||||
.into_iter()
|
||||
.map(|package| PackageName::from(package.as_str()))
|
||||
.collect())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
|
||||
self.extension
|
||||
.call({
|
||||
let id = self.id.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let database_resource = store.data_mut().table().push(database as _)?;
|
||||
extension
|
||||
.call_index_docs(
|
||||
store,
|
||||
id.as_ref(),
|
||||
package.as_ref(),
|
||||
database_resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err:?}"))?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
153
crates/extensions_ui/src/extension_registration_hooks.rs
Normal file
153
crates/extensions_ui/src/extension_registration_hooks.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use indexed_docs::{IndexedDocsRegistry, ProviderId};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use theme::{ThemeRegistry, ThemeSettings};
|
||||
use ui::SharedString;
|
||||
|
||||
use crate::{extension_indexed_docs_provider, extension_slash_command::ExtensionSlashCommand};
|
||||
|
||||
pub struct ConcreteExtensionRegistrationHooks {
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl ConcreteExtensionRegistrationHooks {
|
||||
pub fn new(
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &AppContext,
|
||||
) -> Arc<dyn extension_host::ExtensionRegistrationHooks> {
|
||||
Arc::new(Self {
|
||||
theme_registry,
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry,
|
||||
executor: cx.background_executor().clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistrationHooks {
|
||||
fn remove_user_themes(&self, themes: Vec<SharedString>) {
|
||||
self.theme_registry.remove_user_themes(&themes);
|
||||
}
|
||||
|
||||
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
|
||||
let theme_registry = self.theme_registry.clone();
|
||||
self.executor
|
||||
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
|
||||
}
|
||||
|
||||
fn register_slash_command(
|
||||
&self,
|
||||
command: wasm_host::SlashCommand,
|
||||
extension: wasm_host::WasmExtension,
|
||||
host: Arc<wasm_host::WasmHost>,
|
||||
) {
|
||||
self.slash_command_registry.register_command(
|
||||
ExtensionSlashCommand {
|
||||
command,
|
||||
extension,
|
||||
host,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn register_docs_provider(
|
||||
&self,
|
||||
extension: wasm_host::WasmExtension,
|
||||
host: Arc<wasm_host::WasmHost>,
|
||||
provider_id: Arc<str>,
|
||||
) {
|
||||
self.indexed_docs_registry.register_provider(Box::new(
|
||||
extension_indexed_docs_provider::ExtensionIndexedDocsProvider {
|
||||
extension,
|
||||
host,
|
||||
id: ProviderId(provider_id),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {
|
||||
self.snippet_registry
|
||||
.register_snippets(path, snippet_contents)
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
&self,
|
||||
server_name: language::LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
) {
|
||||
self.language_registry
|
||||
.update_lsp_status(server_name, status);
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn remove_lsp_adapter(
|
||||
&self,
|
||||
language_name: &language::LanguageName,
|
||||
server_name: &language::LanguageServerName,
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_lsp_adapter(language_name, server_name);
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
languages_to_remove: &[language::LanguageName],
|
||||
grammars_to_remove: &[Arc<str>],
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_languages(&languages_to_remove, &grammars_to_remove);
|
||||
}
|
||||
|
||||
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
|
||||
self.language_registry.register_wasm_grammars(grammars)
|
||||
}
|
||||
|
||||
fn register_language(
|
||||
&self,
|
||||
language: language::LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: language::LanguageMatcher,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_language(language, grammar, matcher, load)
|
||||
}
|
||||
|
||||
fn reload_current_theme(&self, cx: &mut AppContext) {
|
||||
ThemeSettings::reload_current_theme(cx)
|
||||
}
|
||||
|
||||
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
|
||||
self.executor.spawn(async move {
|
||||
let themes = theme::read_user_theme(&path, fs).await?;
|
||||
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
|
||||
})
|
||||
}
|
||||
}
|
135
crates/extensions_ui/src/extension_slash_command.rs
Normal file
135
crates/extensions_ui/src/extension_slash_command.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use wasmtime_wasi::WasiView;
|
||||
use workspace::Workspace;
|
||||
|
||||
use extension_host::wasm_host::{WasmExtension, WasmHost};
|
||||
|
||||
pub struct ExtensionSlashCommand {
|
||||
pub(crate) extension: WasmExtension,
|
||||
#[allow(unused)]
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
pub(crate) command: extension_host::wasm_host::SlashCommand,
|
||||
}
|
||||
|
||||
impl SlashCommand for ExtensionSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
self.command.name.clone()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
self.command.description.clone()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.command.tooltip_text.clone()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
self.command.requires_argument
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let arguments = arguments.to_owned();
|
||||
cx.background_executor().spawn(async move {
|
||||
self.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
move |extension, store| {
|
||||
async move {
|
||||
let completions = extension
|
||||
.call_complete_slash_command_argument(
|
||||
store,
|
||||
&this.command,
|
||||
&arguments,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
|
||||
anyhow::Ok(
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|completion| ArgumentCompletion {
|
||||
label: completion.label.into(),
|
||||
new_text: completion.new_text,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: completion.run_command.into(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let arguments = arguments.to_owned();
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
self.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
move |extension, store| {
|
||||
async move {
|
||||
let resource = if let Some(delegate) = delegate {
|
||||
Some(store.data_mut().table().push(delegate)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let output = extension
|
||||
.call_run_slash_command(store, &this.command, &arguments, resource)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
|
||||
anyhow::Ok(output)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let output = output.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text: output.text,
|
||||
sections: output
|
||||
.sections
|
||||
.into_iter()
|
||||
.map(|section| SlashCommandOutputSection {
|
||||
range: section.range.into(),
|
||||
icon: IconName::Code,
|
||||
label: section.label.into(),
|
||||
metadata: None,
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.to_event_stream())
|
||||
})
|
||||
}
|
||||
}
|
797
crates/extensions_ui/src/extension_store_test.rs
Normal file
797
crates/extensions_ui/src/extension_store_test.rs
Normal file
|
@ -0,0 +1,797 @@
|
|||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use extension_host::ExtensionSettings;
|
||||
use extension_host::SchemaVersion;
|
||||
use extension_host::{
|
||||
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
|
||||
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
|
||||
RELOAD_DEBOUNCE_DURATION,
|
||||
};
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{Context, SemanticVersion, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, DEFAULT_COMPLETION_CONTEXT};
|
||||
use release_channel::AppVersion;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use serde_json::json;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ThemeRegistry;
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let http_client = FakeHttpClient::with_200_response();
|
||||
|
||||
fs.insert_tree(
|
||||
"/the-extension-dir",
|
||||
json!({
|
||||
"installed": {
|
||||
"zed-monokai": {
|
||||
"extension.json": r#"{
|
||||
"id": "zed-monokai",
|
||||
"name": "Zed Monokai",
|
||||
"version": "2.0.0",
|
||||
"themes": {
|
||||
"Monokai Dark": "themes/monokai.json",
|
||||
"Monokai Light": "themes/monokai.json",
|
||||
"Monokai Pro Dark": "themes/monokai-pro.json",
|
||||
"Monokai Pro Light": "themes/monokai-pro.json"
|
||||
}
|
||||
}"#,
|
||||
"themes": {
|
||||
"monokai.json": r#"{
|
||||
"name": "Monokai",
|
||||
"author": "Someone",
|
||||
"themes": [
|
||||
{
|
||||
"name": "Monokai Dark",
|
||||
"appearance": "dark",
|
||||
"style": {}
|
||||
},
|
||||
{
|
||||
"name": "Monokai Light",
|
||||
"appearance": "light",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
"monokai-pro.json": r#"{
|
||||
"name": "Monokai Pro",
|
||||
"author": "Someone",
|
||||
"themes": [
|
||||
{
|
||||
"name": "Monokai Pro Dark",
|
||||
"appearance": "dark",
|
||||
"style": {}
|
||||
},
|
||||
{
|
||||
"name": "Monokai Pro Light",
|
||||
"appearance": "light",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
}
|
||||
},
|
||||
"zed-ruby": {
|
||||
"extension.json": r#"{
|
||||
"id": "zed-ruby",
|
||||
"name": "Zed Ruby",
|
||||
"version": "1.0.0",
|
||||
"grammars": {
|
||||
"ruby": "grammars/ruby.wasm",
|
||||
"embedded_template": "grammars/embedded_template.wasm"
|
||||
},
|
||||
"languages": {
|
||||
"ruby": "languages/ruby",
|
||||
"erb": "languages/erb"
|
||||
}
|
||||
}"#,
|
||||
"grammars": {
|
||||
"ruby.wasm": "",
|
||||
"embedded_template.wasm": "",
|
||||
},
|
||||
"languages": {
|
||||
"ruby": {
|
||||
"config.toml": r#"
|
||||
name = "Ruby"
|
||||
grammar = "ruby"
|
||||
path_suffixes = ["rb"]
|
||||
"#,
|
||||
"highlights.scm": "",
|
||||
},
|
||||
"erb": {
|
||||
"config.toml": r#"
|
||||
name = "ERB"
|
||||
grammar = "embedded_template"
|
||||
path_suffixes = ["erb"]
|
||||
"#,
|
||||
"highlights.scm": "",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut expected_index = ExtensionIndex {
|
||||
extensions: [
|
||||
(
|
||||
"zed-ruby".into(),
|
||||
ExtensionIndexEntry {
|
||||
manifest: Arc::new(ExtensionManifest {
|
||||
id: "zed-ruby".into(),
|
||||
name: "Zed Ruby".into(),
|
||||
version: "1.0.0".into(),
|
||||
schema_version: SchemaVersion::ZERO,
|
||||
description: None,
|
||||
authors: Vec::new(),
|
||||
repository: None,
|
||||
themes: Default::default(),
|
||||
lib: Default::default(),
|
||||
languages: vec!["languages/erb".into(), "languages/ruby".into()],
|
||||
grammars: [
|
||||
("embedded_template".into(), GrammarManifestEntry::default()),
|
||||
("ruby".into(), GrammarManifestEntry::default()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
language_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"zed-monokai".into(),
|
||||
ExtensionIndexEntry {
|
||||
manifest: Arc::new(ExtensionManifest {
|
||||
id: "zed-monokai".into(),
|
||||
name: "Zed Monokai".into(),
|
||||
version: "2.0.0".into(),
|
||||
schema_version: SchemaVersion::ZERO,
|
||||
description: None,
|
||||
authors: vec![],
|
||||
repository: None,
|
||||
themes: vec![
|
||||
"themes/monokai-pro.json".into(),
|
||||
"themes/monokai.json".into(),
|
||||
],
|
||||
lib: Default::default(),
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
languages: [
|
||||
(
|
||||
"ERB".into(),
|
||||
ExtensionIndexLanguageEntry {
|
||||
extension: "zed-ruby".into(),
|
||||
path: "languages/erb".into(),
|
||||
grammar: Some("embedded_template".into()),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["erb".into()],
|
||||
first_line_pattern: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
"Ruby".into(),
|
||||
ExtensionIndexLanguageEntry {
|
||||
extension: "zed-ruby".into(),
|
||||
path: "languages/ruby".into(),
|
||||
grammar: Some("ruby".into()),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rb".into()],
|
||||
first_line_pattern: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
themes: [
|
||||
(
|
||||
"Monokai Dark".into(),
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Light".into(),
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Pro Dark".into(),
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai-pro.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Pro Light".into(),
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai-pro.json".into(),
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let store = cx.new_model(|cx| {
|
||||
let extension_registration_hooks = crate::ConcreteExtensionRegistrationHooks::new(
|
||||
theme_registry.clone(),
|
||||
slash_command_registry.clone(),
|
||||
indexed_docs_registry.clone(),
|
||||
snippet_registry.clone(),
|
||||
language_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
None,
|
||||
extension_registration_hooks,
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
http_client.clone(),
|
||||
None,
|
||||
node_runtime.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
store.read_with(cx, |store, _| {
|
||||
let index = &store.extension_index;
|
||||
assert_eq!(index.extensions, expected_index.extensions);
|
||||
assert_eq!(index.languages, expected_index.languages);
|
||||
assert_eq!(index.themes, expected_index.themes);
|
||||
|
||||
assert_eq!(
|
||||
language_registry.language_names(),
|
||||
["ERB", "Plain Text", "Ruby"]
|
||||
);
|
||||
assert_eq!(
|
||||
theme_registry.list_names(),
|
||||
[
|
||||
"Monokai Dark",
|
||||
"Monokai Light",
|
||||
"Monokai Pro Dark",
|
||||
"Monokai Pro Light",
|
||||
"One Dark",
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
fs.insert_tree(
|
||||
"/the-extension-dir/installed/zed-gruvbox",
|
||||
json!({
|
||||
"extension.json": r#"{
|
||||
"id": "zed-gruvbox",
|
||||
"name": "Zed Gruvbox",
|
||||
"version": "1.0.0",
|
||||
"themes": {
|
||||
"Gruvbox": "themes/gruvbox.json"
|
||||
}
|
||||
}"#,
|
||||
"themes": {
|
||||
"gruvbox.json": r#"{
|
||||
"name": "Gruvbox",
|
||||
"author": "Someone Else",
|
||||
"themes": [
|
||||
{
|
||||
"name": "Gruvbox",
|
||||
"appearance": "dark",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
expected_index.extensions.insert(
|
||||
"zed-gruvbox".into(),
|
||||
ExtensionIndexEntry {
|
||||
manifest: Arc::new(ExtensionManifest {
|
||||
id: "zed-gruvbox".into(),
|
||||
name: "Zed Gruvbox".into(),
|
||||
version: "1.0.0".into(),
|
||||
schema_version: SchemaVersion::ZERO,
|
||||
description: None,
|
||||
authors: vec![],
|
||||
repository: None,
|
||||
themes: vec!["themes/gruvbox.json".into()],
|
||||
lib: Default::default(),
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
);
|
||||
expected_index.themes.insert(
|
||||
"Gruvbox".into(),
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-gruvbox".into(),
|
||||
path: "themes/gruvbox.json".into(),
|
||||
},
|
||||
);
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = store.update(cx, |store, cx| store.reload(None, cx));
|
||||
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
store.read_with(cx, |store, _| {
|
||||
let index = &store.extension_index;
|
||||
assert_eq!(index.extensions, expected_index.extensions);
|
||||
assert_eq!(index.languages, expected_index.languages);
|
||||
assert_eq!(index.themes, expected_index.themes);
|
||||
|
||||
assert_eq!(
|
||||
theme_registry.list_names(),
|
||||
[
|
||||
"Gruvbox",
|
||||
"Monokai Dark",
|
||||
"Monokai Light",
|
||||
"Monokai Pro Dark",
|
||||
"Monokai Pro Light",
|
||||
"One Dark",
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let prev_fs_metadata_call_count = fs.metadata_call_count();
|
||||
let prev_fs_read_dir_call_count = fs.read_dir_call_count();
|
||||
|
||||
// Create new extension store, as if Zed were restarting.
|
||||
drop(store);
|
||||
let store = cx.new_model(|cx| {
|
||||
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
|
||||
theme_registry.clone(),
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
None,
|
||||
extension_api,
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
http_client.clone(),
|
||||
None,
|
||||
node_runtime.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
store.read_with(cx, |store, _| {
|
||||
assert_eq!(store.extension_index, expected_index);
|
||||
assert_eq!(
|
||||
language_registry.language_names(),
|
||||
["ERB", "Plain Text", "Ruby"]
|
||||
);
|
||||
assert_eq!(
|
||||
language_registry.grammar_names(),
|
||||
["embedded_template".into(), "ruby".into()]
|
||||
);
|
||||
assert_eq!(
|
||||
theme_registry.list_names(),
|
||||
[
|
||||
"Gruvbox",
|
||||
"Monokai Dark",
|
||||
"Monokai Light",
|
||||
"Monokai Pro Dark",
|
||||
"Monokai Pro Light",
|
||||
"One Dark",
|
||||
]
|
||||
);
|
||||
|
||||
// The on-disk manifest limits the number of FS calls that need to be made
|
||||
// on startup.
|
||||
assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
|
||||
assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
|
||||
});
|
||||
|
||||
store.update(cx, |store, cx| {
|
||||
store.uninstall_extension("zed-ruby".into(), cx)
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
expected_index.extensions.remove("zed-ruby");
|
||||
expected_index.languages.remove("Ruby");
|
||||
expected_index.languages.remove("ERB");
|
||||
|
||||
store.read_with(cx, |store, _| {
|
||||
assert_eq!(store.extension_index, expected_index);
|
||||
assert_eq!(language_registry.language_names(), ["Plain Text"]);
|
||||
assert_eq!(language_registry.grammar_names(), []);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap();
|
||||
let cache_dir = root_dir.join("target");
|
||||
let test_extension_id = "test-extension";
|
||||
let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
|
||||
|
||||
let fs = Arc::new(RealFs::default());
|
||||
let extensions_dir = temp_tree(json!({
|
||||
"installed": {},
|
||||
"work": {}
|
||||
}));
|
||||
let project_dir = temp_tree(json!({
|
||||
"test.gleam": ""
|
||||
}));
|
||||
|
||||
let extensions_dir = extensions_dir.path().canonicalize().unwrap();
|
||||
let project_dir = project_dir.path().canonicalize().unwrap();
|
||||
|
||||
let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let mut status_updates = language_registry.language_server_binary_statuses();
|
||||
|
||||
struct FakeLanguageServerVersion {
|
||||
version: String,
|
||||
binary_contents: String,
|
||||
http_request_count: usize,
|
||||
}
|
||||
|
||||
let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
|
||||
version: "v1.2.3".into(),
|
||||
binary_contents: "the-binary-contents".into(),
|
||||
http_request_count: 0,
|
||||
}));
|
||||
|
||||
let extension_client = FakeHttpClient::create({
|
||||
let language_server_version = language_server_version.clone();
|
||||
move |request| {
|
||||
let language_server_version = language_server_version.clone();
|
||||
async move {
|
||||
let version = language_server_version.lock().version.clone();
|
||||
let binary_contents = language_server_version.lock().binary_contents.clone();
|
||||
|
||||
let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
|
||||
let asset_download_uri =
|
||||
format!("https://fake-download.example.com/gleam-{version}");
|
||||
|
||||
let uri = request.uri().to_string();
|
||||
if uri == github_releases_uri {
|
||||
language_server_version.lock().http_request_count += 1;
|
||||
Ok(Response::new(
|
||||
json!([
|
||||
{
|
||||
"tag_name": version,
|
||||
"prerelease": false,
|
||||
"tarball_url": "",
|
||||
"zipball_url": "",
|
||||
"assets": [
|
||||
{
|
||||
"name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
|
||||
"browser_download_url": asset_download_uri
|
||||
},
|
||||
{
|
||||
"name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
|
||||
"browser_download_url": asset_download_uri
|
||||
},
|
||||
{
|
||||
"name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
|
||||
"browser_download_url": asset_download_uri
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
.to_string()
|
||||
.into(),
|
||||
))
|
||||
} else if uri == asset_download_uri {
|
||||
language_server_version.lock().http_request_count += 1;
|
||||
let mut bytes = Vec::<u8>::new();
|
||||
let mut archive = async_tar::Builder::new(&mut bytes);
|
||||
let mut header = async_tar::Header::new_gnu();
|
||||
header.set_size(binary_contents.len() as u64);
|
||||
archive
|
||||
.append_data(&mut header, "gleam", binary_contents.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
archive.into_inner().await.unwrap();
|
||||
let mut gzipped_bytes = Vec::new();
|
||||
let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
|
||||
encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
|
||||
Ok(Response::new(gzipped_bytes.into()))
|
||||
} else {
|
||||
Ok(Response::builder().status(404).body("not found".into())?)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let user_agent = cx.update(|cx| {
|
||||
format!(
|
||||
"Zed/{} ({}; {})",
|
||||
AppVersion::global(cx),
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
)
|
||||
});
|
||||
let builder_client =
|
||||
Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
|
||||
|
||||
let extension_store = cx.new_model(|cx| {
|
||||
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
|
||||
theme_registry.clone(),
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
ExtensionStore::new(
|
||||
extensions_dir.clone(),
|
||||
Some(cache_dir),
|
||||
extension_api,
|
||||
fs.clone(),
|
||||
extension_client.clone(),
|
||||
builder_client,
|
||||
None,
|
||||
node_runtime,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Ensure that debounces fire.
|
||||
let mut events = cx.events(&extension_store);
|
||||
let executor = cx.executor();
|
||||
let _task = cx.executor().spawn(async move {
|
||||
while let Some(event) = events.next().await {
|
||||
if let extension_host::Event::StartedReloading = event {
|
||||
executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
extension_store.update(cx, |_, cx| {
|
||||
cx.subscribe(&extension_store, |_, _, event, _| {
|
||||
if matches!(event, Event::ExtensionFailedToLoad(_)) {
|
||||
panic!("extension failed to load");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
extension_store
|
||||
.update(cx, |store, cx| {
|
||||
store.install_dev_extension(test_extension_dir.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut fake_servers = language_registry.register_fake_language_server(
|
||||
LanguageServerName("gleam".into()),
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(project_dir.join("test.gleam"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let expected_server_path =
|
||||
extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
|
||||
let expected_binary_contents = language_server_version.lock().binary_contents.clone();
|
||||
|
||||
assert_eq!(fake_server.binary.path, expected_server_path);
|
||||
assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
|
||||
assert_eq!(
|
||||
fs.load(&expected_server_path).await.unwrap(),
|
||||
expected_binary_contents
|
||||
);
|
||||
assert_eq!(language_server_version.lock().http_request_count, 2);
|
||||
assert_eq!(
|
||||
[
|
||||
status_updates.next().await.unwrap(),
|
||||
status_updates.next().await.unwrap(),
|
||||
status_updates.next().await.unwrap(),
|
||||
],
|
||||
[
|
||||
(
|
||||
LanguageServerName("gleam".into()),
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
),
|
||||
(
|
||||
LanguageServerName("gleam".into()),
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
),
|
||||
(
|
||||
LanguageServerName("gleam".into()),
|
||||
LanguageServerBinaryStatus::None
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// The extension creates custom labels for completion items.
|
||||
fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "foo".into(),
|
||||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||||
detail: Some("fn() -> Result(Nil, Error)".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "bar.baz".into(),
|
||||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||||
detail: Some("fn(List(a)) -> a".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "Quux".into(),
|
||||
kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
|
||||
detail: Some("fn(String) -> T".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "my_string".into(),
|
||||
kind: Some(lsp::CompletionItemKind::CONSTANT),
|
||||
detail: Some("String".into()),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
let completion_labels = project
|
||||
.update(cx, |project, cx| {
|
||||
project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|c| c.label.text)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completion_labels,
|
||||
[
|
||||
"foo: fn() -> Result(Nil, Error)".to_string(),
|
||||
"bar.baz: fn(List(a)) -> a".to_string(),
|
||||
"Quux: fn(String) -> T".to_string(),
|
||||
"my_string: String".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
// Simulate a new version of the language server being released
|
||||
language_server_version.lock().version = "v2.0.0".into();
|
||||
language_server_version.lock().binary_contents = "the-new-binary-contents".into();
|
||||
language_server_version.lock().http_request_count = 0;
|
||||
|
||||
// Start a new instance of the language server.
|
||||
project.update(cx, |project, cx| {
|
||||
project.restart_language_servers_for_buffers([buffer.clone()], cx)
|
||||
});
|
||||
|
||||
// The extension has cached the binary path, and does not attempt
|
||||
// to reinstall it.
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
assert_eq!(fake_server.binary.path, expected_server_path);
|
||||
assert_eq!(
|
||||
fs.load(&expected_server_path).await.unwrap(),
|
||||
expected_binary_contents
|
||||
);
|
||||
assert_eq!(language_server_version.lock().http_request_count, 0);
|
||||
|
||||
// Reload the extension, clearing its cache.
|
||||
// Start a new instance of the language server.
|
||||
extension_store
|
||||
.update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
project.update(cx, |project, cx| {
|
||||
project.restart_language_servers_for_buffers([buffer.clone()], cx)
|
||||
});
|
||||
|
||||
// The extension re-fetches the latest version of the language server.
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let new_expected_server_path =
|
||||
extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
|
||||
let expected_binary_contents = language_server_version.lock().binary_contents.clone();
|
||||
assert_eq!(fake_server.binary.path, new_expected_server_path);
|
||||
assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
|
||||
assert_eq!(
|
||||
fs.load(&new_expected_server_path).await.unwrap(),
|
||||
expected_binary_contents
|
||||
);
|
||||
|
||||
// The old language server directory has been cleaned up.
|
||||
assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init_settings(cx);
|
||||
ExtensionSettings::register(cx);
|
||||
language::init(cx);
|
||||
});
|
||||
}
|
|
@ -1,7 +1,15 @@
|
|||
mod components;
|
||||
mod extension_indexed_docs_provider;
|
||||
mod extension_registration_hooks;
|
||||
mod extension_slash_command;
|
||||
mod extension_suggest;
|
||||
mod extension_version_selector;
|
||||
|
||||
#[cfg(test)]
|
||||
mod extension_store_test;
|
||||
|
||||
pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks;
|
||||
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue