1319 lines · 53.3 KB · Rust
RawDownload
mod components;mod extension_suggest;mod extension_version_selector; use std::sync::OnceLock;use std::time::Duration;use std::{ops::Range, sync::Arc}; use client::{ExtensionMetadata, ExtensionProvides};use collections::{BTreeMap, BTreeSet};use editor::{Editor, EditorElement, EditorStyle};use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};use feature_flags::FeatureFlagAppExt as _;use fuzzy::{match_strings, StringMatchCandidate};use gpui::{    actions, uniform_list, Action, App, ClipboardItem, Context, Entity, EventEmitter, Flatten,    Focusable, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,    UniformListScrollHandle, WeakEntity, Window,};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 App) {    cx.observe_new(move |workspace: &mut Workspace, window, cx| {        let Some(window) = window else {            return;        };        workspace            .register_action(move |workspace, _: &zed_actions::Extensions, window, 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, window, cx);                } else {                    let extensions_page = ExtensionsPage::new(workspace, window, cx);                    workspace.add_item_to_active_pane(                        Box::new(extensions_page),                        None,                        true,                        window,                        cx,                    )                }            })            .register_action(move |workspace, _: &InstallDevExtension, window, 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()),                    window,                    cx,                );                 let workspace_handle = cx.entity().downgrade();                window                    .spawn(cx, |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;                                }                            };                         let install_task = store                            .update(&mut cx, |store, cx| {                                store.install_dev_extension(extension_path, cx)                            })                            .ok()?;                         match install_task.await {                            Ok(_) => {}                            Err(err) => {                                workspace_handle                                    .update(&mut cx, |workspace, cx| {                                        workspace.show_error(                                            &err.context("failed to install dev extension"),                                            cx,                                        );                                    })                                    .ok();                            }                        }                         Some(())                    })                    .detach();            });         cx.subscribe_in(workspace.project(), window, |_, _, event, window, cx| {            if let project::Event::LanguageNotFound(buffer) = event {                extension_suggest::suggest(buffer.clone(), window, 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: WeakEntity<Workspace>,    list: UniformListScrollHandle,    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: Entity<Editor>,    query_contains_error: bool,    provides_filter: Option<ExtensionProvides>,    _subscriptions: [gpui::Subscription; 2],    extension_fetch_task: Option<Task<()>>,    upsells: BTreeSet<Feature>,} impl ExtensionsPage {    pub fn new(        workspace: &Workspace,        window: &mut Window,        cx: &mut Context<Workspace>,    ) -> Entity<Self> {        cx.new(|cx| {            let store = ExtensionStore::global(cx);            let workspace_handle = workspace.weak_handle();            let subscriptions = [                cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),                cx.subscribe_in(                    &store,                    window,                    move |this, _, event, window, 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,                                window,                                cx,                            ),                        _ => {}                    },                ),            ];             let query_editor = cx.new(|cx| {                let mut input = Editor::single_line(window, 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(),                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,                provides_filter: None,                extension_fetch_task: None,                _subscriptions: subscriptions,                query_editor,                upsells: BTreeSet::default(),            };            this.fetch_extensions(None, None, cx);            this        })    }     fn on_extension_installed(        &mut self,        workspace: WeakEntity<Workspace>,        extension_id: &str,        window: &mut Window,        cx: &mut Context<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| {                    window.dispatch_action(                        zed_actions::theme_selector::Toggle {                            themes_filter: Some(themes),                        }                        .boxed_clone(),                        cx,                    );                })                .ok();            return;        }         let icon_themes = extension_store            .extension_icon_themes(extension_id)            .map(|name| name.to_string())            .collect::<Vec<_>>();        if !icon_themes.is_empty() {            workspace                .update(cx, |_workspace, cx| {                    window.dispatch_action(                        zed_actions::icon_theme_selector::Toggle {                            themes_filter: Some(icon_themes),                        }                        .boxed_clone(),                        cx,                    );                })                .ok();        }    }     /// 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(_))                    }                    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>,        provides_filter: Option<BTreeSet<ExtensionProvides>>,        cx: &mut Context<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(), provides_filter.as_ref(), 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::new(ix, &manifest.name))                    .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>,        _: &mut Window,        cx: &mut Context<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 Context<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| {

Lines 1–500 of 1319.