feat(editor): group shortcuts by scope with conflict tiers
Closes #9075
This commit is contained in:
1 parent
b2d56625e0
commit
ea1de14517
17 files changed
+970
-80
No files matched your search
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
closeOtherTabsDefaultShortcut,
|
||||
countShortcutConflictPairs,
|
||||
DEFAULT_SHORTCUT_SETTINGS,
|
||||
findCrossScopeShortcutConflicts,
|
||||
findShortcutConflict,
|
||||
formatShortcut,
|
||||
isReservedShortcut,
|
||||
@@ -431,4 +433,69 @@ describe("shortcutRegistry editor actions", () => {
|
||||
expect(windows.find).toBe("Mod+H");
|
||||
expect(windows.formatSql).toBe("Mod+F");
|
||||
});
|
||||
|
||||
describe("findCrossScopeShortcutConflicts", () => {
|
||||
it("reports the intentional cross-scope overlaps in the defaults", () => {
|
||||
// 默认配置里确实存在跨作用域同键,这是设计使然;设置界面把它作为“提示”
|
||||
// 展示,而 normalizeShortcutSettings 的占用判定刻意不管它们。
|
||||
const conflicts = findCrossScopeShortcutConflicts(DEFAULT_SHORTCUT_SETTINGS);
|
||||
expect(conflicts.find).toContain("focusSearch");
|
||||
expect(conflicts.focusSearch).toContain("find");
|
||||
expect(conflicts.acceptCompletion).toContain("toggleTranspose");
|
||||
expect(conflicts.explainSql).toContain("editSidebarConnection");
|
||||
// Shift+Mod+D 与 Mod+Shift+D 经 formatShortcut 规范化后同键。
|
||||
expect(conflicts.viewTableDdl).toContain("editTableStructure");
|
||||
});
|
||||
|
||||
it("never reports same-scope duplicates (those are blocking conflicts)", () => {
|
||||
// uppercaseSelection 改成 Mod+A 只与同作用域的 selectAll 重复,
|
||||
// 跨作用域提示不应把它列出来——那属于 findShortcutConflict 的职责。
|
||||
const shortcuts = normalizeShortcutSettings({ uppercaseSelection: "Mod+A" });
|
||||
expect(findShortcutConflict("uppercaseSelection", shortcuts.uppercaseSelection, shortcuts)).toBe("selectAll");
|
||||
expect(findCrossScopeShortcutConflicts(shortcuts).uppercaseSelection).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores unbound shortcuts", () => {
|
||||
const shortcuts = normalizeShortcutSettings({ find: "", focusSearch: "" });
|
||||
expect(shortcuts.find).toBe("");
|
||||
expect(shortcuts.focusSearch).toBe("");
|
||||
const conflicts = findCrossScopeShortcutConflicts(shortcuts);
|
||||
expect(conflicts.find).toBeUndefined();
|
||||
expect(conflicts.focusSearch).toBeUndefined();
|
||||
// goToColumn 默认未绑定,也不参与比较。
|
||||
expect(conflicts.goToColumn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves platform-dependent keys before comparing", () => {
|
||||
// Win32 上 Mod+F 展开为 Ctrl+F,两边仍然是同键,提示照旧成立;
|
||||
// 未自定义的组合也不会被误判。
|
||||
const windows = findCrossScopeShortcutConflicts(normalizeShortcutSettings(undefined, "Win32"), "Win32");
|
||||
expect(windows.find).toContain("focusSearch");
|
||||
const mac = findCrossScopeShortcutConflicts(normalizeShortcutSettings(undefined, "MacIntel"), "MacIntel");
|
||||
expect(mac.find).toContain("focusSearch");
|
||||
});
|
||||
|
||||
it("does not mutate the settings it inspects", () => {
|
||||
const shortcuts = normalizeShortcutSettings({ duplicateLine: "Mod+Alt+K" });
|
||||
const before = { ...shortcuts };
|
||||
findCrossScopeShortcutConflicts(shortcuts);
|
||||
expect(shortcuts).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("countShortcutConflictPairs", () => {
|
||||
it("counts an unordered pair once even though the map is bidirectional", () => {
|
||||
expect(countShortcutConflictPairs({ find: "focusSearch", focusSearch: "find" })).toBe(1);
|
||||
});
|
||||
|
||||
it("flattens multi-partner entries", () => {
|
||||
expect(countShortcutConflictPairs({ find: ["focusSearch", "formatSql"], formatSql: ["find"] })).toBe(2);
|
||||
});
|
||||
|
||||
it("ignores empty entries", () => {
|
||||
expect(countShortcutConflictPairs({})).toBe(0);
|
||||
expect(countShortcutConflictPairs({ find: "" })).toBe(0);
|
||||
expect(countShortcutConflictPairs({ find: [] })).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -778,3 +778,49 @@ export function findShortcutConflict(actionId: ShortcutActionId, shortcut: strin
|
||||
const conflict = SHORTCUT_DEFINITIONS.find((item) => item.id !== actionId && item.scope === definition.scope && shortcutsUseSameKeys(shortcuts[item.id], shortcut, platform));
|
||||
return conflict?.id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨作用域同键(**仅用于提示,不参与任何修复**)。
|
||||
*
|
||||
* 不同作用域共用同一个组合是**有意的设计**:`find`(editor)与
|
||||
* `focusSearch`(global)默认都是 `Mod+F`,运行时由焦点/目标路由区分
|
||||
* (`App.vue` 在 `defaultPrevented` 时让位,`ContentArea.focusSearch()`
|
||||
* 按聚焦面路由)。因此本函数只**只读地报告**这类重叠,供设置界面以
|
||||
* “提示”级别展示;绝不能拿它的结果去改写配置——`normalizeShortcutSettings`
|
||||
* 的占用判定刻意只统计**同作用域**(见该函数内的两张占用轮),跨作用域
|
||||
* 重叠不在其范围内。
|
||||
*
|
||||
* 同作用域重复**不**在此返回(那是必须阻断的冲突,见 `findShortcutConflict`)。
|
||||
* 未绑定(空串)不参与比较。比较口径与 `findShortcutConflict` 一致:
|
||||
* `formatShortcut` 展开 `Mod`、修饰键排序、大小写不敏感,因此
|
||||
* `Mod+Shift+D` 与 `Shift+Mod+D` 视为同键。
|
||||
*/
|
||||
export function findCrossScopeShortcutConflicts(shortcuts: ShortcutSettings, platform = globalThis.navigator?.platform || ""): Partial<Record<ShortcutActionId, ShortcutActionId[]>> {
|
||||
const conflicts: Partial<Record<ShortcutActionId, ShortcutActionId[]>> = {};
|
||||
for (const definition of SHORTCUT_DEFINITIONS) {
|
||||
const shortcut = shortcuts[definition.id];
|
||||
if (!shortcut) continue;
|
||||
const others = SHORTCUT_DEFINITIONS.filter((item) => item.id !== definition.id && item.scope !== definition.scope && shortcutsUseSameKeys(shortcuts[item.id], shortcut, platform)).map((item) => item.id);
|
||||
if (others.length > 0) conflicts[definition.id] = others;
|
||||
}
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重后的重复对数。行→对方 的映射会把同一对重复算两次(双向各一次),
|
||||
* 而摘要文案要说的是“有多少组重复”,故统一按无序对折算。
|
||||
*
|
||||
* 同时接受 L1(每行单个对方)与 L2(每行多个对方)两种形状,
|
||||
* 让同作用域与跨作用域的计数口径保持一致。
|
||||
*/
|
||||
export function countShortcutConflictPairs(conflicts: Partial<Record<ShortcutActionId, ShortcutActionId | ShortcutActionId[]>>): number {
|
||||
const pairs = new Set<string>();
|
||||
for (const [actionId, value] of Object.entries(conflicts)) {
|
||||
if (!value) continue;
|
||||
for (const other of Array.isArray(value) ? value : [value]) {
|
||||
if (!other) continue;
|
||||
pairs.add([actionId, other].sort().join("|"));
|
||||
}
|
||||
}
|
||||
return pairs.size;
|
||||
}
|
||||
Reference in new issue
Block a user