diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index 56937bf1..f751c6a6 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -2187,6 +2187,22 @@ impl Tty7App { ) .menu(t(L10nKey::AppMenuSelectAll), Box::new(edit::SelectAll)) .separator() + // 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::EditorDeleteLine), Box::new(edit::DeleteLine)) + .separator() .menu(t(L10nKey::AppMenuFind), Box::new(edit::Search)) .menu( t(L10nKey::EditorGoToLineAction), @@ -2196,6 +2212,10 @@ impl Tty7App { t(L10nKey::EditorGitRevertChange), Box::new(crate::core::actions::EditorRevertChange), !this.editor_gutter_can_revert(id, cx), + ) + .menu( + t(L10nKey::EditorGoToMatchingBracket), + Box::new(edit::MoveToMatchingBracket), ); let menu = this.lsp_menu_items(menu, id, cx); this.editor_file_menu_items(menu, id, app, cx) diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 362c7c07..9f03e611 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -1208,6 +1208,12 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::EditorAlreadyOpen => "\"{path}\" is already open in the editor", L10nKey::EditorGoToLine => "Go to line", L10nKey::EditorGoToLineAction => "Go to Line…", + L10nKey::EditorGoToMatchingBracket => "Go to Matching Bracket", + L10nKey::EditorToggleComment => "Toggle Comment", + L10nKey::EditorMoveLineUp => "Move Line Up", + L10nKey::EditorMoveLineDown => "Move Line Down", + L10nKey::EditorDuplicateLine => "Duplicate Line", + L10nKey::EditorDeleteLine => "Delete Line", L10nKey::EditorCopyRelativePath => "Copy Relative Path", L10nKey::EditorGitNextChange => "Go to Next Change", L10nKey::EditorGitPrevChange => "Go to Previous Change", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index e90fa5e3..dcf1568b 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -1256,6 +1256,12 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::EditorAlreadyOpen => "「{path}」はすでにエディタで開かれています", L10nKey::EditorGoToLine => "行へ移動", L10nKey::EditorGoToLineAction => "行へ移動…", + L10nKey::EditorGoToMatchingBracket => "対応する括弧へ移動", + L10nKey::EditorToggleComment => "コメントの切り替え", + L10nKey::EditorMoveLineUp => "行を上へ移動", + L10nKey::EditorMoveLineDown => "行を下へ移動", + L10nKey::EditorDuplicateLine => "行を複製", + L10nKey::EditorDeleteLine => "行を削除", L10nKey::EditorCopyRelativePath => "相対パスをコピー", L10nKey::EditorGitNextChange => "次の変更へ移動", L10nKey::EditorGitPrevChange => "前の変更へ移動", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 7e06a0f4..146d7ca3 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -940,6 +940,12 @@ l10n_keys! { EditorAlreadyOpen, EditorGoToLine, EditorGoToLineAction, + EditorGoToMatchingBracket, + EditorToggleComment, + EditorMoveLineUp, + EditorMoveLineDown, + EditorDuplicateLine, + EditorDeleteLine, EditorCopyRelativePath, EditorGitNextChange, EditorGitPrevChange, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index b8c3658b..e4016d4d 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -1128,6 +1128,12 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::EditorAlreadyOpen => "“{path}”已在编辑器中打开", L10nKey::EditorGoToLine => "跳转到行", L10nKey::EditorGoToLineAction => "跳转到行…", + L10nKey::EditorGoToMatchingBracket => "跳转到匹配的括号", + L10nKey::EditorToggleComment => "切换注释", + L10nKey::EditorMoveLineUp => "上移行", + L10nKey::EditorMoveLineDown => "下移行", + L10nKey::EditorDuplicateLine => "复制行", + L10nKey::EditorDeleteLine => "删除行", L10nKey::EditorCopyRelativePath => "复制相对路径", L10nKey::EditorGitNextChange => "转到下一处更改", L10nKey::EditorGitPrevChange => "转到上一处更改", diff --git a/src/ui/keymap.rs b/src/ui/keymap.rs index 2b15fa14..8d851d0c 100644 --- a/src/ui/keymap.rs +++ b/src/ui/keymap.rs @@ -110,6 +110,32 @@ fn fixed_bindings() -> Vec { bindings.push(KeyBinding::new("alt-f5", EditorNextChange, input)); bindings.push(KeyBinding::new("shift-alt-f5", EditorPrevChange, 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 } @@ -2730,6 +2756,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 = keys + .split(' ') + .map(|k| Keystroke::parse(k).expect("the typed keystroke parses")) + .collect(); + let stack: Vec = 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 _;