From 58bf076d0f8e357a5db595bd44bab724c3586d28 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:16:58 +0800 Subject: [PATCH 1/2] feat(editor): line commands in the editor's right-click menu Toggle Comment, Move Line Up/Down, Duplicate Line, Delete Line and Go to Matching Bracket, dispatched to gpui-component's new editing actions so each row shows its chord. Strings in en, zh and ja. --- src/ui/code_editor.rs | 17 +++++++++++++++++ src/ui/i18n/en.rs | 6 ++++++ src/ui/i18n/ja.rs | 6 ++++++ src/ui/i18n/mod.rs | 6 ++++++ src/ui/i18n/zh.rs | 6 ++++++ 5 files changed, 41 insertions(+) diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index b03f169e..e336018e 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -2100,10 +2100,27 @@ 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. + .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), Box::new(crate::core::actions::EditorGoToLine), + ) + .menu( + t(L10nKey::EditorGoToMatchingBracket), + Box::new(edit::MoveToMatchingBracket), ); this.editor_file_menu_items(menu, id, app, cx) } diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 22f913e7..5badfcb1 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -1201,6 +1201,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::EditorGoToLinePlaceholder => "Line, or line:column (1–{total})", L10nKey::EditorSaveAs => "Save as", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index dfebc354..785e02cc 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -1249,6 +1249,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::EditorGoToLinePlaceholder => "行番号、または 行:列(1–{total})", L10nKey::EditorSaveAs => "名前を付けて保存", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 746dbdb1..6e022afe 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -935,6 +935,12 @@ l10n_keys! { EditorAlreadyOpen, EditorGoToLine, EditorGoToLineAction, + EditorGoToMatchingBracket, + EditorToggleComment, + EditorMoveLineUp, + EditorMoveLineDown, + EditorDuplicateLine, + EditorDeleteLine, EditorCopyRelativePath, EditorGoToLinePlaceholder, EditorSaveAs, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 7c1fc618..d1c60559 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -1121,6 +1121,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::EditorGoToLinePlaceholder => "行号,或 行:列(1–{total})", L10nKey::EditorSaveAs => "另存为", From e20e42aef7a2295d4858b75ee55729c70ce91ade Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:19:06 +0800 Subject: [PATCH 2/2] fix(keymap): let the code editor's line commands beat the app's chords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ui/code_editor.rs | 13 ++++--- src/ui/keymap.rs | 79 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index f921d835..a93b3e0a 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -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)) diff --git a/src/ui/keymap.rs b/src/ui/keymap.rs index 14497b41..66ff839b 100644 --- a/src/ui/keymap.rs +++ b/src/ui/keymap.rs @@ -92,6 +92,32 @@ fn fixed_bindings() -> Vec { 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 = 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 _;