From 6b9ddbfef2f609603b8574c368fbad517dc62221 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 18 Jun 2024 12:16:54 -0700 Subject: [PATCH] Add more menus to Zed (#12940) ### TODO - [x] Make sure keybinding shows up in pane + menu - [x] Selection tool in the editor toolbar - [x] Application Menu - [x] Add more options to pane + menu - Go to File... - Go to Symbol in Project... - [x] Add go items to the selection tool in the editor: - Go to Symbol in Editor... - Go to Line/Column... - Next Problem - Previous Problem - [x] Fix a bug where modals opened from a context menu aren't focused correclty - [x] Determine if or what needs to be done with project actions: - Difficulty is that these are exposed in the UI via clicking the project name in the titlebar or by right clicking the root entry in the project panel. But they require reading and are two clicks away. Is that sufficient? - Add Folder to Project - Open a new project - Open recent - [x] Get a style pass - [x] Implement style pass - [x] Fix the wrong actions in the selection menu - [x] Show selection tool toggle in the 'editor settings' thing - [x] Put preferences section from the app menu onto the right hand user menu - [x] Add Project menu into app menu to replace 'preferences' section, and put the rest of the actions there - [ ] ~~Adopt `...` convention for opening a surface~~ uncertain what this convention is. - [x] Adopt link styling for webview actions - [x] Set lucide hamburger for menu icon - [x] Gate application menu to only show on Linux and Windows Release Notes: - Added a 'selection and movement' tool to the Editor's toolbar, as well as controls to toggle it and a setting to remove it (`"toolbar": {"selections_menu": true/false }`) - Changed the behavior of the `+` menu in the tab bar to use standard actions and keybindings. Replaced 'New Center Terminal' with 'New Terminal', and 'New Search', with the usual 'Deploy Search'. Also added item-creating actions to this menu. - Added an 'application' menu to the titlebar to Linux and Windows builds of Zed --- Cargo.lock | 2 + assets/icons/rotate_ccw.svg | 1 + assets/icons/text-cursor.svg | 1 + assets/settings/default.json | 4 +- crates/assistant/src/prompt_library.rs | 7 +- crates/breadcrumbs/src/breadcrumbs.rs | 10 +- crates/collab_ui/Cargo.toml | 2 + crates/collab_ui/src/collab_titlebar_item.rs | 183 +++++++++++++++++- .../incoming_call_notification.rs | 11 +- .../project_shared_notification.rs | 11 +- crates/editor/src/actions.rs | 6 + crates/editor/src/editor.rs | 16 ++ crates/editor/src/editor_settings.rs | 9 +- crates/file_finder/src/file_finder.rs | 15 +- crates/file_finder/src/file_finder_tests.rs | 20 +- crates/go_to_line/src/cursor_position.rs | 8 +- crates/go_to_line/src/go_to_line.rs | 8 +- crates/gpui/src/action.rs | 68 ++++++- crates/gpui/src/geometry.rs | 6 + crates/outline/src/outline.rs | 16 +- crates/project_symbols/src/project_symbols.rs | 8 +- .../quick_action_bar/src/quick_action_bar.rs | 137 +++++++++++-- crates/theme/src/settings.rs | 53 ++++- crates/theme/src/theme.rs | 2 +- crates/ui/src/components/context_menu.rs | 90 ++++++--- crates/ui/src/components/icon.rs | 4 + crates/ui/src/components/list/list_item.rs | 29 ++- crates/workspace/src/modal_layer.rs | 4 +- crates/workspace/src/pane.rs | 30 ++- crates/workspace/src/workspace.rs | 62 +++--- crates/zed/src/zed.rs | 49 +++-- crates/zed/src/zed/app_menus.rs | 20 +- crates/zed_actions/src/lib.rs | 18 +- 33 files changed, 712 insertions(+), 198 deletions(-) create mode 100644 assets/icons/rotate_ccw.svg create mode 100644 assets/icons/text-cursor.svg diff --git a/Cargo.lock b/Cargo.lock index bcb4885f666144579ef6925336b1fd4afdbd7831..ba8f7f1b88bc354bb1c0d677200540995cb3e391 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2475,11 +2475,13 @@ dependencies = [ "channel", "client", "collections", + "command_palette", "db", "dev_server_projects", "editor", "emojis", "extensions_ui", + "feedback", "futures 0.3.28", "fuzzy", "gpui", diff --git a/assets/icons/rotate_ccw.svg b/assets/icons/rotate_ccw.svg new file mode 100644 index 0000000000000000000000000000000000000000..4eff13b94b698d0f5ebd3dbe56dabedcdfcf0c74 --- /dev/null +++ b/assets/icons/rotate_ccw.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/text-cursor.svg b/assets/icons/text-cursor.svg new file mode 100644 index 0000000000000000000000000000000000000000..2e7b95b2039455f8a5154af2dc496bbab31d2e52 --- /dev/null +++ b/assets/icons/text-cursor.svg @@ -0,0 +1 @@ + diff --git a/assets/settings/default.json b/assets/settings/default.json index 709b418b9844fe33637da24187f9d1347b684194..65762815547e75dfd735c333b7a32153f62920a9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -183,7 +183,9 @@ // Whether to show breadcrumbs. "breadcrumbs": true, // Whether to show quick action buttons. - "quick_actions": true + "quick_actions": true, + // Whether to show the Selections menu in the editor toolbar + "selections_menu": true }, // Scrollbar related settings "scrollbar": { diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 0a4a40889117121bfcf0385b81a06ea46b26408a..c3047c243d67c6fdb398adf1c534e5c231574d46 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -832,13 +832,8 @@ impl PromptLibrary { impl Render for PromptLibrary { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let (ui_font, ui_font_size) = { - let theme_settings = ThemeSettings::get_global(cx); - (theme_settings.ui_font.clone(), theme_settings.ui_font_size) - }; - + let ui_font = theme::setup_ui_font(cx); let theme = cx.theme().clone(); - cx.set_rem_size(ui_font_size); h_flex() .id("prompt-manager") diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index d70b1cb2278904ca135a1424bba166eba27e7f52..f370c7dd44a96a7d6352a902bda2ee49da2e7864 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -86,10 +86,16 @@ impl Render for Breadcrumbs { .style(ButtonStyle::Subtle) .on_click(move |_, cx| { if let Some(editor) = editor.upgrade() { - outline::toggle(editor, &outline::Toggle, cx) + outline::toggle(editor, &editor::actions::ToggleOutline, cx) } }) - .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)), + .tooltip(|cx| { + Tooltip::for_action( + "Show symbol outline", + &editor::actions::ToggleOutline, + cx, + ) + }), ), None => element // Match the height of the `ButtonLike` in the other arm. diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 01da2ac15b4346d0278580a2db9f7d275e098e86..c1715c68e0ff96ebb7019f22a9c6b7a22410e9c1 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -35,10 +35,12 @@ call.workspace = true channel.workspace = true client.workspace = true collections.workspace = true +command_palette.workspace = true db.workspace = true editor.workspace = true emojis.workspace = true extensions_ui.workspace = true +feedback.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index e98ce855d7d1cb1bb61e1fc4737a5db2211df622..a234a2c28da35ce2138fe83fbaf30f4a09e77ad9 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -10,8 +10,9 @@ use gpui::{ use project::{Project, RepositoryEntry}; use recent_projects::RecentProjects; use rpc::proto::{self, DevServerStatus}; +use settings::Settings; use std::sync::Arc; -use theme::ActiveTheme; +use theme::{ActiveTheme, ThemeSettings}; use ui::{ h_flex, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton, IconName, Indicator, PopoverMenu, TintColor, TitleBar, Tooltip, @@ -73,6 +74,7 @@ impl Render for CollabTitlebarItem { .child( h_flex() .gap_1() + .children(self.render_application_menu(cx)) .children(self.render_project_host(cx)) .child(self.render_project_name(cx)) .children(self.render_project_branch(cx)) @@ -386,8 +388,173 @@ impl CollabTitlebarItem { } } - // resolve if you are in a room -> render_project_owner - // render_project_owner -> resolve if you are in a room -> Option + pub fn render_application_menu(&self, cx: &mut ViewContext) -> Option { + cfg!(not(target_os = "macos")).then(|| { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + let font = cx.text_style().font(); + let font_id = cx.text_system().resolve_font(&font); + let width = cx + .text_system() + .typographic_bounds(font_id, ui_font_size, 'm') + .unwrap() + .size + .width + * 3.0; + + PopoverMenu::new("application-menu") + .menu(move |cx| { + let width = width; + ContextMenu::build(cx, move |menu, _cx| { + let width = width; + menu.header("Workspace") + .action("Open Command Palette", Box::new(command_palette::Toggle)) + .custom_row(move |cx| { + div() + .w_full() + .flex() + .flex_row() + .justify_between() + .cursor(gpui::CursorStyle::Arrow) + .child(Label::new("Buffer Font Size")) + .child( + div() + .flex() + .flex_row() + .child(div().w(px(16.0))) + .child( + IconButton::new( + "reset-buffer-zoom", + IconName::RotateCcw, + ) + .on_click(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::ResetBufferFontSize, + )) + }), + ) + .child( + IconButton::new("--buffer-zoom", IconName::Dash) + .on_click(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::DecreaseBufferFontSize, + )) + }), + ) + .child( + div() + .w(width) + .flex() + .flex_row() + .justify_around() + .child(Label::new( + theme::get_buffer_font_size(cx).to_string(), + )), + ) + .child( + IconButton::new("+-buffer-zoom", IconName::Plus) + .on_click(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::IncreaseBufferFontSize, + )) + }), + ), + ) + .into_any_element() + }) + .custom_row(move |cx| { + div() + .w_full() + .flex() + .flex_row() + .justify_between() + .cursor(gpui::CursorStyle::Arrow) + .child(Label::new("UI Font Size")) + .child( + div() + .flex() + .flex_row() + .child( + IconButton::new( + "reset-ui-zoom", + IconName::RotateCcw, + ) + .on_click(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::ResetUiFontSize, + )) + }), + ) + .child( + IconButton::new("--ui-zoom", IconName::Dash) + .on_click(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::DecreaseUiFontSize, + )) + }), + ) + .child( + div() + .w(width) + .flex() + .flex_row() + .justify_around() + .child(Label::new( + theme::get_ui_font_size(cx).to_string(), + )), + ) + .child( + IconButton::new("+-ui-zoom", IconName::Plus) + .on_click(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::IncreaseUiFontSize, + )) + }), + ), + ) + .into_any_element() + }) + .header("Project") + .action( + "Add Folder to Project...", + Box::new(workspace::AddFolderToProject), + ) + .action("Open a new Project...", Box::new(workspace::Open)) + .action( + "Open Recent Projects...", + Box::new(recent_projects::OpenRecent { + create_new_window: false, + }), + ) + .header("Help") + .action("About Zed", Box::new(zed_actions::About)) + .action("Welcome", Box::new(workspace::Welcome)) + .link( + "Documentation", + Box::new(zed_actions::OpenBrowser { + url: "https://zed.dev/docs".into(), + }), + ) + .action("Give Feedback", Box::new(feedback::GiveFeedback)) + .action("Check for Updates", Box::new(auto_update::Check)) + .action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog)) + .action( + "View Dependency Licenses", + Box::new(zed_actions::OpenLicenses), + ) + .separator() + .action("Quit", Box::new(zed_actions::Quit)) + }) + .into() + }) + .trigger( + IconButton::new("application-menu", ui::IconName::Menu) + .style(ButtonStyle::Subtle) + .tooltip(|cx| Tooltip::text("Open Application Menu", cx)) + .icon_size(IconSize::Small), + ) + .into_any_element() + }) + } pub fn render_project_host(&self, cx: &mut ViewContext) -> Option { if let Some(dev_server) = @@ -743,8 +910,9 @@ impl CollabTitlebarItem { .menu(|cx| { ContextMenu::build(cx, |menu, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) - .action("Extensions", extensions_ui::Extensions.boxed_clone()) - .action("Themes…", theme_selector::Toggle::default().boxed_clone()) + .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) + .action("Themes", theme_selector::Toggle::default().boxed_clone()) + .action("Extensions...", extensions_ui::Extensions.boxed_clone()) .separator() .action("Sign Out", client::SignOut.boxed_clone()) }) @@ -771,8 +939,9 @@ impl CollabTitlebarItem { .menu(|cx| { ContextMenu::build(cx, |menu, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) - .action("Extensions", extensions_ui::Extensions.boxed_clone()) - .action("Themes…", theme_selector::Toggle::default().boxed_clone()) + .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) + .action("Themes", theme_selector::Toggle::default().boxed_clone()) + .action("Extensions...", extensions_ui::Extensions.boxed_clone()) }) .into() }) diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index 97f5c2d437c46271debb2b919b0b322a46b1d425..cca67cb5e7cf212c87a21027aca194ea061dcd25 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -3,9 +3,8 @@ use crate::notifications::collab_notification::CollabNotification; use call::{ActiveCall, IncomingCall}; use futures::StreamExt; use gpui::{prelude::*, AppContext, WindowHandle}; -use settings::Settings; + use std::sync::{Arc, Weak}; -use theme::ThemeSettings; use ui::{prelude::*, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -113,13 +112,7 @@ impl IncomingCallNotification { impl Render for IncomingCallNotification { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - // TODO: Is there a better place for us to initialize the font? - let (ui_font, ui_font_size) = { - let theme_settings = ThemeSettings::get_global(cx); - (theme_settings.ui_font.clone(), theme_settings.ui_font_size) - }; - - cx.set_rem_size(ui_font_size); + let ui_font = theme::setup_ui_font(cx); div().size_full().font(ui_font).child( CollabNotification::new( diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index 9970c1feeea3f097d81401055124d173635b7818..2634bf1c6f49d98e58d8093f7fdd7b4fdbae8cb2 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -4,9 +4,8 @@ use call::{room, ActiveCall}; use client::User; use collections::HashMap; use gpui::{AppContext, Size}; -use settings::Settings; use std::sync::{Arc, Weak}; -use theme::ThemeSettings; + use ui::{prelude::*, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -124,13 +123,7 @@ impl ProjectSharedNotification { impl Render for ProjectSharedNotification { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - // TODO: Is there a better place for us to initialize the font? - let (ui_font, ui_font_size) = { - let theme_settings = ThemeSettings::get_global(cx); - (theme_settings.ui_font.clone(), theme_settings.ui_font_size) - }; - - cx.set_rem_size(ui_font_size); + let ui_font = theme::setup_ui_font(cx); div().size_full().font(ui_font).child( CollabNotification::new( diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 39d8b3036f4bf94305292b9083197160dc42319e..88d6df6cad0dce459d0c3367c222c9947e9e325a 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -1,5 +1,6 @@ //! This module contains all actions supported by [`Editor`]. use super::*; +use gpui::action_as; use util::serde::default_true; #[derive(PartialEq, Clone, Deserialize, Default)] @@ -290,6 +291,7 @@ gpui::actions!( TabPrev, ToggleGitBlame, ToggleGitBlameInline, + ToggleSelectionMenu, ToggleHunkDiff, ToggleInlayHints, ToggleLineNumbers, @@ -304,3 +306,7 @@ gpui::actions!( UniqueLinesCaseSensitive, ] ); + +action_as!(outline, ToggleOutline as Toggle); + +action_as!(go_to_line, ToggleGoToLine as Toggle); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 62f4caaa952cf439c0d7fb9bfcc843954e6aa148..0696e4b5acd0ab03e3f641fedf178b97fbb469a1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -537,6 +537,7 @@ pub struct Editor { show_git_blame_inline: bool, show_git_blame_inline_delay_task: Option>, git_blame_inline_enabled: bool, + show_selection_menu: Option, blame: Option>, blame_subscription: Option, custom_context_menu: Option< @@ -1833,6 +1834,7 @@ impl Editor { custom_context_menu: None, show_git_blame_gutter: false, show_git_blame_inline: false, + show_selection_menu: None, show_git_blame_inline_delay_task: None, git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), blame: None, @@ -10182,6 +10184,20 @@ impl Editor { self.git_blame_inline_enabled } + pub fn toggle_selection_menu(&mut self, _: &ToggleSelectionMenu, cx: &mut ViewContext) { + self.show_selection_menu = self + .show_selection_menu + .map(|show_selections_menu| !show_selections_menu) + .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu)); + + cx.notify(); + } + + pub fn selection_menu_enabled(&self, cx: &AppContext) -> bool { + self.show_selection_menu + .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu) + } + fn start_git_blame(&mut self, user_triggered: bool, cx: &mut ViewContext) { if let Some(project) = self.project.as_ref() { let Some(buffer) = self.buffer().read(cx).as_singleton() else { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 4de22ee954f10bef2bd6a46fe6d34abf93e02b16..3aa407d6a028a9ecb30511cce99e19f2230be717 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -67,6 +67,7 @@ pub enum DoubleClickInMultibuffer { pub struct Toolbar { pub breadcrumbs: bool, pub quick_actions: bool, + pub selections_menu: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -129,6 +130,7 @@ pub struct EditorSettingsContent { /// /// Default: true pub hover_popover_enabled: Option, + /// Whether to pop the completions menu while typing in an editor without /// explicitly requesting it. /// @@ -202,10 +204,15 @@ pub struct ToolbarContent { /// /// Default: true pub breadcrumbs: Option, - /// Whether to display quik action buttons in the editor toolbar. + /// Whether to display quick action buttons in the editor toolbar. /// /// Default: true pub quick_actions: Option, + + /// Whether to show the selections menu in the editor toolbar + /// + /// Default: true + pub selections_menu: Option, } /// Scrollbar related settings diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 62f9ebf23d191db81ba2ea73270bd93a564afb0a..b83bcb3d919ebaa5d54930cc578bdda9d451bd98 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -7,9 +7,9 @@ use collections::{BTreeSet, HashMap}; use editor::{scroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, - Styled, Task, View, ViewContext, VisualContext, WeakView, + actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, + FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, + View, ViewContext, VisualContext, WeakView, }; use itertools::Itertools; use new_path_prompt::NewPathPrompt; @@ -30,13 +30,6 @@ use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::{item::PreviewTabsSettings, ModalView, Workspace}; actions!(file_finder, [SelectPrev]); -impl_actions!(file_finder, [Toggle]); - -#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)] -pub struct Toggle { - #[serde(default)] - pub separate_history: bool, -} impl ModalView for FileFinder {} @@ -52,7 +45,7 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, action: &Toggle, cx| { + workspace.register_action(|workspace, action: &workspace::ToggleFileFinder, cx| { let Some(file_finder) = workspace.active_modal::(cx) else { Self::open(workspace, action.separate_history, cx); return; diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 9d70581f9196375a6c145befe10182da33367051..13c628a023d9662f01bcb1a525599749d6d2b283 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -6,7 +6,7 @@ use gpui::{Entity, TestAppContext, VisualTestContext}; use menu::{Confirm, SelectNext, SelectPrev}; use project::FS_WATCH_LATENCY; use serde_json::json; -use workspace::{AppState, Workspace}; +use workspace::{AppState, ToggleFileFinder, Workspace}; #[ctor::ctor] fn init_logger() { @@ -872,7 +872,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; for expected_selected_index in 0..current_history.len() { - cx.dispatch_action(Toggle::default()); + cx.dispatch_action(ToggleFileFinder::default()); let picker = active_file_picker(&workspace, cx); let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index()); assert_eq!( @@ -881,7 +881,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { ); } - cx.dispatch_action(Toggle::default()); + cx.dispatch_action(ToggleFileFinder::default()); let selected_index = workspace.update(cx, |workspace, cx| { workspace .active_modal::(cx) @@ -1201,7 +1201,7 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) { open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; open_queried_buffer("main", 1, "main.rs", &workspace, cx).await; - cx.dispatch_action(Toggle::default()); + cx.dispatch_action(ToggleFileFinder::default()); let picker = active_file_picker(&workspace, cx); // main.rs is on top, previously used is selected picker.update(cx, |finder, _| { @@ -1653,7 +1653,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav( // Back to navigation with initial shortcut // Open file on modifiers release cx.simulate_modifiers_change(Modifiers::secondary_key()); - cx.dispatch_action(Toggle::default()); + cx.dispatch_action(ToggleFileFinder::default()); cx.simulate_modifiers_change(Modifiers::none()); cx.read(|cx| { let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); @@ -1769,7 +1769,7 @@ async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) { let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - cx.dispatch_action(Toggle::default()); + cx.dispatch_action(ToggleFileFinder::default()); let picker = active_file_picker(&workspace, cx); picker.update(cx, |picker, _| { assert_eq!(picker.delegate.selected_index, 0); @@ -1777,9 +1777,9 @@ async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) { }); // When toggling repeatedly, the picker scrolls to reveal the selected item. - cx.dispatch_action(Toggle::default()); - cx.dispatch_action(Toggle::default()); - cx.dispatch_action(Toggle::default()); + cx.dispatch_action(ToggleFileFinder::default()); + cx.dispatch_action(ToggleFileFinder::default()); + cx.dispatch_action(ToggleFileFinder::default()); picker.update(cx, |picker, _| { assert_eq!(picker.delegate.selected_index, 3); assert_eq!(picker.logical_scroll_top_index(), 3); @@ -1886,7 +1886,7 @@ fn open_file_picker( workspace: &View, cx: &mut VisualTestContext, ) -> View> { - cx.dispatch_action(Toggle { + cx.dispatch_action(ToggleFileFinder { separate_history: true, }); active_file_picker(workspace, cx) diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 420cb858e67fc8b1a5b76134e1373339a835e4c1..0f14af3bd11fa1c0a7942ab2a69ebd50eeb19780 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -134,7 +134,13 @@ impl Render for CursorPosition { }); } })) - .tooltip(|cx| Tooltip::for_action("Go to Line/Column", &crate::Toggle, cx)), + .tooltip(|cx| { + Tooltip::for_action( + "Go to Line/Column", + &editor::actions::ToggleGoToLine, + cx, + ) + }), ) }) } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index bce517997d640b092d8b6a908106507d726b60a0..4efef28d0ed017e48526ccac2a58bf2ca67fd58c 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -3,7 +3,7 @@ pub mod cursor_position; use cursor_position::LineIndicatorFormat; use editor::{scroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle, + div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, }; use settings::Settings; @@ -13,8 +13,6 @@ use ui::{h_flex, prelude::*, v_flex, Label}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::ModalView; -actions!(go_to_line, [Toggle]); - pub fn init(cx: &mut AppContext) { LineIndicatorFormat::register(cx); cx.observe_new_views(GoToLine::register).detach(); @@ -43,7 +41,7 @@ impl GoToLine { fn register(editor: &mut Editor, cx: &mut ViewContext) { let handle = cx.view().downgrade(); editor - .register_action(move |_: &Toggle, cx| { + .register_action(move |_: &editor::actions::ToggleGoToLine, cx| { let Some(editor) = handle.upgrade() else { return; }; @@ -341,7 +339,7 @@ mod tests { workspace: &View, cx: &mut VisualTestContext, ) -> View { - cx.dispatch_action(Toggle); + cx.dispatch_action(editor::actions::ToggleGoToLine); workspace.update(cx, |workspace, cx| { workspace.active_modal::(cx).unwrap().clone() }) diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index cf0ad7e598be137b8f12512ede6088bd6940a8b6..2b5b3b9756e04f10702ecaa80697982db67bf940 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -189,7 +189,7 @@ macro_rules! actions { #[serde(crate = "gpui::private::serde")] pub struct $name; - gpui::__impl_action!($namespace, $name, + gpui::__impl_action!($namespace, $name, $name, fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box> { Ok(Box::new(Self)) } @@ -200,12 +200,48 @@ macro_rules! actions { }; } +/// Defines a unit struct that can be used as an actions, with a name +/// that differs from it's type name. +/// +/// To use more complex data types as actions, and rename them use +/// `impl_action_as!` +#[macro_export] +macro_rules! action_as { + ($namespace:path, $name:ident as $visual_name:tt) => { + #[doc = "The `"] + #[doc = stringify!($name)] + #[doc = "` action, see [`gpui::actions!`]"] + #[derive( + ::std::cmp::PartialEq, + ::std::clone::Clone, + ::std::default::Default, + ::std::fmt::Debug, + gpui::private::serde_derive::Deserialize, + )] + #[serde(crate = "gpui::private::serde")] + pub struct $name; + + gpui::__impl_action!( + $namespace, + $name, + $visual_name, + fn build( + _: gpui::private::serde_json::Value, + ) -> gpui::Result<::std::boxed::Box> { + Ok(Box::new(Self)) + } + ); + + gpui::register_action!($name); + }; +} + /// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize #[macro_export] macro_rules! impl_actions { ($namespace:path, [ $($name:ident),* $(,)? ]) => { $( - gpui::__impl_action!($namespace, $name, + gpui::__impl_action!($namespace, $name, $name, fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box> { Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::(value)?)) } @@ -216,17 +252,39 @@ macro_rules! impl_actions { }; } +/// Implements the Action trait for a struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize +/// Allows you to rename the action visually, without changing the struct's name +#[macro_export] +macro_rules! impl_action_as { + ($namespace:path, $name:ident as $visual_name:tt ) => { + gpui::__impl_action!( + $namespace, + $name, + $visual_name, + fn build( + value: gpui::private::serde_json::Value, + ) -> gpui::Result<::std::boxed::Box> { + Ok(std::boxed::Box::new( + gpui::private::serde_json::from_value::(value)?, + )) + } + ); + + gpui::register_action!($name); + }; +} + #[doc(hidden)] #[macro_export] macro_rules! __impl_action { - ($namespace:path, $name:ident, $build:item) => { + ($namespace:path, $name:ident, $visual_name:tt, $build:item) => { impl gpui::Action for $name { fn name(&self) -> &'static str { concat!( stringify!($namespace), "::", - stringify!($name), + stringify!($visual_name), ) } @@ -237,7 +295,7 @@ macro_rules! __impl_action { concat!( stringify!($namespace), "::", - stringify!($name), + stringify!($visual_name), ) } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 1d3c32daa442c91ffc9823a679cc6441e24be81f..feceae2e6cfd3047a8c52a1acbab7519d297df2d 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -2157,6 +2157,12 @@ impl From for Radians { #[repr(transparent)] pub struct Pixels(pub f32); +impl std::fmt::Display for Pixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{}px", self.0)) + } +} + impl std::ops::Div for Pixels { type Output = f32; diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index f78d62f95f1a6a1ee17eb3ef568d25638ae515ee..6a0d37e3952f12147c6cce2022394c5c0547f498 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,9 +1,11 @@ -use editor::{scroll::Autoscroll, Anchor, AnchorRangeExt, Editor, EditorMode}; +use editor::{ + actions::ToggleOutline, scroll::Autoscroll, Anchor, AnchorRangeExt, Editor, EditorMode, +}; use fuzzy::StringMatch; use gpui::{ - actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, - HighlightStyle, ParentElement, Point, Render, Styled, Task, View, ViewContext, VisualContext, - WeakView, WindowContext, + div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle, + ParentElement, Point, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use language::Outline; use ordered_float::OrderedFloat; @@ -18,13 +20,11 @@ use ui::{prelude::*, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{DismissDecision, ModalView}; -actions!(outline, [Toggle]); - pub fn init(cx: &mut AppContext) { cx.observe_new_views(OutlineView::register).detach(); } -pub fn toggle(editor: View, _: &Toggle, cx: &mut WindowContext) { +pub fn toggle(editor: View, _: &ToggleOutline, cx: &mut WindowContext) { let outline = editor .read(cx) .buffer() @@ -423,7 +423,7 @@ mod tests { workspace: &View, cx: &mut VisualTestContext, ) -> View> { - cx.dispatch_action(Toggle); + cx.dispatch_action(ToggleOutline); workspace.update(cx, |workspace, cx| { workspace .active_modal::(cx) diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index e45db2ba8649053a9d9c099b80598f93d142c4f2..75b02e6826142d9b1412f1471b4551064586d929 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -1,8 +1,8 @@ use editor::{scroll::Autoscroll, styled_runs_for_code_label, Bias, Editor}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task, - View, ViewContext, WeakView, WindowContext, + rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task, View, + ViewContext, WeakView, WindowContext, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -15,12 +15,10 @@ use workspace::{ Workspace, }; -actions!(project_symbols, [Toggle]); - pub fn init(cx: &mut AppContext) { cx.observe_new_views( |workspace: &mut Workspace, _: &mut ViewContext| { - workspace.register_action(|workspace, _: &Toggle, cx| { + workspace.register_action(|workspace, _: &workspace::ToggleProjectSymbols, cx| { let project = workspace.project().clone(); let handle = cx.view().downgrade(); workspace.toggle_modal(cx, move |cx| { diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 620c21c807ae2a3139fbbaca3236ec80781c2f39..4b41674730cc90a8a92910f8c47c40d89b7f5d72 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -1,5 +1,10 @@ use assistant::assistant_settings::AssistantSettings; use assistant::{AssistantPanel, InlineAssist}; +use editor::actions::{ + AddSelectionAbove, AddSelectionBelow, DuplicateLineDown, GoToDiagnostic, GoToHunk, + GoToPrevDiagnostic, GoToPrevHunk, MoveLineDown, MoveLineUp, SelectAll, SelectLargerSyntaxNode, + SelectNext, SelectSmallerSyntaxNode, ToggleGoToLine, ToggleOutline, +}; use editor::{Editor, EditorSettings}; use gpui::{ @@ -18,6 +23,7 @@ use workspace::{ pub struct QuickActionBar { buffer_search_bar: View, toggle_settings_menu: Option>, + toggle_selections_menu: Option>, active_item: Option>, _inlay_hints_enabled_subscription: Option, workspace: WeakView, @@ -33,6 +39,7 @@ impl QuickActionBar { let mut this = Self { buffer_search_bar, toggle_settings_menu: None, + toggle_selections_menu: None, active_item: None, _inlay_hints_enabled_subscription: None, workspace: workspace.weak_handle(), @@ -86,22 +93,43 @@ impl Render for QuickActionBar { return div().id("empty quick action bar"); }; - let search_button = Some(QuickActionBarButton::new( - "toggle buffer search", - IconName::MagnifyingGlass, - !self.buffer_search_bar.read(cx).is_dismissed(), - Box::new(buffer_search::Deploy::find()), - "Buffer Search", - { - let buffer_search_bar = self.buffer_search_bar.clone(); - move |_, cx| { - buffer_search_bar.update(cx, |search_bar, cx| { - search_bar.toggle(&buffer_search::Deploy::find(), cx) - }); - } - }, - )) - .filter(|_| editor.is_singleton(cx)); + let ( + selection_menu_enabled, + inlay_hints_enabled, + supports_inlay_hints, + git_blame_inline_enabled, + ) = { + let editor = editor.read(cx); + let selection_menu_enabled = editor.selection_menu_enabled(cx); + let inlay_hints_enabled = editor.inlay_hints_enabled(); + let supports_inlay_hints = editor.supports_inlay_hints(cx); + let git_blame_inline_enabled = editor.git_blame_inline_enabled(); + + ( + selection_menu_enabled, + inlay_hints_enabled, + supports_inlay_hints, + git_blame_inline_enabled, + ) + }; + + let search_button = editor.is_singleton(cx).then(|| { + QuickActionBarButton::new( + "toggle buffer search", + IconName::MagnifyingGlass, + !self.buffer_search_bar.read(cx).is_dismissed(), + Box::new(buffer_search::Deploy::find()), + "Buffer Search", + { + let buffer_search_bar = self.buffer_search_bar.clone(); + move |_, cx| { + buffer_search_bar.update(cx, |search_bar, cx| { + search_bar.toggle(&buffer_search::Deploy::find(), cx) + }); + } + }, + ) + }); let assistant_button = QuickActionBarButton::new( "toggle inline assistant", @@ -121,6 +149,55 @@ impl Render for QuickActionBar { }, ); + let editor_selections_dropdown = selection_menu_enabled.then(|| { + IconButton::new("toggle_editor_selections_icon", IconName::TextCursor) + .size(ButtonSize::Compact) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .selected(self.toggle_selections_menu.is_some()) + .on_click({ + let focus = editor.focus_handle(cx); + cx.listener(move |quick_action_bar, _, cx| { + let focus = focus.clone(); + let menu = ContextMenu::build(cx, move |menu, _| { + menu.context(focus.clone()) + .action("Select All", Box::new(SelectAll)) + .action( + "Select Next Occurrence", + Box::new(SelectNext { + replace_newest: false, + }), + ) + .action("Expand Selection", Box::new(SelectLargerSyntaxNode)) + .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode)) + .action("Add Cursor Above", Box::new(AddSelectionAbove)) + .action("Add Cursor Below", Box::new(AddSelectionBelow)) + .separator() + .action("Go to Symbol", Box::new(ToggleOutline)) + .action("Go to Line/Column", Box::new(ToggleGoToLine)) + .separator() + .action("Next Problem", Box::new(GoToDiagnostic)) + .action("Previous Problem", Box::new(GoToPrevDiagnostic)) + .separator() + .action("Next Hunk", Box::new(GoToHunk)) + .action("Previous Hunk", Box::new(GoToPrevHunk)) + .separator() + .action("Move Line Up", Box::new(MoveLineUp)) + .action("Move Line Down", Box::new(MoveLineDown)) + .action("Duplicate Selection", Box::new(DuplicateLineDown)) + }); + cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| { + quick_action_bar.toggle_selections_menu = None; + }) + .detach(); + quick_action_bar.toggle_selections_menu = Some(menu); + }) + }) + .when(self.toggle_selections_menu.is_none(), |this| { + this.tooltip(|cx| Tooltip::text("Selection Controls", cx)) + }) + }); + let editor_settings_dropdown = IconButton::new("toggle_editor_settings_icon", IconName::Sliders) .size(ButtonSize::Compact) @@ -130,10 +207,6 @@ impl Render for QuickActionBar { .on_click({ let editor = editor.clone(); cx.listener(move |quick_action_bar, _, cx| { - let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); - let supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); - let git_blame_inline_enabled = editor.read(cx).git_blame_inline_enabled(); - let menu = ContextMenu::build(cx, |mut menu, _| { if supports_inlay_hints { menu = menu.toggleable_entry( @@ -171,6 +244,23 @@ impl Render for QuickActionBar { }, ); + menu = menu.toggleable_entry( + "Show Selection Menu", + selection_menu_enabled, + Some(editor::actions::ToggleSelectionMenu.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor.update(cx, |editor, cx| { + editor.toggle_selection_menu( + &editor::actions::ToggleSelectionMenu, + cx, + ) + }); + } + }, + ); + menu }); cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| { @@ -191,6 +281,7 @@ impl Render for QuickActionBar { h_flex() .gap_1p5() .children(search_button) + .children(editor_selections_dropdown) .when(AssistantSettings::get_global(cx).button, |bar| { bar.child(assistant_button) }), @@ -202,6 +293,12 @@ impl Render for QuickActionBar { el.child(Self::render_menu_overlay(toggle_settings_menu)) }, ) + .when_some( + self.toggle_selections_menu.as_ref(), + |el, toggle_selections_menu| { + el.child(Self::render_menu_overlay(toggle_selections_menu)) + }, + ) } } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index ac7d2ba3051d4017c16de6c3d02f46b354abdefc..d1b329dfb49faf1a1acae030e93a0b9eebea4a6f 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -4,7 +4,7 @@ use anyhow::Result; use derive_more::{Deref, DerefMut}; use gpui::{ px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Global, Pixels, Subscription, - ViewContext, + ViewContext, WindowContext, }; use refineable::Refineable; use schemars::{ @@ -167,6 +167,11 @@ pub(crate) struct AdjustedBufferFontSize(Pixels); impl Global for AdjustedBufferFontSize {} +#[derive(Default)] +pub(crate) struct AdjustedUiFontSize(Pixels); + +impl Global for AdjustedUiFontSize {} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(untagged)] pub enum ThemeSelection { @@ -358,7 +363,13 @@ pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels { .max(MIN_FONT_SIZE) } -pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { +pub fn get_buffer_font_size(cx: &AppContext) -> Pixels { + let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; + cx.try_global::() + .map_or(buffer_font_size, |adjusted_size| adjusted_size.0) +} + +pub fn adjust_buffer_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; let mut adjusted_size = cx .try_global::() @@ -370,13 +381,49 @@ pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { cx.refresh(); } -pub fn reset_font_size(cx: &mut AppContext) { +pub fn reset_buffer_font_size(cx: &mut AppContext) { if cx.has_global::() { cx.remove_global::(); cx.refresh(); } } +pub fn setup_ui_font(cx: &mut WindowContext) -> gpui::Font { + let (ui_font, ui_font_size) = { + let theme_settings = ThemeSettings::get_global(cx); + let font = theme_settings.ui_font.clone(); + (font, get_ui_font_size(cx)) + }; + + cx.set_rem_size(ui_font_size); + ui_font +} + +pub fn get_ui_font_size(cx: &WindowContext) -> Pixels { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + cx.try_global::() + .map_or(ui_font_size, |adjusted_size| adjusted_size.0) +} + +pub fn adjust_ui_font_size(cx: &mut WindowContext, f: fn(&mut Pixels)) { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + let mut adjusted_size = cx + .try_global::() + .map_or(ui_font_size, |adjusted_size| adjusted_size.0); + + f(&mut adjusted_size); + adjusted_size = adjusted_size.max(MIN_FONT_SIZE); + cx.set_global(AdjustedUiFontSize(adjusted_size)); + cx.refresh(); +} + +pub fn reset_ui_font_size(cx: &mut WindowContext) { + if cx.has_global::() { + cx.remove_global::(); + cx.refresh(); + } +} + impl settings::Settings for ThemeSettings { const KEY: Option<&'static str> = None; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4e3e6ae1bdc1ff7bc73793ebb883eaba2b00f769..fa54159f610318f983017fa9cbc343ba7edfcfd7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -84,7 +84,7 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; if buffer_font_size != prev_buffer_font_size { prev_buffer_font_size = buffer_font_size; - reset_font_size(cx); + reset_buffer_font_size(cx); } }) .detach(); diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index aa2670a6883420ac4796b29c277c1f05169ee9b7..11f24c037725130b74e7841e14f018fb23d22504 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -18,12 +18,13 @@ enum ContextMenuItem { toggled: Option, label: SharedString, icon: Option, - handler: Rc, + handler: Rc, &mut WindowContext)>, action: Option>, }, CustomEntry { entry_render: Box AnyElement>, - handler: Rc, + handler: Rc, &mut WindowContext)>, + selectable: bool, }, } @@ -97,7 +98,7 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry { toggled: None, label: label.into(), - handler: Rc::new(handler), + handler: Rc::new(move |_, cx| handler(cx)), icon: None, action, }); @@ -114,13 +115,25 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry { toggled: Some(toggled), label: label.into(), - handler: Rc::new(handler), + handler: Rc::new(move |_, cx| handler(cx)), icon: None, action, }); self } + pub fn custom_row( + mut self, + entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static, + ) -> Self { + self.items.push(ContextMenuItem::CustomEntry { + entry_render: Box::new(entry_render), + handler: Rc::new(|_, _| {}), + selectable: false, + }); + self + } + pub fn custom_entry( mut self, entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static, @@ -128,7 +141,8 @@ impl ContextMenu { ) -> Self { self.items.push(ContextMenuItem::CustomEntry { entry_render: Box::new(entry_render), - handler: Rc::new(handler), + handler: Rc::new(move |_, cx| handler(cx)), + selectable: true, }); self } @@ -138,7 +152,13 @@ impl ContextMenu { toggled: None, label: label.into(), action: Some(action.boxed_clone()), - handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), + + handler: Rc::new(move |context, cx| { + if let Some(context) = &context { + cx.focus(context); + } + cx.dispatch_action(action.boxed_clone()); + }), icon: None, }); self @@ -148,19 +168,21 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry { toggled: None, label: label.into(), + action: Some(action.boxed_clone()), - handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), + handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())), icon: Some(IconName::Link), }); self } pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + let context = self.action_context.as_ref(); match self.selected_index.and_then(|ix| self.items.get(ix)) { Some( ContextMenuItem::Entry { handler, .. } | ContextMenuItem::CustomEntry { handler, .. }, - ) => (handler)(cx), + ) => (handler)(context, cx), _ => {} } @@ -260,7 +282,12 @@ impl ContextMenu { impl ContextMenuItem { fn is_selectable(&self) -> bool { - matches!(self, Self::Entry { .. } | Self::CustomEntry { .. }) + match self { + ContextMenuItem::Separator => false, + ContextMenuItem::Header(_) => false, + ContextMenuItem::Entry { .. } => true, + ContextMenuItem::CustomEntry { selectable, .. } => *selectable, + } } } @@ -360,32 +387,47 @@ impl Render for ContextMenu { .map(|binding| div().ml_4().child(binding)) })), ) - .on_click(move |_, cx| { - handler(cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); - }) - .ok(); + .on_click({ + let context = self.action_context.clone(); + move |_, cx| { + handler(context.as_ref(), cx); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } }) .into_any_element() } ContextMenuItem::CustomEntry { entry_render, handler, + selectable, } => { let handler = handler.clone(); let menu = cx.view().downgrade(); ListItem::new(ix) .inset(true) - .selected(Some(ix) == self.selected_index) - .on_click(move |_, cx| { - handler(cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); - }) - .ok(); + .selected(if *selectable { + Some(ix) == self.selected_index + } else { + false + }) + .selectable(*selectable) + .on_click({ + let context = self.action_context.clone(); + let selectable = *selectable; + move |_, cx| { + if selectable { + handler(context.as_ref(), cx); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } + } }) .child(entry_render(cx)) .into_any_element() diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 67204c429a67dc09fbef92193f4cf3ebc8202c58..b752da75db6befa67c6b2f805b477b2db7824983 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -174,6 +174,7 @@ pub enum IconName { Rerun, Return, Reveal, + RotateCcw, RotateCw, Save, Screen, @@ -199,6 +200,7 @@ pub enum IconName { SupermavenInit, Tab, Terminal, + TextCursor, Trash, TriangleRight, Update, @@ -307,6 +309,7 @@ impl IconName { IconName::Rerun => "icons/rerun.svg", IconName::Return => "icons/return.svg", IconName::RotateCw => "icons/rotate_cw.svg", + IconName::RotateCcw => "icons/rotate_ccw.svg", IconName::Save => "icons/save.svg", IconName::Screen => "icons/desktop.svg", IconName::SelectAll => "icons/select_all.svg", @@ -331,6 +334,7 @@ impl IconName { IconName::SupermavenInit => "icons/supermaven_init.svg", IconName::Tab => "icons/tab.svg", IconName::Terminal => "icons/terminal.svg", + IconName::TextCursor => "icons/text-cursor.svg", IconName::Trash => "icons/trash.svg", IconName::TriangleRight => "icons/triangle_right.svg", IconName::Update => "icons/update.svg", diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 736d972e45b2acbd3f7846e71904713bf3e888ff..e7720afb6c28ebe51e2386ab9780e9e60d122c3d 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -35,6 +35,7 @@ pub struct ListItem { tooltip: Option AnyView + 'static>>, on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, + selectable: bool, } impl ListItem { @@ -56,6 +57,7 @@ impl ListItem { on_toggle: None, tooltip: None, children: SmallVec::new(), + selectable: true, } } @@ -64,6 +66,11 @@ impl ListItem { self } + pub fn selectable(mut self, has_hover: bool) -> Self { + self.selectable = has_hover; + self + } + pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { self.on_click = Some(Box::new(handler)); self @@ -164,10 +171,12 @@ impl RenderOnce for ListItem { // this.border_1() // .border_color(cx.theme().colors().border_focused) // }) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) + .when(self.selectable, |this| { + this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) }) }) .child( @@ -189,10 +198,14 @@ impl RenderOnce for ListItem { // this.border_1() // .border_color(cx.theme().colors().border_focused) // }) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) + .when(self.selectable, |this| { + this.hover(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) }) }) .when_some(self.on_click, |this, on_click| { diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index b735b4c7109109f129e8e2a9976813eb39391f40..1c8071da198b122467720f541286e3d011589011 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -96,7 +96,9 @@ impl ModalLayer { previous_focus_handle: cx.focused(), focus_handle, }); - cx.focus_view(&new_modal); + cx.defer(move |_, cx| { + cx.focus_view(&new_modal); + }); cx.notify(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e690a07d7225eebb0fe57c4c053299b3d8408528..d940bee2efcd6689cb229a63a2458fdaeeefa8ae 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -5,8 +5,8 @@ use crate::{ }, toolbar::Toolbar, workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, - CloseWindow, NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible, - SplitDirection, ToggleZoom, Workspace, + CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenTerminal, OpenVisible, SplitDirection, + ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, }; use anyhow::Result; use collections::{BTreeSet, HashMap, HashSet, VecDeque}; @@ -366,8 +366,24 @@ impl Pane { .on_click(cx.listener(|pane, _, cx| { let menu = ContextMenu::build(cx, |menu, _| { menu.action("New File", NewFile.boxed_clone()) - .action("New Terminal", NewCenterTerminal.boxed_clone()) - .action("New Search", NewSearch.boxed_clone()) + .action( + "Open File", + ToggleFileFinder::default().boxed_clone(), + ) + .separator() + .action( + "Search Project", + DeploySearch { + replace_enabled: false, + } + .boxed_clone(), + ) + .action( + "Search Symbols", + ToggleProjectSymbols.boxed_clone(), + ) + .separator() + .action("New Terminal", NewTerminal.boxed_clone()) }); cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| { pane.focus(cx); @@ -1818,7 +1834,11 @@ impl Pane { .track_scroll(self.tab_bar_scroll_handle.clone()) .when( self.display_nav_history_buttons.unwrap_or_default(), - |tab_bar| tab_bar.start_children(vec![navigate_backward, navigate_forward]), + |tab_bar| { + tab_bar + .start_child(navigate_backward) + .start_child(navigate_forward) + }, ) .when(self.has_focus(cx), |tab_bar| { tab_bar.end_child({ diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 83a88bbc6b0da393c21d39745deff257181f102e..387aea6143714ceb9b78593b700d33bee4990e2d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -27,11 +27,11 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, canvas, impl_actions, point, relative, size, Action, AnyElement, AnyView, AnyWeakView, - AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DragMoveEvent, Entity as _, EntityId, - EventEmitter, FocusHandle, FocusableView, Global, KeyContext, Keystroke, ManagedView, Model, - ModelContext, PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, - WeakView, WindowBounds, WindowHandle, WindowOptions, + action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, Action, + AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, + DragMoveEvent, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, + KeyContext, Keystroke, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, + Render, Size, Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions, }; use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, @@ -112,30 +112,30 @@ pub struct RemoveWorktreeFromProject(pub WorktreeId); actions!( workspace, [ - Open, - OpenInTerminal, - NewFile, - NewWindow, - CloseWindow, - AddFolderToProject, - Unfollow, - SaveAs, - SaveWithoutFormat, - ReloadActiveItem, - ActivatePreviousPane, ActivateNextPane, + ActivatePreviousPane, + AddFolderToProject, + CloseAllDocks, + CloseWindow, + Feedback, FollowNextCollaborator, - NewTerminal, NewCenterTerminal, + NewFile, NewSearch, - Feedback, - Welcome, - ToggleZoom, - ToggleLeftDock, - ToggleRightDock, + NewTerminal, + NewWindow, + Open, + OpenInTerminal, + ReloadActiveItem, + SaveAs, + SaveWithoutFormat, ToggleBottomDock, ToggleCenteredLayout, - CloseAllDocks, + ToggleLeftDock, + ToggleRightDock, + ToggleZoom, + Unfollow, + Welcome, ] ); @@ -188,6 +188,16 @@ pub struct Reload { pub binary_path: Option, } +action_as!(project_symbols, ToggleProjectSymbols as Toggle); + +#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)] +pub struct ToggleFileFinder { + #[serde(default)] + pub separate_history: bool, +} + +impl_action_as!(file_finder, ToggleFileFinder as Toggle); + impl_actions!( workspace, [ @@ -4144,14 +4154,10 @@ impl Render for Workspace { } else { (None, None) }; - let (ui_font, ui_font_size) = { - let theme_settings = ThemeSettings::get_global(cx); - (theme_settings.ui_font.clone(), theme_settings.ui_font_size) - }; + let ui_font = theme::setup_ui_font(cx); let theme = cx.theme().clone(); let colors = theme.colors(); - cx.set_rem_size(ui_font_size); self.actions(div(), cx) .key_context(context) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e02c0e12442e453628721bb0e08af0eb6d33cc63..9407c39bade66cc23fa8f95524b6c7ae70b4ed1d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -52,22 +52,15 @@ use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit}; actions!( zed, [ - About, DebugElements, - DecreaseBufferFontSize, Hide, HideOthers, - IncreaseBufferFontSize, Minimize, OpenDefaultKeymap, OpenDefaultSettings, - OpenKeymap, - OpenLicenses, OpenLocalSettings, OpenLocalTasks, OpenTasks, - OpenTelemetryLog, - ResetBufferFontSize, ResetDatabase, ShowAll, ToggleFullScreen, @@ -252,13 +245,33 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { OpenListener::global(cx).open_urls(vec![action.url.clone()]) }) .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) - .register_action(move |_, _: &IncreaseBufferFontSize, cx| { - theme::adjust_font_size(cx, |size| *size += px(1.0)) + .register_action(move |_, _: &zed_actions::IncreaseBufferFontSize, cx| { + theme::adjust_buffer_font_size(cx, |size| *size += px(1.0)) }) - .register_action(move |_, _: &DecreaseBufferFontSize, cx| { - theme::adjust_font_size(cx, |size| *size -= px(1.0)) + .register_action(move |_, _: &zed_actions::DecreaseBufferFontSize, cx| { + theme::adjust_buffer_font_size(cx, |size| *size -= px(1.0)) + }) + .register_action(move |_, _: &zed_actions::ResetBufferFontSize, cx| { + theme::reset_buffer_font_size(cx) + }) + .register_action(move |_, _: &zed_actions::IncreaseUiFontSize, cx| { + theme::adjust_ui_font_size(cx, |size| *size += px(1.0)) + }) + .register_action(move |_, _: &zed_actions::DecreaseUiFontSize, cx| { + theme::adjust_ui_font_size(cx, |size| *size -= px(1.0)) + }) + .register_action(move |_, _: &zed_actions::ResetUiFontSize, cx| { + theme::reset_ui_font_size(cx) + }) + .register_action(move |_, _: &zed_actions::IncreaseBufferFontSize, cx| { + theme::adjust_buffer_font_size(cx, |size| *size += px(1.0)) + }) + .register_action(move |_, _: &zed_actions::DecreaseBufferFontSize, cx| { + theme::adjust_buffer_font_size(cx, |size| *size -= px(1.0)) + }) + .register_action(move |_, _: &zed_actions::ResetBufferFontSize, cx| { + theme::reset_buffer_font_size(cx) }) - .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx)) .register_action(|_, _: &install_cli::Install, cx| { cx.spawn(|workspace, mut cx| async move { if cfg!(target_os = "linux") { @@ -323,7 +336,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { .register_action(|workspace, _: &OpenLog, cx| { open_log_file(workspace, cx); }) - .register_action(|workspace, _: &OpenLicenses, cx| { + .register_action(|workspace, _: &zed_actions::OpenLicenses, cx| { open_bundled_file( workspace, asset_str::("licenses.md"), @@ -334,14 +347,16 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .register_action( move |workspace: &mut Workspace, - _: &OpenTelemetryLog, + _: &zed_actions::OpenTelemetryLog, cx: &mut ViewContext| { open_telemetry_log_file(workspace, cx); }, ) .register_action( - move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { - open_settings_file(paths::keymap_file(), Rope::default, cx); + move |_: &mut Workspace, + _: &zed_actions::OpenKeymap, + cx: &mut ViewContext| { + open_settings_file(&paths::keymap_file(), Rope::default, cx); }, ) .register_action( @@ -485,7 +500,7 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewCo }); } -fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { +fn about(_: &mut Workspace, _: &zed_actions::About, cx: &mut gpui::ViewContext) { let release_channel = ReleaseChannel::global(cx).display_name(); let version = env!("CARGO_PKG_VERSION"); let message = format!("{release_channel} {version}"); diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 8a12df90cb0b3edd92e95cedb382f285cd659207..403b28e360fef9660c11f19e53a0e4538daf94f1 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -9,14 +9,14 @@ pub fn app_menus() -> Vec> { Menu { name: "Zed", items: vec![ - MenuItem::action("About Zed…", super::About), + MenuItem::action("About Zed…", zed_actions::About), MenuItem::action("Check for Updates", auto_update::Check), MenuItem::separator(), MenuItem::submenu(Menu { name: "Preferences", items: vec![ MenuItem::action("Open Settings", super::OpenSettings), - MenuItem::action("Open Key Bindings", super::OpenKeymap), + MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), MenuItem::action("Open Local Settings", super::OpenLocalSettings), @@ -104,9 +104,9 @@ pub fn app_menus() -> Vec> { Menu { name: "View", items: vec![ - MenuItem::action("Zoom In", super::IncreaseBufferFontSize), - MenuItem::action("Zoom Out", super::DecreaseBufferFontSize), - MenuItem::action("Reset Zoom", super::ResetBufferFontSize), + MenuItem::action("Zoom In", zed_actions::IncreaseBufferFontSize), + MenuItem::action("Zoom Out", zed_actions::DecreaseBufferFontSize), + MenuItem::action("Reset Zoom", zed_actions::ResetBufferFontSize), MenuItem::separator(), MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), @@ -139,10 +139,10 @@ pub fn app_menus() -> Vec> { MenuItem::separator(), MenuItem::action("Command Palette...", command_palette::Toggle), MenuItem::separator(), - MenuItem::action("Go to File...", file_finder::Toggle::default()), + MenuItem::action("Go to File...", workspace::ToggleFileFinder::default()), // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), - MenuItem::action("Go to Symbol in Editor...", outline::Toggle), - MenuItem::action("Go to Line/Column...", go_to_line::Toggle), + MenuItem::action("Go to Symbol in Editor...", editor::actions::ToggleOutline), + MenuItem::action("Go to Line/Column...", editor::actions::ToggleGoToLine), MenuItem::separator(), MenuItem::action("Go to Definition", editor::actions::GoToDefinition), MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition), @@ -163,8 +163,8 @@ pub fn app_menus() -> Vec> { Menu { name: "Help", items: vec![ - MenuItem::action("View Telemetry", super::OpenTelemetryLog), - MenuItem::action("View Dependency Licenses", super::OpenLicenses), + MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog), + MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses), MenuItem::action("Show Welcome", workspace::Welcome), MenuItem::action("Give Feedback...", feedback::GiveFeedback), MenuItem::separator(), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 9c62e225c73c1edd704924344469c23ceac2694d..7e2c8a096e9a768ce88bd08f3e4296f457ac7ba1 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -22,4 +22,20 @@ pub struct OpenZedUrl { impl_actions!(zed, [OpenBrowser, OpenZedUrl]); -actions!(zed, [OpenSettings, Quit]); +actions!( + zed, + [ + OpenSettings, + Quit, + OpenKeymap, + About, + OpenLicenses, + OpenTelemetryLog, + DecreaseBufferFontSize, + IncreaseBufferFontSize, + ResetBufferFontSize, + DecreaseUiFontSize, + IncreaseUiFontSize, + ResetUiFontSize + ] +);