mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 13:13:25 +00:00
fix(app): preserve number groups when rebinding navigation shortcuts (#7065)
Recording a digit for tab or workspace navigation replaced the existing 1–9 wildcard with one literal key. Preserve the group when capturing a replacement and display its range after saving and reloading. Adapted the number-group fix from getpaseo/paseo#5237, with independent failing-first renderer reproduction and tab/workspace matcher coverage. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: dbunk903 <74584941+dbunk903@users.noreply.github.com>
This commit is contained in:
co-authored by
dbunk903
parent
e0de0fdf6f
commit
5c2a1552df
@@ -4,6 +4,7 @@ import { keyboardEventToComboString } from "./shortcut-string";
|
||||
import {
|
||||
buildKeyboardShortcutHelpSections,
|
||||
buildEffectiveBindings,
|
||||
normalizeCapturedShortcutCombo,
|
||||
getBindingIdForAction,
|
||||
getDefaultKeysForAction,
|
||||
getWorkspaceIndexJumpModifierKey,
|
||||
@@ -823,9 +824,9 @@ describe("keyboard-shortcut help sections", () => {
|
||||
expect(rowChord({}, "show-shortcuts")).toEqual([["?"]]);
|
||||
});
|
||||
|
||||
it("replaces the default-only wildcard when the index jump is rebound", () => {
|
||||
it("keeps the digit range readable when the index jump is rebound", () => {
|
||||
expect(rowChord({ [MAC_INDEX_BINDING]: "Ctrl+Digit" }, "workspace-jump-index")).toEqual([
|
||||
["ctrl", "Digit"],
|
||||
["ctrl", "1-9"],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1347,3 +1348,45 @@ describe("direct new-tab target shortcuts", () => {
|
||||
).toEqual([["ctrl", "shift", "H"]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("number-group shortcut capture", () => {
|
||||
const tabBindingId = "workspace-tab-navigate-index-cmd-alt-digit-mac-desktop";
|
||||
it.each([
|
||||
{ id: tabBindingId, action: "workspace.tab.navigate.index", row: "workspace-tab-jump-index" },
|
||||
{
|
||||
id: "workspace-navigate-index-cmd-digit-mac",
|
||||
action: "workspace.navigate.index",
|
||||
row: "workspace-jump-index",
|
||||
},
|
||||
])("captures Ctrl+1 as the entire number group for $row", ({ id, action, row }) => {
|
||||
const combo = normalizeCapturedShortcutCombo(id, "Ctrl+1");
|
||||
expect(combo).toBe("Ctrl+Digit");
|
||||
const overrides = { [id]: combo };
|
||||
const bindings = buildEffectiveBindings(overrides);
|
||||
for (let index = 1; index <= 9; index++) {
|
||||
const result = resolveShortcut({
|
||||
event: { key: String(index), code: `Digit${index}`, ctrlKey: true },
|
||||
context: { isMac: true, isDesktop: true, focusScope: "message-input" },
|
||||
bindings,
|
||||
});
|
||||
expect(result.match?.action).toBe(action);
|
||||
expect(result.match?.payload).toEqual({ index });
|
||||
}
|
||||
expect(resolveShortcutKeysForAction(row, overrides, { isMac: true, isDesktop: true })).toEqual([
|
||||
["ctrl", "1-9"],
|
||||
]);
|
||||
expect(
|
||||
resolveShortcut({
|
||||
event: { key: "0", code: "Digit0", ctrlKey: true },
|
||||
context: { isMac: true, isDesktop: true },
|
||||
bindings,
|
||||
}).match,
|
||||
).toBeNull();
|
||||
});
|
||||
it("preserves literal numbers for non-index actions and existing nonnumeric combos", () => {
|
||||
expect(normalizeCapturedShortcutCombo("show-shortcuts", "Ctrl+1")).toBe("Ctrl+1");
|
||||
expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+K")).toBe("Ctrl+K");
|
||||
expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+0")).toBe("Ctrl+0");
|
||||
expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+9")).toBe("Ctrl+Digit");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1210,6 +1210,13 @@ export const DEFAULT_BINDINGS: readonly ParsedShortcutBinding[] =
|
||||
|
||||
export type ShortcutOverrides = Record<string, string | null>;
|
||||
|
||||
/** Keep number-group actions bound to all nine indices when recording a new modifier. */
|
||||
export function normalizeCapturedShortcutCombo(bindingId: string, combo: string): string {
|
||||
const binding = DEFAULT_BINDINGS.find((candidate) => candidate.id === bindingId);
|
||||
if (binding?.payload?.type !== "index") return combo;
|
||||
return combo.replace(/(^|\+)[1-9]$/, "$1Digit");
|
||||
}
|
||||
|
||||
export function buildEffectiveBindings(overrides: ShortcutOverrides): ParsedShortcutBinding[] {
|
||||
return DEFAULT_BINDINGS.map(function (binding) {
|
||||
const override = overrides[binding.id];
|
||||
|
||||
@@ -192,6 +192,7 @@ const MODIFIER_CODES = new Set([
|
||||
* override on a named key renders as a raw code — `ARROWLEFT`, not `←`.
|
||||
*/
|
||||
const DISPLAY_KEY_ALIASES: Record<string, string> = {
|
||||
Digit: "1-9",
|
||||
ArrowLeft: "Left",
|
||||
ArrowRight: "Right",
|
||||
ArrowUp: "Up",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-over
|
||||
import {
|
||||
buildKeyboardShortcutHelpSections,
|
||||
getBindingIdForAction,
|
||||
normalizeCapturedShortcutCombo,
|
||||
getDefaultKeysForAction,
|
||||
resolveShortcutKeysForAction,
|
||||
type KeyboardShortcutHelpRow,
|
||||
@@ -389,7 +390,8 @@ export function KeyboardShortcutsSection() {
|
||||
}
|
||||
|
||||
setHeldModifiers(null);
|
||||
setCapturedCombos((current) => [...current, comboString]);
|
||||
const capturedCombo = normalizeCapturedShortcutCombo(capturingBindingId!, comboString);
|
||||
setCapturedCombos((current) => [...current, capturedCombo]);
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown, true);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { clickNewChat, gotoWorkspace } from "../../app/e2e/support/helpers/launcher";
|
||||
import { clickSettingsBackToWorkspace } from "../../app/e2e/support/helpers/settings";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "../../app/e2e/support/fixtures";
|
||||
import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app";
|
||||
@@ -213,3 +215,49 @@ for (const { platform, keys, label, firstCombo, secondCombo } of [
|
||||
await expectInterruptShortcut(page, label);
|
||||
});
|
||||
}
|
||||
|
||||
async function rebindTabNumbers(page: Page) {
|
||||
await captureShortcut(page, "Jump to tab", "workspace-tab-jump-index", "Control+1");
|
||||
await page.getByRole("button", { name: "Done", exact: true }).click();
|
||||
}
|
||||
|
||||
async function expectTabNumberRange(page: Page) {
|
||||
const row = page.getByText("Jump to tab", { exact: true }).locator("..");
|
||||
await expect(row.getByText("⌃1-9", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
async function openTwoDraftTabs(page: Page, workspaceId: string) {
|
||||
await installDesktopBridge(page);
|
||||
await gotoWorkspace(page, workspaceId);
|
||||
await clickNewChat(page);
|
||||
await clickNewChat(page);
|
||||
await expect(
|
||||
page.locator('[data-testid^="workspace-tab-draft_"]').filter({ visible: true }),
|
||||
).toHaveCount(2);
|
||||
await openSettings(page);
|
||||
await openSettingsSection(page, "shortcuts");
|
||||
}
|
||||
|
||||
async function expectTabNumberNavigation(page: Page) {
|
||||
const tabs = page.locator('[data-testid^="workspace-tab-draft_"]').filter({ visible: true });
|
||||
await page.keyboard.press("Control+1");
|
||||
await expect(tabs.nth(0)).toHaveAttribute("aria-selected", "true");
|
||||
await page.keyboard.press("Control+2");
|
||||
await expect(tabs.nth(1)).toHaveAttribute("aria-selected", "true");
|
||||
}
|
||||
|
||||
test("rebinding Jump to tab preserves the number group after reload", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}) => {
|
||||
const workspace = await withWorkspace({ prefix: "tab-number-rebind-" });
|
||||
await openTwoDraftTabs(page, workspace.workspaceId);
|
||||
await rebindTabNumbers(page);
|
||||
await clickSettingsBackToWorkspace(page);
|
||||
await expectTabNumberNavigation(page);
|
||||
await page.reload();
|
||||
await expectTabNumberNavigation(page);
|
||||
await openSettings(page);
|
||||
await openSettingsSection(page, "shortcuts");
|
||||
await expectTabNumberRange(page);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user