keymap_ui: Support unbinding non-user defined keybindings (#34318)

Closes #ISSUE

Makes it so that `KeymapFile::update_keybinding` treats removals of
bindings that weren't user-defined as creating a new binding to
`zed::NoAction`.


Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle 2025-07-11 16:23:14 -05:00 committed by GitHub
parent c3edc2cfc1
commit 206cce6783
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 73 additions and 47 deletions

1
Cargo.lock generated
View file

@ -14677,6 +14677,7 @@ dependencies = [
"schemars", "schemars",
"search", "search",
"serde", "serde",
"serde_json",
"settings", "settings",
"theme", "theme",
"tree-sitter-json", "tree-sitter-json",

View file

@ -607,8 +607,8 @@ impl KeymapFile {
mut keymap_contents: String, mut keymap_contents: String,
tab_size: usize, tab_size: usize,
) -> Result<String> { ) -> Result<String> {
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
match operation { match operation {
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
KeybindUpdateOperation::Replace { KeybindUpdateOperation::Replace {
target_keybind_source: target_source, target_keybind_source: target_source,
source, source,
@ -616,6 +616,16 @@ impl KeymapFile {
} if target_source != KeybindSource::User => { } if target_source != KeybindSource::User => {
operation = KeybindUpdateOperation::Add(source); operation = KeybindUpdateOperation::Add(source);
} }
// if trying to remove a keybinding that is not user-defined, treat it as creating a binding
// that binds it to `zed::NoAction`
KeybindUpdateOperation::Remove {
mut target,
target_keybind_source,
} if target_keybind_source != KeybindSource::User => {
target.action_name = gpui::NoAction.name();
target.input.take();
operation = KeybindUpdateOperation::Add(target);
}
_ => {} _ => {}
} }
@ -623,14 +633,7 @@ impl KeymapFile {
// We don't want to modify the file if it's invalid. // We don't want to modify the file if it's invalid.
let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?; let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?;
if let KeybindUpdateOperation::Remove { if let KeybindUpdateOperation::Remove { target, .. } = operation {
target,
target_keybind_source,
} = operation
{
if target_keybind_source != KeybindSource::User {
anyhow::bail!("Cannot remove non-user created keybinding. Not implemented yet");
}
let target_action_value = target let target_action_value = target
.action_value() .action_value()
.context("Failed to generate target action JSON value")?; .context("Failed to generate target action JSON value")?;

View file

@ -31,6 +31,7 @@ project.workspace = true
schemars.workspace = true schemars.workspace = true
search.workspace = true search.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true
settings.workspace = true settings.workspace = true
theme.workspace = true theme.workspace = true
tree-sitter-json.workspace = true tree-sitter-json.workspace = true

View file

@ -397,11 +397,10 @@ impl KeymapEditor {
SearchMode::KeyStroke => { SearchMode::KeyStroke => {
matches.retain(|item| { matches.retain(|item| {
this.keybindings[item.candidate_id] this.keybindings[item.candidate_id]
.ui_key_binding .keystrokes()
.as_ref() .is_some_and(|keystrokes| {
.is_some_and(|binding| {
keystroke_query.iter().all(|key| { keystroke_query.iter().all(|key| {
binding.keystrokes.iter().any(|keystroke| { keystrokes.iter().any(|keystroke| {
keystroke.key == key.key keystroke.key == key.key
&& keystroke.modifiers == key.modifiers && keystroke.modifiers == key.modifiers
}) })
@ -623,7 +622,7 @@ impl KeymapEditor {
.and_then(KeybindContextString::local) .and_then(KeybindContextString::local)
.is_none(); .is_none();
let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none(); let selected_binding_is_unbound = selected_binding.keystrokes().is_none();
let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| { let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.action_disabled_when( menu.action_disabled_when(
@ -876,6 +875,12 @@ impl ProcessedKeybinding {
.cloned(), .cloned(),
) )
} }
fn keystrokes(&self) -> Option<&[Keystroke]> {
self.ui_key_binding
.as_ref()
.map(|binding| binding.keystrokes.as_slice())
}
} }
#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)] #[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
@ -1303,16 +1308,8 @@ impl KeybindingEditorModal {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Self { ) -> Self {
let keybind_editor = cx.new(|cx| { let keybind_editor = cx
KeystrokeInput::new( .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
editing_keybind
.ui_key_binding
.as_ref()
.map(|keybinding| keybinding.keystrokes.clone()),
window,
cx,
)
});
let context_editor = cx.new(|cx| { let context_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx); let mut editor = Editor::single_line(window, cx);
@ -1398,6 +1395,24 @@ impl KeybindingEditorModal {
} }
} }
fn validate_action_input(&self, cx: &App) -> anyhow::Result<Option<String>> {
let input = self
.input_editor
.as_ref()
.map(|editor| editor.read(cx).text(cx));
let value = input
.as_ref()
.map(|input| {
serde_json::from_str(input).context("Failed to parse action input as JSON")
})
.transpose()?;
cx.build_action(&self.editing_keybind.action_name, value)
.context("Failed to validate action input")?;
Ok(input)
}
fn save(&mut self, cx: &mut Context<Self>) { fn save(&mut self, cx: &mut Context<Self>) {
let existing_keybind = self.editing_keybind.clone(); let existing_keybind = self.editing_keybind.clone();
let fs = self.fs.clone(); let fs = self.fs.clone();
@ -1425,6 +1440,14 @@ impl KeybindingEditorModal {
return; return;
} }
let new_input = match self.validate_action_input(cx) {
Err(input_err) => {
self.set_error(InputError::error(input_err.to_string()), cx);
return;
}
Ok(input) => input,
};
let action_mapping: ActionMapping = ( let action_mapping: ActionMapping = (
ui::text_for_keystrokes(&new_keystrokes, cx).into(), ui::text_for_keystrokes(&new_keystrokes, cx).into(),
new_context new_context
@ -1481,6 +1504,7 @@ impl KeybindingEditorModal {
existing_keybind, existing_keybind,
&new_keystrokes, &new_keystrokes,
new_context.as_deref(), new_context.as_deref(),
new_input.as_deref(),
&fs, &fs,
tab_size, tab_size,
) )
@ -1711,6 +1735,7 @@ async fn save_keybinding_update(
existing: ProcessedKeybinding, existing: ProcessedKeybinding,
new_keystrokes: &[Keystroke], new_keystrokes: &[Keystroke],
new_context: Option<&str>, new_context: Option<&str>,
new_input: Option<&str>,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -1718,41 +1743,36 @@ async fn save_keybinding_update(
.await .await
.context("Failed to load keymap file")?; .context("Failed to load keymap file")?;
let existing_keystrokes = existing let operation = if !create {
.ui_key_binding let existing_keystrokes = existing.keystrokes().unwrap_or_default();
.as_ref()
.map(|keybinding| keybinding.keystrokes.as_slice())
.unwrap_or_default();
let existing_context = existing let existing_context = existing
.context .context
.as_ref() .as_ref()
.and_then(KeybindContextString::local_str); .and_then(KeybindContextString::local_str);
let existing_input = existing
let input = existing
.action_input .action_input
.as_ref() .as_ref()
.map(|input| input.text.as_ref()); .map(|input| input.text.as_ref());
let operation = if !create {
settings::KeybindUpdateOperation::Replace { settings::KeybindUpdateOperation::Replace {
target: settings::KeybindUpdateTarget { target: settings::KeybindUpdateTarget {
context: existing_context, context: existing_context,
keystrokes: existing_keystrokes, keystrokes: existing_keystrokes,
action_name: &existing.action_name, action_name: &existing.action_name,
use_key_equivalents: false, use_key_equivalents: false,
input, input: existing_input,
}, },
target_keybind_source: existing target_keybind_source: existing
.source .source
.map(|(source, _name)| source) .as_ref()
.map(|(source, _name)| *source)
.unwrap_or(KeybindSource::User), .unwrap_or(KeybindSource::User),
source: settings::KeybindUpdateTarget { source: settings::KeybindUpdateTarget {
context: new_context, context: new_context,
keystrokes: new_keystrokes, keystrokes: new_keystrokes,
action_name: &existing.action_name, action_name: &existing.action_name,
use_key_equivalents: false, use_key_equivalents: false,
input, input: new_input,
}, },
} }
} else { } else {
@ -1761,7 +1781,7 @@ async fn save_keybinding_update(
keystrokes: new_keystrokes, keystrokes: new_keystrokes,
action_name: &existing.action_name, action_name: &existing.action_name,
use_key_equivalents: false, use_key_equivalents: false,
input, input: new_input,
}) })
}; };
let updated_keymap_contents = let updated_keymap_contents =
@ -1778,7 +1798,7 @@ async fn remove_keybinding(
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let Some(ui_key_binding) = existing.ui_key_binding else { let Some(keystrokes) = existing.keystrokes() else {
anyhow::bail!("Cannot remove a keybinding that does not exist"); anyhow::bail!("Cannot remove a keybinding that does not exist");
}; };
let keymap_contents = settings::KeymapFile::load_keymap_file(fs) let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
@ -1791,7 +1811,7 @@ async fn remove_keybinding(
.context .context
.as_ref() .as_ref()
.and_then(KeybindContextString::local_str), .and_then(KeybindContextString::local_str),
keystrokes: &ui_key_binding.keystrokes, keystrokes,
action_name: &existing.action_name, action_name: &existing.action_name,
use_key_equivalents: false, use_key_equivalents: false,
input: existing input: existing
@ -1801,7 +1821,8 @@ async fn remove_keybinding(
}, },
target_keybind_source: existing target_keybind_source: existing
.source .source
.map(|(source, _name)| source) .as_ref()
.map(|(source, _name)| *source)
.unwrap_or(KeybindSource::User), .unwrap_or(KeybindSource::User),
}; };