fix(keymap): let the code editor's line commands beat the app's chords

The editor binds toggle comment, move/copy/delete line, insert line, select
line and go-to-bracket on its CodeEditor key context. A context-free app
binding ranks as deep as the focused context and wins the tie by being
added later, so ⌘/ (shortcut sheet), ⌘↵ (fullscreen), ⌘⇧↵ (maximize) and,
off macOS, ⌥↑/⌥↓ (pane focus) took those keys inside the editor. Re-add
them in fixed_bindings on CodeEditor, which only a multi-line code editor
declares, so plain text fields and the terminal keep the app's keys.
This commit is contained in:
l0ng-ai
2026-09-28 17:19:06 +08:00
parent cc36fdb3f6
commit e20e42aef7
2 changed files with 87 additions and 5 deletions
+8 -5
View File
@@ -2100,17 +2100,20 @@ impl Tty7App {
)
.menu(t(L10nKey::AppMenuSelectAll), Box::new(edit::SelectAll))
.separator()
// The line commands act on the caret's line or every line the
// selection touches; the chords come from gpui-component's
// `CodeEditor` key context, which sits deeper than any of the
// app's own bindings and so wins while the editor has focus.
// The line commands act on every line a cursor or selection
// touches. Their chords live on gpui-component's `CodeEditor` key
// context and are re-added in `keymap::fixed_bindings`, so they
// beat the app's own keys while the editor has focus.
.menu(
t(L10nKey::EditorToggleComment),
Box::new(edit::ToggleLineComment),
)
.menu(t(L10nKey::EditorMoveLineUp), Box::new(edit::MoveLineUp))
.menu(t(L10nKey::EditorMoveLineDown), Box::new(edit::MoveLineDown))
.menu(t(L10nKey::EditorDuplicateLine), Box::new(edit::CopyLineDown))
.menu(
t(L10nKey::EditorDuplicateLine),
Box::new(edit::CopyLineDown),
)
.menu(t(L10nKey::EditorDeleteLine), Box::new(edit::DeleteLine))
.separator()
.menu(t(L10nKey::AppMenuFind), Box::new(edit::Search))
+79
View File
@@ -92,6 +92,32 @@ fn fixed_bindings() -> Vec<KeyBinding> {
bindings.push(KeyBinding::new("secondary-alt-up", AddCursorAbove, input));
bindings.push(KeyBinding::new("secondary-alt-down", AddCursorBelow, input));
}
// The code editor's line commands, for the same tie: ⌘/ is the shortcut
// sheet, ⌘↵ fullscreen, ⌘⇧↵ maximize and ⌥↑/⌥↓ pane focus off macOS, all
// context-free. Bound on `CodeEditor`, which only a multi-line code
// editor declares, so every other text field and the terminal keep the
// app's keys.
{
use gpui_component::input::{
CODE_EDITOR_CONTEXT, CopyLineDown, CopyLineUp, DeleteLine, InsertLineAbove,
InsertLineBelow, MoveLineDown, MoveLineUp, MoveToMatchingBracket, SelectLine,
ToggleBlockComment, ToggleLineComment,
};
let editor = Some(CODE_EDITOR_CONTEXT);
bindings.extend([
KeyBinding::new("secondary-/", ToggleLineComment, editor),
KeyBinding::new("alt-shift-a", ToggleBlockComment, editor),
KeyBinding::new("alt-up", MoveLineUp, editor),
KeyBinding::new("alt-down", MoveLineDown, editor),
KeyBinding::new("alt-shift-up", CopyLineUp, editor),
KeyBinding::new("alt-shift-down", CopyLineDown, editor),
KeyBinding::new("secondary-shift-k", DeleteLine, editor),
KeyBinding::new("secondary-enter", InsertLineBelow, editor),
KeyBinding::new("secondary-shift-enter", InsertLineAbove, editor),
KeyBinding::new("secondary-l", SelectLine, editor),
KeyBinding::new("secondary-shift-\\", MoveToMatchingBracket, editor),
]);
}
bindings
}
@@ -2617,6 +2643,59 @@ mod gpui_tests {
.collect()
}
/// The code editor's line commands sit on chords the app also spends —
/// ⌘/ is the shortcut sheet, ⌘↵ fullscreen, ⌘⇧↵ maximize, ⌥↑/⌥↓ pane
/// focus off macOS. `fixed_bindings` re-adds them on the `CodeEditor`
/// context the editor declares beside `Input`, after the app's table, so
/// inside the editor they win and everywhere else nothing changes.
#[gpui::test]
fn the_code_editor_chords_win_inside_the_editor(cx: &mut TestAppContext) {
cx.update(|cx| {
running_on_json(cx, "{}");
let first = |keys: &str, stack: &[&str]| {
let input: Vec<Keystroke> = keys
.split(' ')
.map(|k| Keystroke::parse(k).expect("the typed keystroke parses"))
.collect();
let stack: Vec<gpui::KeyContext> = stack
.iter()
.map(|c| gpui::KeyContext::parse(c).expect("the context parses"))
.collect();
cx.key_bindings()
.borrow()
.bindings_for_input(&input, &stack)
.0
.first()
.map(|b| b.action().name())
};
let editor = ["Workspace", "Input CodeEditor"];
for (keys, action) in [
("secondary-/", "input::ToggleLineComment"),
("alt-up", "input::MoveLineUp"),
("alt-down", "input::MoveLineDown"),
("alt-shift-up", "input::CopyLineUp"),
("alt-shift-down", "input::CopyLineDown"),
("secondary-shift-k", "input::DeleteLine"),
("secondary-enter", "input::InsertLineBelow"),
("secondary-shift-enter", "input::InsertLineAbove"),
("secondary-l", "input::SelectLine"),
("secondary-shift-\\", "input::MoveToMatchingBracket"),
] {
assert_eq!(first(keys, &editor), Some(action), "{keys} in the editor");
assert_ne!(
first(keys, &["Workspace", "Input"]),
Some(action),
"{keys} in a plain text field"
);
assert_ne!(
first(keys, &["Workspace", "Terminal"]),
Some(action),
"{keys} in a terminal"
);
}
});
}
#[gpui::test]
fn a_chord_added_in_config_keeps_the_default_one(cx: &mut TestAppContext) {
use gpui::Action as _;