keymap_ui: Editor for action input in modal (#34080)

Ben Kunkle created

Closes #ISSUE

Adds a very simple editor for editing action input to the edit keybind
modal. No auto-complete yet.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/settings/src/keymap_file.rs    |   8 
crates/settings_ui/src/keybindings.rs | 206 ++++++++++++++++++----------
2 files changed, 140 insertions(+), 74 deletions(-)

Detailed changes

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();

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<KeymapEditor>) {
         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<Workspace>,
-        cx: &mut AsyncApp,
-    ) -> Arc<Language> {
-        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<Workspace>,
-        cx: &mut AsyncApp,
-    ) -> Arc<Language> {
-        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<Self>) -> 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<ui::KeyBinding>,
-    action: SharedString,
+    action_name: SharedString,
     action_input: Option<SyntaxHighlightedText>,
     action_docs: Option<&'static str>,
+    action_schema: Option<schemars::Schema>,
     context: Option<KeybindContextString>,
     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<KeystrokeInput>,
     context_editor: Entity<Editor>,
+    input_editor: Option<Entity<Editor>>,
     fs: Arc<dyn Fs>,
     error: Option<String>,
 }
@@ -845,6 +799,7 @@ impl Focusable for KeybindingEditorModal {
 impl KeybindingEditorModal {
     pub fn new(
         editing_keybind: ProcessedKeybinding,
+        workspace: WeakEntity<Workspace>,
         fs: Arc<dyn Fs>,
         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<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
+    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<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
+    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,
             },