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

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 ...
This commit is contained in:
Ben Kunkle 2025-07-08 14:39:55 -05:00 committed by GitHub
parent 1220049089
commit 6b7c30d7ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 140 additions and 74 deletions

View file

@ -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 { pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
// instead of using DefaultDenyUnknownFields, actions typically use // instead of using DefaultDenyUnknownFields, actions typically use
// `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This // `#[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 // is because the rest of the keymap will still load in these cases, whereas other settings
// files would not. // 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 action_schemas = cx.action_schemas(&mut generator);
let deprecations = cx.deprecated_actions_to_preferred_actions(); let deprecations = cx.deprecated_actions_to_preferred_actions();

View file

@ -4,7 +4,7 @@ use std::{
}; };
use anyhow::{Context as _, anyhow}; use anyhow::{Context as _, anyhow};
use collections::HashSet; use collections::{HashMap, HashSet};
use editor::{CompletionProvider, Editor, EditorEvent}; use editor::{CompletionProvider, Editor, EditorEvent};
use feature_flags::FeatureFlagViewExt; use feature_flags::FeatureFlagViewExt;
use fs::Fs; use fs::Fs;
@ -240,7 +240,7 @@ impl KeymapEditor {
Some(Default) => 3, Some(Default) => 3,
None => 4, None => 4,
}; };
return (source_precedence, keybind.action.as_ref()); return (source_precedence, keybind.action_name.as_ref());
}); });
} }
this.selected_index.take(); this.selected_index.take();
@ -261,6 +261,12 @@ impl KeymapEditor {
let mut unmapped_action_names = let mut unmapped_action_names =
HashSet::from_iter(cx.all_action_names().into_iter().copied()); HashSet::from_iter(cx.all_action_names().into_iter().copied());
let action_documentation = cx.action_documentation(); 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 processed_bindings = Vec::new();
let mut string_match_candidates = Vec::new(); let mut string_match_candidates = Vec::new();
@ -295,9 +301,10 @@ impl KeymapEditor {
processed_bindings.push(ProcessedKeybinding { processed_bindings.push(ProcessedKeybinding {
keystroke_text: keystroke_text.into(), keystroke_text: keystroke_text.into(),
ui_key_binding, ui_key_binding,
action: action_name.into(), action_name: action_name.into(),
action_input, action_input,
action_docs, action_docs,
action_schema: action_schema.get(action_name).cloned(),
context: Some(context), context: Some(context),
source, source,
}); });
@ -311,9 +318,10 @@ impl KeymapEditor {
processed_bindings.push(ProcessedKeybinding { processed_bindings.push(ProcessedKeybinding {
keystroke_text: empty.clone(), keystroke_text: empty.clone(),
ui_key_binding: None, ui_key_binding: None,
action: action_name.into(), action_name: action_name.into(),
action_input: None, action_input: None,
action_docs: action_documentation.get(action_name).copied(), action_docs: action_documentation.get(action_name).copied(),
action_schema: action_schema.get(action_name).cloned(),
context: None, context: None,
source: None, source: None,
}); });
@ -326,8 +334,8 @@ impl KeymapEditor {
fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) { fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let json_language = Self::load_json_language(workspace.clone(), cx).await; let json_language = load_json_language(workspace.clone(), cx).await;
let rust_language = Self::load_rust_language(workspace.clone(), cx).await; let rust_language = load_rust_language(workspace.clone(), cx).await;
let query = this.update(cx, |this, cx| { let query = this.update(cx, |this, cx| {
let (key_bindings, string_match_candidates) = let (key_bindings, string_match_candidates) =
@ -353,64 +361,6 @@ impl KeymapEditor {
.detach_and_log_err(cx); .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 { fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults(); let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("KeymapEditor"); dispatch_context.add("KeymapEditor");
@ -526,8 +476,10 @@ impl KeymapEditor {
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone(); let fs = workspace.app_state().fs.clone();
let workspace_weak = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| { 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)); window.focus(&modal.focus_handle(cx));
modal modal
}); });
@ -564,7 +516,7 @@ impl KeymapEditor {
) { ) {
let action = self let action = self
.selected_binding() .selected_binding()
.map(|binding| binding.action.to_string()); .map(|binding| binding.action_name.to_string());
let Some(action) = action else { let Some(action) = action else {
return; return;
}; };
@ -576,9 +528,10 @@ impl KeymapEditor {
struct ProcessedKeybinding { struct ProcessedKeybinding {
keystroke_text: SharedString, keystroke_text: SharedString,
ui_key_binding: Option<ui::KeyBinding>, ui_key_binding: Option<ui::KeyBinding>,
action: SharedString, action_name: SharedString,
action_input: Option<SyntaxHighlightedText>, action_input: Option<SyntaxHighlightedText>,
action_docs: Option<&'static str>, action_docs: Option<&'static str>,
action_schema: Option<schemars::Schema>,
context: Option<KeybindContextString>, context: Option<KeybindContextString>,
source: Option<(KeybindSource, SharedString)>, source: Option<(KeybindSource, SharedString)>,
} }
@ -685,10 +638,10 @@ impl Render for KeymapEditor {
let binding = &this.keybindings[candidate_id]; let binding = &this.keybindings[candidate_id];
let action = div() let action = div()
.child(binding.action.clone()) .child(binding.action_name.clone())
.id(("keymap action", index)) .id(("keymap action", index))
.tooltip({ .tooltip({
let action_name = binding.action.clone(); let action_name = binding.action_name.clone();
let action_docs = binding.action_docs; let action_docs = binding.action_docs;
move |_, cx| { move |_, cx| {
let action_tooltip = Tooltip::new( let action_tooltip = Tooltip::new(
@ -828,6 +781,7 @@ struct KeybindingEditorModal {
editing_keybind: ProcessedKeybinding, editing_keybind: ProcessedKeybinding,
keybind_editor: Entity<KeystrokeInput>, keybind_editor: Entity<KeystrokeInput>,
context_editor: Entity<Editor>, context_editor: Entity<Editor>,
input_editor: Option<Entity<Editor>>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
error: Option<String>, error: Option<String>,
} }
@ -845,6 +799,7 @@ impl Focusable for KeybindingEditorModal {
impl KeybindingEditorModal { impl KeybindingEditorModal {
pub fn new( pub fn new(
editing_keybind: ProcessedKeybinding, editing_keybind: ProcessedKeybinding,
workspace: WeakEntity<Workspace>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
@ -881,11 +836,39 @@ impl KeybindingEditorModal {
editor 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 { Self {
editing_keybind, editing_keybind,
fs, fs,
keybind_editor, keybind_editor,
context_editor, context_editor,
input_editor,
error: None, error: None,
} }
} }
@ -964,13 +947,38 @@ impl Render for KeybindingEditorModal {
) )
.child(self.keybind_editor.clone()), .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( .child(
v_flex() v_flex()
.p_3() .p_3()
.gap_3() .gap_3()
.child( .child(
v_flex().child(Label::new("Edit Keystroke")).child( v_flex().child(Label::new("Edit Context")).child(
Label::new("Input the desired keystroke for the selected action.") Label::new("Input the desired context for the binding.")
.color(Color::Muted), .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( async fn save_keybinding_update(
existing: ProcessedKeybinding, existing: ProcessedKeybinding,
new_keystrokes: &[Keystroke], new_keystrokes: &[Keystroke],
@ -1113,7 +1173,7 @@ async fn save_keybinding_update(
target: settings::KeybindUpdateTarget { target: settings::KeybindUpdateTarget {
context: existing_context, context: existing_context,
keystrokes: existing_keystrokes, keystrokes: existing_keystrokes,
action_name: &existing.action, action_name: &existing.action_name,
use_key_equivalents: false, use_key_equivalents: false,
input, input,
}, },
@ -1124,7 +1184,7 @@ async fn save_keybinding_update(
source: settings::KeybindUpdateTarget { source: settings::KeybindUpdateTarget {
context: new_context, context: new_context,
keystrokes: new_keystrokes, keystrokes: new_keystrokes,
action_name: &existing.action, action_name: &existing.action_name,
use_key_equivalents: false, use_key_equivalents: false,
input, input,
}, },