diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 10421ef8f8984a7b5af58ec06d12e21889ea3bba..cab35c1ad2a69fabf95ac1772d84c108ad1b93b5 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -4,7 +4,7 @@ use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, - NoAction, SharedString, + NoAction, SharedString, register_action, }; use schemars::{JsonSchema, json_schema}; use serde::Deserialize; @@ -330,6 +330,40 @@ impl KeymapFile { use_key_equivalents: bool, cx: &App, ) -> std::result::Result { + let (action, action_input_string) = Self::build_keymap_action(action, cx)?; + + let key_binding = match KeyBinding::load( + keystrokes, + action, + context, + use_key_equivalents, + action_input_string.map(SharedString::from), + cx.keyboard_mapper().as_ref(), + ) { + Ok(key_binding) => key_binding, + Err(InvalidKeystrokeError { keystroke }) => { + return Err(format!( + "invalid keystroke {}. {}", + MarkdownInlineCode(&format!("\"{}\"", &keystroke)), + KEYSTROKE_PARSE_EXPECTED_MESSAGE + )); + } + }; + + if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) { + match validator.validate(&key_binding) { + Ok(()) => Ok(key_binding), + Err(error) => Err(error.0), + } + } else { + Ok(key_binding) + } + } + + fn build_keymap_action( + action: &KeymapAction, + cx: &App, + ) -> std::result::Result<(Box, Option), String> { let (build_result, action_input_string) = match &action.0 { Value::Array(items) => { if items.len() != 2 { @@ -347,11 +381,18 @@ impl KeymapFile { )); }; let action_input = items[1].clone(); - let action_input_string = action_input.to_string(); - ( - cx.build_action(name, Some(action_input)), - Some(action_input_string), - ) + if name.as_str() == ActionSequence::name_for_type() { + (ActionSequence::build_sequence(action_input, cx), None) + } else { + let action_input_string = action_input.to_string(); + ( + cx.build_action(name, Some(action_input)), + Some(action_input_string), + ) + } + } + Value::String(name) if name.as_str() == ActionSequence::name_for_type() => { + (Err(ActionSequence::expected_array_error()), None) } Value::String(name) => (cx.build_action(name, None), None), Value::Null => (Ok(NoAction.boxed_clone()), None), @@ -391,32 +432,7 @@ impl KeymapFile { }, }; - let key_binding = match KeyBinding::load( - keystrokes, - action, - context, - use_key_equivalents, - action_input_string.map(SharedString::from), - cx.keyboard_mapper().as_ref(), - ) { - Ok(key_binding) => key_binding, - Err(InvalidKeystrokeError { keystroke }) => { - return Err(format!( - "invalid keystroke {}. {}", - MarkdownInlineCode(&format!("\"{}\"", &keystroke)), - KEYSTROKE_PARSE_EXPECTED_MESSAGE - )); - } - }; - - if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) { - match validator.validate(&key_binding) { - Ok(()) => Ok(key_binding), - Err(error) => Err(error.0), - } - } else { - Ok(key_binding) - } + Ok((action, action_input_string)) } /// Creates a JSON schema generator, suitable for generating json schemas @@ -1027,6 +1043,119 @@ impl From for KeyBindingMetaIndex { } } +/// Runs a sequence of actions. Does not wait for asynchronous actions to complete before running +/// the next action. Currently only works in workspace windows. +/// +/// This action is special-cased in keymap parsing to allow it to access `App` while parsing, so +/// that it can parse its input actions. +pub struct ActionSequence(pub Vec>); + +register_action!(ActionSequence); + +impl ActionSequence { + fn build_sequence( + value: Value, + cx: &App, + ) -> std::result::Result, ActionBuildError> { + match value { + Value::Array(values) => { + let actions = values + .into_iter() + .enumerate() + .map(|(index, action)| { + match KeymapFile::build_keymap_action(&KeymapAction(action), cx) { + Ok((action, _)) => Ok(action), + Err(err) => { + return Err(ActionBuildError::BuildError { + name: Self::name_for_type().to_string(), + error: anyhow::anyhow!( + "error at sequence index {index}: {err}" + ), + }); + } + } + }) + .collect::, _>>()?; + Ok(Box::new(Self(actions))) + } + _ => Err(Self::expected_array_error()), + } + } + + fn expected_array_error() -> ActionBuildError { + ActionBuildError::BuildError { + name: Self::name_for_type().to_string(), + error: anyhow::anyhow!("expected array of actions"), + } + } +} + +impl Action for ActionSequence { + fn name(&self) -> &'static str { + Self::name_for_type() + } + + fn name_for_type() -> &'static str + where + Self: Sized, + { + "action::Sequence" + } + + fn partial_eq(&self, action: &dyn Action) -> bool { + action + .as_any() + .downcast_ref::() + .map_or(false, |other| { + self.0.len() == other.0.len() + && self + .0 + .iter() + .zip(other.0.iter()) + .all(|(a, b)| a.partial_eq(b.as_ref())) + }) + } + + fn boxed_clone(&self) -> Box { + Box::new(ActionSequence( + self.0 + .iter() + .map(|action| action.boxed_clone()) + .collect::>(), + )) + } + + fn build(_value: Value) -> Result> { + Err(anyhow::anyhow!( + "{} cannot be built directly", + Self::name_for_type() + )) + } + + fn action_json_schema(generator: &mut schemars::SchemaGenerator) -> Option { + let keymap_action_schema = generator.subschema_for::(); + Some(json_schema!({ + "type": "array", + "items": keymap_action_schema + })) + } + + fn deprecated_aliases() -> &'static [&'static str] { + &[] + } + + fn deprecation_message() -> Option<&'static str> { + None + } + + fn documentation() -> Option<&'static str> { + Some( + "Runs a sequence of actions.\n\n\ + NOTE: This does **not** wait for asynchronous actions to complete before running the next action.", + ) + } +} + #[cfg(test)] mod tests { use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 3a8dcfb8546c9a13ffd0927052d3a514c772b547..6fe078301abab974ba202660319966e7df42027a 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -30,6 +30,8 @@ pub use settings_store::{ pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; +pub use keymap_file::ActionSequence; + #[derive(Clone, Debug, PartialEq)] pub struct ActiveSettingsProfileName(pub String); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3b10f0c88310beabf41a946af376390fd73ca514..a5487411983c01c5e26ea349a3c3c0c23e408554 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5553,6 +5553,13 @@ impl Workspace { fn actions(&self, div: Div, window: &mut Window, cx: &mut Context) -> Div { self.add_workspace_actions_listeners(div, window, cx) + .on_action(cx.listener( + |_workspace, action_sequence: &settings::ActionSequence, window, cx| { + for action in &action_sequence.0 { + window.dispatch_action(action.boxed_clone(), cx); + } + }, + )) .on_action(cx.listener(Self::close_inactive_items_and_panes)) .on_action(cx.listener(Self::close_all_items_and_panes)) .on_action(cx.listener(Self::save_all)) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0d6eb5a622e61bbb37b8dfea185de28e402b1e95..6f0051cbf5bab14f8bc63f7736cf330dabf6a7c3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4393,6 +4393,7 @@ mod tests { | "workspace::OpenTerminal" | "workspace::SendKeystrokes" | "agent::NewNativeAgentThreadFromSummary" + | "action::Sequence" | "zed::OpenBrowser" | "zed::OpenZedUrl" => {} _ => { @@ -4454,6 +4455,7 @@ mod tests { assert_eq!(actions_without_namespace, Vec::<&str>::new()); let expected_namespaces = vec![ + "action", "activity_indicator", "agent", #[cfg(not(target_os = "macos"))]