1193 lines · 47.4 KB · Rust
RawDownload
mod components;mod extension_suggest;mod extension_version_selector; use std::ops::DerefMut;use std::sync::OnceLock;use std::time::Duration;use std::{ops::Range, sync::Arc}; use client::telemetry::Telemetry;use client::ExtensionMetadata;use collections::{BTreeMap, BTreeSet};use editor::{Editor, EditorElement, EditorStyle};use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};use fuzzy::{match_strings, StringMatchCandidate};use gpui::{    actions, uniform_list, Action, AppContext, ClipboardItem, EventEmitter, Flatten, FocusableView,    InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,    UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,};use num_format::{Locale, ToFormattedString};use project::DirectoryLister;use release_channel::ReleaseChannel;use settings::Settings;use theme::ThemeSettings;use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};use vim_mode_setting::VimModeSetting;use workspace::{    item::{Item, ItemEvent},    Workspace, WorkspaceId,}; use crate::components::{ExtensionCard, FeatureUpsell};use crate::extension_version_selector::{    ExtensionVersionSelector, ExtensionVersionSelectorDelegate,}; actions!(zed, [InstallDevExtension]); pub fn init(cx: &mut AppContext) {    cx.observe_new_views(move |workspace: &mut Workspace, cx| {        workspace            .register_action(move |workspace, _: &zed_actions::Extensions, cx| {                let existing = workspace                    .active_pane()                    .read(cx)                    .items()                    .find_map(|item| item.downcast::<ExtensionsPage>());                 if let Some(existing) = existing {                    workspace.activate_item(&existing, true, true, cx);                } else {                    let extensions_page = ExtensionsPage::new(workspace, cx);                    workspace.add_item_to_active_pane(Box::new(extensions_page), None, true, cx)                }            })            .register_action(move |workspace, _: &InstallDevExtension, cx| {                let store = ExtensionStore::global(cx);                let prompt = workspace.prompt_for_open_path(                    gpui::PathPromptOptions {                        files: false,                        directories: true,                        multiple: false,                    },                    DirectoryLister::Local(workspace.app_state().fs.clone()),                    cx,                );                 let workspace_handle = cx.view().downgrade();                cx.deref_mut()                    .spawn(|mut cx| async move {                        let extension_path =                            match Flatten::flatten(prompt.await.map_err(|e| e.into())) {                                Ok(Some(mut paths)) => paths.pop()?,                                Ok(None) => return None,                                Err(err) => {                                    workspace_handle                                        .update(&mut cx, |workspace, cx| {                                            workspace.show_portal_error(err.to_string(), cx);                                        })                                        .ok();                                    return None;                                }                            };                         store                            .update(&mut cx, |store, cx| {                                store                                    .install_dev_extension(extension_path, cx)                                    .detach_and_log_err(cx)                            })                            .ok()?;                        Some(())                    })                    .detach();            });         cx.subscribe(workspace.project(), |_, _, event, cx| {            if let project::Event::LanguageNotFound(buffer) = event {                extension_suggest::suggest(buffer.clone(), cx);            }        })        .detach();    })    .detach();} #[derive(Clone)]pub enum ExtensionStatus {    NotInstalled,    Installing,    Upgrading,    Installed(Arc<str>),    Removing,} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]enum ExtensionFilter {    All,    Installed,    NotInstalled,} impl ExtensionFilter {    pub fn include_dev_extensions(&self) -> bool {        match self {            Self::All | Self::Installed => true,            Self::NotInstalled => false,        }    }} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]enum Feature {    Git,    OpenIn,    Vim,    LanguageBash,    LanguageC,    LanguageCpp,    LanguageGo,    LanguagePython,    LanguageReact,    LanguageRust,    LanguageTypescript,} fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {    static KEYWORDS_BY_FEATURE: OnceLock<BTreeMap<Feature, Vec<&'static str>>> = OnceLock::new();    KEYWORDS_BY_FEATURE.get_or_init(|| {        BTreeMap::from_iter([            (Feature::Git, vec!["git"]),            (                Feature::OpenIn,                vec![                    "github",                    "gitlab",                    "bitbucket",                    "codeberg",                    "sourcehut",                    "permalink",                    "link",                    "open in",                ],            ),            (Feature::Vim, vec!["vim"]),            (Feature::LanguageBash, vec!["sh", "bash"]),            (Feature::LanguageC, vec!["c", "clang"]),            (Feature::LanguageCpp, vec!["c++", "cpp", "clang"]),            (Feature::LanguageGo, vec!["go", "golang"]),            (Feature::LanguagePython, vec!["python", "py"]),            (Feature::LanguageReact, vec!["react"]),            (Feature::LanguageRust, vec!["rust", "rs"]),            (                Feature::LanguageTypescript,                vec!["type", "typescript", "ts"],            ),        ])    })} pub struct ExtensionsPage {    workspace: WeakView<Workspace>,    list: UniformListScrollHandle,    telemetry: Arc<Telemetry>,    is_fetching_extensions: bool,    filter: ExtensionFilter,    remote_extension_entries: Vec<ExtensionMetadata>,    dev_extension_entries: Vec<Arc<ExtensionManifest>>,    filtered_remote_extension_indices: Vec<usize>,    query_editor: View<Editor>,    query_contains_error: bool,    _subscriptions: [gpui::Subscription; 2],    extension_fetch_task: Option<Task<()>>,    upsells: BTreeSet<Feature>,} 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 workspace_handle = workspace.weak_handle();            let subscriptions = [                cx.observe(&store, |_, _, cx| cx.notify()),                cx.subscribe(&store, move |this, _, event, cx| match event {                    extension_host::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx),                    extension_host::Event::ExtensionInstalled(extension_id) => {                        this.on_extension_installed(workspace_handle.clone(), extension_id, cx)                    }                    _ => {}                }),            ];             let query_editor = cx.new_view(|cx| {                let mut input = Editor::single_line(cx);                input.set_placeholder_text("Search extensions...", cx);                input            });            cx.subscribe(&query_editor, Self::on_query_change).detach();             let mut this = Self {                workspace: workspace.weak_handle(),                list: UniformListScrollHandle::new(),                telemetry: workspace.client().telemetry().clone(),                is_fetching_extensions: false,                filter: ExtensionFilter::All,                dev_extension_entries: Vec::new(),                filtered_remote_extension_indices: Vec::new(),                remote_extension_entries: Vec::new(),                query_contains_error: false,                extension_fetch_task: None,                _subscriptions: subscriptions,                query_editor,                upsells: BTreeSet::default(),            };            this.fetch_extensions(None, cx);            this        })    }     fn on_extension_installed(        &mut self,        workspace: WeakView<Workspace>,        extension_id: &str,        cx: &mut ViewContext<Self>,    ) {        let extension_store = ExtensionStore::global(cx).read(cx);        let themes = extension_store            .extension_themes(extension_id)            .map(|name| name.to_string())            .collect::<Vec<_>>();        if !themes.is_empty() {            workspace                .update(cx, |_workspace, cx| {                    cx.dispatch_action(                        zed_actions::theme_selector::Toggle {                            themes_filter: Some(themes),                        }                        .boxed_clone(),                    );                })                .ok();        }    }     /// Returns whether a dev extension currently exists for the extension with the given ID.    fn dev_extension_exists(extension_id: &str, cx: &mut ViewContext<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 ViewContext<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 ViewContext<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(_))                    }                    ExtensionFilter::NotInstalled => {                        let status = Self::extension_status(&extension.id, cx);                         matches!(status, ExtensionStatus::NotInstalled)                    }                })                .map(|(ix, _)| ix),        );        cx.notify();    }     fn fetch_extensions(&mut self, search: Option<String>, cx: &mut ViewContext<Self>) {        self.is_fetching_extensions = true;        cx.notify();         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 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(),                    })                    .collect::<Vec<_>>();                 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<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_dev_extension(        &self,        extension: &ExtensionManifest,        cx: &mut ViewContext<Self>,    ) -> ExtensionCard {        let status = Self::extension_status(&extension.id, cx);         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()                    .gap_2()                    .justify_between()                    .child(                        div().overflow_x_hidden().text_ellipsis().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()                    .gap_2()                    .justify_between()                    .children(extension.description.as_ref().map(|description| {                        div().overflow_x_hidden().text_ellipsis().child(                            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 repository_url = repository_url.clone();                            move |_, _, cx| {                                cx.open_url(&repository_url);                            }                        }))                        .tooltip(move |cx| Tooltip::text(repository_url.clone(), cx))                    })),            )    } 

Lines 1–500 of 1193.