fix(settings): normalize shortcut edits to the persisted form

This commit is contained in:
zipg authored and GitHub committed 2026-09-21 19:11:56 +08:00
1 parent 0c6fc610cd
commit 776f44c1cb
14 files changed
+174 -4

No files matched your search

@@ -11,6 +11,7 @@ import {
MACOS_RESERVED_SHORTCUTS,
normalizeModifierOnlyShortcut,
normalizeShortcutSettings,
resolveCapturedShortcutEdit,
selectionOccurrenceDefaultShortcut,
SHORTCUT_DEFINITIONS,
shortcutToCodeMirrorKey,
@@ -526,6 +527,67 @@ describe("shortcutRegistry editor actions", () => {
});
});
describe("resolveCapturedShortcutEdit", () => {
// #9881:设置面板「应用」点了毫无反应、「应用并关闭」每次都弹未保存确认,
// 根因就是捕获到的组合被落盘口径改写,草稿和 store 值再也对不上。
const macDefaults = normalizeShortcutSettings(undefined, "MacIntel");
const crossPlatformCases: [ShortcutActionId, string][] = [
["closeOtherTabs", "Shift+Alt+W"],
["navigateTabHistoryBack", "Mod+Alt+ArrowLeft"],
["navigateTabHistoryForward", "Mod+Alt+ArrowRight"],
["addNextSelectionOccurrence", "Alt+J"],
["selectAllSelectionOccurrences", "Ctrl+Alt+Shift+J"],
["toggleAiPanel", "Ctrl+Alt+I"],
["gotoLine", "Mod+G"],
];
it("flags combinations that are another platform's default for the same action", () => {
for (const [actionId, shortcut] of crossPlatformCases) {
const edit = resolveCapturedShortcutEdit(actionId, shortcut, macDefaults, "MacIntel");
expect(edit.rejectedByPlatformDefault).toBe(true);
// 落盘值是该动作本机平台的默认键,绝不能用它当草稿值写入
expect(edit.shortcuts[actionId]).not.toBe(shortcut);
}
});
it("accepts a plain rebind and keeps the draft a fixed point of the persistence normalizer", () => {
const edit = resolveCapturedShortcutEdit("executeSql", "Shift+Ctrl+U", macDefaults, "MacIntel");
expect(edit.rejectedByPlatformDefault).toBe(false);
expect(edit.changed).toBe(true);
expect(edit.shortcuts.executeSql).toBe("Shift+Ctrl+U");
// 关键不变式:草稿再走一遍落盘口径不会被改写,否则“未保存”永远消不掉
expect(normalizeShortcutSettings(edit.shortcuts, "MacIntel")).toEqual(edit.shortcuts);
});
it("adopts the copyCurrentRow/editTableStructure legacy migration into the draft", () => {
const draft = normalizeShortcutSettings({ ...macDefaults, copyCurrentRow: "" }, "MacIntel");
const edit = resolveCapturedShortcutEdit("editTableStructure", "Mod+D", draft, "MacIntel");
expect(edit.rejectedByPlatformDefault).toBe(false);
expect(edit.changed).toBe(true);
expect(edit.shortcuts).toMatchObject({ editTableStructure: "Mod+Shift+D", copyCurrentRow: "Mod+D" });
expect(normalizeShortcutSettings(edit.shortcuts, "MacIntel")).toEqual(edit.shortcuts);
});
it("reports no changes when the user re-presses the current combination", () => {
const edit = resolveCapturedShortcutEdit("closeOtherTabs", macDefaults.closeOtherTabs, macDefaults, "MacIntel");
expect(edit.rejectedByPlatformDefault).toBe(false);
expect(edit.changed).toBe(false);
expect(edit.shortcuts).toEqual(macDefaults);
});
it("still applies the cross-platform rule on Windows for the macOS default literals", () => {
const windowsDefaults = normalizeShortcutSettings(undefined, "Win32");
const edit = resolveCapturedShortcutEdit("closeOtherTabs", "Alt+Mod+W", windowsDefaults, "Win32");
expect(edit.rejectedByPlatformDefault).toBe(true);
expect(edit.shortcuts.closeOtherTabs).toBe(closeOtherTabsDefaultShortcut("Win32"));
});
});
describe("countShortcutConflictPairs", () => {
it("counts an unordered pair once even though the map is bidirectional", () => {
expect(countShortcutConflictPairs({ find: "focusSearch", focusSearch: "find" })).toBe(1);
@@ -745,6 +745,39 @@ export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>,
return normalized;
}
export interface CapturedShortcutEdit {
/** 归一化后的完整键位表:与 store 落盘口径一致,草稿直接采用它就不会与落盘值分叉。 */
shortcuts: ShortcutSettings;
/**
* 用户按下的组合命中了“另一平台的默认键”规则:落盘时该字面量会被换成本机平台
* 的默认键,所以这个组合在本平台不可能被保存(见上方 PLATFORM_DEFAULT_SHORTCUTS)。
*/
rejectedByPlatformDefault: boolean;
/** 归一化是否改动了任意一行(跨平台默认键、旧默认键、遗留迁移都会改动)。 */
changed: boolean;
}
/**
* 捕获阶段的归一化:把用户在设置面板按下的组合先过一遍落盘时会走的
* `normalizeShortcutSettings`,让草稿始终是持久化值的**不动点**。
*
* 设置面板的“有无未保存改动”是拿草稿和 store 里的值做 JSON 比较
* (`editorSettingsDraftChanged`)得来的:草稿里只要存在一个会被落盘口径改写的
* 值,这个比较就永远为真——「应用」看起来毫无反应,「应用并关闭」每次都弹
* “未保存的更改”,改了多少次都存不进去(#9881:把「关闭其他标签页」改成 ⇧⌥W,
* 落盘时被跨平台默认键规则换回 ⌥⌘W)。调用方拿到结果后必须采用 `shortcuts`,
* 并在 `rejectedByPlatformDefault` 为真时提示用户换一个组合。
*/
export function resolveCapturedShortcutEdit(actionId: ShortcutActionId, shortcut: string, shortcuts: ShortcutSettings, platform = globalThis.navigator?.platform || ""): CapturedShortcutEdit {
const next = normalizeShortcutSettings({ ...shortcuts, [actionId]: shortcut }, platform);
const rejectedByPlatformDefault = next[actionId] !== shortcut && (PLATFORM_DEFAULT_SHORTCUTS[actionId]?.has(shortcut) === true || (actionId === "closeTab" && shortcut === LEGACY_CLOSE_TAB_DEFAULT));
return {
shortcuts: next,
rejectedByPlatformDefault,
changed: SHORTCUT_DEFINITIONS.some((definition) => next[definition.id] !== shortcuts[definition.id]),
};
}
export function shortcutToCodeMirrorKey(shortcut: string): string {
return parseShortcutStrokes(shortcut)
.map((parts) =>