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

View file

@ -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,
},