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:
parent
1220049089
commit
6b7c30d7ad
2 changed files with 140 additions and 74 deletions
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue