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 bcb4885f66..ba8f7f1b88 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 0000000000..4eff13b94b --- /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 0000000000..2e7b95b203 --- /dev/null +++ b/assets/icons/text-cursor.svg @@ -0,0 +1 @@ + diff --git a/assets/settings/default.json b/assets/settings/default.json index 709b418b98..6576281554 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 0a4a408891..c3047c243d 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 d70b1cb227..f370c7dd44 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 01da2ac15b..c1715c68e0 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 e98ce855d7..a234a2c28d 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 97f5c2d437..cca67cb5e7 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 9970c1feee..2634bf1c6f 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 39d8b3036f..88d6df6cad 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 62f4caaa95..0696e4b5ac 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 4de22ee954..3aa407d6a0 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 62f9ebf23d..b83bcb3d91 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 9d70581f91..13c628a023 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 420cb858e6..0f14af3bd1 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 bce517997d..4efef28d0e 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 cf0ad7e598..2b5b3b9756 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 1d3c32daa4..feceae2e6c 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 f78d62f95f..6a0d37e395 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 e45db2ba86..75b02e6826 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 620c21c807..4b41674730 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 ac7d2ba305..d1b329dfb4 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 4e3e6ae1bd..fa54159f61 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 aa2670a688..11f24c0377 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 67204c429a..b752da75db 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 736d972e45..e7720afb6c 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 b735b4c710..1c8071da19 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 e690a07d72..d940bee2ef 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 83a88bbc6b..387aea6143 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, [ + ActivateNextPane, + ActivatePreviousPane, + AddFolderToProject, + CloseAllDocks, + CloseWindow, + Feedback, + FollowNextCollaborator, + NewCenterTerminal, + NewFile, + NewSearch, + NewTerminal, + NewWindow, Open, OpenInTerminal, - NewFile, - NewWindow, - CloseWindow, - AddFolderToProject, - Unfollow, + ReloadActiveItem, SaveAs, SaveWithoutFormat, - ReloadActiveItem, - ActivatePreviousPane, - ActivateNextPane, - FollowNextCollaborator, - NewTerminal, - NewCenterTerminal, - NewSearch, - Feedback, - Welcome, - ToggleZoom, - ToggleLeftDock, - ToggleRightDock, 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 e02c0e1244..9407c39bad 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 8a12df90cb..403b28e360 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 9c62e225c7..7e2c8a096e 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 + ] +);