keymap_ui: Add ability to delete user created bindings (#34248)
Closes #ISSUE Adds an action and special handling in `KeymapFile::update_keybinding` for removals. If the binding being removed is the last in a keymap section, the keymap section will be removed entirely instead of left empty. Still to do is the ability to unbind/remove non-user created bindings such as those in the default keymap by binding them to `NoAction`, however, this will be done in a follow up PR. Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
parent
33f1ac8b34
commit
7915b9f93f
3 changed files with 401 additions and 90 deletions
|
@ -623,49 +623,55 @@ 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 {
|
||||||
|
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
|
||||||
|
.action_value()
|
||||||
|
.context("Failed to generate target action JSON value")?;
|
||||||
|
let Some((index, keystrokes_str)) =
|
||||||
|
find_binding(&keymap, &target, &target_action_value)
|
||||||
|
else {
|
||||||
|
anyhow::bail!("Failed to find keybinding to remove");
|
||||||
|
};
|
||||||
|
let is_only_binding = keymap.0[index]
|
||||||
|
.bindings
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |bindings| bindings.len() == 1);
|
||||||
|
let key_path: &[&str] = if is_only_binding {
|
||||||
|
&[]
|
||||||
|
} else {
|
||||||
|
&["bindings", keystrokes_str]
|
||||||
|
};
|
||||||
|
let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
|
||||||
|
&keymap_contents,
|
||||||
|
key_path,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
index,
|
||||||
|
tab_size,
|
||||||
|
)
|
||||||
|
.context("Failed to remove keybinding")?;
|
||||||
|
keymap_contents.replace_range(replace_range, &replace_value);
|
||||||
|
return Ok(keymap_contents);
|
||||||
|
}
|
||||||
|
|
||||||
if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
|
if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
|
||||||
let mut found_index = None;
|
|
||||||
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")?;
|
||||||
let source_action_value = source
|
let source_action_value = source
|
||||||
.action_value()
|
.action_value()
|
||||||
.context("Failed to generate source action JSON value")?;
|
.context("Failed to generate source action JSON value")?;
|
||||||
'sections: for (index, section) in keymap.sections().enumerate() {
|
|
||||||
if section.context != target.context.unwrap_or("") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if section.use_key_equivalents != target.use_key_equivalents {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(bindings) = §ion.bindings else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
for (keystrokes, action) in bindings {
|
|
||||||
let Ok(keystrokes) = keystrokes
|
|
||||||
.split_whitespace()
|
|
||||||
.map(Keystroke::parse)
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if keystrokes.len() != target.keystrokes.len()
|
|
||||||
|| !keystrokes
|
|
||||||
.iter()
|
|
||||||
.zip(target.keystrokes)
|
|
||||||
.all(|(a, b)| a.should_match(b))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if action.0 != target_action_value {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
found_index = Some(index);
|
|
||||||
break 'sections;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(index) = found_index {
|
if let Some((index, keystrokes_str)) =
|
||||||
|
find_binding(&keymap, &target, &target_action_value)
|
||||||
|
{
|
||||||
if target.context == source.context {
|
if target.context == source.context {
|
||||||
// if we are only changing the keybinding (common case)
|
// if we are only changing the keybinding (common case)
|
||||||
// not the context, etc. Then just update the binding in place
|
// not the context, etc. Then just update the binding in place
|
||||||
|
@ -673,7 +679,7 @@ impl KeymapFile {
|
||||||
let (replace_range, replace_value) =
|
let (replace_range, replace_value) =
|
||||||
replace_top_level_array_value_in_json_text(
|
replace_top_level_array_value_in_json_text(
|
||||||
&keymap_contents,
|
&keymap_contents,
|
||||||
&["bindings", &target.keystrokes_unparsed()],
|
&["bindings", keystrokes_str],
|
||||||
Some(&source_action_value),
|
Some(&source_action_value),
|
||||||
Some(&source.keystrokes_unparsed()),
|
Some(&source.keystrokes_unparsed()),
|
||||||
index,
|
index,
|
||||||
|
@ -695,7 +701,7 @@ impl KeymapFile {
|
||||||
let (replace_range, replace_value) =
|
let (replace_range, replace_value) =
|
||||||
replace_top_level_array_value_in_json_text(
|
replace_top_level_array_value_in_json_text(
|
||||||
&keymap_contents,
|
&keymap_contents,
|
||||||
&["bindings", &target.keystrokes_unparsed()],
|
&["bindings", keystrokes_str],
|
||||||
Some(&source_action_value),
|
Some(&source_action_value),
|
||||||
Some(&source.keystrokes_unparsed()),
|
Some(&source.keystrokes_unparsed()),
|
||||||
index,
|
index,
|
||||||
|
@ -725,7 +731,7 @@ impl KeymapFile {
|
||||||
let (replace_range, replace_value) =
|
let (replace_range, replace_value) =
|
||||||
replace_top_level_array_value_in_json_text(
|
replace_top_level_array_value_in_json_text(
|
||||||
&keymap_contents,
|
&keymap_contents,
|
||||||
&["bindings", &target.keystrokes_unparsed()],
|
&["bindings", keystrokes_str],
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
index,
|
index,
|
||||||
|
@ -771,6 +777,46 @@ impl KeymapFile {
|
||||||
keymap_contents.replace_range(replace_range, &replace_value);
|
keymap_contents.replace_range(replace_range, &replace_value);
|
||||||
}
|
}
|
||||||
return Ok(keymap_contents);
|
return Ok(keymap_contents);
|
||||||
|
|
||||||
|
fn find_binding<'a, 'b>(
|
||||||
|
keymap: &'b KeymapFile,
|
||||||
|
target: &KeybindUpdateTarget<'a>,
|
||||||
|
target_action_value: &Value,
|
||||||
|
) -> Option<(usize, &'b str)> {
|
||||||
|
for (index, section) in keymap.sections().enumerate() {
|
||||||
|
if section.context != target.context.unwrap_or("") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if section.use_key_equivalents != target.use_key_equivalents {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(bindings) = §ion.bindings else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for (keystrokes_str, action) in bindings {
|
||||||
|
let Ok(keystrokes) = keystrokes_str
|
||||||
|
.split_whitespace()
|
||||||
|
.map(Keystroke::parse)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if keystrokes.len() != target.keystrokes.len()
|
||||||
|
|| !keystrokes
|
||||||
|
.iter()
|
||||||
|
.zip(target.keystrokes)
|
||||||
|
.all(|(a, b)| a.should_match(b))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if &action.0 != target_action_value {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Some((index, &keystrokes_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,6 +829,10 @@ pub enum KeybindUpdateOperation<'a> {
|
||||||
target_keybind_source: KeybindSource,
|
target_keybind_source: KeybindSource,
|
||||||
},
|
},
|
||||||
Add(KeybindUpdateTarget<'a>),
|
Add(KeybindUpdateTarget<'a>),
|
||||||
|
Remove {
|
||||||
|
target: KeybindUpdateTarget<'a>,
|
||||||
|
target_keybind_source: KeybindSource,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct KeybindUpdateTarget<'a> {
|
pub struct KeybindUpdateTarget<'a> {
|
||||||
|
@ -1300,5 +1350,118 @@ mod tests {
|
||||||
]"#
|
]"#
|
||||||
.unindent(),
|
.unindent(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
check_keymap_update(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"a": "foo::bar",
|
||||||
|
"c": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
KeybindUpdateOperation::Remove {
|
||||||
|
target: KeybindUpdateTarget {
|
||||||
|
context: Some("SomeContext"),
|
||||||
|
keystrokes: &parse_keystrokes("a"),
|
||||||
|
action_name: "foo::bar",
|
||||||
|
use_key_equivalents: false,
|
||||||
|
input: None,
|
||||||
|
},
|
||||||
|
target_keybind_source: KeybindSource::User,
|
||||||
|
},
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"c": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
check_keymap_update(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"a": ["foo::bar", true],
|
||||||
|
"c": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
KeybindUpdateOperation::Remove {
|
||||||
|
target: KeybindUpdateTarget {
|
||||||
|
context: Some("SomeContext"),
|
||||||
|
keystrokes: &parse_keystrokes("a"),
|
||||||
|
action_name: "foo::bar",
|
||||||
|
use_key_equivalents: false,
|
||||||
|
input: Some("true"),
|
||||||
|
},
|
||||||
|
target_keybind_source: KeybindSource::User,
|
||||||
|
},
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"c": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
check_keymap_update(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"b": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"a": ["foo::bar", true],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"c": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
KeybindUpdateOperation::Remove {
|
||||||
|
target: KeybindUpdateTarget {
|
||||||
|
context: Some("SomeContext"),
|
||||||
|
keystrokes: &parse_keystrokes("a"),
|
||||||
|
action_name: "foo::bar",
|
||||||
|
use_key_equivalents: false,
|
||||||
|
input: Some("true"),
|
||||||
|
},
|
||||||
|
target_keybind_source: KeybindSource::User,
|
||||||
|
},
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"b": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "SomeContext",
|
||||||
|
"bindings": {
|
||||||
|
"c": "foo::baz",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -353,29 +353,58 @@ pub fn replace_top_level_array_value_in_json_text(
|
||||||
let range = cursor.node().range();
|
let range = cursor.node().range();
|
||||||
let indent_width = range.start_point.column;
|
let indent_width = range.start_point.column;
|
||||||
let offset = range.start_byte;
|
let offset = range.start_byte;
|
||||||
let value_str = &text[range.start_byte..range.end_byte];
|
let text_range = range.start_byte..range.end_byte;
|
||||||
|
let value_str = &text[text_range.clone()];
|
||||||
let needs_indent = range.start_point.row > 0;
|
let needs_indent = range.start_point.row > 0;
|
||||||
|
|
||||||
let (mut replace_range, mut replace_value) =
|
if new_value.is_none() && key_path.is_empty() {
|
||||||
replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
|
let mut remove_range = text_range.clone();
|
||||||
|
if index == 0 {
|
||||||
replace_range.start += offset;
|
while cursor.goto_next_sibling()
|
||||||
replace_range.end += offset;
|
&& (cursor.node().is_extra() || cursor.node().is_missing())
|
||||||
|
{}
|
||||||
if needs_indent {
|
if cursor.node().kind() == "," {
|
||||||
let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
|
remove_range.end = cursor.node().range().end_byte;
|
||||||
replace_value = replace_value.replace('\n', &increased_indent);
|
}
|
||||||
// replace_value.push('\n');
|
if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') {
|
||||||
|
if text[remove_range.end + 1..remove_range.end + next_newline]
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_whitespace())
|
||||||
|
{
|
||||||
|
remove_range.end = remove_range.end + next_newline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while cursor.goto_previous_sibling()
|
||||||
|
&& (cursor.node().is_extra() || cursor.node().is_missing())
|
||||||
|
{}
|
||||||
|
if cursor.node().kind() == "," {
|
||||||
|
remove_range.start = cursor.node().range().start_byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok((remove_range, String::new()));
|
||||||
} else {
|
} else {
|
||||||
while let Some(idx) = replace_value.find("\n ") {
|
let (mut replace_range, mut replace_value) =
|
||||||
replace_value.remove(idx + 1);
|
replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
|
||||||
}
|
|
||||||
while let Some(idx) = replace_value.find("\n") {
|
|
||||||
replace_value.replace_range(idx..idx + 1, " ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok((replace_range, replace_value));
|
replace_range.start += offset;
|
||||||
|
replace_range.end += offset;
|
||||||
|
|
||||||
|
if needs_indent {
|
||||||
|
let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
|
||||||
|
replace_value = replace_value.replace('\n', &increased_indent);
|
||||||
|
// replace_value.push('\n');
|
||||||
|
} else {
|
||||||
|
while let Some(idx) = replace_value.find("\n ") {
|
||||||
|
replace_value.remove(idx + 1);
|
||||||
|
}
|
||||||
|
while let Some(idx) = replace_value.find("\n") {
|
||||||
|
replace_value.replace_range(idx..idx + 1, " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok((replace_range, replace_value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_top_level_array_value_in_json_text(
|
pub fn append_top_level_array_value_in_json_text(
|
||||||
|
@ -1005,14 +1034,14 @@ mod tests {
|
||||||
input: impl ToString,
|
input: impl ToString,
|
||||||
index: usize,
|
index: usize,
|
||||||
key_path: &[&str],
|
key_path: &[&str],
|
||||||
value: Value,
|
value: Option<Value>,
|
||||||
expected: impl ToString,
|
expected: impl ToString,
|
||||||
) {
|
) {
|
||||||
let input = input.to_string();
|
let input = input.to_string();
|
||||||
let result = replace_top_level_array_value_in_json_text(
|
let result = replace_top_level_array_value_in_json_text(
|
||||||
&input,
|
&input,
|
||||||
key_path,
|
key_path,
|
||||||
Some(&value),
|
value.as_ref(),
|
||||||
None,
|
None,
|
||||||
index,
|
index,
|
||||||
4,
|
4,
|
||||||
|
@ -1023,10 +1052,10 @@ mod tests {
|
||||||
pretty_assertions::assert_eq!(expected.to_string(), result_str);
|
pretty_assertions::assert_eq!(expected.to_string(), result_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
check_array_replace(r#"[1, 3, 3]"#, 1, &[], json!(2), r#"[1, 2, 3]"#);
|
check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
|
||||||
check_array_replace(r#"[1, 3, 3]"#, 2, &[], json!(2), r#"[1, 3, 2]"#);
|
check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
|
||||||
check_array_replace(r#"[1, 3, 3,]"#, 3, &[], json!(2), r#"[1, 3, 3, 2]"#);
|
check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
|
||||||
check_array_replace(r#"[1, 3, 3,]"#, 100, &[], json!(2), r#"[1, 3, 3, 2]"#);
|
check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
|
||||||
check_array_replace(
|
check_array_replace(
|
||||||
r#"[
|
r#"[
|
||||||
1,
|
1,
|
||||||
|
@ -1036,7 +1065,7 @@ mod tests {
|
||||||
.unindent(),
|
.unindent(),
|
||||||
1,
|
1,
|
||||||
&[],
|
&[],
|
||||||
json!({"foo": "bar", "baz": "qux"}),
|
Some(json!({"foo": "bar", "baz": "qux"})),
|
||||||
r#"[
|
r#"[
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
|
@ -1051,7 +1080,7 @@ mod tests {
|
||||||
r#"[1, 3, 3,]"#,
|
r#"[1, 3, 3,]"#,
|
||||||
1,
|
1,
|
||||||
&[],
|
&[],
|
||||||
json!({"foo": "bar", "baz": "qux"}),
|
Some(json!({"foo": "bar", "baz": "qux"})),
|
||||||
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
|
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1059,7 +1088,7 @@ mod tests {
|
||||||
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
|
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
|
||||||
1,
|
1,
|
||||||
&["baz"],
|
&["baz"],
|
||||||
json!({"qux": "quz"}),
|
Some(json!({"qux": "quz"})),
|
||||||
r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
|
r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1074,7 +1103,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
1,
|
1,
|
||||||
&["baz"],
|
&["baz"],
|
||||||
json!({"qux": "quz"}),
|
Some(json!({"qux": "quz"})),
|
||||||
r#"[
|
r#"[
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
|
@ -1100,7 +1129,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
1,
|
1,
|
||||||
&["baz"],
|
&["baz"],
|
||||||
json!("qux"),
|
Some(json!("qux")),
|
||||||
r#"[
|
r#"[
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
|
@ -1127,7 +1156,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
1,
|
1,
|
||||||
&["baz"],
|
&["baz"],
|
||||||
json!("qux"),
|
Some(json!("qux")),
|
||||||
r#"[
|
r#"[
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
|
@ -1151,7 +1180,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
2,
|
2,
|
||||||
&[],
|
&[],
|
||||||
json!("replaced"),
|
Some(json!("replaced")),
|
||||||
r#"[
|
r#"[
|
||||||
1,
|
1,
|
||||||
// This is element 2
|
// This is element 2
|
||||||
|
@ -1169,7 +1198,7 @@ mod tests {
|
||||||
.unindent(),
|
.unindent(),
|
||||||
0,
|
0,
|
||||||
&[],
|
&[],
|
||||||
json!("first"),
|
Some(json!("first")),
|
||||||
r#"[
|
r#"[
|
||||||
// Empty array with comment
|
// Empty array with comment
|
||||||
"first"
|
"first"
|
||||||
|
@ -1180,7 +1209,7 @@ mod tests {
|
||||||
r#"[]"#.unindent(),
|
r#"[]"#.unindent(),
|
||||||
0,
|
0,
|
||||||
&[],
|
&[],
|
||||||
json!("first"),
|
Some(json!("first")),
|
||||||
r#"[
|
r#"[
|
||||||
"first"
|
"first"
|
||||||
]"#
|
]"#
|
||||||
|
@ -1197,7 +1226,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
0,
|
0,
|
||||||
&[],
|
&[],
|
||||||
json!({"new": "object"}),
|
Some(json!({"new": "object"})),
|
||||||
r#"[
|
r#"[
|
||||||
// Leading comment
|
// Leading comment
|
||||||
// Another leading comment
|
// Another leading comment
|
||||||
|
@ -1217,7 +1246,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
1,
|
1,
|
||||||
&[],
|
&[],
|
||||||
json!("deep"),
|
Some(json!("deep")),
|
||||||
r#"[
|
r#"[
|
||||||
1,
|
1,
|
||||||
"deep",
|
"deep",
|
||||||
|
@ -1230,7 +1259,7 @@ mod tests {
|
||||||
r#"[1,2, 3, 4]"#,
|
r#"[1,2, 3, 4]"#,
|
||||||
2,
|
2,
|
||||||
&[],
|
&[],
|
||||||
json!("spaced"),
|
Some(json!("spaced")),
|
||||||
r#"[1,2, "spaced", 4]"#,
|
r#"[1,2, "spaced", 4]"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1243,7 +1272,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
1,
|
1,
|
||||||
&[],
|
&[],
|
||||||
json!(["a", "b", "c", "d"]),
|
Some(json!(["a", "b", "c", "d"])),
|
||||||
r#"[
|
r#"[
|
||||||
[1, 2, 3],
|
[1, 2, 3],
|
||||||
[
|
[
|
||||||
|
@ -1268,7 +1297,7 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
0,
|
0,
|
||||||
&[],
|
&[],
|
||||||
json!("updated"),
|
Some(json!("updated")),
|
||||||
r#"[
|
r#"[
|
||||||
/*
|
/*
|
||||||
* This is a
|
* This is a
|
||||||
|
@ -1284,7 +1313,7 @@ mod tests {
|
||||||
r#"[true, false, true]"#,
|
r#"[true, false, true]"#,
|
||||||
1,
|
1,
|
||||||
&[],
|
&[],
|
||||||
json!(null),
|
Some(json!(null)),
|
||||||
r#"[true, null, true]"#,
|
r#"[true, null, true]"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1293,7 +1322,7 @@ mod tests {
|
||||||
r#"[42]"#,
|
r#"[42]"#,
|
||||||
0,
|
0,
|
||||||
&[],
|
&[],
|
||||||
json!({"answer": 42}),
|
Some(json!({"answer": 42})),
|
||||||
r#"[{ "answer": 42 }]"#,
|
r#"[{ "answer": 42 }]"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1307,7 +1336,7 @@ mod tests {
|
||||||
.unindent(),
|
.unindent(),
|
||||||
10,
|
10,
|
||||||
&[],
|
&[],
|
||||||
json!(123),
|
Some(json!(123)),
|
||||||
r#"[
|
r#"[
|
||||||
// Comment 1
|
// Comment 1
|
||||||
// Comment 2
|
// Comment 2
|
||||||
|
@ -1316,6 +1345,54 @@ mod tests {
|
||||||
]"#
|
]"#
|
||||||
.unindent(),
|
.unindent(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
check_array_replace(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value2"
|
||||||
|
}
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
0,
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"key": "value2"
|
||||||
|
}
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
check_array_replace(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value3"
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
1,
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value3"
|
||||||
|
},
|
||||||
|
]"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -23,7 +23,10 @@ use ui::{
|
||||||
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render,
|
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render,
|
||||||
SharedString, Styled as _, Tooltip, Window, prelude::*, right_click_menu,
|
SharedString, Styled as _, Tooltip, Window, prelude::*, right_click_menu,
|
||||||
};
|
};
|
||||||
use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
|
use workspace::{
|
||||||
|
Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
|
||||||
|
register_serializable_item,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
SettingsUiFeatureFlag,
|
SettingsUiFeatureFlag,
|
||||||
|
@ -49,6 +52,8 @@ actions!(
|
||||||
EditBinding,
|
EditBinding,
|
||||||
/// Creates a new key binding for the selected action.
|
/// Creates a new key binding for the selected action.
|
||||||
CreateBinding,
|
CreateBinding,
|
||||||
|
/// Deletes the selected key binding.
|
||||||
|
DeleteBinding,
|
||||||
/// Copies the action name to clipboard.
|
/// Copies the action name to clipboard.
|
||||||
CopyAction,
|
CopyAction,
|
||||||
/// Copies the context predicate to clipboard.
|
/// Copies the context predicate to clipboard.
|
||||||
|
@ -613,6 +618,21 @@ impl KeymapEditor {
|
||||||
self.open_edit_keybinding_modal(true, window, cx);
|
self.open_edit_keybinding_modal(true, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(to_remove) = self.selected_binding().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(fs) = self
|
||||||
|
.workspace
|
||||||
|
.read_with(cx, |workspace, _| workspace.app_state().fs.clone())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
|
||||||
|
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
|
||||||
|
.detach_and_notify_err(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_context_to_clipboard(
|
fn copy_context_to_clipboard(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &CopyContext,
|
_: &CopyContext,
|
||||||
|
@ -740,6 +760,7 @@ impl Render for KeymapEditor {
|
||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
.on_action(cx.listener(Self::edit_binding))
|
.on_action(cx.listener(Self::edit_binding))
|
||||||
.on_action(cx.listener(Self::create_binding))
|
.on_action(cx.listener(Self::create_binding))
|
||||||
|
.on_action(cx.listener(Self::delete_binding))
|
||||||
.on_action(cx.listener(Self::copy_action_to_clipboard))
|
.on_action(cx.listener(Self::copy_action_to_clipboard))
|
||||||
.on_action(cx.listener(Self::copy_context_to_clipboard))
|
.on_action(cx.listener(Self::copy_context_to_clipboard))
|
||||||
.size_full()
|
.size_full()
|
||||||
|
@ -1458,6 +1479,47 @@ async fn save_keybinding_update(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remove_keybinding(
|
||||||
|
existing: ProcessedKeybinding,
|
||||||
|
fs: &Arc<dyn Fs>,
|
||||||
|
tab_size: usize,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let Some(ui_key_binding) = existing.ui_key_binding else {
|
||||||
|
anyhow::bail!("Cannot remove a keybinding that does not exist");
|
||||||
|
};
|
||||||
|
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||||
|
.await
|
||||||
|
.context("Failed to load keymap file")?;
|
||||||
|
|
||||||
|
let operation = settings::KeybindUpdateOperation::Remove {
|
||||||
|
target: settings::KeybindUpdateTarget {
|
||||||
|
context: existing
|
||||||
|
.context
|
||||||
|
.as_ref()
|
||||||
|
.and_then(KeybindContextString::local_str),
|
||||||
|
keystrokes: &ui_key_binding.keystrokes,
|
||||||
|
action_name: &existing.action_name,
|
||||||
|
use_key_equivalents: false,
|
||||||
|
input: existing
|
||||||
|
.action_input
|
||||||
|
.as_ref()
|
||||||
|
.map(|input| input.text.as_ref()),
|
||||||
|
},
|
||||||
|
target_keybind_source: existing
|
||||||
|
.source
|
||||||
|
.map(|(source, _name)| source)
|
||||||
|
.unwrap_or(KeybindSource::User),
|
||||||
|
};
|
||||||
|
|
||||||
|
let updated_keymap_contents =
|
||||||
|
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
||||||
|
.context("Failed to update keybinding")?;
|
||||||
|
fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
|
||||||
|
.await
|
||||||
|
.context("Failed to write keymap file")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
struct KeystrokeInput {
|
struct KeystrokeInput {
|
||||||
keystrokes: Vec<Keystroke>,
|
keystrokes: Vec<Keystroke>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
@ -1667,16 +1729,25 @@ fn build_keybind_context_menu(
|
||||||
.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_action = selected_binding.ui_key_binding.is_none();
|
||||||
|
|
||||||
menu.action_disabled_when(selected_binding_is_unbound, "Edit", Box::new(EditBinding))
|
menu.action_disabled_when(
|
||||||
.action("Create", Box::new(CreateBinding))
|
selected_binding_is_unbound_action,
|
||||||
.action("Copy action", Box::new(CopyAction))
|
"Edit",
|
||||||
.action_disabled_when(
|
Box::new(EditBinding),
|
||||||
selected_binding_has_no_context,
|
)
|
||||||
"Copy Context",
|
.action("Create", Box::new(CreateBinding))
|
||||||
Box::new(CopyContext),
|
.action_disabled_when(
|
||||||
)
|
selected_binding_is_unbound_action,
|
||||||
|
"Delete",
|
||||||
|
Box::new(DeleteBinding),
|
||||||
|
)
|
||||||
|
.action("Copy action", Box::new(CopyAction))
|
||||||
|
.action_disabled_when(
|
||||||
|
selected_binding_has_no_context,
|
||||||
|
"Copy Context",
|
||||||
|
Box::new(CopyContext),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue