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, }); let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::()); 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.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) => { workspace_handle .update(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(); } 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", } } #[derive(Clone)] pub enum ExtensionStatus { NotInstalled, Installing, Upgrading, Installed(Arc), 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> { static KEYWORDS_BY_FEATURE: OnceLock>> = 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