mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 20:04:15 +03:00
Add an extensions installation view (#7689)
This PR adds a view for installing extensions within Zed. My subtasks: - [X] Page Extensions and assign in App Menu - [X] List extensions - [X] Button to Install/Uninstall - [x] Search Input to search in extensions registry API - [x] Get Extensions from API - [x] Action install to download extension and copy in /extensions folder - [x] Action uninstall to remove from /extensions folder - [x] Filtering - [x] Better UI Design Open to collab! Release Notes: - Added an extension installation view. Open it using the `zed: extensions` action in the command palette ([#7096](https://github.com/zed-industries/zed/issues/7096)). --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Carlos <foxkdev@gmail.com> Co-authored-by: Marshall Bowers <elliott.codes@gmail.com> Co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
33f713a8ab
commit
fecb5a82f1
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -2675,20 +2675,52 @@ name = "extension"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-compression",
|
||||||
|
"async-tar",
|
||||||
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
|
"log",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"settings",
|
||||||
"theme",
|
"theme",
|
||||||
"toml",
|
"toml",
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extensions_ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-compression",
|
||||||
|
"async-tar",
|
||||||
|
"client",
|
||||||
|
"db",
|
||||||
|
"editor",
|
||||||
|
"extension",
|
||||||
|
"fs",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"fuzzy",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"picker",
|
||||||
|
"project",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -10792,6 +10824,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"extension",
|
"extension",
|
||||||
|
"extensions_ui",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"feedback",
|
"feedback",
|
||||||
"file_finder",
|
"file_finder",
|
||||||
|
@ -22,6 +22,7 @@ members = [
|
|||||||
"crates/diagnostics",
|
"crates/diagnostics",
|
||||||
"crates/editor",
|
"crates/editor",
|
||||||
"crates/extension",
|
"crates/extension",
|
||||||
|
"crates/extensions_ui",
|
||||||
"crates/feature_flags",
|
"crates/feature_flags",
|
||||||
"crates/feedback",
|
"crates/feedback",
|
||||||
"crates/file_finder",
|
"crates/file_finder",
|
||||||
@ -113,6 +114,7 @@ db = { path = "crates/db" }
|
|||||||
diagnostics = { path = "crates/diagnostics" }
|
diagnostics = { path = "crates/diagnostics" }
|
||||||
editor = { path = "crates/editor" }
|
editor = { path = "crates/editor" }
|
||||||
extension = { path = "crates/extension" }
|
extension = { path = "crates/extension" }
|
||||||
|
extensions_ui = { path = "crates/extensions_ui" }
|
||||||
feature_flags = { path = "crates/feature_flags" }
|
feature_flags = { path = "crates/feature_flags" }
|
||||||
feedback = { path = "crates/feedback" }
|
feedback = { path = "crates/feedback" }
|
||||||
file_finder = { path = "crates/file_finder" }
|
file_finder = { path = "crates/file_finder" }
|
||||||
@ -177,6 +179,7 @@ zed_actions = { path = "crates/zed_actions" }
|
|||||||
|
|
||||||
anyhow = "1.0.57"
|
anyhow = "1.0.57"
|
||||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||||
|
async-tar = "0.4.2"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
ctor = "0.2.6"
|
ctor = "0.2.6"
|
||||||
|
@ -14,20 +14,26 @@ path = "src/extension_json_schemas.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
async-compression.workspace = true
|
||||||
|
async-tar.workspace = true
|
||||||
|
client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
log.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
client = { workspace = true, features = ["test-support"] }
|
||||||
fs = { workspace = true, features = ["test-support"] }
|
fs = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{anyhow, bail, Context as _, Result};
|
||||||
use collections::HashMap;
|
use async_compression::futures::bufread::GzipDecoder;
|
||||||
use fs::Fs;
|
use async_tar::Archive;
|
||||||
|
use client::ClientSettings;
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use fs::{Fs, RemoveOptions};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
|
use futures::{io::BufReader, AsyncReadExt as _};
|
||||||
use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Task};
|
use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Task};
|
||||||
use language::{
|
use language::{
|
||||||
LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
|
LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
|
||||||
};
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::Settings as _;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -15,15 +20,43 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::{ThemeRegistry, ThemeSettings};
|
use theme::{ThemeRegistry, ThemeSettings};
|
||||||
use util::{paths::EXTENSIONS_DIR, ResultExt};
|
use util::http::AsyncBody;
|
||||||
|
use util::{http::HttpClient, paths::EXTENSIONS_DIR, ResultExt};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod extension_store_test;
|
mod extension_store_test;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ExtensionsApiResponse {
|
||||||
|
pub data: Vec<Extension>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Extension {
|
||||||
|
pub id: Arc<str>,
|
||||||
|
pub version: Arc<str>,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub authors: Vec<String>,
|
||||||
|
pub repository: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ExtensionStatus {
|
||||||
|
NotInstalled,
|
||||||
|
Installing,
|
||||||
|
Upgrading,
|
||||||
|
Installed(Arc<str>),
|
||||||
|
Removing,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ExtensionStore {
|
pub struct ExtensionStore {
|
||||||
manifest: Arc<RwLock<Manifest>>,
|
manifest: Arc<RwLock<Manifest>>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
http_client: Arc<dyn HttpClient>,
|
||||||
extensions_dir: PathBuf,
|
extensions_dir: PathBuf,
|
||||||
|
extensions_being_installed: HashSet<Arc<str>>,
|
||||||
|
extensions_being_uninstalled: HashSet<Arc<str>>,
|
||||||
manifest_path: PathBuf,
|
manifest_path: PathBuf,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
theme_registry: Arc<ThemeRegistry>,
|
theme_registry: Arc<ThemeRegistry>,
|
||||||
@ -36,6 +69,7 @@ impl Global for GlobalExtensionStore {}
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize, Default)]
|
#[derive(Deserialize, Serialize, Default)]
|
||||||
pub struct Manifest {
|
pub struct Manifest {
|
||||||
|
pub extensions: HashMap<Arc<str>, Arc<str>>,
|
||||||
pub grammars: HashMap<Arc<str>, GrammarManifestEntry>,
|
pub grammars: HashMap<Arc<str>, GrammarManifestEntry>,
|
||||||
pub languages: HashMap<Arc<str>, LanguageManifestEntry>,
|
pub languages: HashMap<Arc<str>, LanguageManifestEntry>,
|
||||||
pub themes: HashMap<String, ThemeManifestEntry>,
|
pub themes: HashMap<String, ThemeManifestEntry>,
|
||||||
@ -65,6 +99,7 @@ actions!(zed, [ReloadExtensions]);
|
|||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
fs: Arc<fs::RealFs>,
|
fs: Arc<fs::RealFs>,
|
||||||
|
http_client: Arc<dyn HttpClient>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
theme_registry: Arc<ThemeRegistry>,
|
theme_registry: Arc<ThemeRegistry>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
@ -73,6 +108,7 @@ pub fn init(
|
|||||||
ExtensionStore::new(
|
ExtensionStore::new(
|
||||||
EXTENSIONS_DIR.clone(),
|
EXTENSIONS_DIR.clone(),
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
|
http_client.clone(),
|
||||||
language_registry.clone(),
|
language_registry.clone(),
|
||||||
theme_registry,
|
theme_registry,
|
||||||
cx,
|
cx,
|
||||||
@ -90,9 +126,14 @@ pub fn init(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionStore {
|
impl ExtensionStore {
|
||||||
|
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||||
|
cx.global::<GlobalExtensionStore>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
extensions_dir: PathBuf,
|
extensions_dir: PathBuf,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
http_client: Arc<dyn HttpClient>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
theme_registry: Arc<ThemeRegistry>,
|
theme_registry: Arc<ThemeRegistry>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
@ -101,7 +142,10 @@ impl ExtensionStore {
|
|||||||
manifest: Default::default(),
|
manifest: Default::default(),
|
||||||
extensions_dir: extensions_dir.join("installed"),
|
extensions_dir: extensions_dir.join("installed"),
|
||||||
manifest_path: extensions_dir.join("manifest.json"),
|
manifest_path: extensions_dir.join("manifest.json"),
|
||||||
|
extensions_being_installed: Default::default(),
|
||||||
|
extensions_being_uninstalled: Default::default(),
|
||||||
fs,
|
fs,
|
||||||
|
http_client,
|
||||||
language_registry,
|
language_registry,
|
||||||
theme_registry,
|
theme_registry,
|
||||||
_watch_extensions_dir: [Task::ready(()), Task::ready(())],
|
_watch_extensions_dir: [Task::ready(()), Task::ready(())],
|
||||||
@ -140,6 +184,132 @@ impl ExtensionStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extensions_dir(&self) -> PathBuf {
|
||||||
|
self.extensions_dir.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extension_status(&self, extension_id: &str) -> ExtensionStatus {
|
||||||
|
let is_uninstalling = self.extensions_being_uninstalled.contains(extension_id);
|
||||||
|
if is_uninstalling {
|
||||||
|
return ExtensionStatus::Removing;
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed_version = self.manifest.read().extensions.get(extension_id).cloned();
|
||||||
|
let is_installing = self.extensions_being_installed.contains(extension_id);
|
||||||
|
match (installed_version, is_installing) {
|
||||||
|
(Some(_), true) => ExtensionStatus::Upgrading,
|
||||||
|
(Some(version), false) => ExtensionStatus::Installed(version.clone()),
|
||||||
|
(None, true) => ExtensionStatus::Installing,
|
||||||
|
(None, false) => ExtensionStatus::NotInstalled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_extensions(
|
||||||
|
&self,
|
||||||
|
search: Option<&str>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Vec<Extension>>> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/{}{query}",
|
||||||
|
ClientSettings::get_global(cx).server_url,
|
||||||
|
"api/extensions",
|
||||||
|
query = search
|
||||||
|
.map(|search| format!("?filter={search}"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
);
|
||||||
|
let http_client = self.http_client.clone();
|
||||||
|
cx.spawn(move |_, _| async move {
|
||||||
|
let mut response = http_client.get(&url, AsyncBody::empty(), true).await?;
|
||||||
|
|
||||||
|
let mut body = Vec::new();
|
||||||
|
response
|
||||||
|
.body_mut()
|
||||||
|
.read_to_end(&mut body)
|
||||||
|
.await
|
||||||
|
.context("error reading extensions")?;
|
||||||
|
|
||||||
|
if response.status().is_client_error() {
|
||||||
|
let text = String::from_utf8_lossy(body.as_slice());
|
||||||
|
bail!(
|
||||||
|
"status error {}, response: {text:?}",
|
||||||
|
response.status().as_u16()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: ExtensionsApiResponse = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
|
Ok(response.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_extension(
|
||||||
|
&mut self,
|
||||||
|
extension_id: Arc<str>,
|
||||||
|
version: Arc<str>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
log::info!("installing extension {extension_id} {version}");
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/extensions/{extension_id}/{version}/download",
|
||||||
|
ClientSettings::get_global(cx).server_url
|
||||||
|
);
|
||||||
|
|
||||||
|
let extensions_dir = self.extensions_dir();
|
||||||
|
let http_client = self.http_client.clone();
|
||||||
|
|
||||||
|
self.extensions_being_installed.insert(extension_id.clone());
|
||||||
|
|
||||||
|
cx.spawn(move |this, mut cx| async move {
|
||||||
|
let mut response = http_client
|
||||||
|
.get(&url, Default::default(), true)
|
||||||
|
.await
|
||||||
|
.map_err(|err| anyhow!("error downloading extension: {}", err))?;
|
||||||
|
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||||
|
let archive = Archive::new(decompressed_bytes);
|
||||||
|
archive
|
||||||
|
.unpack(extensions_dir.join(extension_id.as_ref()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |store, cx| {
|
||||||
|
store
|
||||||
|
.extensions_being_installed
|
||||||
|
.remove(extension_id.as_ref());
|
||||||
|
store.reload(cx)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uninstall_extension(
|
||||||
|
&mut self,
|
||||||
|
extension_id: Arc<str>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let extensions_dir = self.extensions_dir();
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
|
self.extensions_being_uninstalled
|
||||||
|
.insert(extension_id.clone());
|
||||||
|
|
||||||
|
cx.spawn(move |this, mut cx| async move {
|
||||||
|
fs.remove_dir(
|
||||||
|
&extensions_dir.join(extension_id.as_ref()),
|
||||||
|
RemoveOptions {
|
||||||
|
recursive: true,
|
||||||
|
ignore_if_not_exists: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.extensions_being_uninstalled
|
||||||
|
.remove(extension_id.as_ref());
|
||||||
|
this.reload(cx)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn manifest_updated(&mut self, manifest: Manifest, cx: &mut ModelContext<Self>) {
|
fn manifest_updated(&mut self, manifest: Manifest, cx: &mut ModelContext<Self>) {
|
||||||
self.language_registry
|
self.language_registry
|
||||||
.register_wasm_grammars(manifest.grammars.iter().map(|(grammar_name, grammar)| {
|
.register_wasm_grammars(manifest.grammars.iter().map(|(grammar_name, grammar)| {
|
||||||
@ -235,11 +405,13 @@ impl ExtensionStore {
|
|||||||
language_registry.reload_languages(&changed_languages, &changed_grammars);
|
language_registry.reload_languages(&changed_languages, &changed_grammars);
|
||||||
|
|
||||||
for theme_path in &changed_themes {
|
for theme_path in &changed_themes {
|
||||||
theme_registry
|
if fs.is_file(&theme_path).await {
|
||||||
.load_user_theme(&theme_path, fs.clone())
|
theme_registry
|
||||||
.await
|
.load_user_theme(&theme_path, fs.clone())
|
||||||
.context("failed to load user theme")
|
.await
|
||||||
.log_err();
|
.context("failed to load user theme")
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !changed_themes.is_empty() {
|
if !changed_themes.is_empty() {
|
||||||
@ -284,6 +456,19 @@ impl ExtensionStore {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ExtensionJson {
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let extension_json_path = extension_dir.join("extension.json");
|
||||||
|
let extension_json: ExtensionJson =
|
||||||
|
serde_json::from_str(&fs.load(&extension_json_path).await?)?;
|
||||||
|
|
||||||
|
manifest
|
||||||
|
.extensions
|
||||||
|
.insert(extension_name.into(), extension_json.version.into());
|
||||||
|
|
||||||
if let Ok(mut grammar_paths) =
|
if let Ok(mut grammar_paths) =
|
||||||
fs.read_dir(&extension_dir.join("grammars")).await
|
fs.read_dir(&extension_dir.join("grammars")).await
|
||||||
{
|
{
|
||||||
|
@ -7,16 +7,23 @@ use language::{LanguageMatcher, LanguageRegistry};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
|
use util::http::FakeHttpClient;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_extension_store(cx: &mut TestAppContext) {
|
async fn test_extension_store(cx: &mut TestAppContext) {
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let http_client = FakeHttpClient::with_200_response();
|
||||||
|
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/the-extension-dir",
|
"/the-extension-dir",
|
||||||
json!({
|
json!({
|
||||||
"installed": {
|
"installed": {
|
||||||
"zed-monokai": {
|
"zed-monokai": {
|
||||||
|
"extension.json": r#"{
|
||||||
|
"id": "zed-monokai",
|
||||||
|
"name": "Zed Monokai",
|
||||||
|
"version": "2.0.0"
|
||||||
|
}"#,
|
||||||
"themes": {
|
"themes": {
|
||||||
"monokai.json": r#"{
|
"monokai.json": r#"{
|
||||||
"name": "Monokai",
|
"name": "Monokai",
|
||||||
@ -53,6 +60,11 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zed-ruby": {
|
"zed-ruby": {
|
||||||
|
"extension.json": r#"{
|
||||||
|
"id": "zed-ruby",
|
||||||
|
"name": "Zed Ruby",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}"#,
|
||||||
"grammars": {
|
"grammars": {
|
||||||
"ruby.wasm": "",
|
"ruby.wasm": "",
|
||||||
"embedded_template.wasm": "",
|
"embedded_template.wasm": "",
|
||||||
@ -82,6 +94,12 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut expected_manifest = Manifest {
|
let mut expected_manifest = Manifest {
|
||||||
|
extensions: [
|
||||||
|
("zed-ruby".into(), "1.0.0".into()),
|
||||||
|
("zed-monokai".into(), "2.0.0".into()),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
grammars: [
|
grammars: [
|
||||||
(
|
(
|
||||||
"embedded_template".into(),
|
"embedded_template".into(),
|
||||||
@ -169,6 +187,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
ExtensionStore::new(
|
ExtensionStore::new(
|
||||||
PathBuf::from("/the-extension-dir"),
|
PathBuf::from("/the-extension-dir"),
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
|
http_client.clone(),
|
||||||
language_registry.clone(),
|
language_registry.clone(),
|
||||||
theme_registry.clone(),
|
theme_registry.clone(),
|
||||||
cx,
|
cx,
|
||||||
@ -201,6 +220,11 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/the-extension-dir/installed/zed-gruvbox",
|
"/the-extension-dir/installed/zed-gruvbox",
|
||||||
json!({
|
json!({
|
||||||
|
"extension.json": r#"{
|
||||||
|
"id": "zed-gruvbox",
|
||||||
|
"name": "Zed Gruvbox",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}"#,
|
||||||
"themes": {
|
"themes": {
|
||||||
"gruvbox.json": r#"{
|
"gruvbox.json": r#"{
|
||||||
"name": "Gruvbox",
|
"name": "Gruvbox",
|
||||||
@ -260,6 +284,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
ExtensionStore::new(
|
ExtensionStore::new(
|
||||||
PathBuf::from("/the-extension-dir"),
|
PathBuf::from("/the-extension-dir"),
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
|
http_client.clone(),
|
||||||
language_registry.clone(),
|
language_registry.clone(),
|
||||||
theme_registry.clone(),
|
theme_registry.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
38
crates/extensions_ui/Cargo.toml
Normal file
38
crates/extensions_ui/Cargo.toml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "extensions_ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/extensions_ui.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
async-compression.workspace = true
|
||||||
|
async-tar.workspace = true
|
||||||
|
client.workspace = true
|
||||||
|
db.workspace = true
|
||||||
|
editor.workspace = true
|
||||||
|
extension.workspace = true
|
||||||
|
fs.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
fuzzy.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
picker.workspace = true
|
||||||
|
project.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
|
theme.workspace = true
|
||||||
|
ui.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
workspace.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { workspace = true, features = ["test-support"] }
|
1
crates/extensions_ui/LICENSE-GPL
Symbolic link
1
crates/extensions_ui/LICENSE-GPL
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../LICENSE-GPL
|
422
crates/extensions_ui/src/extensions_ui.rs
Normal file
422
crates/extensions_ui/src/extensions_ui.rs
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
use client::telemetry::Telemetry;
|
||||||
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
|
use extension::{Extension, ExtensionStatus, ExtensionStore};
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::{
|
||||||
|
actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
|
||||||
|
FontWeight, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||||
|
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::{ops::Range, sync::Arc};
|
||||||
|
use theme::ThemeSettings;
|
||||||
|
use ui::prelude::*;
|
||||||
|
|
||||||
|
use workspace::{
|
||||||
|
item::{Item, ItemEvent},
|
||||||
|
Workspace, WorkspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
actions!(zed, [Extensions]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
|
||||||
|
workspace.register_action(move |workspace, _: &Extensions, cx| {
|
||||||
|
let extensions_page = ExtensionsPage::new(workspace, cx);
|
||||||
|
workspace.add_item(Box::new(extensions_page), cx)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExtensionsPage {
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
list: UniformListScrollHandle,
|
||||||
|
telemetry: Arc<Telemetry>,
|
||||||
|
extensions_entries: Vec<Extension>,
|
||||||
|
query_editor: View<Editor>,
|
||||||
|
query_contains_error: bool,
|
||||||
|
extension_fetch_task: Option<Task<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ExtensionsPage {
|
||||||
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.full()
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.full()
|
||||||
|
.p_4()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.child(Headline::new("Extensions").size(HeadlineSize::XLarge)),
|
||||||
|
)
|
||||||
|
.child(h_flex().w_56().my_4().child(self.render_search(cx)))
|
||||||
|
.child(
|
||||||
|
h_flex().flex_col().items_start().full().child(
|
||||||
|
uniform_list::<_, Div, _>(
|
||||||
|
cx.view().clone(),
|
||||||
|
"entries",
|
||||||
|
self.extensions_entries.len(),
|
||||||
|
Self::render_extensions,
|
||||||
|
)
|
||||||
|
.size_full()
|
||||||
|
.track_scroll(self.list.clone()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionsPage {
|
||||||
|
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||||
|
let extensions_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||||
|
let query_editor = cx.new_view(|cx| Editor::single_line(cx));
|
||||||
|
cx.subscribe(&query_editor, Self::on_query_change).detach();
|
||||||
|
|
||||||
|
let mut this = Self {
|
||||||
|
fs: workspace.project().read(cx).fs().clone(),
|
||||||
|
workspace: workspace.weak_handle(),
|
||||||
|
list: UniformListScrollHandle::new(),
|
||||||
|
telemetry: workspace.client().telemetry().clone(),
|
||||||
|
extensions_entries: Vec::new(),
|
||||||
|
query_contains_error: false,
|
||||||
|
extension_fetch_task: None,
|
||||||
|
query_editor,
|
||||||
|
};
|
||||||
|
this.fetch_extensions(None, cx);
|
||||||
|
this
|
||||||
|
});
|
||||||
|
extensions_panel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_extension(
|
||||||
|
&self,
|
||||||
|
extension_id: Arc<str>,
|
||||||
|
version: Arc<str>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let install = ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||||
|
store.install_extension(extension_id, version, cx)
|
||||||
|
});
|
||||||
|
cx.spawn(move |this, mut cx| async move {
|
||||||
|
install.await?;
|
||||||
|
this.update(&mut cx, |_, cx| cx.notify())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uninstall_extension(&self, extension_id: Arc<str>, cx: &mut ViewContext<Self>) {
|
||||||
|
let install = ExtensionStore::global(cx)
|
||||||
|
.update(cx, |store, cx| store.uninstall_extension(extension_id, cx));
|
||||||
|
cx.spawn(move |this, mut cx| async move {
|
||||||
|
install.await?;
|
||||||
|
this.update(&mut cx, |_, cx| cx.notify())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_extensions(&mut self, search: Option<&str>, cx: &mut ViewContext<Self>) {
|
||||||
|
let extensions =
|
||||||
|
ExtensionStore::global(cx).update(cx, |store, cx| store.fetch_extensions(search, cx));
|
||||||
|
|
||||||
|
cx.spawn(move |this, mut cx| async move {
|
||||||
|
let extensions = extensions.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.extensions_entries = extensions;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_extensions(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Vec<Div> {
|
||||||
|
self.extensions_entries[range]
|
||||||
|
.iter()
|
||||||
|
.map(|extension| self.render_entry(extension, cx))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_entry(&self, extension: &Extension, cx: &mut ViewContext<Self>) -> Div {
|
||||||
|
let status = ExtensionStore::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.extension_status(&extension.id);
|
||||||
|
|
||||||
|
let upgrade_button = match status.clone() {
|
||||||
|
ExtensionStatus::NotInstalled
|
||||||
|
| ExtensionStatus::Installing
|
||||||
|
| ExtensionStatus::Removing => None,
|
||||||
|
ExtensionStatus::Installed(installed_version) => {
|
||||||
|
if installed_version != extension.version {
|
||||||
|
Some(
|
||||||
|
Button::new(
|
||||||
|
SharedString::from(format!("upgrade-{}", extension.id)),
|
||||||
|
"Upgrade",
|
||||||
|
)
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let extension_id = extension.id.clone();
|
||||||
|
let version = extension.version.clone();
|
||||||
|
move |this, _, cx| {
|
||||||
|
this.telemetry
|
||||||
|
.report_app_event("extensions: install extension".to_string());
|
||||||
|
this.install_extension(extension_id.clone(), version.clone(), cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.color(Color::Accent),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExtensionStatus::Upgrading => Some(
|
||||||
|
Button::new(
|
||||||
|
SharedString::from(format!("upgrade-{}", extension.id)),
|
||||||
|
"Upgrade",
|
||||||
|
)
|
||||||
|
.color(Color::Accent)
|
||||||
|
.disabled(true),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let install_or_uninstall_button = match status {
|
||||||
|
ExtensionStatus::NotInstalled | ExtensionStatus::Installing => {
|
||||||
|
Button::new(SharedString::from(extension.id.clone()), "Install")
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let extension_id = extension.id.clone();
|
||||||
|
let version = extension.version.clone();
|
||||||
|
move |this, _, cx| {
|
||||||
|
this.telemetry
|
||||||
|
.report_app_event("extensions: install extension".to_string());
|
||||||
|
this.install_extension(extension_id.clone(), version.clone(), cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.disabled(matches!(status, ExtensionStatus::Installing))
|
||||||
|
}
|
||||||
|
ExtensionStatus::Installed(_)
|
||||||
|
| ExtensionStatus::Upgrading
|
||||||
|
| ExtensionStatus::Removing => {
|
||||||
|
Button::new(SharedString::from(extension.id.clone()), "Uninstall")
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let extension_id = extension.id.clone();
|
||||||
|
move |this, _, cx| {
|
||||||
|
this.telemetry
|
||||||
|
.report_app_event("extensions: uninstall extension".to_string());
|
||||||
|
this.uninstall_extension(extension_id.clone(), cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.disabled(matches!(
|
||||||
|
status,
|
||||||
|
ExtensionStatus::Upgrading | ExtensionStatus::Removing
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color(Color::Accent);
|
||||||
|
|
||||||
|
div().w_full().child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.p_3()
|
||||||
|
.mt_4()
|
||||||
|
.gap_2()
|
||||||
|
.bg(cx.theme().colors().elevated_surface_background)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.rounded_md()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.items_end()
|
||||||
|
.child(
|
||||||
|
Headline::new(extension.name.clone())
|
||||||
|
.size(HeadlineSize::Medium),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Headline::new(format!("v{}", extension.version))
|
||||||
|
.size(HeadlineSize::XSmall),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.justify_between()
|
||||||
|
.children(upgrade_button)
|
||||||
|
.child(install_or_uninstall_button),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex().justify_between().child(
|
||||||
|
Label::new(format!(
|
||||||
|
"{}: {}",
|
||||||
|
if extension.authors.len() > 1 {
|
||||||
|
"Authors"
|
||||||
|
} else {
|
||||||
|
"Author"
|
||||||
|
},
|
||||||
|
extension.authors.join(", ")
|
||||||
|
))
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
|
.children(extension.description.as_ref().map(|description| {
|
||||||
|
Label::new(description.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Default)
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_search(&self, cx: &mut ViewContext<Self>) -> Div {
|
||||||
|
let mut key_context = KeyContext::default();
|
||||||
|
key_context.add("BufferSearchBar");
|
||||||
|
|
||||||
|
let editor_border = if self.query_contains_error {
|
||||||
|
Color::Error.color(cx)
|
||||||
|
} else {
|
||||||
|
cx.theme().colors().border
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.key_context(key_context)
|
||||||
|
// .capture_action(cx.listener(Self::tab))
|
||||||
|
// .on_action(cx.listener(Self::dismiss))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.gap_2()
|
||||||
|
.border_1()
|
||||||
|
.border_color(editor_border)
|
||||||
|
.min_w(rems(384. / 16.))
|
||||||
|
.rounded_lg()
|
||||||
|
.child(Icon::new(IconName::MagnifyingGlass))
|
||||||
|
.child(self.render_text_input(&self.query_editor, cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let settings = ThemeSettings::get_global(cx);
|
||||||
|
let text_style = TextStyle {
|
||||||
|
color: if editor.read(cx).read_only(cx) {
|
||||||
|
cx.theme().colors().text_disabled
|
||||||
|
} else {
|
||||||
|
cx.theme().colors().text
|
||||||
|
},
|
||||||
|
font_family: settings.ui_font.family.clone(),
|
||||||
|
font_features: settings.ui_font.features,
|
||||||
|
font_size: rems(0.875).into(),
|
||||||
|
font_weight: FontWeight::NORMAL,
|
||||||
|
font_style: FontStyle::Normal,
|
||||||
|
line_height: relative(1.3).into(),
|
||||||
|
background_color: None,
|
||||||
|
underline: None,
|
||||||
|
strikethrough: None,
|
||||||
|
white_space: WhiteSpace::Normal,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorElement::new(
|
||||||
|
&editor,
|
||||||
|
EditorStyle {
|
||||||
|
background: cx.theme().colors().editor_background,
|
||||||
|
local_player: cx.theme().players().local(),
|
||||||
|
text: text_style,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_query_change(
|
||||||
|
&mut self,
|
||||||
|
_: View<Editor>,
|
||||||
|
event: &editor::EditorEvent,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if let editor::EditorEvent::Edited = event {
|
||||||
|
self.query_contains_error = false;
|
||||||
|
self.extension_fetch_task = Some(cx.spawn(|this, mut cx| async move {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(250))
|
||||||
|
.await;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.fetch_extensions(this.search_query(cx).as_deref(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_query(&self, cx: &WindowContext) -> Option<String> {
|
||||||
|
let search = self.query_editor.read(cx).text(cx);
|
||||||
|
if search.trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(search)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ItemEvent> for ExtensionsPage {}
|
||||||
|
|
||||||
|
impl FocusableView for ExtensionsPage {
|
||||||
|
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||||
|
self.query_editor.read(cx).focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for ExtensionsPage {
|
||||||
|
type Event = ItemEvent;
|
||||||
|
|
||||||
|
fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
|
||||||
|
Label::new("Extensions")
|
||||||
|
.color(if selected {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
Some("extensions page")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_toolbar(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_on_split(
|
||||||
|
&self,
|
||||||
|
_workspace_id: WorkspaceId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<View<Self>> {
|
||||||
|
Some(cx.new_view(|_| ExtensionsPage {
|
||||||
|
fs: self.fs.clone(),
|
||||||
|
workspace: self.workspace.clone(),
|
||||||
|
list: UniformListScrollHandle::new(),
|
||||||
|
telemetry: self.telemetry.clone(),
|
||||||
|
extensions_entries: Default::default(),
|
||||||
|
query_editor: self.query_editor.clone(),
|
||||||
|
query_contains_error: false,
|
||||||
|
extension_fetch_task: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||||
|
f(*event)
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ assets.workspace = true
|
|||||||
assistant.workspace = true
|
assistant.workspace = true
|
||||||
async-compression.workspace = true
|
async-compression.workspace = true
|
||||||
async-recursion = "0.3"
|
async-recursion = "0.3"
|
||||||
async-tar = "0.4.2"
|
async-tar.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
audio.workspace = true
|
audio.workspace = true
|
||||||
auto_update.workspace = true
|
auto_update.workspace = true
|
||||||
@ -45,6 +45,7 @@ diagnostics.workspace = true
|
|||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
extension.workspace = true
|
extension.workspace = true
|
||||||
|
extensions_ui.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
feedback.workspace = true
|
feedback.workspace = true
|
||||||
file_finder.workspace = true
|
file_finder.workspace = true
|
||||||
|
@ -21,6 +21,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
|
|||||||
MenuItem::action("Select Theme", theme_selector::Toggle),
|
MenuItem::action("Select Theme", theme_selector::Toggle),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
MenuItem::action("Extensions", extensions_ui::Extensions),
|
||||||
MenuItem::action("Install CLI", install_cli::Install),
|
MenuItem::action("Install CLI", install_cli::Install),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::action("Hide Zed", super::Hide),
|
MenuItem::action("Hide Zed", super::Hide),
|
||||||
|
@ -173,7 +173,13 @@ fn main() {
|
|||||||
);
|
);
|
||||||
assistant::init(cx);
|
assistant::init(cx);
|
||||||
|
|
||||||
extension::init(fs.clone(), languages.clone(), ThemeRegistry::global(cx), cx);
|
extension::init(
|
||||||
|
fs.clone(),
|
||||||
|
http.clone(),
|
||||||
|
languages.clone(),
|
||||||
|
ThemeRegistry::global(cx),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
load_user_themes_in_background(fs.clone(), cx);
|
load_user_themes_in_background(fs.clone(), cx);
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@ -254,6 +260,7 @@ fn main() {
|
|||||||
feedback::init(cx);
|
feedback::init(cx);
|
||||||
markdown_preview::init(cx);
|
markdown_preview::init(cx);
|
||||||
welcome::init(cx);
|
welcome::init(cx);
|
||||||
|
extensions_ui::init(cx);
|
||||||
|
|
||||||
cx.set_menus(app_menus());
|
cx.set_menus(app_menus());
|
||||||
initialize_workspace(app_state.clone(), cx);
|
initialize_workspace(app_state.clone(), cx);
|
||||||
|
@ -2396,6 +2396,7 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let app_state = AppState::test(cx);
|
let app_state = AppState::test(cx);
|
||||||
@ -2409,6 +2410,7 @@ mod tests {
|
|||||||
app_state
|
app_state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
|
async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
|
||||||
let executor = cx.executor();
|
let executor = cx.executor();
|
||||||
|
Loading…
Reference in New Issue
Block a user