Merge upstream main into fix/windows-ctrl-slash

This commit is contained in:
GeekMr committed 2026-09-26 20:41:49 +08:00
commit cc269503e8
4 files changed
+423 -34

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.
+130 -3
View File
@@ -88,6 +88,52 @@ pub(crate) const READONLY_ROWS: &[(&str, &str, &str)] = &[
("贴入 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(
row: &(Action, &'static str, &'static str),
language: super::UiLanguage,
@@ -118,13 +164,33 @@ fn cached_defaults() -> &'static [KeyBinding] {
}
pub(crate) fn default_shortcuts() -> Vec<(String, Action)> {
cached_defaults()
let mut shortcuts: Vec<(String, Action)> = cached_defaults()
.iter()
.filter_map(|binding| {
display_combo(binding.mods, &binding.trigger)
.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.
@@ -335,7 +401,10 @@ pub(crate) fn mods_prefix(mods: ModifiersState) -> String {
out.push_str("Alt+");
}
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
}
@@ -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,23 +44,31 @@ pub(super) const STATIC_DEFAULT_COMBOS: &[&str] = &[
/// 存储格式 combo(`ctrl+shift+t`)→ gpui 绑定串(`ctrl-shift-t`)。键名
/// 两套体系同构(小写命名键 + 单字符);digitN 折回数字,plus/minus 折回
/// `+`/`-`(`+` 是存储分隔符,必须先占位再替换)。
/// 最后由 GPUI 解析并规范化修饰键别名和顺序,保证旧 Win+、新 Cmd+ 和
/// 录制结果在覆盖/恢复默认时具有同一个运行时身份。
pub(super) fn gpui_binding_combo(combo: &str) -> String {
combo
let combo = combo
.to_ascii_lowercase()
.replace("plus", "\u{1}")
.replace("minus", "\u{2}")
.replace('+', "-")
.replace("digit", "")
.replace('\u{1}', "+")
.replace('\u{2}', "-")
.replace('\u{2}', "-");
gpui::Keystroke::parse(&combo).map(|key| key.unparse()).unwrap_or(combo)
}
/// 注册工作区快捷键;在 `gpui_component::init` 之后调用一次。
pub(super) fn init(cx: &mut App) {
cx.bind_keys(default_workspace_bindings());
#[cfg(target_os = "macos")]
// 平台判定复用已有入口;共享表不意味着给其他平台注册 ⌘ 快捷键。
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`] 互为镜像。
pub(super) fn default_workspace_bindings() -> Vec<KeyBinding> {
@@ -132,37 +140,23 @@ pub(super) fn default_workspace_bindings() -> Vec<KeyBinding> {
/// 追加而非替换有两个原因:Ctrl+Shift 组合在 Mac 终端里没有别的含义,留着
/// 不碍事;而 ⌘C/⌘V 必须存在,否则 Mac 用户第一反应就是「复制粘贴坏了」。
/// 终端里的 Ctrl+C 仍然是 SIGINT——这里只绑 ⌘,不碰 Ctrl 的语义。
#[cfg(target_os = "macos")]
///
/// 键位来自 `display::keymap::MACOS_COMMAND_ALIASES`:设置页的反查、解绑与
/// 恢复读的是同一张表,两处不会再各自漂移。注册必须留在这里、且早于用户
/// 自定义键,这样 `clear_action` 注入的 NoAction 才压得住静态默认绑定。
fn bind_macos_command_keys(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("cmd-t", NewTerminal, None),
KeyBinding::new("cmd-n", NewWindow, None),
KeyBinding::new("cmd-w", CloseActiveTerminal, None),
let mut bindings: Vec<KeyBinding> = crate::display::keymap::MACOS_COMMAND_ALIASES
.iter()
.filter_map(|(combo, action)| workspace_binding_in_context(combo, action, None))
.collect();
// 剩下这些没有对应的 `config::Action`,或者需要单独的作用域。
bindings.extend([
KeyBinding::new("cmd-b", ToggleSidebar, None),
KeyBinding::new("cmd-,", OpenSettings, None),
KeyBinding::new("cmd-shift-p", ToggleCommandPalette, None),
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)),
// 设置页与对话框的输入框自带 ⌘V,作用域必须和终端分开。
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
@@ -173,6 +167,12 @@ pub(super) struct SelectTab {
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 {
fn from_config(action: &crate::config::Action) -> Option<Self> {
use crate::config::Action;
@@ -250,8 +250,9 @@ fn workspace_binding_in_context(
)),
Action::ToggleFullscreen => Some(KeyBinding::new(&combo, ToggleFullscreen, scope)),
Action::OpenQuickJump => Some(KeyBinding::new(&combo, OpenQuickJump, scope)),
// Mask the app action. ReceiveChar entries from the editor allow
// the terminal input handler to encode the released key.
// macOS 的退出键走与托盘退出同一条路径:先落盘会话与草稿,再停 PTY。
Action::Quit => Some(KeyBinding::new(&combo, QuitApp, scope)),
// 屏蔽应用动作后,ReceiveChar 仍交给终端编码,保留改键释放旧键的语义。
Action::None | Action::ReceiveChar => Some(KeyBinding::new(&combo, gpui::NoAction, scope)),
_ => None,
}
@@ -1,4 +1,90 @@
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]
fn cleared_shortcut_reaches_terminal_and_can_be_restored_without_restart() {
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]
fn digits_switch_real_tabs_and_focus_in_both_tab_layouts(cx: &mut TestAppContext) {
let (_directory, workspace, mut cx) = open_workspace(10, cx);