Merge upstream main into fix/windows-ctrl-slash
This commit is contained in:
commit
cc269503e8
4 files changed
+424
-35
No files matched your search
@@ -0,0 +1,116 @@
|
|||||||
|
# macOS command-key aliases
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Implemented for issue #238.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The GPUI shell registered a macOS ⌘ shortcut set that existed nowhere else.
|
||||||
|
`bind_macos_command_keys` hand-wrote roughly two dozen bindings after
|
||||||
|
`default_workspace_bindings`, while `display::keymap::default_shortcuts()` — the
|
||||||
|
table that `clear_action` / `reset_action`, the settings keymap page and the
|
||||||
|
keybind reload path read — knew nothing about them. Half of those keys also
|
||||||
|
appear in the config macOS platform table (`config/bindings/defaults.rs`); the
|
||||||
|
other half only existed in the shell.
|
||||||
|
|
||||||
|
Two user-visible consequences followed. Clearing the Shell picker row wrote only
|
||||||
|
`Ctrl+K:ReceiveChar`, so the static ⌘K binding kept consuming the key and the
|
||||||
|
panel still opened. `Action::Quit`, declared for macOS as `"q", SUPER`, fell into
|
||||||
|
the `_ => None` arm of `workspace_binding_in_context`, so ⌘Q did nothing at all
|
||||||
|
(GPUI installs no application menu). The display layer also rendered the super
|
||||||
|
modifier as `Win+`, so the settings page and the lines the app wrote into
|
||||||
|
`pebrel_settings.txt` showed `Ctrl+Win+F` / `Win+F` on macOS.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Issue #238 reproduction, with the settings file the released build wrote:
|
||||||
|
`keybind=Ctrl+Win+F:ReceiveChar`, `keybind=Ctrl+K:ReceiveChar`,
|
||||||
|
`keybind=shift+win+k:ReceiveChar` — the unbind persisted, yet ⌘K still opened
|
||||||
|
the picker.
|
||||||
|
- `display::keymap` regressions (`clearing_releases_the_macos_command_alias`,
|
||||||
|
`macos_command_aliases_round_trip_through_storage`,
|
||||||
|
`super_modifier_renders_per_platform`) and shell regressions
|
||||||
|
(`every_macos_command_alias_binds_a_gpui_action`,
|
||||||
|
`released_cmd_k_is_swallowed_by_no_action`).
|
||||||
|
- GPUI keymap semantics: a no-action binding for a keystroke suppresses matching
|
||||||
|
bindings from equal or weaker sources, and a keystroke with no remaining
|
||||||
|
binding falls through to the view's key handler — that is why a cleared ⌘ key
|
||||||
|
reaches the terminal instead of being eaten.
|
||||||
|
- Manual acceptance on macOS 26 (arm64, debug build, window driven through
|
||||||
|
accessibility): default ⌘K opens the picker; clearing the row writes both
|
||||||
|
`Ctrl+K:ReceiveChar` and `cmd+k:ReceiveChar` and releases ⌘K and Ctrl+K without
|
||||||
|
a restart; restoring the row brings ⌘K back; ⌘Q quits with `clean_exit` true.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
`display::keymap::MACOS_COMMAND_ALIASES` is the single authority for the shell's
|
||||||
|
⌘ set. `init()` derives the static gpui bindings from it, and
|
||||||
|
`default_shortcuts()` adds it to the unbind/restore vocabulary on macOS, deduped
|
||||||
|
against entries the config table already provides for the same action and key.
|
||||||
|
The alias table writes `cmd+…` into `keybind=` lines. Existing files and shortcut
|
||||||
|
capture can still use `win+…`, and manually edited files can use `super+…` or a
|
||||||
|
different modifier order. The GPUI adapter parses and canonicalizes its binding
|
||||||
|
strings before comparing overrides and restored defaults. Display spelling must
|
||||||
|
not determine a shortcut's runtime identity.
|
||||||
|
|
||||||
|
`Action::Quit` maps to a `QuitApp` action whose handler defers
|
||||||
|
`windowing::quit_all`, the same path as tray quit, so ⌘Q saves the session and
|
||||||
|
drafts before stopping PTYs. `mods_prefix` renders the super modifier per
|
||||||
|
platform, which also corrects the spelling of lines the app writes.
|
||||||
|
|
||||||
|
## Rejected alternatives
|
||||||
|
|
||||||
|
- Showing the ⌘ aliases on the settings keymap page (reverse-looking-up the
|
||||||
|
alias table in `effective_combo`) changes about twenty keycaps on macOS, the
|
||||||
|
shadowing verdicts other rows derive from them, and needs its own visual
|
||||||
|
acceptance. The page keeps showing the config table's Ctrl key.
|
||||||
|
- Moving the ⌘ set into the config macOS platform table would feed the legacy
|
||||||
|
terminal shell UI-only actions and keep the ⌘K collision, which the table
|
||||||
|
already resolves differently (`Esc("\x0c")` plus `ClearHistory`).
|
||||||
|
- Comparing raw display/storage spellings fails for equivalent `Win+…`,
|
||||||
|
`Cmd+…` and `super+…` keys. Display text is not a canonical identity;
|
||||||
|
`Ctrl++`-style display spellings also do not parse back as storage strings.
|
||||||
|
- Mapping the removed static ⌘ bindings to `ReceiveChar` from `init()` was not
|
||||||
|
needed: the user keybind table already injects the no-action binding.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Cleared actions now also write `keybind=cmd+…:ReceiveChar` lines. Users coming
|
||||||
|
from an earlier build must clear the row once more, because their existing file
|
||||||
|
predates the alias vocabulary and holds no line for the ⌘ half.
|
||||||
|
- The keymap page still shows Ctrl keys for actions whose macOS binding set
|
||||||
|
includes ⌘. Releasing them works; seeing them does not.
|
||||||
|
- ⌘K after unbinding does not clear the screen. The GPUI shell has no
|
||||||
|
clear-screen action (`Action::Esc` and `Action::ClearHistory` are unmapped) and
|
||||||
|
the terminal encoder ignores pure ⌘ combinations, so the key is released but
|
||||||
|
inert. Out of scope here; `Ctrl+L` still passes through to the shell.
|
||||||
|
- ⌘B and ⌘, stay static bindings: they have no editable keymap row, so there is
|
||||||
|
nothing to clear.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
`cargo fmt --all -- --check`, `python3 scripts/check_architecture.py --base
|
||||||
|
f7ca0ec`, `python3 scripts/check_platform_cfg.py` (budget unchanged at 484) and
|
||||||
|
the `display::keymap` / `keyboard_bindings` unit suites pass; the two new
|
||||||
|
released-key tests fail without the change. Manual acceptance is listed under
|
||||||
|
Evidence. The alias table is shared data; runtime registration uses the existing
|
||||||
|
`Platform::current()` entry and remains macOS-only. The two macOS restore
|
||||||
|
regressions remain platform-gated. This avoids duplicating platform compile
|
||||||
|
branches in the UI adapter without increasing the current cfg budget.
|
||||||
|
A cross-platform GPUI dispatch regression confirms that sharing the table does
|
||||||
|
not register Command shortcuts on Windows/Linux.
|
||||||
|
|
||||||
|
The 2026-09-23 review reproduced two restore failures through GPUI keyboard
|
||||||
|
dispatch: recording Cmd+K then resetting the row, and resetting the old
|
||||||
|
`Ctrl+Win+F:ReceiveChar` override from #238. Both failed before canonicalization.
|
||||||
|
Regression coverage includes those paths and equivalent modifier spellings/order.
|
||||||
|
|
||||||
|
## Supersedes
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Revisit when
|
||||||
|
|
||||||
|
The keymap page should show platform-native keycaps, or macOS ⌘K should be
|
||||||
|
assigned to a clear-screen action instead of the Shell picker.
|
||||||
@@ -88,6 +88,52 @@ pub(crate) const READONLY_ROWS: &[(&str, &str, &str)] = &[
|
|||||||
("贴入 AI 修复建议", "Paste AI fix suggestion", "Ctrl+."),
|
("贴入 AI 修复建议", "Paste AI fix suggestion", "Ctrl+."),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// macOS 的 ⌘ 命令键别名:`(存储写法, 动作)`。
|
||||||
|
///
|
||||||
|
/// GPUI 壳在配置表之外**追加**一整套 ⌘ 绑定:配置表的 macOS 段只覆盖其中
|
||||||
|
/// 一半,另一半(⌘K、⌘D、⌘⇧P…)是壳自己加的。这张表是那套绑定的唯一权威:
|
||||||
|
/// `init()` 按它注册静态绑定,解绑与恢复按它写回 `keybind=` 行。留在表外
|
||||||
|
/// 就成了影子绑定——`clear_action` 不知道它,于是出现「⌘K 解绑后仍然弹出
|
||||||
|
/// Shell 选择器」(#238)。
|
||||||
|
///
|
||||||
|
/// 表里的写法会写进设置文件,超级键写 `cmd`(macOS 的读法)。历史配置和
|
||||||
|
/// 录制结果仍可使用 `win` / `super`;GPUI 适配层按解析后的按键规范化,
|
||||||
|
/// 覆盖与恢复默认不依赖这些别名或修饰键顺序的逐字相等。
|
||||||
|
///
|
||||||
|
/// 这张表只进 [`default_shortcuts`](解绑/恢复口径),不进 `effective_combo`
|
||||||
|
/// 的键帽反查:设置页仍按配置表的 Ctrl 键显示。
|
||||||
|
///
|
||||||
|
/// 别名表是共享数据;实际注册由平台层入口限定为 macOS,便于跨平台核验分派。
|
||||||
|
pub(crate) const MACOS_COMMAND_ALIASES: &[(&str, Action)] = &[
|
||||||
|
("cmd+t", Action::CreateNewTab),
|
||||||
|
("cmd+n", Action::CreateNewWindow),
|
||||||
|
// ⌘W 关标签页是 GPUI 壳的实际行为;配置表的 macOS 段把 ⌘W 记作 Quit,
|
||||||
|
// 这里必须压过它,否则解绑恢复会把 ⌘W 还给退出。
|
||||||
|
("cmd+w", Action::CloseTab),
|
||||||
|
("shift+cmd+p", Action::ToggleCommandPalette),
|
||||||
|
("cmd+k", Action::ToggleShellPicker),
|
||||||
|
("shift+cmd+f", Action::ToggleFilesPanel),
|
||||||
|
("cmd+d", Action::SplitRight),
|
||||||
|
("shift+cmd+d", Action::SplitDown),
|
||||||
|
("shift+cmd+enter", Action::ToggleZoom),
|
||||||
|
("alt+cmd+left", Action::FocusPaneLeft),
|
||||||
|
("alt+cmd+right", Action::FocusPaneRight),
|
||||||
|
("alt+cmd+up", Action::FocusPaneUp),
|
||||||
|
("alt+cmd+down", Action::FocusPaneDown),
|
||||||
|
("shift+cmd+]", Action::SelectNextTab),
|
||||||
|
("shift+cmd+[", Action::SelectPreviousTab),
|
||||||
|
("shift+cmd+g", Action::ToggleGitPanel),
|
||||||
|
("cmd+plus", Action::IncreaseFontSize),
|
||||||
|
("cmd+=", Action::IncreaseFontSize),
|
||||||
|
("cmd+minus", Action::DecreaseFontSize),
|
||||||
|
("cmd+digit0", Action::ResetFontSize),
|
||||||
|
("cmd+c", Action::Copy),
|
||||||
|
("cmd+v", Action::Paste),
|
||||||
|
("ctrl+cmd+f", Action::ToggleFullscreen),
|
||||||
|
("shift+cmd+o", Action::OpenQuickJump),
|
||||||
|
("cmd+q", Action::Quit),
|
||||||
|
];
|
||||||
|
|
||||||
pub(crate) fn action_label(
|
pub(crate) fn action_label(
|
||||||
row: &(Action, &'static str, &'static str),
|
row: &(Action, &'static str, &'static str),
|
||||||
language: super::UiLanguage,
|
language: super::UiLanguage,
|
||||||
@@ -118,13 +164,33 @@ fn cached_defaults() -> &'static [KeyBinding] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_shortcuts() -> Vec<(String, Action)> {
|
pub(crate) fn default_shortcuts() -> Vec<(String, Action)> {
|
||||||
cached_defaults()
|
let mut shortcuts: Vec<(String, Action)> = cached_defaults()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|binding| {
|
.filter_map(|binding| {
|
||||||
display_combo(binding.mods, &binding.trigger)
|
display_combo(binding.mods, &binding.trigger)
|
||||||
.map(|combo| (combo, binding.action.clone()))
|
.map(|combo| (combo, binding.action.clone()))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
// macOS 的 ⌘ 键也算默认键:`clear_action` / `reset_action` 按这份表决定要
|
||||||
|
// 解掉、要还回哪些键,不带上它就会漏掉 ⌘ 那一半(#238)。
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let mut extra: Vec<(String, Action)> = Vec::new();
|
||||||
|
for (combo, action) in MACOS_COMMAND_ALIASES {
|
||||||
|
let parsed = parse_combo(combo);
|
||||||
|
// 配置表已经给了同一动作的同一个键(⌘T、⌘C、⌘Q…)就不重复追加:
|
||||||
|
// 解绑会把每条 combo 都写进设置文件,重复条目只会多留一行废话。
|
||||||
|
let covered = shortcuts
|
||||||
|
.iter()
|
||||||
|
.chain(extra.iter())
|
||||||
|
.any(|(known, candidate)| candidate == action && parse_combo(known) == parsed);
|
||||||
|
if !covered {
|
||||||
|
extra.push(((*combo).to_owned(), action.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shortcuts.extend(extra);
|
||||||
|
}
|
||||||
|
shortcuts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Editing one action moves its shortcut: old keys pass through to the terminal.
|
/// Editing one action moves its shortcut: old keys pass through to the terminal.
|
||||||
@@ -335,7 +401,10 @@ pub(crate) fn mods_prefix(mods: ModifiersState) -> String {
|
|||||||
out.push_str("Alt+");
|
out.push_str("Alt+");
|
||||||
}
|
}
|
||||||
if mods.super_key() {
|
if mods.super_key() {
|
||||||
out.push_str("Win+");
|
// macOS 的超级键是 ⌘。键帽必须写 Cmd+,否则设置页会出现
|
||||||
|
// `Ctrl+Win+F`(全屏)、`Win+F`(搜索)这类用户认不出的组合(#238)。
|
||||||
|
// 存储侧不受影响:`win+` / `super+` / `cmd+` 同等解析。
|
||||||
|
out.push_str(if cfg!(target_os = "macos") { "Cmd+" } else { "Win+" });
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -805,4 +874,62 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 超级键键帽随平台渲染:macOS 是 ⌘,其它平台是 Win。
|
||||||
|
#[test]
|
||||||
|
fn super_modifier_renders_per_platform() {
|
||||||
|
let expected = if cfg!(target_os = "macos") { "Cmd+" } else { "Win+" };
|
||||||
|
assert_eq!(mods_prefix(ModifiersState::SUPER), expected);
|
||||||
|
assert_eq!(
|
||||||
|
mods_prefix(ModifiersState::CONTROL | ModifiersState::SUPER),
|
||||||
|
format!("Ctrl+{expected}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 别名条目要能在存储格式里往返:`clear_action` 把表里的写法逐字写进
|
||||||
|
/// 设置文件,解析层必须读回同一个组合。
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[test]
|
||||||
|
fn macos_command_aliases_round_trip_through_storage() {
|
||||||
|
for (combo, _) in MACOS_COMMAND_ALIASES {
|
||||||
|
let (mods, key) = parse_combo(combo).unwrap_or_else(|| panic!("{combo} 解析失败"));
|
||||||
|
assert!(mods.super_key(), "{combo} 必须是 ⌘ 组合");
|
||||||
|
let canonical =
|
||||||
|
canonical_combo(mods, &key).unwrap_or_else(|| panic!("{combo} 无法写成存储格式"));
|
||||||
|
assert_eq!(parse_combo(&canonical), Some((mods, key)), "{combo} 存储往返不一致");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #238:macOS 上 Shell 选择器除了配置表的 Ctrl+K 还有一条壳自带的
|
||||||
|
/// ⌘K。解绑必须把两条都解掉,否则静态默认绑定继续抢键,用户看到的就是
|
||||||
|
/// 「解绑了但 ⌘K 照样弹面板」。
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[test]
|
||||||
|
fn clearing_releases_the_macos_command_alias() {
|
||||||
|
let action = Action::ToggleShellPicker;
|
||||||
|
// 设置页仍按配置表的 Ctrl+K 展示:别名只进解绑口径,不进键帽反查。
|
||||||
|
assert_eq!(effective_combo(&action, &[]), Some(("Ctrl+K".to_owned(), false)));
|
||||||
|
let mut raw = Vec::new();
|
||||||
|
clear_action(&mut raw, &action);
|
||||||
|
// 解绑使用表里的写法;旧配置和录制结果的等价拼写由 GPUI 层规范化。
|
||||||
|
assert!(
|
||||||
|
raw.iter().any(|(combo, _)| combo == "cmd+k"),
|
||||||
|
"⌘K 必须按表里的写法逐字写回,实际 {raw:?}"
|
||||||
|
);
|
||||||
|
// Ctrl+K 来自配置表、⌘K 来自别名表,两条都要落成 ReceiveChar。
|
||||||
|
for combo in ["ctrl+k", "cmd+k"] {
|
||||||
|
let parsed = parse_combo(combo);
|
||||||
|
assert!(
|
||||||
|
raw.iter().any(|(candidate, name)| parse_combo(candidate) == parsed
|
||||||
|
&& parse_action(name) == Some(Action::ReceiveChar)),
|
||||||
|
"{combo} 必须落成 ReceiveChar,否则运行时解不掉静态默认绑定"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(effective_combo(&action, &build_bindings(&raw)), None, "解绑后不应再有有效键");
|
||||||
|
reset_action(&mut raw, &action);
|
||||||
|
assert!(
|
||||||
|
effective_combo(&action, &build_bindings(&raw)).is_some(),
|
||||||
|
"恢复要把两组默认键都还回来"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,22 +44,30 @@ pub(super) const STATIC_DEFAULT_COMBOS: &[&str] = &[
|
|||||||
/// 存储格式 combo(`ctrl+shift+t`)→ gpui 绑定串(`ctrl-shift-t`)。键名
|
/// 存储格式 combo(`ctrl+shift+t`)→ gpui 绑定串(`ctrl-shift-t`)。键名
|
||||||
/// 两套体系同构(小写命名键 + 单字符);digitN 折回数字,plus/minus 折回
|
/// 两套体系同构(小写命名键 + 单字符);digitN 折回数字,plus/minus 折回
|
||||||
/// `+`/`-`(`+` 是存储分隔符,必须先占位再替换)。
|
/// `+`/`-`(`+` 是存储分隔符,必须先占位再替换)。
|
||||||
|
/// 最后由 GPUI 解析并规范化修饰键别名和顺序,保证旧 Win+、新 Cmd+ 和
|
||||||
|
/// 录制结果在覆盖/恢复默认时具有同一个运行时身份。
|
||||||
pub(super) fn gpui_binding_combo(combo: &str) -> String {
|
pub(super) fn gpui_binding_combo(combo: &str) -> String {
|
||||||
combo
|
let combo = combo
|
||||||
.to_ascii_lowercase()
|
.to_ascii_lowercase()
|
||||||
.replace("plus", "\u{1}")
|
.replace("plus", "\u{1}")
|
||||||
.replace("minus", "\u{2}")
|
.replace("minus", "\u{2}")
|
||||||
.replace('+', "-")
|
.replace('+', "-")
|
||||||
.replace("digit", "")
|
.replace("digit", "")
|
||||||
.replace('\u{1}', "+")
|
.replace('\u{1}', "+")
|
||||||
.replace('\u{2}', "-")
|
.replace('\u{2}', "-");
|
||||||
|
gpui::Keystroke::parse(&combo).map(|key| key.unparse()).unwrap_or(combo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 注册工作区快捷键;在 `gpui_component::init` 之后调用一次。
|
/// 注册工作区快捷键;在 `gpui_component::init` 之后调用一次。
|
||||||
pub(super) fn init(cx: &mut App) {
|
pub(super) fn init(cx: &mut App) {
|
||||||
cx.bind_keys(default_workspace_bindings());
|
cx.bind_keys(default_workspace_bindings());
|
||||||
#[cfg(target_os = "macos")]
|
// 平台判定复用已有入口;共享表不意味着给其他平台注册 ⌘ 快捷键。
|
||||||
bind_macos_command_keys(cx);
|
if crate::platform::Platform::current() == crate::platform::Platform::MacOS {
|
||||||
|
bind_macos_command_keys(cx);
|
||||||
|
}
|
||||||
|
// 退出不属于任何视图,挂全局兜底:⌘Q 与用户自定义的 `keybind=…:Quit`
|
||||||
|
// 都落到与托盘退出同一条「先落盘会话与草稿,再停 PTY」的路径。
|
||||||
|
cx.on_action(|_: &QuitApp, cx: &mut App| cx.defer(super::windowing::quit_all));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 工作区静态默认键位表;与 [`STATIC_DEFAULT_COMBOS`] 互为镜像。
|
/// 工作区静态默认键位表;与 [`STATIC_DEFAULT_COMBOS`] 互为镜像。
|
||||||
@@ -132,37 +140,23 @@ pub(super) fn default_workspace_bindings() -> Vec<KeyBinding> {
|
|||||||
/// 追加而非替换有两个原因:Ctrl+Shift 组合在 Mac 终端里没有别的含义,留着
|
/// 追加而非替换有两个原因:Ctrl+Shift 组合在 Mac 终端里没有别的含义,留着
|
||||||
/// 不碍事;而 ⌘C/⌘V 必须存在,否则 Mac 用户第一反应就是「复制粘贴坏了」。
|
/// 不碍事;而 ⌘C/⌘V 必须存在,否则 Mac 用户第一反应就是「复制粘贴坏了」。
|
||||||
/// 终端里的 Ctrl+C 仍然是 SIGINT——这里只绑 ⌘,不碰 Ctrl 的语义。
|
/// 终端里的 Ctrl+C 仍然是 SIGINT——这里只绑 ⌘,不碰 Ctrl 的语义。
|
||||||
#[cfg(target_os = "macos")]
|
///
|
||||||
|
/// 键位来自 `display::keymap::MACOS_COMMAND_ALIASES`:设置页的反查、解绑与
|
||||||
|
/// 恢复读的是同一张表,两处不会再各自漂移。注册必须留在这里、且早于用户
|
||||||
|
/// 自定义键,这样 `clear_action` 注入的 NoAction 才压得住静态默认绑定。
|
||||||
fn bind_macos_command_keys(cx: &mut App) {
|
fn bind_macos_command_keys(cx: &mut App) {
|
||||||
cx.bind_keys([
|
let mut bindings: Vec<KeyBinding> = crate::display::keymap::MACOS_COMMAND_ALIASES
|
||||||
KeyBinding::new("cmd-t", NewTerminal, None),
|
.iter()
|
||||||
KeyBinding::new("cmd-n", NewWindow, None),
|
.filter_map(|(combo, action)| workspace_binding_in_context(combo, action, None))
|
||||||
KeyBinding::new("cmd-w", CloseActiveTerminal, None),
|
.collect();
|
||||||
|
// 剩下这些没有对应的 `config::Action`,或者需要单独的作用域。
|
||||||
|
bindings.extend([
|
||||||
KeyBinding::new("cmd-b", ToggleSidebar, None),
|
KeyBinding::new("cmd-b", ToggleSidebar, None),
|
||||||
KeyBinding::new("cmd-,", OpenSettings, None),
|
KeyBinding::new("cmd-,", OpenSettings, None),
|
||||||
KeyBinding::new("cmd-shift-p", ToggleCommandPalette, None),
|
// 设置页与对话框的输入框自带 ⌘V,作用域必须和终端分开。
|
||||||
KeyBinding::new("cmd-k", ToggleShellPicker, None),
|
|
||||||
KeyBinding::new("cmd-shift-f", ToggleFileTree, None),
|
|
||||||
KeyBinding::new("cmd-d", SplitRight, None),
|
|
||||||
KeyBinding::new("cmd-shift-d", SplitDown, None),
|
|
||||||
KeyBinding::new("cmd-shift-enter", ToggleZoom, None),
|
|
||||||
KeyBinding::new("cmd-alt-left", FocusPaneLeft, None),
|
|
||||||
KeyBinding::new("cmd-alt-right", FocusPaneRight, None),
|
|
||||||
KeyBinding::new("cmd-alt-up", FocusPaneUp, None),
|
|
||||||
KeyBinding::new("cmd-alt-down", FocusPaneDown, None),
|
|
||||||
KeyBinding::new("cmd-shift-]", SelectNextTab, None),
|
|
||||||
KeyBinding::new("cmd-shift-[", SelectPreviousTab, None),
|
|
||||||
KeyBinding::new("cmd-shift-g", ToggleGitPanel, None),
|
|
||||||
KeyBinding::new("cmd-=", IncreaseFontSize, None),
|
|
||||||
KeyBinding::new("cmd-+", IncreaseFontSize, None),
|
|
||||||
KeyBinding::new("cmd--", DecreaseFontSize, None),
|
|
||||||
KeyBinding::new("cmd-0", ResetFontSize, None),
|
|
||||||
KeyBinding::new("cmd-c", CopySelection, Some(crate::gpui_shell::terminal::KEY_CONTEXT)),
|
|
||||||
KeyBinding::new("cmd-v", PasteClipboard, Some(crate::gpui_shell::terminal::KEY_CONTEXT)),
|
|
||||||
KeyBinding::new("cmd-v", gpui_component::input::Paste, Some("Input")),
|
KeyBinding::new("cmd-v", gpui_component::input::Paste, Some("Input")),
|
||||||
KeyBinding::new("cmd-ctrl-f", ToggleFullscreen, None),
|
|
||||||
KeyBinding::new("cmd-shift-o", OpenQuickJump, None),
|
|
||||||
]);
|
]);
|
||||||
|
cx.bind_keys(bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Typed GPUI adapter for the shared numbered/last-tab actions. The existing
|
/// Typed GPUI adapter for the shared numbered/last-tab actions. The existing
|
||||||
@@ -173,6 +167,12 @@ pub(super) struct SelectTab {
|
|||||||
index: Option<usize>,
|
index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 退出应用,macOS 上绑 ⌘Q(`Action::Quit` 的 GPUI 侧落点)。处理注册在
|
||||||
|
/// [`init`] 的全局兜底上,与绑定同一处维护。
|
||||||
|
#[derive(Clone, Debug, PartialEq, gpui::Action)]
|
||||||
|
#[action(namespace = nebula_workspace, no_json)]
|
||||||
|
pub(super) struct QuitApp;
|
||||||
|
|
||||||
impl SelectTab {
|
impl SelectTab {
|
||||||
fn from_config(action: &crate::config::Action) -> Option<Self> {
|
fn from_config(action: &crate::config::Action) -> Option<Self> {
|
||||||
use crate::config::Action;
|
use crate::config::Action;
|
||||||
@@ -250,8 +250,9 @@ fn workspace_binding_in_context(
|
|||||||
)),
|
)),
|
||||||
Action::ToggleFullscreen => Some(KeyBinding::new(&combo, ToggleFullscreen, scope)),
|
Action::ToggleFullscreen => Some(KeyBinding::new(&combo, ToggleFullscreen, scope)),
|
||||||
Action::OpenQuickJump => Some(KeyBinding::new(&combo, OpenQuickJump, scope)),
|
Action::OpenQuickJump => Some(KeyBinding::new(&combo, OpenQuickJump, scope)),
|
||||||
// Mask the app action. ReceiveChar entries from the editor allow
|
// macOS 的退出键走与托盘退出同一条路径:先落盘会话与草稿,再停 PTY。
|
||||||
// the terminal input handler to encode the released key.
|
Action::Quit => Some(KeyBinding::new(&combo, QuitApp, scope)),
|
||||||
|
// 屏蔽应用动作后,ReceiveChar 仍交给终端编码,保留改键释放旧键的语义。
|
||||||
Action::None | Action::ReceiveChar => Some(KeyBinding::new(&combo, gpui::NoAction, scope)),
|
Action::None | Action::ReceiveChar => Some(KeyBinding::new(&combo, gpui::NoAction, scope)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,90 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_aliases_and_modifier_order_share_one_runtime_identity() {
|
||||||
|
let expected = gpui_binding_combo("shift+cmd+k");
|
||||||
|
for combo in ["Shift+Win+K", "cmd+shift+k", "super+shift+k"] {
|
||||||
|
assert_eq!(gpui_binding_combo(combo), expected, "{combo}");
|
||||||
|
}
|
||||||
|
assert_ne!(gpui_binding_combo("cmd+k"), expected);
|
||||||
|
assert_ne!(gpui_binding_combo("ctrl+shift+k"), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod macos_command_keys {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// #238:别名表里的每条都必须落到 gpui 绑定上。表和运行时各写一份时,
|
||||||
|
/// 设置页显示的键可能按下去没有反应;合一之后这条守住两边不再漂移。
|
||||||
|
#[test]
|
||||||
|
fn every_macos_command_alias_binds_a_gpui_action() {
|
||||||
|
for (combo, action) in crate::display::keymap::MACOS_COMMAND_ALIASES {
|
||||||
|
assert!(
|
||||||
|
workspace_binding_in_context(combo, action, None).is_some(),
|
||||||
|
"{combo} → {action:?} 没有对应的 gpui 绑定"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// ⌘Q 必须落到退出动作,否则退出键不会走保存会话与草稿的路径。
|
||||||
|
let quit = workspace_binding_in_context("cmd+q", &crate::config::Action::Quit, None)
|
||||||
|
.expect("⌘Q 必须有绑定");
|
||||||
|
assert!(quit.action().as_any().is::<QuitApp>(), "⌘Q 必须绑定 QuitApp");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_close_and_quit_dispatch_to_distinct_actions() {
|
||||||
|
use gpui::{KeyContext, Keymap, Keystroke};
|
||||||
|
// 合并后仍按共享别名表分派,防止配置中的 ⌘W/Quit 抢走关闭标签页动作。
|
||||||
|
let bindings = crate::display::keymap::MACOS_COMMAND_ALIASES
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(combo, action)| workspace_binding_in_context(combo, action, None))
|
||||||
|
.collect();
|
||||||
|
let keymap = Keymap::new(bindings);
|
||||||
|
for context in ["Root", crate::gpui_shell::terminal::KEY_CONTEXT] {
|
||||||
|
let contexts = [KeyContext::parse(context).unwrap()];
|
||||||
|
for (combo, quit) in [("cmd-w", false), ("cmd-q", true)] {
|
||||||
|
let input = [Keystroke::parse(combo).unwrap()];
|
||||||
|
let (bindings, pending) = keymap.bindings_for_input(&input, &contexts);
|
||||||
|
assert!(!pending);
|
||||||
|
let action = bindings.first().expect("命令键必须有明确动作").action();
|
||||||
|
if quit {
|
||||||
|
assert!(action.as_any().is::<QuitApp>());
|
||||||
|
} else {
|
||||||
|
assert!(action.as_any().is::<CloseActiveTerminal>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #238:⌘K 被解绑后,用户注入的 NoAction 必须压得住静态默认绑定。
|
||||||
|
#[test]
|
||||||
|
fn released_cmd_k_is_swallowed_by_no_action() {
|
||||||
|
use crate::config::Action;
|
||||||
|
use gpui::{KeyContext, Keymap, Keystroke};
|
||||||
|
let contexts = [
|
||||||
|
KeyContext::parse("Root").unwrap(),
|
||||||
|
KeyContext::parse(crate::gpui_shell::terminal::KEY_CONTEXT).unwrap(),
|
||||||
|
];
|
||||||
|
let input = [Keystroke::parse("cmd-k").unwrap()];
|
||||||
|
// 反向对照:只有静态默认键时 ⌘K 必须命中 Shell 选择器,否则本用例空跑。
|
||||||
|
let default =
|
||||||
|
custom_workspace_binding("cmd+k", &Action::ToggleShellPicker).expect("静态默认键");
|
||||||
|
let (bindings, pending) =
|
||||||
|
Keymap::new(vec![default.clone()]).bindings_for_input(&input, &contexts);
|
||||||
|
assert!(!pending, "对照组必须命中");
|
||||||
|
assert!(bindings[0].action().as_any().is::<ToggleShellPicker>());
|
||||||
|
// `update_keybinds` 在两个作用域各注入一条 NoAction,这里照抄运行时形状。
|
||||||
|
let clear = workspace_binding_in_context("cmd+k", &Action::ReceiveChar, None).unwrap();
|
||||||
|
let clear_terminal = workspace_binding_in_context(
|
||||||
|
"cmd+k",
|
||||||
|
&Action::ReceiveChar,
|
||||||
|
Some(crate::gpui_shell::terminal::KEY_CONTEXT),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let released = Keymap::new(vec![default, clear, clear_terminal]);
|
||||||
|
let (bindings, pending) = released.bindings_for_input(&input, &contexts);
|
||||||
|
assert!(bindings.is_empty() && !pending, "解绑后不应有任何应用动作消费 ⌘K");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cleared_shortcut_reaches_terminal_and_can_be_restored_without_restart() {
|
fn cleared_shortcut_reaches_terminal_and_can_be_restored_without_restart() {
|
||||||
use crate::config::Action;
|
use crate::config::Action;
|
||||||
@@ -120,6 +206,65 @@ mod dispatch {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn command_shortcut_registration_follows_runtime_platform(cx: &mut TestAppContext) {
|
||||||
|
let (_directory, workspace, mut cx) = open_workspace(1, cx);
|
||||||
|
press("cmd-k", &mut cx);
|
||||||
|
assert_eq!(
|
||||||
|
workspace.read_with(&cx, |workspace, _| workspace.shell_picker_open),
|
||||||
|
crate::platform::Platform::current() == crate::platform::Platform::MacOS,
|
||||||
|
"共享别名表只应在 macOS 注册原生命令键"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn recorded_command_key_can_restore_default(cx: &mut TestAppContext) {
|
||||||
|
use crate::display::keymap;
|
||||||
|
let (_directory, workspace, mut cx) = open_workspace(1, cx);
|
||||||
|
press("cmd-k", &mut cx);
|
||||||
|
assert!(workspace.read_with(&cx, |workspace, _| workspace.shell_picker_open));
|
||||||
|
press("escape", &mut cx);
|
||||||
|
let keymap::CaptureOutcome::Bind(combo) =
|
||||||
|
keymap::capture_gpui(&Keystroke::parse("cmd-k").unwrap())
|
||||||
|
else {
|
||||||
|
panic!("expected captured shortcut")
|
||||||
|
};
|
||||||
|
assert_eq!(combo, "win+k");
|
||||||
|
let mut raw = vec![(combo, "ToggleShellPicker".into())];
|
||||||
|
workspace.update(&mut cx, |workspace, cx| workspace.update_keybinds(raw.clone(), cx));
|
||||||
|
press("cmd-k", &mut cx);
|
||||||
|
assert!(workspace.read_with(&cx, |workspace, _| workspace.shell_picker_open));
|
||||||
|
press("escape", &mut cx);
|
||||||
|
keymap::reset_action(&mut raw, &crate::config::Action::ToggleShellPicker);
|
||||||
|
workspace.update(&mut cx, |workspace, cx| workspace.update_keybinds(raw, cx));
|
||||||
|
press("cmd-k", &mut cx);
|
||||||
|
assert!(
|
||||||
|
workspace.read_with(&cx, |workspace, _| workspace.shell_picker_open),
|
||||||
|
"Reset after recording Cmd+K must restore its default action"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn old_fullscreen_unbind_can_restore_default(cx: &mut TestAppContext) {
|
||||||
|
use crate::display::keymap;
|
||||||
|
let (_directory, workspace, mut cx) = open_workspace(1, cx);
|
||||||
|
for combo in ["Ctrl+Cmd+F", "Ctrl+Win+F", "super+ctrl+f"] {
|
||||||
|
let mut raw = vec![(combo.into(), "ReceiveChar".into())];
|
||||||
|
workspace.update(&mut cx, |workspace, cx| workspace.update_keybinds(raw.clone(), cx));
|
||||||
|
press("ctrl-cmd-f", &mut cx);
|
||||||
|
assert!(!cx.update(|window, _| window.is_fullscreen()));
|
||||||
|
keymap::reset_action(&mut raw, &crate::config::Action::ToggleFullscreen);
|
||||||
|
assert!(raw.is_empty());
|
||||||
|
workspace.update(&mut cx, |workspace, cx| workspace.update_keybinds(raw, cx));
|
||||||
|
press("ctrl-cmd-f", &mut cx);
|
||||||
|
assert!(cx.update(|window, _| window.is_fullscreen()), "reset failed for {combo}");
|
||||||
|
press("ctrl-cmd-f", &mut cx);
|
||||||
|
assert!(!cx.update(|window, _| window.is_fullscreen()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn digits_switch_real_tabs_and_focus_in_both_tab_layouts(cx: &mut TestAppContext) {
|
fn digits_switch_real_tabs_and_focus_in_both_tab_layouts(cx: &mut TestAppContext) {
|
||||||
let (_directory, workspace, mut cx) = open_workspace(10, cx);
|
let (_directory, workspace, mut cx) = open_workspace(10, cx);
|
||||||
|
|||||||
Reference in new issue
Block a user