2096 lines · 81.6 KB · Rust
RawDownload
mod components;mod extension_suggest;mod extension_version_selector; use std::sync::OnceLock;use std::time::Duration;use std::{any::TypeId, ops::Range, sync::Arc}; use anyhow::Context as _;use cloud_api_types::{ExtensionMetadata, ExtensionProvides};use collections::{BTreeMap, BTreeSet};use command_palette_hooks::CommandPaletteFilter;use editor::{Editor, EditorElement, EditorStyle};use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};use fuzzy::{StringMatch, StringMatchCandidate, match_strings};use gpui::{    Action, Anchor, App, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, Focusable,    InteractiveElement, KeyContext, ParentElement, Point, Render, Styled, Task, TaskExt, TextStyle,    UniformListScrollHandle, WeakEntity, Window, actions, point, uniform_list,};use num_format::{Locale, ToFormattedString};use picker::{Picker, PickerDelegate};use project::DirectoryLister;use release_channel::ReleaseChannel;use schemars::JsonSchema;use serde::Deserialize;use settings::{Settings, SettingsContent};use strum::IntoEnumIterator as _;use theme_settings::ThemeSettings;use ui::{    Banner, Chip, ContextMenu, Divider, ListItem, ListItemSpacing, PopoverMenu, ScrollableHandle,    Switch, ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple,    Tooltip, WithScrollbar, prelude::*,};use util::ResultExt;use vim_mode_setting::VimModeSetting;use workspace::{    Workspace,    item::{Item, ItemEvent},    workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError},};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,    ]); /// Rebuilds an installed dev extension.#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, gpui::Action)]#[action(namespace = zed)]#[serde(deny_unknown_fields)]pub struct RebuildDevExtension {    /// The ID of the dev extension to rebuild.    ///    /// Default: opens a picker if multiple dev extensions are installed.    #[serde(default)]    pub extension_id: Option<String>,} #[derive(Default)]struct DevExtensionNotInstalledError {    extension_id: Option<SharedString>,} impl WorkspaceError for DevExtensionNotInstalledError {    fn primary_message(&self) -> SharedString {        match &self.extension_id {            Some(extension_id) => {                format!("Dev extension '{extension_id}' is not installed.").into()            }            None => "No dev extensions are installed.".into(),        }    }     fn primary_action(&self) -> ErrorAction {        ErrorAction::new("Install Dev Extension", InstallDevExtension)    }     fn severity(&self) -> ErrorSeverity {        ErrorSeverity::Warning    }} fn update_rebuild_dev_extension_visibility(store: &Entity<ExtensionStore>, cx: &mut App) {    let has_dev_extensions = store.read(cx).dev_extensions().next().is_some();    CommandPaletteFilter::update_global(cx, |filter, _cx| {        if has_dev_extensions {            filter.show_action_types(&[TypeId::of::<RebuildDevExtension>()]);        } else {            filter.hide_action_types(&[TypeId::of::<RebuildDevExtension>()]);        }    });} pub fn init(cx: &mut App) {    let store = ExtensionStore::global(cx);    update_rebuild_dev_extension_visibility(&store, cx);    cx.observe(&store, |store, cx| {        update_rebuild_dev_extension_visibility(&store, cx);    })    .detach();     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::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_error(                                            workspace::workspace_error::PortalError::new(                                                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| {                                        // NOTE: using `anyhow::context` here ends up not printing                                        // the error                                        workspace.show_error(                                            format!("Failed to install dev extension: {}", err),                                            cx,                                        );                                    })                                    .ok();                            }                        }                         Some(())                    })                    .detach();            })            .register_action(move |workspace, action: &RebuildDevExtension, window, cx| {                if let Some(target_id) = action.extension_id.as_deref() {                    let extension_id = ExtensionStore::global(cx)                        .read(cx)                        .dev_extensions()                        .find_map(|m| {                            if m.id.as_ref() == target_id {                                Some(m.id.clone())                            } else {                                None                            }                        });                    if let Some(extension_id) = extension_id {                        ExtensionStore::global(cx).update(cx, |store, cx| {                            store.rebuild_dev_extension(extension_id, cx);                        });                    } else {                        workspace.show_error(                            DevExtensionNotInstalledError {                                extension_id: Some(SharedString::from(target_id.to_owned())),                            },                            cx,                        );                    }                    return;                }                 let dev_extensions = ExtensionStore::global(cx)                    .read(cx)                    .dev_extensions()                    .cloned()                    .collect::<Vec<_>>();                 match dev_extensions.len() {                    0 => {                        workspace.show_error(DevExtensionNotInstalledError::default(), cx);                    }                    1 => {                        let extension_id = dev_extensions[0].id.clone();                        ExtensionStore::global(cx).update(cx, |store, cx| {                            store.rebuild_dev_extension(extension_id, cx);                        });                    }                    _ => {                        workspace.toggle_modal(window, cx, |window, cx| {                            let delegate = DevExtensionRebuildPickerDelegate::new(dev_extensions);                            Picker::uniform_list(delegate, window, cx)                        });                    }                }            });         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 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>,} 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(),            };            this.fetch_extensions(                this.search_query(cx),                Some(BTreeSet::from_iter(this.provides_filter)),                None,                cx,            );            this        })

Lines 1–500 of 2096.