Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8253339
fix(quick-restart): handle quick restart in long tests (@Leonabcd123)
Leonabcd123 Jun 4, 2026
0b642a5
Merge branch 'master' into fix/quick-restart-long-test
Leonabcd123 Jun 4, 2026
e1d61f7
Different approach
Leonabcd123 Jun 4, 2026
e333ec3
Revert "Different approach"
Leonabcd123 Jun 4, 2026
94fce17
Another approach
Leonabcd123 Jun 5, 2026
26cbc89
Format
Leonabcd123 Jun 5, 2026
bc9b927
Another
Leonabcd123 Jun 5, 2026
72fad7c
Comment
Leonabcd123 Jun 5, 2026
bdb68db
Comment
Leonabcd123 Jun 5, 2026
abd6a19
Comments
Leonabcd123 Jun 5, 2026
5aeda0b
Comment
Leonabcd123 Jun 5, 2026
4e5fa5e
Comments
Leonabcd123 Jun 5, 2026
58b028a
Comment
Leonabcd123 Jun 5, 2026
5562399
Primary
Leonabcd123 Jun 5, 2026
6e79f3a
Comment
Leonabcd123 Jun 5, 2026
c3cf182
Safety
Leonabcd123 Jun 5, 2026
1d96608
Comment
Leonabcd123 Jun 5, 2026
ce15b01
Comment
Leonabcd123 Jun 5, 2026
399feae
Start
Leonabcd123 Jun 5, 2026
74a4334
Options
Leonabcd123 Jun 5, 2026
49b3e19
Comment
Leonabcd123 Jun 5, 2026
f5e6970
Default
Leonabcd123 Jun 5, 2026
5b0683b
Not partial
Leonabcd123 Jun 5, 2026
0865af0
Not partial
Leonabcd123 Jun 5, 2026
45121c5
Semi
Leonabcd123 Jun 5, 2026
3a39844
Comments
Leonabcd123 Jun 5, 2026
14aafe6
Before callback
Leonabcd123 Jun 5, 2026
16225ad
Comment
Leonabcd123 Jun 5, 2026
c0c7871
Comment
Leonabcd123 Jun 5, 2026
4c1181a
Refactor
Leonabcd123 Jun 5, 2026
57e5149
Resolved
Leonabcd123 Jun 5, 2026
d90a1d2
Revert "Resolved"
Leonabcd123 Jun 5, 2026
beead94
Fix
Leonabcd123 Jun 5, 2026
b0e96a9
Rename
Leonabcd123 Jun 5, 2026
96622b6
NoKey
Leonabcd123 Jun 5, 2026
fac53e3
Less untracks
Leonabcd123 Jun 5, 2026
350e8e3
Comments
Leonabcd123 Jun 5, 2026
fb406c8
Space
Leonabcd123 Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 41 additions & 23 deletions frontend/src/ts/input/hotkeys/quickrestart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { navigate } from "../../controllers/route-controller";
import { restartTestEvent } from "../../events/test";
import { getActivePage } from "../../states/core";
import { hotkeys, quickRestartHotkeyMap } from "../../states/hotkeys";
import { createHotkey } from "./utils";
import { createHotkeys } from "./utils";
import { getConfig } from "../../config/store";
import { isLongTest, wordsHaveNewline, wordsHaveTab } from "../../states/test";
import { untrack } from "solid-js";

