extensions_ui.rs

  1use client::telemetry::Telemetry;
  2use editor::{Editor, EditorElement, EditorStyle};
  3use extension::{Extension, ExtensionStatus, ExtensionStore};
  4use fs::Fs;
  5use gpui::{
  6    actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
  7    FontWeight, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
  8    UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
  9};
 10use settings::Settings;
 11use std::time::Duration;
 12use std::{ops::Range, sync::Arc};
 13use theme::ThemeSettings;
 14use ui::prelude::*;
 15
 16use workspace::{
 17    item::{Item, ItemEvent},
 18    Workspace, WorkspaceId,
 19};
 20
 21actions!(zed, [Extensions]);
 22
 23pub fn init(cx: &mut AppContext) {
 24    cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
 25        workspace.register_action(move |workspace, _: &Extensions, cx| {
 26            let extensions_page = ExtensionsPage::new(workspace, cx);
 27            workspace.add_item(Box::new(extensions_page), cx)
 28        });
 29    })
 30    .detach();
 31}
 32
 33pub struct ExtensionsPage {
 34    workspace: WeakView<Workspace>,
 35    fs: Arc<dyn Fs>,
 36    list: UniformListScrollHandle,
 37    telemetry: Arc<Telemetry>,
 38    extensions_entries: Vec<Extension>,
 39    query_editor: View<Editor>,
 40    query_contains_error: bool,
 41    extension_fetch_task: Option<Task<()>>,
 42}
 43
 44impl Render for ExtensionsPage {
 45    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
 46        h_flex()
 47            .full()
 48            .bg(cx.theme().colors().editor_background)
 49            .child(
 50                v_flex()
 51                    .full()
 52                    .p_4()
 53                    .child(
 54                        h_flex()
 55                            .w_full()
 56                            .child(Headline::new("Extensions").size(HeadlineSize::XLarge)),
 57                    )
 58                    .child(h_flex().w_56().my_4().child(self.render_search(cx)))
 59                    .child(
 60                        h_flex().flex_col().items_start().full().child(
 61                            uniform_list::<_, Div, _>(
 62                                cx.view().clone(),
 63                                "entries",
 64                                self.extensions_entries.len(),
 65                                Self::render_extensions,
 66                            )
 67                            .size_full()
 68                            .track_scroll(self.list.clone()),
 69                        ),
 70                    ),
 71            )
 72    }
 73}
 74
 75impl ExtensionsPage {
 76    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 77        let extensions_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
 78            let query_editor = cx.new_view(|cx| Editor::single_line(cx));
 79            cx.subscribe(&query_editor, Self::on_query_change).detach();
 80
 81            let mut this = Self {
 82                fs: workspace.project().read(cx).fs().clone(),
 83                workspace: workspace.weak_handle(),
 84                list: UniformListScrollHandle::new(),
 85                telemetry: workspace.client().telemetry().clone(),
 86                extensions_entries: Vec::new(),
 87                query_contains_error: false,
 88                extension_fetch_task: None,
 89                query_editor,
 90            };
 91            this.fetch_extensions(None, cx);
 92            this
 93        });
 94        extensions_panel
 95    }
 96
 97    fn install_extension(
 98        &self,
 99        extension_id: Arc<str>,
100        version: Arc<str>,
101        cx: &mut ViewContext<Self>,
102    ) {
103        let install = ExtensionStore::global(cx).update(cx, |store, cx| {
104            store.install_extension(extension_id, version, cx)
105        });
106        cx.spawn(move |this, mut cx| async move {
107            install.await?;
108            this.update(&mut cx, |_, cx| cx.notify())
109        })
110        .detach_and_log_err(cx);
111        cx.notify();
112    }
113
114    fn uninstall_extension(&self, extension_id: Arc<str>, cx: &mut ViewContext<Self>) {
115        let install = ExtensionStore::global(cx)
116            .update(cx, |store, cx| store.uninstall_extension(extension_id, cx));
117        cx.spawn(move |this, mut cx| async move {
118            install.await?;
119            this.update(&mut cx, |_, cx| cx.notify())
120        })
121        .detach_and_log_err(cx);
122        cx.notify();
123    }
124
125    fn fetch_extensions(&mut self, search: Option<&str>, cx: &mut ViewContext<Self>) {
126        let extensions =
127            ExtensionStore::global(cx).update(cx, |store, cx| store.fetch_extensions(search, cx));
128
129        cx.spawn(move |this, mut cx| async move {
130            let extensions = extensions.await?;
131            this.update(&mut cx, |this, cx| {
132                this.extensions_entries = extensions;
133                cx.notify();
134            })
135        })
136        .detach_and_log_err(cx);
137    }
138
139    fn render_extensions(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Vec<Div> {
140        self.extensions_entries[range]
141            .iter()
142            .map(|extension| self.render_entry(extension, cx))
143            .collect()
144    }
145
146    fn render_entry(&self, extension: &Extension, cx: &mut ViewContext<Self>) -> Div {
147        let status = ExtensionStore::global(cx)
148            .read(cx)
149            .extension_status(&extension.id);
150
151        let upgrade_button = match status.clone() {
152            ExtensionStatus::NotInstalled
153            | ExtensionStatus::Installing
154            | ExtensionStatus::Removing => None,
155            ExtensionStatus::Installed(installed_version) => {
156                if installed_version != extension.version {
157                    Some(
158                        Button::new(
159                            SharedString::from(format!("upgrade-{}", extension.id)),
160                            "Upgrade",
161                        )
162                        .on_click(cx.listener({
163                            let extension_id = extension.id.clone();
164                            let version = extension.version.clone();
165                            move |this, _, cx| {
166                                this.telemetry
167                                    .report_app_event("extensions: install extension".to_string());
168                                this.install_extension(extension_id.clone(), version.clone(), cx);
169                            }
170                        }))
171                        .color(Color::Accent),
172                    )
173                } else {
174                    None
175                }
176            }
177            ExtensionStatus::Upgrading => Some(
178                Button::new(
179                    SharedString::from(format!("upgrade-{}", extension.id)),
180                    "Upgrade",
181                )
182                .color(Color::Accent)
183                .disabled(true),
184            ),
185        };
186
187        let install_or_uninstall_button = match status {
188            ExtensionStatus::NotInstalled | ExtensionStatus::Installing => {
189                Button::new(SharedString::from(extension.id.clone()), "Install")
190                    .on_click(cx.listener({
191                        let extension_id = extension.id.clone();
192                        let version = extension.version.clone();
193                        move |this, _, cx| {
194                            this.telemetry
195                                .report_app_event("extensions: install extension".to_string());
196                            this.install_extension(extension_id.clone(), version.clone(), cx);
197                        }
198                    }))
199                    .disabled(matches!(status, ExtensionStatus::Installing))
200            }
201            ExtensionStatus::Installed(_)
202            | ExtensionStatus::Upgrading
203            | ExtensionStatus::Removing => {
204                Button::new(SharedString::from(extension.id.clone()), "Uninstall")
205                    .on_click(cx.listener({
206                        let extension_id = extension.id.clone();
207                        move |this, _, cx| {
208                            this.telemetry
209                                .report_app_event("extensions: uninstall extension".to_string());
210                            this.uninstall_extension(extension_id.clone(), cx);
211                        }
212                    }))
213                    .disabled(matches!(
214                        status,
215                        ExtensionStatus::Upgrading | ExtensionStatus::Removing
216                    ))
217            }
218        }
219        .color(Color::Accent);
220
221        div().w_full().child(
222            v_flex()
223                .w_full()
224                .p_3()
225                .mt_4()
226                .gap_2()
227                .bg(cx.theme().colors().elevated_surface_background)
228                .border_1()
229                .border_color(cx.theme().colors().border)
230                .rounded_md()
231                .child(
232                    h_flex()
233                        .justify_between()
234                        .child(
235                            h_flex()
236                                .gap_2()
237                                .items_end()
238                                .child(
239                                    Headline::new(extension.name.clone())
240                                        .size(HeadlineSize::Medium),
241                                )
242                                .child(
243                                    Headline::new(format!("v{}", extension.version))
244                                        .size(HeadlineSize::XSmall),
245                                ),
246                        )
247                        .child(
248                            h_flex()
249                                .gap_2()
250                                .justify_between()
251                                .children(upgrade_button)
252                                .child(install_or_uninstall_button),
253                        ),
254                )
255                .child(
256                    h_flex().justify_between().child(
257                        Label::new(format!(
258                            "{}: {}",
259                            if extension.authors.len() > 1 {
260                                "Authors"
261                            } else {
262                                "Author"
263                            },
264                            extension.authors.join(", ")
265                        ))
266                        .size(LabelSize::Small),
267                    ),
268                )
269                .child(
270                    h_flex()
271                        .justify_between()
272                        .children(extension.description.as_ref().map(|description| {
273                            Label::new(description.clone())
274                                .size(LabelSize::Small)
275                                .color(Color::Default)
276                        })),
277                ),
278        )
279    }
280
281    fn render_search(&self, cx: &mut ViewContext<Self>) -> Div {
282        let mut key_context = KeyContext::default();
283        key_context.add("BufferSearchBar");
284
285        let editor_border = if self.query_contains_error {
286            Color::Error.color(cx)
287        } else {
288            cx.theme().colors().border
289        };
290
291        h_flex()
292            .w_full()
293            .gap_2()
294            .key_context(key_context)
295            // .capture_action(cx.listener(Self::tab))
296            // .on_action(cx.listener(Self::dismiss))
297            .child(
298                h_flex()
299                    .flex_1()
300                    .px_2()
301                    .py_1()
302                    .gap_2()
303                    .border_1()
304                    .border_color(editor_border)
305                    .min_w(rems(384. / 16.))
306                    .rounded_lg()
307                    .child(Icon::new(IconName::MagnifyingGlass))
308                    .child(self.render_text_input(&self.query_editor, cx)),
309            )
310    }
311
312    fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
313        let settings = ThemeSettings::get_global(cx);
314        let text_style = TextStyle {
315            color: if editor.read(cx).read_only(cx) {
316                cx.theme().colors().text_disabled
317            } else {
318                cx.theme().colors().text
319            },
320            font_family: settings.ui_font.family.clone(),
321            font_features: settings.ui_font.features,
322            font_size: rems(0.875).into(),
323            font_weight: FontWeight::NORMAL,
324            font_style: FontStyle::Normal,
325            line_height: relative(1.3).into(),
326            background_color: None,
327            underline: None,
328            strikethrough: None,
329            white_space: WhiteSpace::Normal,
330        };
331
332        EditorElement::new(
333            &editor,
334            EditorStyle {
335                background: cx.theme().colors().editor_background,
336                local_player: cx.theme().players().local(),
337                text: text_style,
338                ..Default::default()
339            },
340        )
341    }
342
343    fn on_query_change(
344        &mut self,
345        _: View<Editor>,
346        event: &editor::EditorEvent,
347        cx: &mut ViewContext<Self>,
348    ) {
349        if let editor::EditorEvent::Edited = event {
350            self.query_contains_error = false;
351            self.extension_fetch_task = Some(cx.spawn(|this, mut cx| async move {
352                cx.background_executor()
353                    .timer(Duration::from_millis(250))
354                    .await;
355                this.update(&mut cx, |this, cx| {
356                    this.fetch_extensions(this.search_query(cx).as_deref(), cx);
357                })
358                .ok();
359            }));
360        }
361    }
362
363    pub fn search_query(&self, cx: &WindowContext) -> Option<String> {
364        let search = self.query_editor.read(cx).text(cx);
365        if search.trim().is_empty() {
366            None
367        } else {
368            Some(search)
369        }
370    }
371}
372
373impl EventEmitter<ItemEvent> for ExtensionsPage {}
374
375impl FocusableView for ExtensionsPage {
376    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
377        self.query_editor.read(cx).focus_handle(cx)
378    }
379}
380
381impl Item for ExtensionsPage {
382    type Event = ItemEvent;
383
384    fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
385        Label::new("Extensions")
386            .color(if selected {
387                Color::Default
388            } else {
389                Color::Muted
390            })
391            .into_any_element()
392    }
393
394    fn telemetry_event_text(&self) -> Option<&'static str> {
395        Some("extensions page")
396    }
397
398    fn show_toolbar(&self) -> bool {
399        false
400    }
401
402    fn clone_on_split(
403        &self,
404        _workspace_id: WorkspaceId,
405        cx: &mut ViewContext<Self>,
406    ) -> Option<View<Self>> {
407        Some(cx.new_view(|_| ExtensionsPage {
408            fs: self.fs.clone(),
409            workspace: self.workspace.clone(),
410            list: UniformListScrollHandle::new(),
411            telemetry: self.telemetry.clone(),
412            extensions_entries: Default::default(),
413            query_editor: self.query_editor.clone(),
414            query_contains_error: false,
415            extension_fetch_task: None,
416        }))
417    }
418
419    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
420        f(*event)
421    }
422}