diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index b91739ca87..19bc58ea23 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -426,12 +426,18 @@ impl KeymapFile { } } + /// Creates a JSON schema generator, suitable for generating json schemas + /// for actions + pub fn action_schema_generator() -> schemars::SchemaGenerator { + schemars::generate::SchemaSettings::draft2019_09().into_generator() + } + pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value { // instead of using DefaultDenyUnknownFields, actions typically use // `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This // is because the rest of the keymap will still load in these cases, whereas other settings // files would not. - let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator(); + let mut generator = Self::action_schema_generator(); let action_schemas = cx.action_schemas(&mut generator); let deprecations = cx.deprecated_actions_to_preferred_actions(); diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 2dd693c798..951dd77541 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::{Context as _, anyhow}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use editor::{CompletionProvider, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use fs::Fs; @@ -240,7 +240,7 @@ impl KeymapEditor { Some(Default) => 3, None => 4, }; - return (source_precedence, keybind.action.as_ref()); + return (source_precedence, keybind.action_name.as_ref()); }); } this.selected_index.take(); @@ -261,6 +261,12 @@ impl KeymapEditor { let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names().into_iter().copied()); let action_documentation = cx.action_documentation(); + let mut generator = KeymapFile::action_schema_generator(); + let action_schema = HashMap::from_iter( + cx.action_schemas(&mut generator) + .into_iter() + .filter_map(|(name, schema)| schema.map(|schema| (name, schema))), + ); let mut processed_bindings = Vec::new(); let mut string_match_candidates = Vec::new(); @@ -295,9 +301,10 @@ impl KeymapEditor { processed_bindings.push(ProcessedKeybinding { keystroke_text: keystroke_text.into(), ui_key_binding, - action: action_name.into(), + action_name: action_name.into(), action_input, action_docs, + action_schema: action_schema.get(action_name).cloned(), context: Some(context), source, }); @@ -311,9 +318,10 @@ impl KeymapEditor { processed_bindings.push(ProcessedKeybinding { keystroke_text: empty.clone(), ui_key_binding: None, - action: action_name.into(), + action_name: action_name.into(), action_input: None, action_docs: action_documentation.get(action_name).copied(), + action_schema: action_schema.get(action_name).cloned(), context: None, source: None, }); @@ -326,8 +334,8 @@ impl KeymapEditor { fn update_keybindings(&mut self, cx: &mut Context) { let workspace = self.workspace.clone(); cx.spawn(async move |this, cx| { - let json_language = Self::load_json_language(workspace.clone(), cx).await; - let rust_language = Self::load_rust_language(workspace.clone(), cx).await; + let json_language = load_json_language(workspace.clone(), cx).await; + let rust_language = load_rust_language(workspace.clone(), cx).await; let query = this.update(cx, |this, cx| { let (key_bindings, string_match_candidates) = @@ -353,64 +361,6 @@ impl KeymapEditor { .detach_and_log_err(cx); } - async fn load_json_language( - workspace: WeakEntity, - cx: &mut AsyncApp, - ) -> Arc { - let json_language_task = workspace - .read_with(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .languages() - .language_for_name("JSON") - }) - .context("Failed to load JSON language") - .log_err(); - let json_language = match json_language_task { - Some(task) => task.await.context("Failed to load JSON language").log_err(), - None => None, - }; - return json_language.unwrap_or_else(|| { - Arc::new(Language::new( - LanguageConfig { - name: "JSON".into(), - ..Default::default() - }, - Some(tree_sitter_json::LANGUAGE.into()), - )) - }); - } - - async fn load_rust_language( - workspace: WeakEntity, - cx: &mut AsyncApp, - ) -> Arc { - let rust_language_task = workspace - .read_with(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .languages() - .language_for_name("Rust") - }) - .context("Failed to load Rust language") - .log_err(); - let rust_language = match rust_language_task { - Some(task) => task.await.context("Failed to load Rust language").log_err(), - None => None, - }; - return rust_language.unwrap_or_else(|| { - Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - )) - }); - } - fn dispatch_context(&self, _window: &Window, _cx: &Context) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("KeymapEditor"); @@ -526,8 +476,10 @@ impl KeymapEditor { self.workspace .update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); + let workspace_weak = cx.weak_entity(); workspace.toggle_modal(window, cx, |window, cx| { - let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx); + let modal = + KeybindingEditorModal::new(keybind.clone(), workspace_weak, fs, window, cx); window.focus(&modal.focus_handle(cx)); modal }); @@ -564,7 +516,7 @@ impl KeymapEditor { ) { let action = self .selected_binding() - .map(|binding| binding.action.to_string()); + .map(|binding| binding.action_name.to_string()); let Some(action) = action else { return; }; @@ -576,9 +528,10 @@ impl KeymapEditor { struct ProcessedKeybinding { keystroke_text: SharedString, ui_key_binding: Option, - action: SharedString, + action_name: SharedString, action_input: Option, action_docs: Option<&'static str>, + action_schema: Option, context: Option, source: Option<(KeybindSource, SharedString)>, } @@ -685,10 +638,10 @@ impl Render for KeymapEditor { let binding = &this.keybindings[candidate_id]; let action = div() - .child(binding.action.clone()) + .child(binding.action_name.clone()) .id(("keymap action", index)) .tooltip({ - let action_name = binding.action.clone(); + let action_name = binding.action_name.clone(); let action_docs = binding.action_docs; move |_, cx| { let action_tooltip = Tooltip::new( @@ -828,6 +781,7 @@ struct KeybindingEditorModal { editing_keybind: ProcessedKeybinding, keybind_editor: Entity, context_editor: Entity, + input_editor: Option>, fs: Arc, error: Option, } @@ -845,6 +799,7 @@ impl Focusable for KeybindingEditorModal { impl KeybindingEditorModal { pub fn new( editing_keybind: ProcessedKeybinding, + workspace: WeakEntity, fs: Arc, window: &mut Window, cx: &mut App, @@ -881,11 +836,39 @@ impl KeybindingEditorModal { editor }); + + let input_editor = editing_keybind.action_schema.clone().map(|_schema| { + cx.new(|cx| { + let mut editor = Editor::auto_height_unbounded(1, window, cx); + if let Some(input) = editing_keybind.action_input.clone() { + editor.set_text(input.text, window, cx); + } else { + // TODO: default value from schema? + editor.set_placeholder_text("Action input", cx); + } + cx.spawn(async |editor, cx| { + let json_language = load_json_language(workspace, cx).await; + editor + .update(cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(json_language), cx) + }); + } + }) + .context("Failed to load JSON language for editing keybinding action input") + }) + .detach_and_log_err(cx); + editor + }) + }); + Self { editing_keybind, fs, keybind_editor, context_editor, + input_editor, error: None, } } @@ -964,13 +947,38 @@ impl Render for KeybindingEditorModal { ) .child(self.keybind_editor.clone()), ) + .when_some(self.input_editor.clone(), |this, editor| { + this.child( + v_flex() + .p_3() + .gap_3() + .child( + v_flex().child(Label::new("Edit Input")).child( + Label::new("Input the desired input to the binding.") + .color(Color::Muted), + ), + ) + .child( + div() + .w_full() + .border_color(cx.theme().colors().border_variant) + .border_1() + .py_2() + .px_3() + .min_h_8() + .rounded_md() + .bg(theme.editor_background) + .child(editor), + ), + ) + }) .child( v_flex() .p_3() .gap_3() .child( - v_flex().child(Label::new("Edit Keystroke")).child( - Label::new("Input the desired keystroke for the selected action.") + v_flex().child(Label::new("Edit Context")).child( + Label::new("Input the desired context for the binding.") .color(Color::Muted), ), ) @@ -1081,6 +1089,58 @@ impl CompletionProvider for KeyContextCompletionProvider { } } +async fn load_json_language(workspace: WeakEntity, cx: &mut AsyncApp) -> Arc { + let json_language_task = workspace + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .languages() + .language_for_name("JSON") + }) + .context("Failed to load JSON language") + .log_err(); + let json_language = match json_language_task { + Some(task) => task.await.context("Failed to load JSON language").log_err(), + None => None, + }; + return json_language.unwrap_or_else(|| { + Arc::new(Language::new( + LanguageConfig { + name: "JSON".into(), + ..Default::default() + }, + Some(tree_sitter_json::LANGUAGE.into()), + )) + }); +} + +async fn load_rust_language(workspace: WeakEntity, cx: &mut AsyncApp) -> Arc { + let rust_language_task = workspace + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .languages() + .language_for_name("Rust") + }) + .context("Failed to load Rust language") + .log_err(); + let rust_language = match rust_language_task { + Some(task) => task.await.context("Failed to load Rust language").log_err(), + None => None, + }; + return rust_language.unwrap_or_else(|| { + Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + )) + }); +} + async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], @@ -1113,7 +1173,7 @@ async fn save_keybinding_update( target: settings::KeybindUpdateTarget { context: existing_context, keystrokes: existing_keystrokes, - action_name: &existing.action, + action_name: &existing.action_name, use_key_equivalents: false, input, }, @@ -1124,7 +1184,7 @@ async fn save_keybinding_update( source: settings::KeybindUpdateTarget { context: new_context, keystrokes: new_keystrokes, - action_name: &existing.action, + action_name: &existing.action_name, use_key_equivalents: false, input, },