1541 lines · 61.4 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 fuzzy::{StringMatchCandidate, match_strings};use gpui::{    Action, App, ClipboardItem, Context, Entity, EventEmitter, Flatten, Focusable,    InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,    UniformListScrollHandle, WeakEntity, Window, actions, point, uniform_list,};use num_format::{Locale, ToFormattedString};use project::DirectoryLister;use release_channel::ReleaseChannel;use settings::Settings;use strum::IntoEnumIterator as _;use theme::ThemeSettings;use ui::{    CheckboxWithLabel, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,    ToggleButton, Tooltip, prelude::*,};use vim_mode_setting::VimModeSetting;use workspace::{    Workspace, WorkspaceId,    item::{Item, ItemEvent},};use zed_actions::ExtensionCategoryFilter; 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, action: &zed_actions::Extensions, window, cx| {                    let provides_filter = action.category_filter.map(|category| match category {                        ExtensionCategoryFilter::Themes => ExtensionProvides::Themes,                        ExtensionCategoryFilter::IconThemes => ExtensionProvides::IconThemes,                        ExtensionCategoryFilter::Languages => ExtensionProvides::Languages,                        ExtensionCategoryFilter::Grammars => ExtensionProvides::Grammars,                        ExtensionCategoryFilter::LanguageServers => {                            ExtensionProvides::LanguageServers                        }                        ExtensionCategoryFilter::ContextServers => {                            ExtensionProvides::ContextServers                        }                        ExtensionCategoryFilter::SlashCommands => ExtensionProvides::SlashCommands,                        ExtensionCategoryFilter::IndexedDocsProviders => {                            ExtensionProvides::IndexedDocsProviders                        }                        ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets,                        ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters,                    });                     let existing = workspace                        .active_pane()                        .read(cx)                        .items()                        .find_map(|item| item.downcast::<ExtensionsPage>());                     if let Some(existing) = existing {                        if provides_filter.is_some() {                            existing.update(cx, |extensions_page, cx| {                                extensions_page.change_provides_filter(provides_filter, cx);                            });                        }                         workspace.activate_item(&existing, true, true, window, cx);                    } else {                        let extensions_page =                            ExtensionsPage::new(workspace, provides_filter, 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.project().clone(),                        workspace.app_state().fs.clone(),                    ),                    window,                    cx,                );                 let workspace_handle = cx.entity().downgrade();                window                    .spawn(cx, async move |cx| {                        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(cx, |workspace, cx| {                                            workspace.show_portal_error(err.to_string(), cx);                                        })                                        .ok();                                    return None;                                }                            };                         let install_task = store                            .update(cx, |store, cx| {                                store.install_dev_extension(extension_path, cx)                            })                            .ok()?;                         match install_task.await {                            Ok(_) => {}                            Err(err) => {                                log::error!("Failed to install dev extension: {:?}", err);                                workspace_handle                                    .update(cx, |workspace, cx| {                                        workspace.show_error(                                            // NOTE: using `anyhow::context` here ends up not printing                                            // the error                                            &format!("Failed to install dev extension: {}", err),                                            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();} 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::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,    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"],            ),        ])    })} struct ExtensionCardButtons {    install_or_uninstall: Button,    upgrade: Option<Button>,    configure: Option<Button>,} 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>,    scrollbar_state: ScrollbarState,} impl ExtensionsPage {    pub fn new(        workspace: &Workspace,        provides_filter: Option<ExtensionProvides>,        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(None, 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 scroll_handle = UniformListScrollHandle::new();             let mut this = Self {                workspace: workspace.weak_handle(),                list: scroll_handle.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,                provides_filter,                extension_fetch_task: None,                _subscriptions: subscriptions,                query_editor,                upsells: BTreeSet::default(),                scrollbar_state: ScrollbarState::new(scroll_handle),            };            this.fetch_extensions(                None,                Some(BTreeSet::from_iter(this.provides_filter)),                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 scroll_to_top(&mut self, cx: &mut Context<Self>) {        self.list.set_offset(point(px(0.), px(0.)));        cx.notify();    }     fn fetch_extensions(        &mut self,        search: Option<String>,        provides_filter: Option<BTreeSet<ExtensionProvides>>,        on_complete: Option<Box<dyn FnOnce(&mut Self, &mut Context<Self>) + Send>>,        cx: &mut Context<Self>,    ) {        self.is_fetching_extensions = true;        cx.notify();         let extension_store = ExtensionStore::global(cx);         let dev_extensions = extension_store            .read(cx)            .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(async move |this, cx| {            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,                    true,                    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(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);                if let Some(callback) = on_complete {                    callback(this, cx);                }

Lines 1–500 of 1541.