function quickRestart(e: KeyboardEvent): void {
if (isAnyPopupVisible()) {
Expand All @@ -22,25 +23,42 @@ function quickRestart(e: KeyboardEvent): void {
}
}

// Disable restart when we're in long test and quick restart key is enter, because `shift + enter, shift +
// enter` is already reserved for bail out keybind.
createHotkey(
() => hotkeys.quickRestart,
quickRestart,
() => ({ enabled: !isLongTest() || getConfig.quickRestart !== "enter" }),
);

// We also want to have a hotkey for quick restart key without shift, so when the
// test is considered long (which means that we can't quick restart), we show a
// notification when the user tries to press the quick restart key without shift,
// and we'll restart when it's pressed with shift.
createHotkey(
() => quickRestartHotkeyMap[getConfig.quickRestart],
quickRestart,
() => ({
enabled:
isLongTest() &&
!(wordsHaveTab() && getConfig.quickRestart === "tab") &&
!(wordsHaveNewline() && getConfig.quickRestart === "enter"),
}),
);
// Notes about the following two hotkeys:
//
// - The order of the hotkeys in the array passed to `createHotkeys` is important. If
// the order was reversed, then the secondary hotkey will override the primary hotkey
// when the test isn't long, which would cause the quick restart hotkey to be disabled.
// - Both hotkeys only rerun when `hotkeys.quickRestart` changes. All other signals are
// accessed inside an `untrack` block.

createHotkeys(() => [
// Secondary hotkey used in long tests.

// We want to have a hotkey for quick restart key without shift, so when the
// test is considered long (which means that we can't quick restart) and the user
// tries to press the quick restart key without shift, we'll show a notification, and
// when the user presses the quick restart key with shift, we'll restart.
untrack(() => ({
hotkey: quickRestartHotkeyMap[getConfig.quickRestart],
callback: quickRestart,
options: {
enabled:
isLongTest() &&
!(wordsHaveTab() && getConfig.quickRestart === "tab") &&
!(wordsHaveNewline() && getConfig.quickRestart === "enter"),
},
})),

// Primary hotkey for quick restart.
{
hotkey: hotkeys.quickRestart,
callback: quickRestart,
options: {
enabled: untrack(
// Disable quick restart when we're in a long test and quick restart key is enter, because `shift + enter, shift +
// enter` is already reserved for bail out keybind.
() => !isLongTest() || getConfig.quickRestart !== "enter",
),
},
},
]);
66 changes: 48 additions & 18 deletions frontend/src/ts/input/hotkeys/utils.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import {
CreateHotkeyOptions,
CreateHotkeyDefinition,
Hotkey,
HotkeyCallback,
HotkeyCallbackContext,
createHotkey as registerHotkey,
createHotkeys as registerHotkeys,
} from "@tanstack/solid-hotkeys";
import { isAnyPopupVisible } from "../../utils/misc";
import { isInputElementFocused } from "../input-element";
import * as CompositionState from "../../legacy-states/composition";

export const NoKey = "" as Hotkey;

const defaultOptions: CreateHotkeyOptions = {
ignoreInputs: false, //hotkeys are active on the words input, but not on other interactive elements
stopPropagation: false, //we set stopPropagation in the callback if the hotkey executes
preventDefault: false, //we set preventDefault in the callback if the hotkey executes
requireReset: true,
conflictBehavior: "replace",
};

function attachBeforeCallback(callback: HotkeyCallback): HotkeyCallback {
return (e, context) => {
if (handleHotkeyOnInteractiveElement(e, context)) return;
e.stopPropagation();
e.preventDefault();
callback(e, context);
};
}

export function createHotkey(
hotkey: Hotkey | (() => Hotkey),
callback: HotkeyCallback,
Expand All @@ -21,24 +40,35 @@ export function createHotkey(
>
> = () => ({}),
): void {
registerHotkey(
hotkey,
(e, context) => {
if (handleHotkeyOnInteractiveElement(e, context)) return;
e.stopPropagation();
e.preventDefault();
callback(e, context);
},
() => ({
ignoreInputs: false, //hotkeys are active on the words input, but not on other interactive elements
stopPropagation: false, //we set stopPropagation in the callback if the hotkey executes
preventDefault: false, //we set preventDefault in the callback if the hotkey executes
requireReset: true,
conflictBehavior: "replace",
enabled: (typeof hotkey === "function" ? hotkey() : hotkey) !== NoKey,
...options(),
}),
);
registerHotkey(hotkey, attachBeforeCallback(callback), () => ({
...defaultOptions,
enabled: (typeof hotkey === "function" ? hotkey() : hotkey) !== NoKey,
...options(),
}));
}

export function createHotkeys(
hotkeys: CreateHotkeyDefinition[] | (() => CreateHotkeyDefinition[]),
commonOptions: () => Partial<
Omit<
CreateHotkeyOptions,
"ignoreInputs" | "stopPropagation" | "preventDefault"
>
> = () => ({}),
): void {
const modifiedHotkeys = (): CreateHotkeyDefinition[] => {
const resolvedHotkeys = typeof hotkeys === "function" ? hotkeys() : hotkeys;
resolvedHotkeys.forEach((hotkey) => {
hotkey.callback = attachBeforeCallback(hotkey.callback);
hotkey.options ??= {};
hotkey.options.enabled ??= hotkey.hotkey !== NoKey;
});
return resolvedHotkeys;
};
registerHotkeys(modifiedHotkeys, () => ({
...defaultOptions,
...commonOptions(),
}));
}

function isInteractiveElementFocused(): boolean {
Expand Down
Loading