1894 lines · 75.1 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 anyhow::Context as _;use client::zed_urls;use cloud_api_types::{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, Corner, Entity, EventEmitter, Focusable,    InteractiveElement, KeyContext, ParentElement, Point, 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, SettingsContent};use strum::IntoEnumIterator as _;use theme::ThemeSettings;use ui::{    Banner, Chip, ContextMenu, Divider, PopoverMenu, ScrollableHandle, Switch, ToggleButtonGroup,    ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, WithScrollbar,    prelude::*,};use vim_mode_setting::VimModeSetting;use workspace::{    Workspace,    item::{Item, ItemEvent},};use zed_actions::ExtensionCategoryFilter; use crate::components::ExtensionCard;use crate::extension_version_selector::{    ExtensionVersionSelector, ExtensionVersionSelectorDelegate,}; actions!(    zed,    [        /// Installs an extension from a local directory for development.        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::AgentServers => ExtensionProvides::AgentServers,                        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 {                        existing.update(cx, |extensions_page, cx| {                            if provides_filter.is_some() {                                extensions_page.change_provides_filter(provides_filter, cx);                            }                            if let Some(id) = action.id.as_ref() {                                extensions_page.focus_extension(id, window, cx);                            }                        });                         workspace.activate_item(&existing, true, true, window, cx);                    } else {                        let extensions_page = ExtensionsPage::new(                            workspace,                            provides_filter,                            action.id.as_deref(),                            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,                        prompt: None,                    },                    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 prompt.await.map_err(anyhow::Error::from) {                            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)                        });                         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::AgentServers => "Agent 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 {    AgentClaude,    AgentCodex,    AgentGemini,    ExtensionBasedpyright,    ExtensionRuff,    ExtensionTailwind,    ExtensionTy,    Git,    LanguageBash,    LanguageC,    LanguageCpp,    LanguageGo,    LanguagePython,    LanguageReact,    LanguageRust,    LanguageTypescript,    OpenIn,    Vim,} 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::AgentClaude,                vec!["claude", "claude code", "claude agent"],            ),            (Feature::AgentCodex, vec!["codex", "codex cli"]),            (Feature::AgentGemini, vec!["gemini", "gemini cli"]),            (                Feature::ExtensionBasedpyright,                vec!["basedpyright", "pyright"],            ),            (Feature::ExtensionRuff, vec!["ruff"]),            (Feature::ExtensionTailwind, vec!["tail", "tailwind"]),            (Feature::ExtensionTy, vec!["ty"]),            (Feature::Git, vec!["git"]),            (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"],            ),            (                Feature::OpenIn,                vec![                    "github",                    "gitlab",                    "bitbucket",                    "codeberg",                    "sourcehut",                    "permalink",                    "link",                    "open in",                ],            ),            (Feature::Vim, vec!["vim"]),        ])    })} fn acp_registry_upsell_keywords() -> &'static [&'static str] {    &[        "opencode",        "mistral",        "auggie",        "stakpak",        "codebuddy",        "autohand",        "factory droid",        "corust",    ]} fn extension_button_id(extension_id: &Arc<str>, operation: ExtensionOperation) -> ElementId {    (SharedString::from(extension_id.clone()), operation as usize).into()} struct ExtensionCardButtons {    install_or_uninstall: Button,    upgrade: Option<Button>,    configure: Option<Button>,} pub struct ExtensionsPage {    workspace: WeakEntity<Workspace>,    list: UniformListScrollHandle,    is_fetching_extensions: bool,    fetch_failed: bool,    filter: ExtensionFilter,    remote_extension_entries: Vec<ExtensionMetadata>,    dev_extension_entries: Vec<Arc<ExtensionManifest>>,    filtered_remote_extension_indices: Vec<usize>,    filtered_dev_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>,    show_acp_registry_upsell: bool,} impl ExtensionsPage {    pub fn new(        workspace: &Workspace,        provides_filter: Option<ExtensionProvides>,        focus_extension_id: Option<&str>,        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...", window, cx);                if let Some(id) = focus_extension_id {                    input.set_text(format!("id:{id}"), window, 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,                is_fetching_extensions: false,                fetch_failed: false,                filter: ExtensionFilter::All,                dev_extension_entries: Vec::new(),                filtered_remote_extension_indices: Vec::new(),                filtered_dev_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(),                show_acp_registry_upsell: false,            };            this.fetch_extensions(                this.search_query(cx),                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)                    }                })                .filter(|(_, extension)| match self.provides_filter {                    Some(provides) => extension.manifest.provides.contains(&provides),                    None => true,                })                .map(|(ix, _)| ix),        );

Lines 1–500 of 1894.