zed/crates/gpui/src/action.rs
Mikayla Maki 6b9ddbfef2
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
2024-06-18 12:16:54 -07:00

326 lines
10 KiB
Rust

use crate::SharedString;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
pub use no_action::NoAction;
use serde_json::json;
use std::any::{Any, TypeId};
/// Actions are used to implement keyboard-driven UI.
/// When you declare an action, you can bind keys to the action in the keymap and
/// listeners for that action in the element tree.
///
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
/// action for each listed action name in the given namespace.
/// ```rust
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
/// ```
/// More complex data types can also be actions, providing they implement Clone, PartialEq,
/// and serde_derive::Deserialize.
/// Use `impl_actions!` to automatically implement the action in the given namespace.
/// ```
/// #[derive(Clone, PartialEq, serde_derive::Deserialize)]
/// pub struct SelectNext {
/// pub replace_newest: bool,
/// }
/// impl_actions!(editor, [SelectNext]);
/// ```
///
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
/// macro, which only generates the code needed to register your action before `main`.
///
/// ```
/// #[derive(gpui::private::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
/// pub struct Paste {
/// pub content: SharedString,
/// }
///
/// impl gpui::Action for Paste {
/// ///...
/// }
/// register_action!(Paste);
/// ```
pub trait Action: 'static + Send {
/// Clone the action into a new box
fn boxed_clone(&self) -> Box<dyn Action>;
/// Cast the action to the any type
fn as_any(&self) -> &dyn Any;
/// Do a partial equality check on this action and the other
fn partial_eq(&self, action: &dyn Action) -> bool;
/// Get the name of this action, for displaying in UI
fn name(&self) -> &str;
/// Get the name of this action for debugging
fn debug_name() -> &'static str
where
Self: Sized;
/// Build this action from a JSON value. This is used to construct actions from the keymap.
/// A value of `{}` will be passed for actions that don't have any parameters.
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where
Self: Sized;
}
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("name", &self.name())
.finish()
}
}
impl dyn Action {
/// Get the type id of this action
pub fn type_id(&self) -> TypeId {
self.as_any().type_id()
}
}
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
pub(crate) struct ActionRegistry {
builders_by_name: HashMap<SharedString, ActionBuilder>,
names_by_type_id: HashMap<TypeId, SharedString>,
all_names: Vec<SharedString>, // So we can return a static slice.
}
impl Default for ActionRegistry {
fn default() -> Self {
let mut this = ActionRegistry {
builders_by_name: Default::default(),
names_by_type_id: Default::default(),
all_names: Default::default(),
};
this.load_actions();
this
}
}
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub type MacroActionBuilder = fn() -> ActionData;
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub struct ActionData {
pub name: &'static str,
pub type_id: TypeId,
pub build: ActionBuilder,
}
/// This constant must be public to be accessible from other crates.
/// But its existence is an implementation detail and should not be used directly.
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __GPUI_ACTIONS: [MacroActionBuilder];
impl ActionRegistry {
/// Load all registered actions into the registry.
pub(crate) fn load_actions(&mut self) {
for builder in __GPUI_ACTIONS {
let action = builder();
self.insert_action(action);
}
}
#[cfg(test)]
pub(crate) fn load_action<A: Action>(&mut self) {
self.insert_action(ActionData {
name: A::debug_name(),
type_id: TypeId::of::<A>(),
build: A::build,
});
}
fn insert_action(&mut self, action: ActionData) {
let name: SharedString = action.name.into();
self.builders_by_name.insert(name.clone(), action.build);
self.names_by_type_id.insert(action.type_id, name.clone());
self.all_names.push(name);
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
let name = self
.names_by_type_id
.get(type_id)
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
.clone();
self.build_action(&name, None)
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action(
&self,
name: &str,
params: Option<serde_json::Value>,
) -> Result<Box<dyn Action>> {
let build_action = self
.builders_by_name
.get(name)
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
(build_action)(params.unwrap_or_else(|| json!({})))
.with_context(|| format!("Attempting to build action {}", name))
}
pub fn all_action_names(&self) -> &[SharedString] {
self.all_names.as_slice()
}
}
/// Defines unit structs that can be used as actions.
/// To use more complex data types as actions, use `impl_actions!`
#[macro_export]
macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
#[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, $name,
fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(Box::new(Self))
}
);
gpui::register_action!($name);
)*
};
}
/// 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<dyn gpui::Action>> {
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, $name,
fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
}
);
gpui::register_action!($name);
)*
};
}
/// 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<dyn gpui::Action>> {
Ok(std::boxed::Box::new(
gpui::private::serde_json::from_value::<Self>(value)?,
))
}
);
gpui::register_action!($name);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {
($namespace:path, $name:ident, $visual_name:tt, $build:item) => {
impl gpui::Action for $name {
fn name(&self) -> &'static str
{
concat!(
stringify!($namespace),
"::",
stringify!($visual_name),
)
}
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
{
concat!(
stringify!($namespace),
"::",
stringify!($visual_name),
)
}
$build
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new(self.clone())
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
}
};
}
mod no_action {
use crate as gpui;
actions!(zed, [NoAction]);
}