Add a command for building and installing a locally-developed Zed extension (#8781)
This PR adds an `zed: Install Local Extension` action, which lets you select a path to a folder containing a Zed extension, and install that . When you select a directory, the extension will be compiled (both the Tree-sitter grammars and the Rust code for the extension itself) and installed as a Zed extension, using a symlink. ### Details A few dependencies are needed to build an extension: * The Rust `wasm32-wasi` target. This is automatically installed if needed via `rustup`. * A wasi-preview1 adapter WASM module, for building WASM components with Rust. This is automatically downloaded if needed from a `wasmtime` GitHub release * For building Tree-sitter parsers, a distribution of `wasi-sdk`. This is automatically downloaded if needed from a `wasi-sdk` GitHub release. The downloaded artifacts are cached in a support directory called `Zed/extensions/build`. ### Tasks UX * [x] Show local extensions in the Extensions view * [x] Provide a button for recompiling a linked extension * [x] Make this action discoverable by adding a button for it on the Extensions view * [ ] Surface errors (don't just write them to the Zed log) Packaging * [ ] Create a separate executable that performs the extension compilation. We'll switch the packaging system in our [extensions](https://github.com/zed-industries/extensions) repo to use this binary, so that there is one canonical definition of how to build/package an extensions. ### Release Notes: - N/A --------- Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
parent
e273198ada
commit
675ae24964
18 changed files with 1662 additions and 763 deletions
|
@ -1,6 +1,10 @@
|
|||
mod components;
|
||||
|
||||
use crate::components::ExtensionCard;
|
||||
use client::telemetry::Telemetry;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::{ExtensionApiResponse, ExtensionStatus, ExtensionStore};
|
||||
use extension::{ExtensionApiResponse, ExtensionManifest, ExtensionStatus, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, canvas, uniform_list, AnyElement, AppContext, AvailableSpace, EventEmitter,
|
||||
FocusableView, FontStyle, FontWeight, InteractiveElement, KeyContext, ParentElement, Render,
|
||||
|
@ -8,24 +12,46 @@ use gpui::{
|
|||
WindowContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::ops::DerefMut;
|
||||
use std::time::Duration;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ToggleButton, Tooltip};
|
||||
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(zed, [Extensions]);
|
||||
actions!(zed, [Extensions, InstallDevExtension]);
|
||||
|
||||
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_to_active_pane(Box::new(extensions_page), cx)
|
||||
});
|
||||
workspace
|
||||
.register_action(move |workspace, _: &Extensions, cx| {
|
||||
let extensions_page = ExtensionsPage::new(workspace, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(extensions_page), cx)
|
||||
})
|
||||
.register_action(move |_, _: &InstallDevExtension, cx| {
|
||||
let store = ExtensionStore::global(cx);
|
||||
let prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
cx.deref_mut()
|
||||
.spawn(|mut cx| async move {
|
||||
let extension_path = prompt.await.log_err()??.pop()?;
|
||||
store
|
||||
.update(&mut cx, |store, cx| {
|
||||
store.install_dev_extension(extension_path, cx);
|
||||
})
|
||||
.ok()?;
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -37,15 +63,26 @@ enum ExtensionFilter {
|
|||
NotInstalled,
|
||||
}
|
||||
|
||||
impl ExtensionFilter {
|
||||
pub fn include_dev_extensions(&self) -> bool {
|
||||
match self {
|
||||
Self::All | Self::Installed => true,
|
||||
Self::NotInstalled => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtensionsPage {
|
||||
list: UniformListScrollHandle,
|
||||
telemetry: Arc<Telemetry>,
|
||||
is_fetching_extensions: bool,
|
||||
filter: ExtensionFilter,
|
||||
extension_entries: Vec<ExtensionApiResponse>,
|
||||
remote_extension_entries: Vec<ExtensionApiResponse>,
|
||||
dev_extension_entries: Vec<Arc<ExtensionManifest>>,
|
||||
filtered_remote_extension_indices: Vec<usize>,
|
||||
query_editor: View<Editor>,
|
||||
query_contains_error: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
extension_fetch_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
|
@ -53,7 +90,14 @@ impl ExtensionsPage {
|
|||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||
cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
let store = ExtensionStore::global(cx);
|
||||
let subscription = cx.observe(&store, |_, _, cx| cx.notify());
|
||||
let subscriptions = [
|
||||
cx.observe(&store, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&store, |this, _, event, cx| match event {
|
||||
extension::Event::ExtensionsUpdated => {
|
||||
this.fetch_extensions_debounced(cx);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
let query_editor = cx.new_view(|cx| {
|
||||
let mut input = Editor::single_line(cx);
|
||||
|
@ -67,10 +111,12 @@ impl ExtensionsPage {
|
|||
telemetry: workspace.client().telemetry().clone(),
|
||||
is_fetching_extensions: false,
|
||||
filter: ExtensionFilter::All,
|
||||
extension_entries: Vec::new(),
|
||||
dev_extension_entries: Vec::new(),
|
||||
filtered_remote_extension_indices: Vec::new(),
|
||||
remote_extension_entries: Vec::new(),
|
||||
query_contains_error: false,
|
||||
extension_fetch_task: None,
|
||||
_subscription: subscription,
|
||||
_subscriptions: subscriptions,
|
||||
query_editor,
|
||||
};
|
||||
this.fetch_extensions(None, cx);
|
||||
|
@ -78,250 +124,374 @@ impl ExtensionsPage {
|
|||
})
|
||||
}
|
||||
|
||||
fn filtered_extension_entries(&self, cx: &mut ViewContext<Self>) -> Vec<ExtensionApiResponse> {
|
||||
fn filter_extension_entries(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
|
||||
self.extension_entries
|
||||
.iter()
|
||||
.filter(|extension| match self.filter {
|
||||
ExtensionFilter::All => true,
|
||||
ExtensionFilter::Installed => {
|
||||
let status = extension_store.extension_status(&extension.id);
|
||||
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 = extension_store.extension_status(&extension.id);
|
||||
matches!(status, ExtensionStatus::Installed(_))
|
||||
}
|
||||
ExtensionFilter::NotInstalled => {
|
||||
let status = extension_store.extension_status(&extension.id);
|
||||
|
||||
matches!(status, ExtensionStatus::Installed(_))
|
||||
}
|
||||
ExtensionFilter::NotInstalled => {
|
||||
let status = extension_store.extension_status(&extension.id);
|
||||
|
||||
matches!(status, ExtensionStatus::NotInstalled)
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn install_extension(
|
||||
&self,
|
||||
extension_id: Arc<str>,
|
||||
version: Arc<str>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.install_extension(extension_id, version, cx)
|
||||
});
|
||||
matches!(status, ExtensionStatus::NotInstalled)
|
||||
}
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn uninstall_extension(&self, extension_id: Arc<str>, cx: &mut ViewContext<Self>) {
|
||||
ExtensionStore::global(cx)
|
||||
.update(cx, |store, cx| store.uninstall_extension(extension_id, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn fetch_extensions(&mut self, search: Option<&str>, cx: &mut ViewContext<Self>) {
|
||||
fn fetch_extensions(&mut self, search: Option<String>, cx: &mut ViewContext<Self>) {
|
||||
self.is_fetching_extensions = true;
|
||||
cx.notify();
|
||||
|
||||
let extensions =
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| store.fetch_extensions(search, cx));
|
||||
let extension_store = ExtensionStore::global(cx);
|
||||
|
||||
let dev_extensions = extension_store.update(cx, |store, _| {
|
||||
store.dev_extensions().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let remote_extensions = extension_store.update(cx, |store, cx| {
|
||||
store.fetch_extensions(search.as_deref(), cx)
|
||||
});
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let fetch_result = extensions.await;
|
||||
match fetch_result {
|
||||
Ok(extensions) => this.update(&mut cx, |this, cx| {
|
||||
this.extension_entries = extensions;
|
||||
this.is_fetching_extensions = false;
|
||||
cx.notify();
|
||||
}),
|
||||
Err(err) => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.is_fetching_extensions = false;
|
||||
cx.notify();
|
||||
let dev_extensions = if let Some(search) = search {
|
||||
let match_candidates = dev_extensions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, manifest)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: manifest.name.clone(),
|
||||
char_bag: manifest.name.as_str().into(),
|
||||
})
|
||||
.ok();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
let matches = match_strings(
|
||||
&match_candidates,
|
||||
&search,
|
||||
false,
|
||||
match_candidates.len(),
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| dev_extensions[mat.candidate_id].clone())
|
||||
.collect()
|
||||
} else {
|
||||
dev_extensions
|
||||
};
|
||||
|
||||
let fetch_result = remote_extensions.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.dev_extension_entries = dev_extensions;
|
||||
this.is_fetching_extensions = false;
|
||||
this.remote_extension_entries = fetch_result?;
|
||||
this.filter_extension_entries(cx);
|
||||
anyhow::Ok(())
|
||||
})?
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_extensions(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Vec<Div> {
|
||||
self.filtered_extension_entries(cx)[range]
|
||||
.iter()
|
||||
.map(|extension| self.render_entry(extension, cx))
|
||||
fn render_extensions(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<ExtensionCard> {
|
||||
let dev_extension_entries_len = if self.filter.include_dev_extensions() {
|
||||
self.dev_extension_entries.len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
range
|
||||
.map(|ix| {
|
||||
if ix < dev_extension_entries_len {
|
||||
let extension = &self.dev_extension_entries[ix];
|
||||
self.render_dev_extension(extension, cx)
|
||||
} else {
|
||||
let extension_ix =
|
||||
self.filtered_remote_extension_indices[ix - dev_extension_entries_len];
|
||||
let extension = &self.remote_extension_entries[extension_ix];
|
||||
self.render_remote_extension(extension, cx)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_entry(&self, extension: &ExtensionApiResponse, cx: &mut ViewContext<Self>) -> Div {
|
||||
fn render_dev_extension(
|
||||
&self,
|
||||
extension: &ExtensionManifest,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ExtensionCard {
|
||||
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",
|
||||
let repository_url = extension.repository.clone();
|
||||
|
||||
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_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("rebuild-{}", extension.id)),
|
||||
"Rebuild",
|
||||
)
|
||||
.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)
|
||||
});
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Upgrading)),
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall")
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, cx| {
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.uninstall_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Removing)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{}: {}",
|
||||
if extension.authors.len() > 1 {
|
||||
"Authors"
|
||||
} else {
|
||||
"Author"
|
||||
},
|
||||
extension.authors.join(", ")
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(Label::new("<>").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)
|
||||
}))
|
||||
.children(repository_url.map(|repository_url| {
|
||||
IconButton::new(
|
||||
SharedString::from(format!("repository-{}", extension.id)),
|
||||
IconName::Github,
|
||||
)
|
||||
.icon_color(Color::Accent)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.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);
|
||||
let repository_url = repository_url.clone();
|
||||
move |_, _, cx| {
|
||||
cx.open_url(&repository_url);
|
||||
}
|
||||
}))
|
||||
.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()),
|
||||
if status.is_installing() {
|
||||
"Installing..."
|
||||
} else {
|
||||
"Install"
|
||||
},
|
||||
.tooltip(move |cx| Tooltip::text(repository_url.clone(), cx))
|
||||
})),
|
||||
)
|
||||
.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(status.is_installing()),
|
||||
ExtensionStatus::Installed(_)
|
||||
| ExtensionStatus::Upgrading
|
||||
| ExtensionStatus::Removing => Button::new(
|
||||
SharedString::from(extension.id.clone()),
|
||||
if status.is_upgrading() {
|
||||
"Upgrading..."
|
||||
} else if status.is_removing() {
|
||||
"Removing..."
|
||||
} else {
|
||||
"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);
|
||||
}
|
||||
|
||||
fn render_remote_extension(
|
||||
&self,
|
||||
extension: &ExtensionApiResponse,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ExtensionCard {
|
||||
let status = ExtensionStore::global(cx)
|
||||
.read(cx)
|
||||
.extension_status(&extension.id);
|
||||
|
||||
let (install_or_uninstall_button, upgrade_button) =
|
||||
self.buttons_for_entry(extension, &status, cx);
|
||||
let repository_url = extension.repository.clone();
|
||||
let tooltip_text = Tooltip::text(repository_url.clone(), cx);
|
||||
|
||||
div().w_full().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.h(rems(7.))
|
||||
.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(", ")
|
||||
))
|
||||
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_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(
|
||||
Label::new(format!("Downloads: {}", extension.download_count))
|
||||
.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)
|
||||
}))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("repository-{}", extension.id)),
|
||||
IconName::Github,
|
||||
)
|
||||
.child(
|
||||
Label::new(format!("Downloads: {}", extension.download_count))
|
||||
.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)
|
||||
}))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("repository-{}", extension.id)),
|
||||
IconName::Github,
|
||||
)
|
||||
.icon_color(Color::Accent)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
.icon_color(Color::Accent)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener({
|
||||
let repository_url = repository_url.clone();
|
||||
move |_, _, cx| {
|
||||
cx.open_url(&repository_url);
|
||||
}))
|
||||
.tooltip(move |_| tooltip_text.clone()),
|
||||
),
|
||||
}
|
||||
}))
|
||||
.tooltip(move |cx| Tooltip::text(repository_url.clone(), cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn buttons_for_entry(
|
||||
&self,
|
||||
extension: &ExtensionApiResponse,
|
||||
status: &ExtensionStatus,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> (Button, Option<Button>) {
|
||||
match status.clone() {
|
||||
ExtensionStatus::NotInstalled => (
|
||||
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());
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.install_extension(extension_id.clone(), version.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
None,
|
||||
),
|
||||
ExtensionStatus::Installing => (
|
||||
Button::new(SharedString::from(extension.id.clone()), "Install").disabled(true),
|
||||
None,
|
||||
),
|
||||
ExtensionStatus::Upgrading => (
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall").disabled(true),
|
||||
Some(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Upgrade").disabled(true),
|
||||
),
|
||||
),
|
||||
ExtensionStatus::Installed(installed_version) => (
|
||||
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());
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.uninstall_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
if installed_version == extension.version {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Button::new(SharedString::from(extension.id.clone()), "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(),
|
||||
);
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.upgrade_extension(
|
||||
extension_id.clone(),
|
||||
version.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
ExtensionStatus::Removing => (
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall").disabled(true),
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_search(&self, cx: &mut ViewContext<Self>) -> Div {
|
||||
|
@ -394,32 +564,36 @@ impl ExtensionsPage {
|
|||
) {
|
||||
if let editor::EditorEvent::Edited = event {
|
||||
self.query_contains_error = false;
|
||||
self.extension_fetch_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
let search = this
|
||||
.update(&mut cx, |this, cx| this.search_query(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
// Only debounce the fetching of extensions if we have a search
|
||||
// query.
|
||||
//
|
||||
// If the search was just cleared then we can just reload the list
|
||||
// of extensions without a debounce, which allows us to avoid seeing
|
||||
// an intermittent flash of a "no extensions" state.
|
||||
if let Some(_) = search {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(250))
|
||||
.await;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.fetch_extensions(search.as_deref(), cx);
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
self.fetch_extensions_debounced(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_extensions_debounced(&mut self, cx: &mut ViewContext<'_, ExtensionsPage>) {
|
||||
self.extension_fetch_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
let search = this
|
||||
.update(&mut cx, |this, cx| this.search_query(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
// Only debounce the fetching of extensions if we have a search
|
||||
// query.
|
||||
//
|
||||
// If the search was just cleared then we can just reload the list
|
||||
// of extensions without a debounce, which allows us to avoid seeing
|
||||
// an intermittent flash of a "no extensions" state.
|
||||
if let Some(_) = search {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(250))
|
||||
.await;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.fetch_extensions(search, 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() {
|
||||
|
@ -479,7 +653,17 @@ impl Render for ExtensionsPage {
|
|||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.child(Headline::new("Extensions").size(HeadlineSize::XLarge)),
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Headline::new("Extensions").size(HeadlineSize::XLarge))
|
||||
.child(
|
||||
Button::new("add-dev-extension", "Add Dev Extension")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.on_click(|_event, cx| {
|
||||
cx.dispatch_action(Box::new(InstallDevExtension))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
@ -494,8 +678,9 @@ impl Render for ExtensionsPage {
|
|||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.selected(self.filter == ExtensionFilter::All)
|
||||
.on_click(cx.listener(|this, _event, _cx| {
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.filter = ExtensionFilter::All;
|
||||
this.filter_extension_entries(cx);
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Show all extensions", cx)
|
||||
|
@ -507,8 +692,9 @@ impl Render for ExtensionsPage {
|
|||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.selected(self.filter == ExtensionFilter::Installed)
|
||||
.on_click(cx.listener(|this, _event, _cx| {
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.filter = ExtensionFilter::Installed;
|
||||
this.filter_extension_entries(cx);
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Show installed extensions", cx)
|
||||
|
@ -520,8 +706,9 @@ impl Render for ExtensionsPage {
|
|||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.selected(self.filter == ExtensionFilter::NotInstalled)
|
||||
.on_click(cx.listener(|this, _event, _cx| {
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.filter = ExtensionFilter::NotInstalled;
|
||||
this.filter_extension_entries(cx);
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Show not installed extensions", cx)
|
||||
|
@ -532,8 +719,12 @@ impl Render for ExtensionsPage {
|
|||
),
|
||||
)
|
||||
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
|
||||
let entries = self.filtered_extension_entries(cx);
|
||||
if entries.is_empty() {
|
||||
let mut count = self.filtered_remote_extension_indices.len();
|
||||
if self.filter.include_dev_extensions() {
|
||||
count += self.dev_extension_entries.len();
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return this.py_4().child(self.render_empty_state(cx));
|
||||
}
|
||||
|
||||
|
@ -541,12 +732,11 @@ impl Render for ExtensionsPage {
|
|||
canvas({
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
let item_count = entries.len();
|
||||
move |bounds, cx| {
|
||||
uniform_list::<_, Div, _>(
|
||||
uniform_list::<_, ExtensionCard, _>(
|
||||
view,
|
||||
"entries",
|
||||
item_count,
|
||||
count,
|
||||
Self::render_extensions,
|
||||
)
|
||||
.size_full()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue