extensions_ui: Move logic to render extension card details into ExtensionCard component (#61850)
This PR reworks the extension card component to take care of most the rendering. It no longer implements `ParentElement` and instead takes care of rendering the details of the card now. This also allows us to finally render the list of features for dev extensions amongst some other cleanup whilst working with these. ## Why this change? This is in preparation for allowing to show extensions locally when the fetch fails based on the information available. Currently, this would require a lot of duplication and would be tedious to add. With this in, we can much more easily just construct an extension card with the upstream metadata missing, removing the need for duplication. Lastly, this also adds a component preview for extensions cards so we can play around more easily with other designs. Release Notes: - Installed dev extensions will now show the features they provide and will also respect feature filters.
This commit is contained in:
1 parent
83b8578aa5
commit
0cf4dacd26
6 files changed
+777
-618
No files matched your search
Generated
+1
-1
@@ -6432,6 +6432,7 @@ dependencies = [
|
||||
"cloud_api_types",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"db",
|
||||
"editor",
|
||||
"extension",
|
||||
@@ -6449,7 +6450,6 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"strum 0.27.2",
|
||||
"telemetry",
|
||||
"theme_settings",
|
||||
|
||||
@@ -52,6 +52,17 @@ pub enum ExtensionProvides {
|
||||
Snippets,
|
||||
}
|
||||
|
||||
impl ExtensionProvides {
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ExtensionProvides::AgentServers
|
||||
| ExtensionProvides::SlashCommands
|
||||
| ExtensionProvides::IndexedDocsProviders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct ExtensionMetadata {
|
||||
pub id: Arc<str>,
|
||||
|
||||
@@ -15,6 +15,7 @@ path = "src/extensions_ui.rs"
|
||||
anyhow.workspace = true
|
||||
cloud_api_types.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
@@ -33,7 +34,6 @@ semver.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme_settings.workspace = true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod extension_card;
|
||||
|
||||
pub use extension_card::*;
|
||||
pub use extension_card::ExtensionCard;
|
||||
pub(crate) use extension_card::{extension_provides_label, remote_extension_status};
|
||||
@@ -1,35 +1,618 @@
|
||||
use gpui::{AnyElement, prelude::*};
|
||||
use smallvec::SmallVec;
|
||||
use ui::prelude::*;
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ExtensionCard {
|
||||
overridden_by_dev_extension: bool,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
use cloud_api_types::{ExtensionApiManifest, ExtensionMetadata, ExtensionProvides};
|
||||
use extension::{ExtensionManifest, SchemaVersion};
|
||||
use extension_host::{ExtensionOperation, ExtensionStore};
|
||||
use gpui::{Anchor, ElementId, Entity, Point, SharedString, prelude::*};
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
use release_channel::ReleaseChannel;
|
||||
use ui::{Chip, ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
|
||||
type ContextMenuBuilder = Box<
|
||||
dyn Fn(Arc<str>, SharedString, &mut Window, &mut App) -> Option<Entity<ContextMenu>> + 'static,
|
||||
>;
|
||||
type ExtensionCardActions = [Option<Button>; 3];
|
||||
|
||||
fn extension_status(extension_id: &str, extension_store: &ExtensionStore) -> ExtensionStatus {
|
||||
match extension_store.outstanding_operations().get(extension_id) {
|
||||
Some(ExtensionOperation::Install) => ExtensionStatus::Installing,
|
||||
Some(ExtensionOperation::Remove) => ExtensionStatus::Removing,
|
||||
Some(ExtensionOperation::Upgrade) => ExtensionStatus::Upgrading,
|
||||
None => match extension_store.installed_extensions().get(extension_id) {
|
||||
Some(extension) => ExtensionStatus::Installed(extension.manifest.version.clone()),
|
||||
None => ExtensionStatus::NotInstalled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionCard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
overridden_by_dev_extension: false,
|
||||
children: SmallVec::new(),
|
||||
pub(crate) fn remote_extension_status(extension_id: &str, cx: &App) -> ExtensionStatus {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
if extension_store
|
||||
.installed_extensions()
|
||||
.get(extension_id)
|
||||
.is_some_and(|extension| extension.dev)
|
||||
{
|
||||
ExtensionStatus::OverriddenByDevExtension
|
||||
} else {
|
||||
extension_status(extension_id, extension_store)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ExtensionStatus {
|
||||
NotInstalled,
|
||||
Installing,
|
||||
Upgrading,
|
||||
Installed(Arc<str>),
|
||||
Removing,
|
||||
OverriddenByDevExtension,
|
||||
}
|
||||
|
||||
impl ExtensionStatus {
|
||||
pub fn disables_actions(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Installing | Self::Upgrading | Self::Removing | Self::OverriddenByDevExtension
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_installed(&self) -> bool {
|
||||
matches!(self, Self::Installed(_) | Self::Upgrading | Self::Removing)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtensionCardDetails {
|
||||
id: Arc<str>,
|
||||
name: SharedString,
|
||||
version: Arc<str>,
|
||||
description: Option<SharedString>,
|
||||
authors: SharedString,
|
||||
repository_url: Option<SharedString>,
|
||||
provided_features: Vec<&'static str>,
|
||||
source: ExtensionCardSource,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ExtensionCardSource {
|
||||
Dev,
|
||||
Remote {
|
||||
status: ExtensionStatus,
|
||||
download_count: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExtensionCardSource {
|
||||
fn installed_version(&self, latest_version: &Arc<str>) -> Option<Arc<str>> {
|
||||
match self {
|
||||
Self::Remote {
|
||||
status: ExtensionStatus::Installed(installed_version),
|
||||
..
|
||||
} if installed_version != latest_version => Some(installed_version.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overridden_by_dev_extension(mut self, overridden: bool) -> Self {
|
||||
self.overridden_by_dev_extension = overridden;
|
||||
fn download_count(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Remote { download_count, .. } => Some(*download_count),
|
||||
Self::Dev => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dev(&self) -> bool {
|
||||
matches!(self, Self::Dev)
|
||||
}
|
||||
|
||||
fn is_overridden(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Remote {
|
||||
status: ExtensionStatus::OverriddenByDevExtension,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct ExtensionCard {
|
||||
details: ExtensionCardDetails,
|
||||
actions: ExtensionCardActions,
|
||||
context_menu: Option<ContextMenuBuilder>,
|
||||
}
|
||||
|
||||
impl ExtensionCard {
|
||||
pub fn for_dev(extension: Arc<ExtensionManifest>, cx: &App) -> Self {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
let status = extension_status(&extension.id, extension_store);
|
||||
Self::dev::<true>(extension, status)
|
||||
}
|
||||
|
||||
pub fn for_remote(extension: &ExtensionMetadata, cx: &App) -> Self {
|
||||
let status = remote_extension_status(&extension.id, cx);
|
||||
Self::remote::<true>(extension, status, cx)
|
||||
}
|
||||
|
||||
fn dev<const ENABLE_HANDLERS: bool>(
|
||||
extension: Arc<ExtensionManifest>,
|
||||
status: ExtensionStatus,
|
||||
) -> Self {
|
||||
let actions = Self::actions_for_dev_extension::<ENABLE_HANDLERS>(&extension, &status);
|
||||
let details = ExtensionCardDetails {
|
||||
id: extension.id.clone(),
|
||||
name: extension.name.clone().into(),
|
||||
version: extension.version.clone(),
|
||||
description: extension.description.clone().map(Into::into),
|
||||
authors: extension.authors.join(", ").into(),
|
||||
repository_url: extension.repository.clone().map(Into::into),
|
||||
provided_features: provided_feature_labels(extension.provides()),
|
||||
source: ExtensionCardSource::Dev,
|
||||
};
|
||||
|
||||
Self {
|
||||
details,
|
||||
actions,
|
||||
context_menu: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote<const ENABLE_HANDLERS: bool>(
|
||||
extension: &ExtensionMetadata,
|
||||
status: ExtensionStatus,
|
||||
cx: &App,
|
||||
) -> Self {
|
||||
let actions = Self::actions_for_remote_extension::<ENABLE_HANDLERS>(extension, &status, cx);
|
||||
let details = ExtensionCardDetails {
|
||||
id: extension.id.clone(),
|
||||
name: extension.manifest.name.clone().into(),
|
||||
version: extension.manifest.version.clone(),
|
||||
description: extension.manifest.description.clone().map(Into::into),
|
||||
authors: extension.manifest.authors.join(", ").into(),
|
||||
repository_url: Some(extension.manifest.repository.clone().into()),
|
||||
provided_features: provided_feature_labels(extension.manifest.provides.iter().copied()),
|
||||
source: ExtensionCardSource::Remote {
|
||||
status,
|
||||
download_count: extension.download_count,
|
||||
},
|
||||
};
|
||||
|
||||
Self {
|
||||
details,
|
||||
actions,
|
||||
context_menu: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn button_id(extension_id: &Arc<str>, operation: ExtensionOperation) -> ElementId {
|
||||
(SharedString::from(extension_id.clone()), operation as usize).into()
|
||||
}
|
||||
|
||||
fn uninstall_button<const ENABLE_HANDLERS: bool>(
|
||||
extension_id: &Arc<str>,
|
||||
is_dev: bool,
|
||||
) -> Button {
|
||||
Button::new(
|
||||
Self::button_id(extension_id, ExtensionOperation::Remove),
|
||||
"Uninstall",
|
||||
)
|
||||
.when(ENABLE_HANDLERS, |button| {
|
||||
button.on_click({
|
||||
let extension_id = extension_id.clone();
|
||||
move |_, _, cx| {
|
||||
if !is_dev {
|
||||
telemetry::event!("Extension Uninstalled", extension_id);
|
||||
}
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store
|
||||
.uninstall_extension(extension_id.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn configure_button<const ENABLE_HANDLERS: bool>(
|
||||
extension_id: &Arc<str>,
|
||||
manifest: Option<Arc<ExtensionManifest>>,
|
||||
) -> Button {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{extension_id}")),
|
||||
"Configure",
|
||||
)
|
||||
.when(ENABLE_HANDLERS, |button| {
|
||||
button.on_click({
|
||||
let extension_id = extension_id.clone();
|
||||
move |_, _, cx| {
|
||||
let manifest = manifest.clone().or_else(|| {
|
||||
ExtensionStore::global(cx)
|
||||
.read(cx)
|
||||
.extension_manifest_for_id(&extension_id)
|
||||
.cloned()
|
||||
});
|
||||
if let Some(manifest) = manifest
|
||||
&& let Some(events) = extension::ExtensionEvents::try_global(cx)
|
||||
{
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(extension::Event::ConfigureExtensionRequested(manifest), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn actions_for_dev_extension<const ENABLE_HANDLERS: bool>(
|
||||
extension: &Arc<ExtensionManifest>,
|
||||
status: &ExtensionStatus,
|
||||
) -> ExtensionCardActions {
|
||||
let rebuild = Button::new(
|
||||
SharedString::from(format!("rebuild-{}", extension.id)),
|
||||
"Rebuild",
|
||||
)
|
||||
.color(Color::Accent)
|
||||
.disabled(status.disables_actions())
|
||||
.when(ENABLE_HANDLERS, |button| {
|
||||
button.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.rebuild_dev_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
let uninstall = Self::uninstall_button::<ENABLE_HANDLERS>(&extension.id, true)
|
||||
.color(Color::Accent)
|
||||
.disabled(status.disables_actions());
|
||||
let configure = (!extension.context_servers.is_empty()).then(|| {
|
||||
Self::configure_button::<ENABLE_HANDLERS>(&extension.id, Some(extension.clone()))
|
||||
.color(Color::Accent)
|
||||
.disabled(status.disables_actions())
|
||||
});
|
||||
|
||||
[Some(rebuild), Some(uninstall), configure]
|
||||
}
|
||||
|
||||
fn install_button<const ENABLE_HANDLERS: bool>(extension_id: &Arc<str>) -> Button {
|
||||
Button::new(
|
||||
Self::button_id(extension_id, ExtensionOperation::Install),
|
||||
"Install",
|
||||
)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.start_icon(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.when(ENABLE_HANDLERS, |button| {
|
||||
button.on_click({
|
||||
let extension_id = extension_id.clone();
|
||||
move |_, _, cx| {
|
||||
telemetry::event!("Extension Installed");
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.install_latest_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn actions_for_remote_extension<const ENABLE_HANDLERS: bool>(
|
||||
extension: &ExtensionMetadata,
|
||||
status: &ExtensionStatus,
|
||||
cx: &App,
|
||||
) -> ExtensionCardActions {
|
||||
let is_configurable = extension
|
||||
.manifest
|
||||
.provides
|
||||
.contains(&ExtensionProvides::ContextServers);
|
||||
|
||||
match status {
|
||||
ExtensionStatus::OverriddenByDevExtension
|
||||
| ExtensionStatus::NotInstalled
|
||||
| ExtensionStatus::Installing => [
|
||||
None,
|
||||
None,
|
||||
Some(
|
||||
Self::install_button::<ENABLE_HANDLERS>(&extension.id)
|
||||
.disabled(status.disables_actions()),
|
||||
),
|
||||
],
|
||||
ExtensionStatus::Upgrading | ExtensionStatus::Removing => {
|
||||
let uninstall = Self::uninstall_button::<ENABLE_HANDLERS>(&extension.id, false)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.disabled(status.disables_actions());
|
||||
let upgrade = matches!(status, ExtensionStatus::Upgrading).then(|| {
|
||||
Button::new(
|
||||
Self::button_id(&extension.id, ExtensionOperation::Upgrade),
|
||||
"Upgrade",
|
||||
)
|
||||
.disabled(status.disables_actions())
|
||||
});
|
||||
let configure = is_configurable.then(|| {
|
||||
Self::configure_button::<ENABLE_HANDLERS>(&extension.id, None)
|
||||
.disabled(status.disables_actions())
|
||||
});
|
||||
|
||||
[upgrade, configure, Some(uninstall)]
|
||||
}
|
||||
ExtensionStatus::Installed(installed_version) => {
|
||||
let uninstall = Self::uninstall_button::<ENABLE_HANDLERS>(&extension.id, false)
|
||||
.style(ButtonStyle::OutlinedGhost);
|
||||
let upgrade = (installed_version != &extension.manifest.version).then(|| {
|
||||
let is_compatible = extension_host::is_version_compatible(
|
||||
ReleaseChannel::global(cx),
|
||||
extension,
|
||||
);
|
||||
Button::new(
|
||||
Self::button_id(&extension.id, ExtensionOperation::Upgrade),
|
||||
"Upgrade",
|
||||
)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.when(!is_compatible, |button| {
|
||||
button.tooltip({
|
||||
let version = extension.manifest.version.clone();
|
||||
move |_, cx| {
|
||||
Tooltip::simple(
|
||||
format!(
|
||||
"v{version} is not compatible with this version of Zed."
|
||||
),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
.disabled(!is_compatible)
|
||||
.when(ENABLE_HANDLERS, |button| {
|
||||
button.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
let version = extension.manifest.version.clone();
|
||||
move |_, _, cx| {
|
||||
telemetry::event!("Extension Installed", extension_id, version);
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store
|
||||
.upgrade_extension(
|
||||
extension_id.clone(),
|
||||
version.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
let configure = is_configurable.then(|| {
|
||||
Self::configure_button::<ENABLE_HANDLERS>(&extension.id, None)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
});
|
||||
|
||||
[upgrade, configure, Some(uninstall)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context_menu(
|
||||
mut self,
|
||||
builder: impl Fn(Arc<str>, SharedString, &mut Window, &mut App) -> Option<Entity<ContextMenu>>
|
||||
+ 'static,
|
||||
) -> Self {
|
||||
self.context_menu = Some(Box::new(builder));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ExtensionCard {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
fn provided_feature_labels(
|
||||
provides: impl IntoIterator<Item = ExtensionProvides>,
|
||||
) -> Vec<&'static str> {
|
||||
provides
|
||||
.into_iter()
|
||||
.filter(|provides| !provides.is_deprecated())
|
||||
.map(extension_provides_label)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn extension_provides_label(provides: ExtensionProvides) -> &'static str {
|
||||
match provides {
|
||||
ExtensionProvides::Themes => "Themes",
|
||||
ExtensionProvides::IconThemes => "Icon Themes",
|
||||
ExtensionProvides::Languages => "Languages",
|
||||
ExtensionProvides::Grammars => "Grammars",
|
||||
ExtensionProvides::LanguageServers => "Language Servers",
|
||||
ExtensionProvides::ContextServers => "MCP Servers",
|
||||
ExtensionProvides::AgentServers => "Agent Servers",
|
||||
ExtensionProvides::SlashCommands => "Slash Commands",
|
||||
ExtensionProvides::IndexedDocsProviders => "Indexed Docs Providers",
|
||||
ExtensionProvides::Snippets => "Snippets",
|
||||
ExtensionProvides::DebugAdapters => "Debug Adapters",
|
||||
}
|
||||
}
|
||||
|
||||
fn preview_dev_card(extension: Arc<ExtensionManifest>, status: ExtensionStatus) -> ExtensionCard {
|
||||
ExtensionCard::dev::<false>(extension, status)
|
||||
}
|
||||
|
||||
fn preview_remote_card(
|
||||
extension: &ExtensionMetadata,
|
||||
status: ExtensionStatus,
|
||||
cx: &App,
|
||||
) -> ExtensionCard {
|
||||
ExtensionCard::remote::<false>(extension, status, cx)
|
||||
}
|
||||
|
||||
impl Component for ExtensionCard {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::DataDisplay
|
||||
}
|
||||
|
||||
fn description() -> &'static str {
|
||||
"A card that displays an extension's details, installation state, and available actions."
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
fn remote_extension(
|
||||
id: &'static str,
|
||||
name: &'static str,
|
||||
version: &'static str,
|
||||
description: &'static str,
|
||||
download_count: u64,
|
||||
provides: impl IntoIterator<Item = ExtensionProvides>,
|
||||
) -> ExtensionMetadata {
|
||||
ExtensionMetadata {
|
||||
id: id.into(),
|
||||
manifest: ExtensionApiManifest {
|
||||
name: name.to_owned(),
|
||||
version: version.into(),
|
||||
description: Some(description.to_owned()),
|
||||
authors: vec!["Zed Industries".to_owned()],
|
||||
repository: "https://github.com/zed-industries/zed".to_owned(),
|
||||
schema_version: Some(1),
|
||||
wasm_api_version: None,
|
||||
provides: BTreeSet::from_iter(provides),
|
||||
},
|
||||
published_at: Default::default(),
|
||||
download_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn dev_extension() -> Arc<ExtensionManifest> {
|
||||
Arc::new(ExtensionManifest {
|
||||
id: "preview-dev-theme".into(),
|
||||
name: "Local Theme".to_owned(),
|
||||
version: "0.1.0".into(),
|
||||
schema_version: SchemaVersion::ZERO,
|
||||
description: Some("A locally installed extension under development.".to_owned()),
|
||||
repository: Some("https://github.com/zed-industries/zed".to_owned()),
|
||||
authors: vec!["Extension Developer".to_owned()],
|
||||
lib: Default::default(),
|
||||
themes: Vec::new(),
|
||||
icon_themes: Vec::new(),
|
||||
languages: Vec::new(),
|
||||
grammars: Default::default(),
|
||||
language_servers: Default::default(),
|
||||
context_servers: Default::default(),
|
||||
slash_commands: Default::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
let examples = vec![
|
||||
single_example(
|
||||
"Available to Install",
|
||||
preview_remote_card(
|
||||
&remote_extension(
|
||||
"preview-toml",
|
||||
"TOML",
|
||||
"0.6.2",
|
||||
"TOML language support.",
|
||||
482_391,
|
||||
[ExtensionProvides::Languages],
|
||||
),
|
||||
ExtensionStatus::NotInstalled,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Installed",
|
||||
preview_remote_card(
|
||||
&remote_extension(
|
||||
"preview-python",
|
||||
"Python",
|
||||
"0.5.1",
|
||||
"Python language support powered by basedpyright.",
|
||||
1_284_613,
|
||||
[
|
||||
ExtensionProvides::Languages,
|
||||
ExtensionProvides::LanguageServers,
|
||||
ExtensionProvides::ContextServers,
|
||||
],
|
||||
),
|
||||
ExtensionStatus::Installed("0.5.1".into()),
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Update Available",
|
||||
preview_remote_card(
|
||||
&remote_extension(
|
||||
"preview-rust",
|
||||
"Rust",
|
||||
"0.4.0",
|
||||
"Rust language support powered by rust-analyzer.",
|
||||
2_947_028,
|
||||
[
|
||||
ExtensionProvides::Languages,
|
||||
ExtensionProvides::LanguageServers,
|
||||
],
|
||||
),
|
||||
ExtensionStatus::Installed("0.3.1".into()),
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Development Extension",
|
||||
preview_dev_card(dev_extension(), ExtensionStatus::Installed("0.1.0".into()))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Overridden by Development Extension",
|
||||
preview_remote_card(
|
||||
&remote_extension(
|
||||
"preview-overridden-theme",
|
||||
"Local Theme",
|
||||
"1.3.0",
|
||||
"The published version of a locally developed extension.",
|
||||
36_512,
|
||||
[ExtensionProvides::Themes],
|
||||
),
|
||||
ExtensionStatus::OverriddenByDevExtension,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
div()
|
||||
.w_128()
|
||||
.child(example_group(examples).vertical())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ExtensionCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let Self {
|
||||
details,
|
||||
actions,
|
||||
context_menu,
|
||||
} = self;
|
||||
let ExtensionCardDetails {
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
authors,
|
||||
repository_url,
|
||||
provided_features,
|
||||
source,
|
||||
} = details;
|
||||
let installed_version = source.installed_version(&version);
|
||||
let download_count = source.download_count();
|
||||
let is_dev = source.is_dev();
|
||||
let is_overridden = source.is_overridden();
|
||||
let repository_button_id = SharedString::from(format!("repository-{id}"));
|
||||
let context_menu_button_id = SharedString::from(format!("more-{id}"));
|
||||
let context_menu_extension_id = id;
|
||||
let context_menu_authors = authors.clone();
|
||||
|
||||
div().w_full().child(
|
||||
v_flex()
|
||||
.mt_4()
|
||||
@@ -41,8 +624,126 @@ impl RenderOnce for ExtensionCard {
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.children(self.children)
|
||||
.when(self.overridden_by_dev_extension, |card| {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink_1()
|
||||
.gap_2()
|
||||
.child(Headline::new(name).size(HeadlineSize::Small))
|
||||
.child(
|
||||
Headline::new(if is_dev {
|
||||
format!("v{version} (dev)")
|
||||
} else {
|
||||
format!("v{version}")
|
||||
})
|
||||
.size(HeadlineSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(installed_version.map(|installed_version| {
|
||||
Headline::new(format!("(v{installed_version} installed)"))
|
||||
.size(HeadlineSize::XSmall)
|
||||
}))
|
||||
.when(!provided_features.is_empty(), |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(provided_features.into_iter().map(Chip::new)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(h_flex().gap_1().children(actions.into_iter().flatten())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.children(description.map(|description| {
|
||||
Label::new(description)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.truncate()
|
||||
}))
|
||||
.children(download_count.map(|download_count| {
|
||||
Label::new(format!(
|
||||
"Downloads: {}",
|
||||
download_count.to_formatted_string(&Locale::en)
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Person)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(authors)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_shrink_0()
|
||||
.when_some(repository_url, |this, repository_url| {
|
||||
let repository_url_for_tooltip = repository_url.clone();
|
||||
this.child(
|
||||
IconButton::new(repository_button_id, IconName::Github)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Visit Extension Repository",
|
||||
None,
|
||||
repository_url_for_tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.open_url(&repository_url);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(context_menu, |this, context_menu| {
|
||||
this.child(
|
||||
PopoverMenu::new(context_menu_button_id.clone())
|
||||
.trigger(
|
||||
IconButton::new(
|
||||
context_menu_button_id,
|
||||
IconName::Ellipsis,
|
||||
)
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.anchor(Anchor::TopRight)
|
||||
.offset(Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
})
|
||||
.menu(move |window, cx| {
|
||||
context_menu(
|
||||
context_menu_extension_id.clone(),
|
||||
context_menu_authors.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when(is_overridden, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
|
||||
@@ -11,26 +11,26 @@ use cloud_api_types::{ExtensionMetadata, ExtensionProvides};
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use extension_host::{ExtensionManifest, ExtensionStore};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, Anchor, App, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, Focusable,
|
||||
InteractiveElement, KeyContext, ParentElement, Point, Render, Styled, Task, TaskExt, TextStyle,
|
||||
Action, App, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, Focusable,
|
||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TaskExt, TextStyle,
|
||||
UniformListScrollHandle, WeakEntity, Window, actions, point, uniform_list,
|
||||
};
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::DirectoryLister;
|
||||
use release_channel::ReleaseChannel;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, SettingsContent};
|
||||
use strum::IntoEnumIterator as _;
|
||||
use theme_settings::ThemeSettings;
|
||||
use ui::{
|
||||
Banner, Chip, ContextMenu, Divider, ListItem, ListItemSpacing, PopoverMenu, ScrollableHandle,
|
||||
Switch, ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple,
|
||||
Tooltip, WithScrollbar, prelude::*,
|
||||
Banner, ContextMenu, Divider, ListItem, ListItemSpacing, ScrollableHandle, Switch,
|
||||
ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple,
|
||||
WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
@@ -41,7 +41,7 @@ use workspace::{
|
||||
};
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
use crate::components::ExtensionCard;
|
||||
use crate::components::{ExtensionCard, extension_provides_label, remote_extension_status};
|
||||
use crate::extension_version_selector::{
|
||||
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
|
||||
};
|
||||
@@ -291,31 +291,6 @@ pub fn init(cx: &mut App) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn extension_provides_label(provides: ExtensionProvides) -> &'static str {
|
||||
match provides {
|
||||
ExtensionProvides::Themes => "Themes",
|
||||
ExtensionProvides::IconThemes => "Icon Themes",
|
||||
ExtensionProvides::Languages => "Languages",
|
||||
ExtensionProvides::Grammars => "Grammars",
|
||||
ExtensionProvides::LanguageServers => "Language Servers",
|
||||
ExtensionProvides::ContextServers => "MCP Servers",
|
||||
ExtensionProvides::AgentServers => "Agent Servers",
|
||||
ExtensionProvides::SlashCommands => "Slash Commands",
|
||||
ExtensionProvides::IndexedDocsProviders => "Indexed Docs Providers",
|
||||
ExtensionProvides::Snippets => "Snippets",
|
||||
ExtensionProvides::DebugAdapters => "Debug Adapters",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ExtensionStatus {
|
||||
NotInstalled,
|
||||
Installing,
|
||||
Upgrading,
|
||||
Installed(Arc<str>),
|
||||
Removing,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ExtensionFilter {
|
||||
All,
|
||||
@@ -401,16 +376,6 @@ fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
|
||||
})
|
||||
}
|
||||
|
||||
fn extension_button_id(extension_id: &Arc<str>, operation: ExtensionOperation) -> ElementId {
|
||||
(SharedString::from(extension_id.clone()), operation as usize).into()
|
||||
}
|
||||
|
||||
struct ExtensionCardButtons {
|
||||
install_or_uninstall: Button,
|
||||
upgrade: Option<Button>,
|
||||
configure: Option<Button>,
|
||||
}
|
||||
|
||||
pub struct ExtensionsPage {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
list: UniformListScrollHandle,
|
||||
@@ -546,45 +511,22 @@ impl ExtensionsPage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a dev extension currently exists for the extension with the given ID.
|
||||
fn dev_extension_exists(extension_id: &str, cx: &mut Context<Self>) -> bool {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
|
||||
extension_store
|
||||
.dev_extensions()
|
||||
.any(|dev_extension| dev_extension.id.as_ref() == extension_id)
|
||||
}
|
||||
|
||||
fn extension_status(extension_id: &str, cx: &mut Context<Self>) -> ExtensionStatus {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
|
||||
match extension_store.outstanding_operations().get(extension_id) {
|
||||
Some(ExtensionOperation::Install) => ExtensionStatus::Installing,
|
||||
Some(ExtensionOperation::Remove) => ExtensionStatus::Removing,
|
||||
Some(ExtensionOperation::Upgrade) => ExtensionStatus::Upgrading,
|
||||
None => match extension_store.installed_extensions().get(extension_id) {
|
||||
Some(extension) => ExtensionStatus::Installed(extension.manifest.version.clone()),
|
||||
None => ExtensionStatus::NotInstalled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_extension_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.filtered_remote_extension_indices.clear();
|
||||
self.filtered_remote_extension_indices.extend(
|
||||
self.remote_extension_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, extension)| match self.filter {
|
||||
ExtensionFilter::All => true,
|
||||
ExtensionFilter::Installed => {
|
||||
let status = Self::extension_status(&extension.id, cx);
|
||||
matches!(status, ExtensionStatus::Installed(_))
|
||||
.filter(|(_, extension)| {
|
||||
if self.filter == ExtensionFilter::All {
|
||||
return true;
|
||||
}
|
||||
ExtensionFilter::NotInstalled => {
|
||||
let status = Self::extension_status(&extension.id, cx);
|
||||
|
||||
matches!(status, ExtensionStatus::NotInstalled)
|
||||
let status = remote_extension_status(&extension.id, cx);
|
||||
match self.filter {
|
||||
ExtensionFilter::Installed => status.is_installed(),
|
||||
ExtensionFilter::NotInstalled => !status.is_installed(),
|
||||
ExtensionFilter::All => true,
|
||||
}
|
||||
})
|
||||
.filter(|(_, extension)| match self.provides_filter {
|
||||
@@ -723,7 +665,7 @@ impl ExtensionsPage {
|
||||
if ix < dev_extension_entries_len {
|
||||
let dev_ix = self.filtered_dev_extension_indices[ix];
|
||||
let extension = &self.dev_extension_entries[dev_ix];
|
||||
self.render_dev_extension(extension, cx)
|
||||
ExtensionCard::for_dev(extension.clone(), cx)
|
||||
} else {
|
||||
let extension_ix =
|
||||
self.filtered_remote_extension_indices[ix - dev_extension_entries_len];
|
||||
@@ -734,347 +676,40 @@ impl ExtensionsPage {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_dev_extension(
|
||||
&self,
|
||||
extension: &ExtensionManifest,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ExtensionCard {
|
||||
let status = Self::extension_status(&extension.id, cx);
|
||||
|
||||
let repository_url = extension.repository.clone();
|
||||
|
||||
let can_configure = !extension.context_servers.is_empty();
|
||||
|
||||
ExtensionCard::new()
|
||||
.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_1()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("rebuild-{}", extension.id)),
|
||||
"Rebuild",
|
||||
)
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Upgrading))
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.rebuild_dev_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new(extension_button_id(&extension.id, ExtensionOperation::Remove), "Uninstall")
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Removing))
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.uninstall_extension(extension_id.clone(), cx).detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(can_configure, |this| {
|
||||
this.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
)
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Installing))
|
||||
.on_click({
|
||||
let manifest = Arc::new(extension.clone());
|
||||
move |_, _, cx| {
|
||||
if let Some(events) =
|
||||
extension::ExtensionEvents::try_global(cx)
|
||||
{
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(
|
||||
extension::Event::ConfigureExtensionRequested(
|
||||
manifest.clone(),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{}: {}",
|
||||
if extension.authors.len() > 1 {
|
||||
"Authors"
|
||||
} else {
|
||||
"Author"
|
||||
},
|
||||
extension.authors.join(", ")
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
)
|
||||
.child(Label::new("<>").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.children(extension.description.as_ref().map(|description| {
|
||||
Label::new(description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.truncate()
|
||||
}))
|
||||
.children(repository_url.map(|repository_url| {
|
||||
IconButton::new(
|
||||
SharedString::from(format!("repository-{}", extension.id)),
|
||||
IconName::Github,
|
||||
)
|
||||
.icon_color(Color::Accent)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let repository_url = repository_url.clone();
|
||||
move |_, _, _, cx| {
|
||||
cx.open_url(&repository_url);
|
||||
}
|
||||
}))
|
||||
.tooltip(Tooltip::text(repository_url))
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_remote_extension(
|
||||
&self,
|
||||
extension: &ExtensionMetadata,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ExtensionCard {
|
||||
let card = ExtensionCard::for_remote(extension, cx);
|
||||
let this = cx.weak_entity();
|
||||
let status = Self::extension_status(&extension.id, cx);
|
||||
let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
|
||||
|
||||
let extension_id = extension.id.clone();
|
||||
let buttons = self.buttons_for_entry(extension, &status, has_dev_extension, cx);
|
||||
let version = extension.manifest.version.clone();
|
||||
let repository_url = extension.manifest.repository.clone();
|
||||
let authors = extension.manifest.authors.clone();
|
||||
|
||||
let installed_version = match status {
|
||||
ExtensionStatus::Installed(installed_version) => Some(installed_version),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ExtensionCard::new()
|
||||
.overridden_by_dev_extension(has_dev_extension)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Headline::new(extension.manifest.name.clone())
|
||||
.size(HeadlineSize::Small),
|
||||
)
|
||||
.child(Headline::new(format!("v{version}")).size(HeadlineSize::XSmall))
|
||||
.children(
|
||||
installed_version
|
||||
.filter(|installed_version| *installed_version != version)
|
||||
.map(|installed_version| {
|
||||
Headline::new(format!("(v{installed_version} installed)",))
|
||||
.size(HeadlineSize::XSmall)
|
||||
}),
|
||||
)
|
||||
.map(|parent| {
|
||||
if extension.manifest.provides.is_empty() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(
|
||||
h_flex().gap_1().children(
|
||||
extension
|
||||
.manifest
|
||||
.provides
|
||||
.iter()
|
||||
.filter_map(|provides| {
|
||||
match provides {
|
||||
ExtensionProvides::AgentServers
|
||||
| ExtensionProvides::SlashCommands
|
||||
| ExtensionProvides::IndexedDocsProviders => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(Chip::new(extension_provides_label(*provides)))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
}),
|
||||
card.context_menu(move |extension_id, authors, window, cx| {
|
||||
let this = this.upgrade()?;
|
||||
Some(ContextMenu::build(window, cx, |context_menu, window, _| {
|
||||
context_menu
|
||||
.entry(
|
||||
"Install Another Version...",
|
||||
None,
|
||||
window.handler_for(&this, {
|
||||
let extension_id = extension_id.clone();
|
||||
move |this, window, cx| {
|
||||
this.show_extension_version_list(extension_id.clone(), window, cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(buttons.upgrade)
|
||||
.children(buttons.configure)
|
||||
.child(buttons.install_or_uninstall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.children(extension.manifest.description.as_ref().map(|description| {
|
||||
Label::new(description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.truncate()
|
||||
}))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"Downloads: {}",
|
||||
extension.download_count.to_formatted_string(&Locale::en)
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Person)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(extension.manifest.authors.join(", "))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_shrink_0()
|
||||
.child({
|
||||
let repo_url_for_tooltip = repository_url.clone();
|
||||
|
||||
IconButton::new(
|
||||
SharedString::from(format!("repository-{}", extension.id)),
|
||||
IconName::Github,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Visit Extension Repository",
|
||||
None,
|
||||
repo_url_for_tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(
|
||||
move |_, _, _, cx| {
|
||||
cx.open_url(&repository_url);
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(
|
||||
PopoverMenu::new(SharedString::from(format!(
|
||||
"more-{}",
|
||||
extension.id
|
||||
)))
|
||||
.trigger(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("more-{}", extension.id)),
|
||||
IconName::Ellipsis,
|
||||
)
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.anchor(Anchor::TopRight)
|
||||
.offset(Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
})
|
||||
.menu(move |window, cx| {
|
||||
this.upgrade().map(|this| {
|
||||
Self::render_remote_extension_context_menu(
|
||||
&this,
|
||||
extension_id.clone(),
|
||||
authors.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_remote_extension_context_menu(
|
||||
this: &Entity<Self>,
|
||||
extension_id: Arc<str>,
|
||||
authors: Vec<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |context_menu, window, _| {
|
||||
context_menu
|
||||
.entry(
|
||||
"Install Another Version...",
|
||||
None,
|
||||
window.handler_for(this, {
|
||||
.entry("Copy Extension ID", None, {
|
||||
let extension_id = extension_id.clone();
|
||||
move |this, window, cx| {
|
||||
this.show_extension_version_list(extension_id.clone(), window, cx)
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
extension_id.to_string(),
|
||||
));
|
||||
}
|
||||
}),
|
||||
)
|
||||
.entry("Copy Extension ID", None, {
|
||||
let extension_id = extension_id.clone();
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(extension_id.to_string()));
|
||||
}
|
||||
})
|
||||
.entry("Copy Author Info", None, {
|
||||
let authors = authors.clone();
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", ")));
|
||||
}
|
||||
})
|
||||
})
|
||||
.entry("Copy Author Info", None, move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(authors.to_string()));
|
||||
})
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1117,195 +752,6 @@ impl ExtensionsPage {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn buttons_for_entry(
|
||||
&self,
|
||||
extension: &ExtensionMetadata,
|
||||
status: &ExtensionStatus,
|
||||
has_dev_extension: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ExtensionCardButtons {
|
||||
let is_compatible =
|
||||
extension_host::is_version_compatible(ReleaseChannel::global(cx), extension);
|
||||
|
||||
if has_dev_extension {
|
||||
// If we have a dev extension for the given extension, just treat it as uninstalled.
|
||||
// The button here is a placeholder, as it won't be interactable anyways.
|
||||
return ExtensionCardButtons {
|
||||
install_or_uninstall: Button::new(
|
||||
extension_button_id(&extension.id, ExtensionOperation::Install),
|
||||
"Install",
|
||||
),
|
||||
configure: None,
|
||||
upgrade: None,
|
||||
};
|
||||
}
|
||||
|
||||
let is_configurable = extension
|
||||
.manifest
|
||||
.provides
|
||||
.contains(&ExtensionProvides::ContextServers);
|
||||
|
||||
match status.clone() {
|
||||
ExtensionStatus::NotInstalled => ExtensionCardButtons {
|
||||
install_or_uninstall: Button::new(
|
||||
extension_button_id(&extension.id, ExtensionOperation::Install),
|
||||
"Install",
|
||||
)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.start_icon(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
telemetry::event!("Extension Installed");
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.install_latest_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
configure: None,
|
||||
upgrade: None,
|
||||
},
|
||||
ExtensionStatus::Installing => ExtensionCardButtons {
|
||||
install_or_uninstall: Button::new(
|
||||
extension_button_id(&extension.id, ExtensionOperation::Install),
|
||||
"Install",
|
||||
)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.start_icon(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.disabled(true),
|
||||
configure: None,
|
||||
upgrade: None,
|
||||
},
|
||||
ExtensionStatus::Upgrading => ExtensionCardButtons {
|
||||
install_or_uninstall: Button::new(
|
||||
extension_button_id(&extension.id, ExtensionOperation::Remove),
|
||||
"Uninstall",
|
||||
)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.disabled(true),
|
||||
configure: is_configurable.then(|| {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
)
|
||||
.disabled(true)
|
||||
}),
|
||||
upgrade: Some(
|
||||
Button::new(
|
||||
extension_button_id(&extension.id, ExtensionOperation::Upgrade),
|
||||
"Upgrade",
|
||||
)
|
||||
.disabled(true),
|
||||
),
|
||||
},
|
||||
ExtensionStatus::Installed(installed_version) => ExtensionCardButtons {
|
||||
install_or_uninstall: Button::new(
|
||||
extension_button_id(&extension.id, ExtensionOperation::Remove),
|
||||
"Uninstall",
|
||||
)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
telemetry::event!("Extension Uninstalled", extension_id);
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store
|
||||
.uninstall_extension(extension_id.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
configure: is_configurable.then(|| {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
if let Some(manifest) = ExtensionStore::global(cx)
|
||||
.read(cx)
|
||||
.extension_manifest_for_id(&extension_id)
|
||||
.cloned()
|
||||
&& let Some(events) = extension::ExtensionEvents::try_global(cx)
|
||||
{
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(
|
||||
extension::Event::ConfigureExtensionRequested(manifest),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
upgrade: if installed_version == extension.manifest.version {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Button::new(extension_button_id(&extension.id, ExtensionOperation::Upgrade), "Upgrade")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.when(!is_compatible, |upgrade_button| {
|
||||
upgrade_button.disabled(true).tooltip({
|
||||
let version = extension.manifest.version.clone();
|
||||
move |_, cx| {
|
||||
Tooltip::simple(
|
||||
format!(
|
||||
"v{version} is not compatible with this version of Zed.",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
.disabled(!is_compatible)
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
let version = extension.manifest.version.clone();
|
||||
move |_, _, cx| {
|
||||
telemetry::event!("Extension Installed", extension_id, version);
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store
|
||||
.upgrade_extension(
|
||||
extension_id.clone(),
|
||||
version.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
ExtensionStatus::Removing => ExtensionCardButtons {
|
||||
install_or_uninstall: Button::new(
|
||||
extension_button_id(&extension.id, ExtensionOperation::Remove),
|
||||
"Uninstall",
|
||||
)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.disabled(true),
|
||||
configure: is_configurable.then(|| {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
)
|
||||
.disabled(true)
|
||||
}),
|
||||
upgrade: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn render_search(&self, cx: &mut Context<Self>) -> Div {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("BufferSearchBar");
|
||||
|
||||
Reference in new issue
Block a user