@witchcraft/spellcraft 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/createManagerOptions.js +1 -0
- package/dist/core/createShortcuts.d.ts +1 -1
- package/dist/core/setShortcutsProp.js +3 -3
- package/dist/defaults/defaultConditionEquals.d.ts +2 -16
- package/dist/defaults/defaultConditionEquals.js +1 -1
- package/dist/helpers/doesShortcutConflict.d.ts +1 -1
- package/dist/helpers/doesShortcutConflict.js +1 -1
- package/dist/helpers/equalsCommand.js +1 -1
- package/dist/helpers/equalsShortcut.d.ts +6 -4
- package/dist/helpers/equalsShortcut.js +5 -2
- package/dist/helpers/getKeyCodesFromKeyIds.d.ts +12 -0
- package/dist/helpers/getKeyCodesFromKeyIds.js +6 -0
- package/dist/helpers/getKeyCodesFromKeys.js +1 -1
- package/dist/module.json +1 -1
- package/dist/runtime/types.d.ts +2 -1
- package/dist/types/condition.d.ts +3 -1
- package/dist/types/manager.d.ts +12 -1
- package/dist/types/shortcuts.d.ts +2 -2
- package/package.json +3 -3
- package/src/core/createManagerOptions.ts +1 -0
- package/src/core/createShortcuts.ts +1 -1
- package/src/core/setShortcutsProp.ts +3 -3
- package/src/defaults/defaultConditionEquals.ts +4 -17
- package/src/helpers/doesShortcutConflict.ts +2 -2
- package/src/helpers/equalsCommand.ts +1 -1
- package/src/helpers/equalsShortcut.ts +17 -5
- package/src/helpers/getKeyCodesFromKeyIds.ts +20 -0
- package/src/helpers/getKeyCodesFromKeys.ts +1 -1
- package/src/runtime/types.ts +2 -1
- package/src/types/condition.ts +3 -1
- package/src/types/manager.ts +13 -1
- package/src/types/shortcuts.ts +2 -2
|
@@ -4,6 +4,7 @@ import { defaultSorter } from "../defaults/KeysSorter.js";
|
|
|
4
4
|
import { defaultStringifier } from "../defaults/Stringifier.js";
|
|
5
5
|
export function createManagerOptions(rawOpts) {
|
|
6
6
|
const options = {
|
|
7
|
+
shortcutEqualityStrategy: "ignoreCommand",
|
|
7
8
|
sorter: defaultSorter,
|
|
8
9
|
stringifier: defaultStringifier,
|
|
9
10
|
conditionEquals: defaultConditionEquals,
|
|
@@ -6,7 +6,7 @@ import type { CanHookErrors, Manager, MultipleErrors, PickManager, Shortcut, Sho
|
|
|
6
6
|
* Creates a set of shortcuts.
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
9
|
-
export declare function createShortcuts<THooks extends Manager["hooks"], TRawShortcuts extends Shortcut[], TCheck extends boolean | "only" = true>(shortcutsList: TRawShortcuts, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter"> & {
|
|
9
|
+
export declare function createShortcuts<THooks extends Manager["hooks"], TRawShortcuts extends Shortcut[], TCheck extends boolean | "only" = true>(shortcutsList: TRawShortcuts, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter" | "shortcutEqualityStrategy"> & {
|
|
10
10
|
hooks?: THooks;
|
|
11
11
|
}, opts?: Partial<Pick<Shortcuts, "ignoreModifierConflicts" | "ignoreChainConflicts">>, { check }?: {
|
|
12
12
|
check?: boolean | "only";
|
|
@@ -20,7 +20,7 @@ export function setShortcutsProp(prop, val, manager, {
|
|
|
20
20
|
castType(manager);
|
|
21
21
|
const shortcut = val;
|
|
22
22
|
const existing = shortcuts.entries.find(
|
|
23
|
-
(_) => equalsShortcut(shortcut, _, manager
|
|
23
|
+
(_) => equalsShortcut(shortcut, _, manager) || doesShortcutConflict(_, shortcut, manager)
|
|
24
24
|
);
|
|
25
25
|
if (existing) {
|
|
26
26
|
return Err(new KnownError(
|
|
@@ -41,7 +41,7 @@ export function setShortcutsProp(prop, val, manager, {
|
|
|
41
41
|
case "entries@remove": {
|
|
42
42
|
const shortcut = val;
|
|
43
43
|
const existing = shortcuts.entries.find(
|
|
44
|
-
(_) => _ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
44
|
+
(_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" })
|
|
45
45
|
);
|
|
46
46
|
if (existing === void 0) {
|
|
47
47
|
return Err(new KnownError(
|
|
@@ -77,7 +77,7 @@ export function setShortcutsProp(prop, val, manager, {
|
|
|
77
77
|
}
|
|
78
78
|
case "entries@remove": {
|
|
79
79
|
const shortcut = val;
|
|
80
|
-
const i = shortcuts.entries.findIndex((_) => _ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
80
|
+
const i = shortcuts.entries.findIndex((_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" }));
|
|
81
81
|
if (i < 0) {
|
|
82
82
|
throw new Error("If used correctly, shortcut should exist at this point, but it does not.");
|
|
83
83
|
}
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import type { Condition } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The default method does a simplistic object check, and otherwise a check of the `text` property for equality. If both conditions are undefined, note this will return true.
|
|
6
|
-
*
|
|
7
|
-
* If you override the default conditionEquals option you will likely want it to just always return false.
|
|
8
|
-
*
|
|
9
|
-
* Why? Because unless you're using simple single variable conditions that you can presort to make them uniquely identifiable (i.e. not boolean expressions, e.g. `!a b !c`), this will return A LOT of false negatives.
|
|
10
|
-
*
|
|
11
|
-
* Why the false negatives? Because two conditions might be functionally equal but have differing representations (e.g: `a && b`, `b && a`). You might think, lets normalize them all, but normalizing boolean expressions (converting them to CNF) can be dangerous with very long expressions because it can take exponential time.
|
|
12
|
-
*
|
|
13
|
-
* Now the main reason for checking the equality of two conditions is to check if two shortcuts might conflict. If we're using boolean expressions it just can't be done safely.
|
|
14
|
-
*
|
|
15
|
-
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
16
|
-
*
|
|
17
|
-
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
3
|
+
* IMPORTANT: Please read {@link ConditionComparer} before using.
|
|
18
4
|
*/
|
|
19
|
-
export declare function defaultConditionEquals<TCondition extends Condition>(conditionA
|
|
5
|
+
export declare function defaultConditionEquals<TCondition extends Condition>(conditionA: TCondition | undefined, conditionB: Condition | undefined, _type: "shortcut" | "command"): conditionB is TCondition;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function defaultConditionEquals(conditionA, conditionB) {
|
|
1
|
+
export function defaultConditionEquals(conditionA, conditionB, _type) {
|
|
2
2
|
if (conditionA === conditionB) return true;
|
|
3
3
|
if (!conditionA || !conditionB) return false;
|
|
4
4
|
return conditionA.text === conditionB.text;
|
|
@@ -15,4 +15,4 @@ import type { Manager, PickManager, Shortcut } from "../types/index.js";
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function doesShortcutConflict<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands" | "shortcuts"> & {
|
|
17
17
|
context?: Manager["context"];
|
|
18
|
-
} & PickManager<"options", "evaluateCondition" | "conditionEquals">): boolean;
|
|
18
|
+
} & PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">): boolean;
|
|
@@ -24,7 +24,7 @@ export function doesShortcutConflict(shortcutA, shortcutB, manager) {
|
|
|
24
24
|
if (!shortcutCondition) return false;
|
|
25
25
|
}
|
|
26
26
|
} else {
|
|
27
|
-
if (!conditionEquals(shortcutA.condition, shortcutB.condition)) return false;
|
|
27
|
+
if (!conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")) return false;
|
|
28
28
|
}
|
|
29
29
|
const { keys } = manager;
|
|
30
30
|
if (shortcutA.chain.length === 0 || shortcutB.chain.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export function equalsCommand(commandA, commandB, manager) {
|
|
2
2
|
if (commandA === commandB) return true;
|
|
3
|
-
return commandA.name === commandB.name && commandA.execute === commandB.execute && manager.options.conditionEquals(commandA.condition, commandB.condition) && commandA.description === commandB.description;
|
|
3
|
+
return commandA.name === commandB.name && commandA.execute === commandB.execute && manager.options.conditionEquals(commandA.condition, commandB.condition, "command") && commandA.description === commandB.description;
|
|
4
4
|
}
|
|
@@ -2,8 +2,10 @@ import type { Manager, PickManager, Shortcut } from "../types/index.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Returns whether the shortcut passed is equal to shortcutA one.
|
|
4
4
|
*
|
|
5
|
-
* To return true, their keys and
|
|
5
|
+
* To return true, their keys, conditions, and commands (depends on the configured `manager.options.shortcutEqualityStrategy`) must be equal.
|
|
6
|
+
*
|
|
7
|
+
* See {@link Manager.options.shortcutEqualityStrategy} for details.
|
|
8
|
+
*
|
|
9
|
+
* You can temporarily override the strategy witht the third parameter.
|
|
6
10
|
*/
|
|
7
|
-
export declare function equalsShortcut<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals"
|
|
8
|
-
ignoreCommand?: boolean;
|
|
9
|
-
}): shortcutB is TShortcut;
|
|
11
|
+
export declare function equalsShortcut<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">, overrides?: PickManager<"options", "shortcutEqualityStrategy">["options"]): shortcutB is TShortcut;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { equalsCommand } from "./equalsCommand.js";
|
|
2
2
|
import { equalsKeys } from "../utils/equalsKeys.js";
|
|
3
|
-
export function equalsShortcut(shortcutA, shortcutB, manager,
|
|
3
|
+
export function equalsShortcut(shortcutA, shortcutB, manager, overrides) {
|
|
4
4
|
if (shortcutA.forceUnequal || shortcutB.forceUnequal) return false;
|
|
5
5
|
if (shortcutA === shortcutB) return true;
|
|
6
|
-
|
|
6
|
+
const commandA = shortcutA.command ? manager.commands.entries[shortcutA.command] : void 0;
|
|
7
|
+
const commandB = shortcutB.command ? manager.commands.entries[shortcutB.command] : void 0;
|
|
8
|
+
const shortcutEqualityStrategy = overrides?.shortcutEqualityStrategy ?? manager.options.shortcutEqualityStrategy;
|
|
9
|
+
return equalsKeys(shortcutA.chain, shortcutB.chain, manager.keys, void 0, { allowVariants: true }) && manager.options.conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut") && (shortcutEqualityStrategy === "ignoreCommand" || shortcutEqualityStrategy === "ignoreCommandWithDifferentCondition" && commandA?.condition && commandB?.condition && manager.options.conditionEquals(commandA.condition, commandB.condition, "command") || shortcutA.command === shortcutB.command && shortcutA.command === void 0 || shortcutA.command !== void 0 && shortcutB.command !== void 0 && equalsCommand(manager.commands.entries[shortcutA.command], manager.commands.entries[shortcutB.command], manager));
|
|
7
10
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js";
|
|
2
|
+
import type { Keys } from "../types/keys.js";
|
|
3
|
+
/**
|
|
4
|
+
* Given an list of key ids, returns an array of deduped key codes. If you have a list of `Key`s instead use {@link getKeyCodesFromKeys}.
|
|
5
|
+
*
|
|
6
|
+
* Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
|
|
7
|
+
*
|
|
8
|
+
* This properly takes a look at the key and all it's variants.
|
|
9
|
+
*
|
|
10
|
+
* `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getKeyCodesFromKeyIds(keyIds: string[], keys: Keys, opts: Parameters<typeof getKeyCodesFromKeys>[1]): string[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js";
|
|
2
|
+
import { getKeyFromIdOrVariant } from "./getKeyFromIdOrVariant.js";
|
|
3
|
+
export function getKeyCodesFromKeyIds(keyIds, keys, opts) {
|
|
4
|
+
const keysList = keyIds.flatMap((id) => getKeyFromIdOrVariant(id, keys).unwrap());
|
|
5
|
+
return getKeyCodesFromKeys(keysList, opts);
|
|
6
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function getKeyCodesFromKeys(keys, { skipIdIfHasVariants = true } = {}) {
|
|
2
2
|
const res = /* @__PURE__ */ new Set();
|
|
3
3
|
for (const key of keys) {
|
|
4
|
-
if (key.variants) {
|
|
4
|
+
if (key.variants && key.variants.length > 0) {
|
|
5
5
|
if (!skipIdIfHasVariants) {
|
|
6
6
|
res.add(key.id);
|
|
7
7
|
}
|
package/dist/module.json
CHANGED
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { OrToAnd } from "@alanscodelog/utils/types";
|
|
1
2
|
export interface Register {
|
|
2
3
|
}
|
|
3
4
|
export type ExtendedShortcutManagerInfo = keyof Register extends `ShortcutManagerContext${infer T}` ? Register extends Record<`ShortcutManagerContext${T}`, infer U> ? U : {} : {};
|
|
4
|
-
export type ContextInfo = ExtendedShortcutManagerInfo & {
|
|
5
|
+
export type ContextInfo = OrToAnd<ExtendedShortcutManagerInfo> & {
|
|
5
6
|
count: Record<string, number>;
|
|
6
7
|
isActive: Record<string, boolean>;
|
|
7
8
|
};
|
|
@@ -24,6 +24,8 @@ export interface Condition {
|
|
|
24
24
|
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
25
25
|
*
|
|
26
26
|
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
27
|
+
*
|
|
28
|
+
* This is why the manager now has the option `shortcutEqualityStrategy` which can be se to `ignoreCommandWithDifferentCondition`. You can implement a comparer that only checks when a command is being checked.
|
|
27
29
|
*/
|
|
28
|
-
export type ConditionComparer = (conditionA
|
|
30
|
+
export type ConditionComparer = (conditionA: Condition | undefined, conditionB: Condition | undefined, type: "shortcut" | "command") => boolean;
|
|
29
31
|
export type ConditionEvaluator<TContext extends Context<any>> = (condition: Condition, context: TContext) => boolean;
|
package/dist/types/manager.d.ts
CHANGED
|
@@ -114,9 +114,20 @@ export type Manager<THooks extends Partial<Hooks> = Partial<Hooks>, TKeys extend
|
|
|
114
114
|
/**
|
|
115
115
|
* Determines if two conditions are equal.
|
|
116
116
|
*
|
|
117
|
-
* This is actually not a good idea to implement if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
117
|
+
* This is actually not a good idea to implement for shortcuts if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
118
|
+
*
|
|
119
|
+
* See also {@link Manager.options.shortcutEqualityStrategy}.
|
|
118
120
|
*/
|
|
119
121
|
conditionEquals: ConditionComparer;
|
|
122
|
+
/**
|
|
123
|
+
* Determines how shortcuts are compared.
|
|
124
|
+
*
|
|
125
|
+
* - `ignoreCommand` (default) will ignore both shortcuts' commands. If everything else is equal, they are considered equal.
|
|
126
|
+
* - `ignoreCommandWithDifferentCondition` will ignore both commands' names and only compare their conditions. If everything else is equal, they are considered equal. Use it if you allow command conditions to equal eachother. See {@link ConditionComparer} for details.
|
|
127
|
+
* - `all` will compare everything.
|
|
128
|
+
*
|
|
129
|
+
*/
|
|
130
|
+
shortcutEqualityStrategy: "ignoreCommand" | "ignoreCommandWithDifferentCondition" | "all";
|
|
120
131
|
/** Determines how conditions are evaluated. */
|
|
121
132
|
evaluateCondition: ConditionEvaluator<TContext>;
|
|
122
133
|
/** Enable/disable triggering of shortcuts. The manager otherwise works as normal. */
|
|
@@ -126,12 +126,12 @@ export type CanHookShortcutProps = Exclude<OnHookShortcutProps, "forceUnequal">;
|
|
|
126
126
|
export type SyntheticOnHookShortcutsProps = "entries@add" | "entries@remove";
|
|
127
127
|
export type CanHookShortcutsProps = SyntheticOnHookShortcutsProps;
|
|
128
128
|
export type OnHookShortcutsProps = SyntheticOnHookShortcutsProps;
|
|
129
|
-
type BaseShortcutsManager = Pick<Manager, "keys" | "commands" | "shortcuts"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier"> & Record<any, any>;
|
|
129
|
+
type BaseShortcutsManager = Pick<Manager, "keys" | "commands" | "shortcuts"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "shortcutEqualityStrategy"> & Record<any, any>;
|
|
130
130
|
export type ShortcutsSetEntries = {
|
|
131
131
|
"entries@add": {
|
|
132
132
|
val: Shortcut;
|
|
133
133
|
hooks: GetShortcutHooks<`entries@add`>;
|
|
134
|
-
manager: BaseShortcutsManager & PickManager<"options", "sorter">;
|
|
134
|
+
manager: BaseShortcutsManager & PickManager<"options", "sorter" | "shortcutEqualityStrategy">;
|
|
135
135
|
error: typeof SHORTCUT_ERROR.DUPLICATE_SHORTCUT | typeof SHORTCUT_ERROR.UNKNOWN_COMMAND | ChainError;
|
|
136
136
|
};
|
|
137
137
|
"entries@remove": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchcraft/spellcraft",
|
|
3
3
|
"description": "A shortcut manager library for handling ALL the shortcut needs of an application.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@witchcraft/expressit": "^0.4.
|
|
42
|
+
"@witchcraft/expressit": "^0.4.2",
|
|
43
43
|
"@witchcraft/ui": "^0.3.20"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@alanscodelog/utils": "^6.0.2",
|
|
55
|
-
"@witchcraft/expressit": "^0.4.
|
|
55
|
+
"@witchcraft/expressit": "^0.4.2",
|
|
56
56
|
"@witchcraft/ui": "^0.3.20",
|
|
57
57
|
"reka-ui": "^2.8.0",
|
|
58
58
|
"vue": "^3.5.27"
|
|
@@ -16,6 +16,7 @@ export function createManagerOptions(
|
|
|
16
16
|
rawOpts: Parameters<typeof createManager>[0]["options"]
|
|
17
17
|
): Manager["options"] {
|
|
18
18
|
const options: Manager["options"] = {
|
|
19
|
+
shortcutEqualityStrategy: "ignoreCommand",
|
|
19
20
|
sorter: defaultSorter,
|
|
20
21
|
stringifier: defaultStringifier,
|
|
21
22
|
conditionEquals: defaultConditionEquals,
|
|
@@ -17,7 +17,7 @@ export function createShortcuts<
|
|
|
17
17
|
>(
|
|
18
18
|
shortcutsList: TRawShortcuts,
|
|
19
19
|
manager: Pick<Manager, "keys" | "commands">
|
|
20
|
-
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter">
|
|
20
|
+
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter" | "shortcutEqualityStrategy">
|
|
21
21
|
& { hooks?: THooks },
|
|
22
22
|
opts?: Partial<Pick<Shortcuts, "ignoreModifierConflicts" | "ignoreChainConflicts">>,
|
|
23
23
|
{
|
|
@@ -43,7 +43,7 @@ export function setShortcutsProp<
|
|
|
43
43
|
|
|
44
44
|
const shortcut = val as any as Shortcut
|
|
45
45
|
const existing = (shortcuts.entries).find(_ =>
|
|
46
|
-
equalsShortcut(shortcut, _, manager
|
|
46
|
+
equalsShortcut(shortcut, _, manager)
|
|
47
47
|
|| doesShortcutConflict(_, shortcut, manager)
|
|
48
48
|
)
|
|
49
49
|
|
|
@@ -67,7 +67,7 @@ export function setShortcutsProp<
|
|
|
67
67
|
const shortcut = val as any as Shortcut
|
|
68
68
|
// note we don't ignore the command here, we want to find an exact match
|
|
69
69
|
const existing = (shortcuts.entries).find(_ =>
|
|
70
|
-
_ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
70
|
+
_ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" })
|
|
71
71
|
)
|
|
72
72
|
if (existing === undefined) {
|
|
73
73
|
return Err(new KnownError(
|
|
@@ -105,7 +105,7 @@ export function setShortcutsProp<
|
|
|
105
105
|
}
|
|
106
106
|
case "entries@remove": {
|
|
107
107
|
const shortcut = val
|
|
108
|
-
const i = shortcuts.entries.findIndex(_ => _ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
108
|
+
const i = shortcuts.entries.findIndex(_ => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" }))
|
|
109
109
|
if (i < 0) {
|
|
110
110
|
throw new Error("If used correctly, shortcut should exist at this point, but it does not.")
|
|
111
111
|
}
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import type { Condition } from "../types/index.js"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The default method does a simplistic object check, and otherwise a check of the `text` property for equality. If both conditions are undefined, note this will return true.
|
|
7
|
-
*
|
|
8
|
-
* If you override the default conditionEquals option you will likely want it to just always return false.
|
|
9
|
-
*
|
|
10
|
-
* Why? Because unless you're using simple single variable conditions that you can presort to make them uniquely identifiable (i.e. not boolean expressions, e.g. `!a b !c`), this will return A LOT of false negatives.
|
|
11
|
-
*
|
|
12
|
-
* Why the false negatives? Because two conditions might be functionally equal but have differing representations (e.g: `a && b`, `b && a`). You might think, lets normalize them all, but normalizing boolean expressions (converting them to CNF) can be dangerous with very long expressions because it can take exponential time.
|
|
13
|
-
*
|
|
14
|
-
* Now the main reason for checking the equality of two conditions is to check if two shortcuts might conflict. If we're using boolean expressions it just can't be done safely.
|
|
15
|
-
*
|
|
16
|
-
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
17
|
-
*
|
|
18
|
-
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
4
|
+
* IMPORTANT: Please read {@link ConditionComparer} before using.
|
|
19
5
|
*/
|
|
20
6
|
export function defaultConditionEquals<TCondition extends Condition>(
|
|
21
|
-
conditionA
|
|
22
|
-
conditionB
|
|
7
|
+
conditionA: TCondition | undefined,
|
|
8
|
+
conditionB: Condition | undefined,
|
|
9
|
+
_type: "shortcut" | "command"
|
|
23
10
|
): conditionB is TCondition {
|
|
24
11
|
// both are the same object or both undefined
|
|
25
12
|
if (conditionA === conditionB) return true
|
|
@@ -23,7 +23,7 @@ export function doesShortcutConflict<TShortcut extends Shortcut>(
|
|
|
23
23
|
shortcutA: TShortcut,
|
|
24
24
|
shortcutB: Shortcut,
|
|
25
25
|
manager: Pick<Manager, "keys" | "commands" | "shortcuts"> & { context?: Manager["context"] }
|
|
26
|
-
& PickManager<"options", "evaluateCondition" | "conditionEquals">
|
|
26
|
+
& PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">
|
|
27
27
|
): boolean {
|
|
28
28
|
const context = manager.context
|
|
29
29
|
if (!context && manager.shortcuts.useContextInConflictCheck) {
|
|
@@ -47,7 +47,7 @@ export function doesShortcutConflict<TShortcut extends Shortcut>(
|
|
|
47
47
|
if (!shortcutCondition) return false
|
|
48
48
|
}
|
|
49
49
|
} else {
|
|
50
|
-
if (!conditionEquals(shortcutA.condition, shortcutB.condition)) return false
|
|
50
|
+
if (!conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")) return false
|
|
51
51
|
}
|
|
52
52
|
const { keys } = manager
|
|
53
53
|
// an empty chain is always in conflict ?
|
|
@@ -13,6 +13,6 @@ export function equalsCommand<TCommand extends Command>(
|
|
|
13
13
|
if (commandA === commandB) return true
|
|
14
14
|
return commandA.name === commandB.name
|
|
15
15
|
&& commandA.execute === commandB.execute
|
|
16
|
-
&& manager.options.conditionEquals(commandA.condition, commandB.condition)
|
|
16
|
+
&& manager.options.conditionEquals(commandA.condition, commandB.condition, "command")
|
|
17
17
|
&& commandA.description === commandB.description
|
|
18
18
|
}
|
|
@@ -6,20 +6,32 @@ import { equalsKeys } from "../utils/equalsKeys.js"
|
|
|
6
6
|
/**
|
|
7
7
|
* Returns whether the shortcut passed is equal to shortcutA one.
|
|
8
8
|
*
|
|
9
|
-
* To return true, their keys and
|
|
9
|
+
* To return true, their keys, conditions, and commands (depends on the configured `manager.options.shortcutEqualityStrategy`) must be equal.
|
|
10
|
+
*
|
|
11
|
+
* See {@link Manager.options.shortcutEqualityStrategy} for details.
|
|
12
|
+
*
|
|
13
|
+
* You can temporarily override the strategy witht the third parameter.
|
|
10
14
|
*/
|
|
11
15
|
export function equalsShortcut<TShortcut extends Shortcut>(
|
|
12
16
|
shortcutA: TShortcut,
|
|
13
17
|
shortcutB: Shortcut,
|
|
14
|
-
manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", | "evaluateCondition" | "conditionEquals">,
|
|
15
|
-
|
|
18
|
+
manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", | "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">,
|
|
19
|
+
overrides?: PickManager<"options", "shortcutEqualityStrategy">["options"]
|
|
16
20
|
): shortcutB is TShortcut {
|
|
17
21
|
if (shortcutA.forceUnequal || shortcutB.forceUnequal) return false
|
|
18
22
|
if (shortcutA === shortcutB) return true
|
|
23
|
+
const commandA = shortcutA.command ? manager.commands.entries[shortcutA.command] : undefined
|
|
24
|
+
const commandB = shortcutB.command ? manager.commands.entries[shortcutB.command] : undefined
|
|
25
|
+
const shortcutEqualityStrategy = overrides?.shortcutEqualityStrategy ?? manager.options.shortcutEqualityStrategy
|
|
19
26
|
return (
|
|
20
27
|
equalsKeys(shortcutA.chain, shortcutB.chain, manager.keys, undefined, { allowVariants: true })
|
|
21
|
-
&& manager.options.conditionEquals(shortcutA.condition, shortcutB.condition)
|
|
22
|
-
&& (ignoreCommand
|
|
28
|
+
&& manager.options.conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")
|
|
29
|
+
&& (shortcutEqualityStrategy === "ignoreCommand"
|
|
30
|
+
|| (
|
|
31
|
+
shortcutEqualityStrategy === "ignoreCommandWithDifferentCondition"
|
|
32
|
+
&& commandA?.condition && commandB?.condition
|
|
33
|
+
&& manager.options.conditionEquals(commandA.condition, commandB.condition, "command")
|
|
34
|
+
)
|
|
23
35
|
|| (
|
|
24
36
|
shortcutA.command === shortcutB.command
|
|
25
37
|
&& shortcutA.command === undefined
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js"
|
|
2
|
+
import { getKeyFromIdOrVariant } from "./getKeyFromIdOrVariant.js"
|
|
3
|
+
|
|
4
|
+
import type { Keys } from "../types/keys.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Given an list of key ids, returns an array of deduped key codes. If you have a list of `Key`s instead use {@link getKeyCodesFromKeys}.
|
|
8
|
+
*
|
|
9
|
+
* Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
|
|
10
|
+
*
|
|
11
|
+
* This properly takes a look at the key and all it's variants.
|
|
12
|
+
*
|
|
13
|
+
* `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export function getKeyCodesFromKeyIds(keyIds: string[], keys: Keys, opts: Parameters<typeof getKeyCodesFromKeys>[1]): string[] {
|
|
18
|
+
const keysList = keyIds.flatMap(id => getKeyFromIdOrVariant(id, keys).unwrap())
|
|
19
|
+
return getKeyCodesFromKeys(keysList, opts)
|
|
20
|
+
}
|
|
@@ -13,7 +13,7 @@ import type { Key } from "../types/keys.js"
|
|
|
13
13
|
export function getKeyCodesFromKeys(keys: Key[], { skipIdIfHasVariants = true}: { skipIdIfHasVariants?: boolean } = {}): string[] {
|
|
14
14
|
const res = new Set<string>()
|
|
15
15
|
for (const key of keys) {
|
|
16
|
-
if (key.variants) {
|
|
16
|
+
if (key.variants && key.variants.length > 0) {
|
|
17
17
|
if (!skipIdIfHasVariants) {
|
|
18
18
|
res.add(key.id)
|
|
19
19
|
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OrToAnd } from "@alanscodelog/utils/types"
|
|
1
2
|
// allows users to register multiple contexts
|
|
2
3
|
// @eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
3
4
|
export interface Register {}
|
|
@@ -16,7 +17,7 @@ export type ExtendedShortcutManagerInfo = keyof Register extends `ShortcutManage
|
|
|
16
17
|
: {}
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
export type ContextInfo = ExtendedShortcutManagerInfo & {
|
|
20
|
+
export type ContextInfo = OrToAnd<ExtendedShortcutManagerInfo> & {
|
|
20
21
|
count: Record<string, number>
|
|
21
22
|
isActive: Record<string, boolean>
|
|
22
23
|
}
|
package/src/types/condition.ts
CHANGED
|
@@ -28,8 +28,10 @@ export interface Condition {
|
|
|
28
28
|
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
29
29
|
*
|
|
30
30
|
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
31
|
+
*
|
|
32
|
+
* This is why the manager now has the option `shortcutEqualityStrategy` which can be se to `ignoreCommandWithDifferentCondition`. You can implement a comparer that only checks when a command is being checked.
|
|
31
33
|
*/
|
|
32
|
-
export type ConditionComparer = (conditionA
|
|
34
|
+
export type ConditionComparer = (conditionA: Condition | undefined, conditionB: Condition | undefined, type: "shortcut" | "command") => boolean
|
|
33
35
|
|
|
34
36
|
export type ConditionEvaluator<TContext extends Context<any>> = (condition: Condition, context: TContext) => boolean
|
|
35
37
|
|
package/src/types/manager.ts
CHANGED
|
@@ -212,9 +212,21 @@ export type Manager<
|
|
|
212
212
|
/**
|
|
213
213
|
* Determines if two conditions are equal.
|
|
214
214
|
*
|
|
215
|
-
* This is actually not a good idea to implement if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
215
|
+
* This is actually not a good idea to implement for shortcuts if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
216
|
+
*
|
|
217
|
+
* See also {@link Manager.options.shortcutEqualityStrategy}.
|
|
216
218
|
*/
|
|
217
219
|
conditionEquals: ConditionComparer
|
|
220
|
+
/**
|
|
221
|
+
* Determines how shortcuts are compared.
|
|
222
|
+
*
|
|
223
|
+
* - `ignoreCommand` (default) will ignore both shortcuts' commands. If everything else is equal, they are considered equal.
|
|
224
|
+
* - `ignoreCommandWithDifferentCondition` will ignore both commands' names and only compare their conditions. If everything else is equal, they are considered equal. Use it if you allow command conditions to equal eachother. See {@link ConditionComparer} for details.
|
|
225
|
+
* - `all` will compare everything.
|
|
226
|
+
*
|
|
227
|
+
*/
|
|
228
|
+
shortcutEqualityStrategy: "ignoreCommand" | "ignoreCommandWithDifferentCondition" | "all"
|
|
229
|
+
|
|
218
230
|
/** Determines how conditions are evaluated. */
|
|
219
231
|
evaluateCondition: ConditionEvaluator<TContext>
|
|
220
232
|
/** Enable/disable triggering of shortcuts. The manager otherwise works as normal. */
|
package/src/types/shortcuts.ts
CHANGED
|
@@ -178,7 +178,7 @@ export type OnHookShortcutsProps = SyntheticOnHookShortcutsProps
|
|
|
178
178
|
|
|
179
179
|
type BaseShortcutsManager
|
|
180
180
|
= & Pick<Manager, "keys" | "commands" | "shortcuts">
|
|
181
|
-
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier">
|
|
181
|
+
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "shortcutEqualityStrategy">
|
|
182
182
|
& Record<any, any>
|
|
183
183
|
|
|
184
184
|
export type ShortcutsSetEntries = {
|
|
@@ -187,7 +187,7 @@ export type ShortcutsSetEntries = {
|
|
|
187
187
|
val: Shortcut
|
|
188
188
|
hooks: GetShortcutHooks<`entries@add`>
|
|
189
189
|
manager: BaseShortcutsManager
|
|
190
|
-
& PickManager<"options", "sorter">
|
|
190
|
+
& PickManager<"options", "sorter" | "shortcutEqualityStrategy">
|
|
191
191
|
|
|
192
192
|
error:
|
|
193
193
|
| typeof SHORTCUT_ERROR.DUPLICATE_SHORTCUT
|