modern-cmdk 1.4.0 → 1.6.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/README.md +54 -34
- package/THIRD-PARTY-NOTICES.md +303 -0
- package/dist/codemod/bin.mjs +14 -0
- package/dist/codemod/chunk-jscodeshift.mjs +36210 -0
- package/dist/codemod/chunk-print-options.mjs +43 -0
- package/dist/codemod/chunk-worker.mjs +485 -0
- package/dist/codemod/cli.mjs +297 -43660
- package/dist/codemod/transforms/data-attrs.mjs +583 -39
- package/dist/codemod/transforms/forward-ref.mjs +197 -32
- package/dist/codemod/transforms/import-rewrite.mjs +389 -30
- package/dist/codemod/transforms/should-filter.mjs +199 -10
- package/dist/codemod/worker.mjs +1 -0
- package/dist/core/index.d.mts +122 -35
- package/dist/core/index.mjs +466 -292
- package/dist/react/dialog.mjs +0 -1
- package/dist/react/index.d.mts +25 -4
- package/dist/react/index.mjs +58 -25
- package/dist/react/styles.css +33 -31
- package/package.json +5 -7
package/dist/core/index.mjs
CHANGED
|
@@ -16,27 +16,68 @@ function ensureWellFormed(str) {
|
|
|
16
16
|
return str.replace(LONE_SURROGATE_RE, REPLACEMENT_CHAR);
|
|
17
17
|
}
|
|
18
18
|
// src/core/frecency/idb-storage.ts
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
frequency: record.frequency,
|
|
23
|
-
lastUsedMs: record.lastUsed
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/** Deserialize a FrecencyRecord from its JSON-safe form */
|
|
27
|
-
function deserializeRecord(serialized) {
|
|
28
|
-
return {
|
|
29
|
-
frequency: serialized.frequency,
|
|
30
|
-
lastUsed: serialized.lastUsedMs
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
/** Serialize FrecencyData for IndexedDB storage — uses Iterator Helpers */
|
|
19
|
+
Symbol.dispose ??= Symbol.for("Symbol.dispose");
|
|
20
|
+
Symbol.asyncDispose ??= Symbol.for("Symbol.asyncDispose");
|
|
21
|
+
/** Serialize FrecencyData as v1 for IndexedDB storage — uses Iterator Helpers */
|
|
34
22
|
function serializeData(data) {
|
|
35
|
-
return { records: data.records.entries().map(([id,
|
|
23
|
+
return { records: data.records.entries().map(([id, { frequency, lastUsed }]) => [id, {
|
|
24
|
+
frequency,
|
|
25
|
+
lastUsedMs: lastUsed
|
|
26
|
+
}]).toArray() };
|
|
27
|
+
}
|
|
28
|
+
const isNonNegative = (n) => Number.isFinite(n) && n >= 0;
|
|
29
|
+
/**
|
|
30
|
+
* Validate a persisted payload (untrusted: it may be corrupt, foreign or from a
|
|
31
|
+
* newer schema). Keeps the rows that are `[string, { frequency, lastUsedMs }]`
|
|
32
|
+
* with finite numbers >= 0 and counts the rest in a single warning; a repeated id
|
|
33
|
+
* counts toward that number and the last row with it wins. A payload
|
|
34
|
+
* that is not a v1 or v2 record map counts as one dropped row and yields empty
|
|
35
|
+
* data; `undefined` (nothing stored) is not a warning.
|
|
36
|
+
*/
|
|
37
|
+
function deserializeData(raw) {
|
|
38
|
+
const payload = raw;
|
|
39
|
+
const rows = raw === void 0 ? [] : Array.isArray(payload?.records) && [
|
|
40
|
+
void 0,
|
|
41
|
+
1,
|
|
42
|
+
2
|
|
43
|
+
].includes(payload.version) ? payload.records : [null];
|
|
44
|
+
const records = new Map(rows.values().filter((row) => Array.isArray(row) && typeof row[0] === "string" && isNonNegative(row[1]?.frequency) && isNonNegative(row[1].lastUsedMs)).map(([id, { frequency, lastUsedMs }]) => [id, {
|
|
45
|
+
frequency,
|
|
46
|
+
lastUsed: lastUsedMs
|
|
47
|
+
}]));
|
|
48
|
+
if (records.size < rows.length) console.warn("[modern-cmdk] dropped invalid frecency rows:", rows.length - records.size);
|
|
49
|
+
return { records };
|
|
36
50
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Open `storeName` in `dbName` per operation, creating the store when missing.
|
|
53
|
+
* idb-keyval 6.3.0's createStore does `request.onupgradeneeded = () =>
|
|
54
|
+
* request.result.createObjectStore(storeName)`, which runs only when the database
|
|
55
|
+
* is new, so a second storeName in an existing database fails with NotFoundError
|
|
56
|
+
* (SEC-35). Here a database without the store is reopened at `version + 1`, whose
|
|
57
|
+
* upgrade creates it. The connection closes right after the transaction starts (the
|
|
58
|
+
* transaction still finishes), so it delays another instance's upgrade only until that
|
|
59
|
+
* transaction is done. An upgrade blocked by a connection that is never closed (for example an
|
|
60
|
+
* idb-keyval createStore connection held by app code, which has no versionchange handler)
|
|
61
|
+
* waits for it, so that load or save does not settle until the connection closes.
|
|
62
|
+
*/
|
|
63
|
+
function openStore(dbName, storeName) {
|
|
64
|
+
const open = async (version) => {
|
|
65
|
+
const request = indexedDB.open(dbName, version);
|
|
66
|
+
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
|
67
|
+
const db = await new Promise((resolve, reject) => {
|
|
68
|
+
request.onsuccess = () => resolve(request.result);
|
|
69
|
+
request.onerror = () => reject(request.error);
|
|
70
|
+
});
|
|
71
|
+
if (db.objectStoreNames.contains(storeName)) return db;
|
|
72
|
+
db.close();
|
|
73
|
+
return open(db.version + 1);
|
|
74
|
+
};
|
|
75
|
+
return async (mode, callback) => {
|
|
76
|
+
const db = await open();
|
|
77
|
+
const transaction = db.transaction(storeName, mode);
|
|
78
|
+
db.close();
|
|
79
|
+
return callback(transaction.objectStore(storeName));
|
|
80
|
+
};
|
|
40
81
|
}
|
|
41
82
|
/** Build the namespaced IDB key — ensures valid Unicode in key */
|
|
42
83
|
function storageKey(namespace) {
|
|
@@ -44,7 +85,7 @@ function storageKey(namespace) {
|
|
|
44
85
|
}
|
|
45
86
|
let idbModule;
|
|
46
87
|
/**
|
|
47
|
-
* IndexedDB-backed frecency storage using idb-keyval.
|
|
88
|
+
* IndexedDB-backed frecency storage using idb-keyval's get, set and del.
|
|
48
89
|
*
|
|
49
90
|
* Persists frecency data across browser sessions. Implements both
|
|
50
91
|
* `Disposable` and `AsyncDisposable` for use with `using` and `await using`.
|
|
@@ -78,35 +119,30 @@ var IdbFrecencyStorage = class {
|
|
|
78
119
|
idbModule ??= import("idb-keyval");
|
|
79
120
|
return idbModule;
|
|
80
121
|
}
|
|
81
|
-
/** Lazy-create the
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const { createStore } = await this.#idb();
|
|
85
|
-
this.#store = createStore(this.#dbName, this.#storeName);
|
|
86
|
-
}
|
|
122
|
+
/** Lazy-create the store opener on first access */
|
|
123
|
+
#getStore() {
|
|
124
|
+
this.#store ??= openStore(this.#dbName, this.#storeName);
|
|
87
125
|
return this.#store;
|
|
88
126
|
}
|
|
89
127
|
/** Load frecency data from IndexedDB for the given namespace */
|
|
90
128
|
async load(namespace) {
|
|
91
129
|
if (this.#disposed) return { records: /* @__PURE__ */ new Map() };
|
|
92
130
|
const { get } = await this.#idb();
|
|
93
|
-
const store =
|
|
94
|
-
|
|
95
|
-
if (!serialized) return { records: /* @__PURE__ */ new Map() };
|
|
96
|
-
return deserializeData(serialized);
|
|
131
|
+
const store = this.#getStore();
|
|
132
|
+
return deserializeData(await get(storageKey(namespace), store));
|
|
97
133
|
}
|
|
98
134
|
/** Save frecency data to IndexedDB for the given namespace */
|
|
99
135
|
async save(namespace, data) {
|
|
100
136
|
if (this.#disposed) return;
|
|
101
137
|
const { set } = await this.#idb();
|
|
102
|
-
const store =
|
|
138
|
+
const store = this.#getStore();
|
|
103
139
|
await set(storageKey(namespace), serializeData(data), store);
|
|
104
140
|
}
|
|
105
141
|
/** Delete frecency data for a namespace from IndexedDB */
|
|
106
142
|
async delete(namespace) {
|
|
107
143
|
if (this.#disposed) return;
|
|
108
144
|
const { del } = await this.#idb();
|
|
109
|
-
const store =
|
|
145
|
+
const store = this.#getStore();
|
|
110
146
|
await del(storageKey(namespace), store);
|
|
111
147
|
}
|
|
112
148
|
/** Synchronous dispose — marks as disposed (best-effort cleanup) */
|
|
@@ -119,6 +155,12 @@ var IdbFrecencyStorage = class {
|
|
|
119
155
|
}
|
|
120
156
|
};
|
|
121
157
|
// src/core/types.ts
|
|
158
|
+
/**
|
|
159
|
+
* The `groupedIds` key of items that have no group. The value is an observable key of
|
|
160
|
+
* `CommandState.groupedIds` and stays `'__ungrouped'`. A group registered under this id shares the
|
|
161
|
+
* bucket: its items are listed with the ungrouped ones and the bucket takes its priority.
|
|
162
|
+
*/
|
|
163
|
+
const UNGROUPED_GROUP_ID = "__ungrouped";
|
|
122
164
|
/** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode. */
|
|
123
165
|
function itemId(id) {
|
|
124
166
|
return ensureWellFormed(id);
|
|
@@ -135,7 +177,10 @@ const DEFAULT_FRECENCY_DECAY = {
|
|
|
135
177
|
monthWeight: 1,
|
|
136
178
|
olderWeight: .5
|
|
137
179
|
};
|
|
138
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Default machine options. The machine reads `loop` and `open` from here when its options leave them out
|
|
182
|
+
* (the other options are opt-ins whose absence is their default); `virtualizeThreshold` is not read yet.
|
|
183
|
+
*/
|
|
139
184
|
const DEFAULT_MACHINE_OPTIONS = {
|
|
140
185
|
loop: true,
|
|
141
186
|
virtualizeThreshold: 100,
|
|
@@ -152,7 +197,7 @@ function createInitialState(options) {
|
|
|
152
197
|
loading: false,
|
|
153
198
|
page: "root",
|
|
154
199
|
pageStack: [],
|
|
155
|
-
open: options?.open ?? false,
|
|
200
|
+
open: options?.open ?? DEFAULT_MACHINE_OPTIONS.open ?? false,
|
|
156
201
|
lastUpdated: Date.now()
|
|
157
202
|
};
|
|
158
203
|
}
|
|
@@ -181,13 +226,6 @@ var MemoryFrecencyStorage = class {
|
|
|
181
226
|
// src/core/frecency/index.ts
|
|
182
227
|
/** Shared empty bonus map — avoids allocation when no frecency records exist */
|
|
183
228
|
const EMPTY_BONUS_MAP = /* @__PURE__ */ new Map();
|
|
184
|
-
/** Bucket boundaries as hour constants — no per-call unit conversion */
|
|
185
|
-
const BUCKET_HOURS = {
|
|
186
|
-
hour: 1,
|
|
187
|
-
day: 24,
|
|
188
|
-
week: 168,
|
|
189
|
-
month: 720
|
|
190
|
-
};
|
|
191
229
|
/**
|
|
192
230
|
* Computes a frecency bonus for an item based on usage frequency and recency.
|
|
193
231
|
* Uses hour/day/week/month bucket boundaries with
|
|
@@ -196,32 +234,74 @@ const BUCKET_HOURS = {
|
|
|
196
234
|
function computeFrecencyBonus(history, now = Date.now(), config = DEFAULT_FRECENCY_DECAY) {
|
|
197
235
|
const hours = (now - history.lastUsed) / 36e5;
|
|
198
236
|
const { hourWeight = 4, dayWeight = 2, weekWeight = 1.5, monthWeight = 1, olderWeight = .5 } = config;
|
|
199
|
-
const recencyWeight = hours <
|
|
237
|
+
const recencyWeight = hours < 1 ? hourWeight : hours < 24 ? dayWeight : hours < 168 ? weekWeight : hours < 720 ? monthWeight : olderWeight;
|
|
200
238
|
return history.frequency * recencyWeight;
|
|
201
239
|
}
|
|
240
|
+
/** Trailing debounce in milliseconds between the last `recordSelection` and the save it schedules */
|
|
241
|
+
const FLUSH_DELAY_MS = 1e3;
|
|
202
242
|
/**
|
|
203
243
|
* Stateful frecency engine that tracks item selection history and provides
|
|
204
|
-
* ranking boosts for search results. Supports pluggable storage backends
|
|
205
|
-
*
|
|
244
|
+
* ranking boosts for search results. Supports pluggable storage backends.
|
|
245
|
+
* Dirty data is saved 1000 ms after the last `recordSelection`, when the page
|
|
246
|
+
* becomes hidden or is hidden for good (`visibilitychange`, `pagehide`), on
|
|
247
|
+
* `flush()` and on dispose; a save waits for a `load()` in flight, so it writes the merged
|
|
248
|
+
* history. Dispose disposes the storage only when the engine created it (no `storage`
|
|
249
|
+
* option): a storage the caller passes is the caller's to dispose. Uses Date.now() for
|
|
250
|
+
* timestamps.
|
|
206
251
|
*/
|
|
207
252
|
var FrecencyEngine = class {
|
|
208
253
|
#storage;
|
|
254
|
+
/** True when the engine created its storage, so dispose disposes it; a caller's storage is left alone */
|
|
255
|
+
#ownsStorage;
|
|
209
256
|
#namespace;
|
|
210
257
|
#data;
|
|
211
258
|
#config;
|
|
212
259
|
#dirty = false;
|
|
260
|
+
#timer;
|
|
261
|
+
/** The last started save, never rejecting: saves queue behind it and flush awaits it */
|
|
262
|
+
#pending = Promise.resolve();
|
|
263
|
+
/** The last started load, never rejecting: a save starts only after it settles */
|
|
264
|
+
#loading = Promise.resolve();
|
|
265
|
+
/** Set by a rejected load: the engine writes nothing afterwards */
|
|
266
|
+
#loadFailed = false;
|
|
267
|
+
#abort = new AbortController();
|
|
213
268
|
constructor(options) {
|
|
269
|
+
this.#ownsStorage = options.storage === void 0;
|
|
214
270
|
this.#storage = options.storage ?? new MemoryFrecencyStorage();
|
|
215
271
|
this.#namespace = options.namespace ?? "default";
|
|
216
272
|
this.#config = options.decayConfig ?? DEFAULT_FRECENCY_DECAY;
|
|
217
273
|
this.#data = { records: /* @__PURE__ */ new Map() };
|
|
274
|
+
const { signal } = this.#abort;
|
|
275
|
+
globalThis.addEventListener?.("pagehide", this.#save, { signal });
|
|
276
|
+
globalThis.document?.addEventListener("visibilitychange", () => document.hidden && this.#save(), { signal });
|
|
218
277
|
}
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Merge the data in storage into the engine: frequencies add, `lastUsed` takes the later
|
|
280
|
+
* time, and selections recorded before the load resolves are kept. Call it once per
|
|
281
|
+
* engine; a second load would add the persisted frequencies again. A save started while
|
|
282
|
+
* the load is in flight waits for it, so the history it writes is the merged one, and a load
|
|
283
|
+
* that never settles holds every save, and `await using`, forever. A failed load rejects here
|
|
284
|
+
* and turns persistence off for this engine: saving would replace a history it could not read.
|
|
285
|
+
*/
|
|
286
|
+
load() {
|
|
287
|
+
const merged = this.#merge();
|
|
288
|
+
this.#loading = merged.catch(() => {
|
|
289
|
+
this.#loadFailed = true;
|
|
290
|
+
});
|
|
291
|
+
return merged;
|
|
292
|
+
}
|
|
293
|
+
async #merge() {
|
|
294
|
+
const { records } = await this.#storage.load(this.#namespace);
|
|
295
|
+
const mine = this.#data.records;
|
|
296
|
+
for (const [id, loaded] of records) {
|
|
297
|
+
const m = mine.get(id);
|
|
298
|
+
mine.set(id, m ? {
|
|
299
|
+
frequency: m.frequency + loaded.frequency,
|
|
300
|
+
lastUsed: Math.max(m.lastUsed, loaded.lastUsed)
|
|
301
|
+
} : loaded);
|
|
302
|
+
}
|
|
223
303
|
}
|
|
224
|
-
/** Record a selection event for an item */
|
|
304
|
+
/** Record a selection event for an item and schedule a save 1000 ms after the last one */
|
|
225
305
|
recordSelection(itemId) {
|
|
226
306
|
const now = Date.now();
|
|
227
307
|
const updated = {
|
|
@@ -230,6 +310,8 @@ var FrecencyEngine = class {
|
|
|
230
310
|
};
|
|
231
311
|
this.#data.records.set(itemId, updated);
|
|
232
312
|
this.#dirty = true;
|
|
313
|
+
clearTimeout(this.#timer);
|
|
314
|
+
if (!this.#abort.signal.aborted) this.#timer = setTimeout(this.#save, FLUSH_DELAY_MS);
|
|
233
315
|
}
|
|
234
316
|
/** Get frecency bonus for an item */
|
|
235
317
|
getBonus(itemId, now) {
|
|
@@ -255,30 +337,49 @@ var FrecencyEngine = class {
|
|
|
255
337
|
...config
|
|
256
338
|
};
|
|
257
339
|
}
|
|
258
|
-
/**
|
|
340
|
+
/**
|
|
341
|
+
* Flush dirty data to storage and wait for every save in flight; saves run one after
|
|
342
|
+
* another, each after the load in flight (one that never settles holds them all), none after a
|
|
343
|
+
* rejected load, and a rejected save leaves the data dirty and rethrows to the flush that started it
|
|
344
|
+
*/
|
|
259
345
|
async flush() {
|
|
260
|
-
|
|
261
|
-
|
|
346
|
+
clearTimeout(this.#timer);
|
|
347
|
+
const dirty = this.#dirty;
|
|
262
348
|
this.#dirty = false;
|
|
349
|
+
const save = Promise.all([this.#pending, this.#loading]).then(() => dirty && !this.#loadFailed && this.#storage.save(this.#namespace, this.#data));
|
|
350
|
+
this.#pending = save.catch(() => {});
|
|
351
|
+
try {
|
|
352
|
+
await save;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
this.#dirty = true;
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
263
357
|
}
|
|
358
|
+
/** Flush for the timer and the page listeners: a failure is reported, never thrown */
|
|
359
|
+
#save = () => this.flush().catch(this.#warn);
|
|
360
|
+
#warn = (error) => {
|
|
361
|
+
console.warn("[modern-cmdk] save failed:", error);
|
|
362
|
+
};
|
|
264
363
|
/** Get the raw frecency data */
|
|
265
364
|
getData() {
|
|
266
365
|
return this.#data;
|
|
267
366
|
}
|
|
268
|
-
/**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
this
|
|
367
|
+
/**
|
|
368
|
+
* Async dispose — flushes dirty data, then disposes the storage the engine created, even
|
|
369
|
+
* when the save rejects (the rejection propagates). Use with `await using`.
|
|
370
|
+
*/
|
|
371
|
+
[Symbol.asyncDispose]() {
|
|
372
|
+
this.#abort.abort();
|
|
373
|
+
return this.flush().finally(this.#disposeStorage);
|
|
275
374
|
}
|
|
375
|
+
/** Sync dispose — starts the flush without awaiting it and disposes the storage the engine created once it settles */
|
|
276
376
|
[Symbol.dispose]() {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
});
|
|
280
|
-
this.#storage[Symbol.dispose]();
|
|
377
|
+
this.#abort.abort();
|
|
378
|
+
this.#save().finally(this.#disposeStorage).catch(this.#warn);
|
|
281
379
|
}
|
|
380
|
+
#disposeStorage = () => {
|
|
381
|
+
if (this.#ownsStorage) this.#storage[Symbol.dispose]();
|
|
382
|
+
};
|
|
282
383
|
};
|
|
283
384
|
// src/core/utils/group-by.ts
|
|
284
385
|
/**
|
|
@@ -397,10 +498,13 @@ function formatShortcut(parsed) {
|
|
|
397
498
|
/**
|
|
398
499
|
* Manages keyboard shortcut bindings with global keydown listening.
|
|
399
500
|
* Supports `using` for automatic cleanup of both individual bindings and the
|
|
400
|
-
* global document listener.
|
|
501
|
+
* global document listener. Every normalized shortcut keeps a stack of bindings, one per item: the later
|
|
502
|
+
* binding wins the keydown, disposing it restores the earlier one, and `getConflicts()` reports the keys with
|
|
503
|
+
* two or more bindings (through `detectConflicts`, which groups with the local mapGroupBy helper).
|
|
401
504
|
*/
|
|
402
505
|
var KeyboardShortcutRegistry = class {
|
|
403
506
|
#bindings = /* @__PURE__ */ new Map();
|
|
507
|
+
#keysByItem = /* @__PURE__ */ new Map();
|
|
404
508
|
#globalListener = null;
|
|
405
509
|
#enabled = true;
|
|
406
510
|
constructor(options) {
|
|
@@ -410,45 +514,72 @@ var KeyboardShortcutRegistry = class {
|
|
|
410
514
|
/** Register a shortcut binding — returns Disposable for `using` auto-deregister */
|
|
411
515
|
register(shortcutStr, itemId, handler) {
|
|
412
516
|
const shortcut = parseShortcut(shortcutStr);
|
|
413
|
-
|
|
517
|
+
const binding = {
|
|
414
518
|
shortcut,
|
|
415
519
|
itemId,
|
|
416
520
|
handler
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
521
|
+
};
|
|
522
|
+
const stack = this.#bindings.get(shortcut.normalized) ?? [];
|
|
523
|
+
const own = stack.findIndex((b) => b.itemId === itemId);
|
|
524
|
+
if (own < 0) stack.push(binding);
|
|
525
|
+
else stack[own] = binding;
|
|
526
|
+
this.#bindings.set(shortcut.normalized, stack);
|
|
527
|
+
const keys = this.#keysByItem.get(itemId) ?? /* @__PURE__ */ new Set();
|
|
528
|
+
this.#keysByItem.set(itemId, keys.add(shortcut.normalized));
|
|
529
|
+
return { [Symbol.dispose]: () => this.#remove(shortcut.normalized, (b) => b === binding) };
|
|
530
|
+
}
|
|
531
|
+
/** Drops the bindings of one key that the predicate selects; an emptied key goes away */
|
|
532
|
+
#remove(key, drop) {
|
|
533
|
+
const stack = this.#bindings.get(key);
|
|
534
|
+
if (!stack) return;
|
|
535
|
+
const rest = stack.filter((b) => !drop(b));
|
|
536
|
+
if (rest.length) this.#bindings.set(key, rest);
|
|
537
|
+
else this.#bindings.delete(key);
|
|
538
|
+
for (const { itemId } of stack) {
|
|
539
|
+
if (rest.some((b) => b.itemId === itemId)) continue;
|
|
540
|
+
const keys = this.#keysByItem.get(itemId);
|
|
541
|
+
keys?.delete(key);
|
|
542
|
+
if (!keys?.size) this.#keysByItem.delete(itemId);
|
|
543
|
+
}
|
|
421
544
|
}
|
|
422
|
-
/** Unregister a shortcut by its string representation */
|
|
545
|
+
/** Unregister a shortcut by its string representation — every binding of that key */
|
|
423
546
|
unregister(shortcutStr) {
|
|
424
|
-
|
|
425
|
-
this.#bindings.delete(shortcut.normalized);
|
|
547
|
+
this.#remove(parseShortcut(shortcutStr).normalized, () => true);
|
|
426
548
|
}
|
|
427
|
-
/**
|
|
428
|
-
|
|
429
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Unregister all shortcuts for a given item — only the stacks of the keys it is bound under are touched.
|
|
551
|
+
* `keep` names one shortcut whose binding stays (a re-bind then replaces it in place, see register).
|
|
552
|
+
*/
|
|
553
|
+
unregisterByItem(itemId, keep) {
|
|
554
|
+
const keys = this.#keysByItem.get(itemId);
|
|
555
|
+
if (!keys) return;
|
|
556
|
+
const kept = keep === void 0 ? void 0 : parseShortcut(keep).normalized;
|
|
557
|
+
for (const key of [...keys]) if (key !== kept) this.#remove(key, (b) => b.itemId === itemId);
|
|
430
558
|
}
|
|
431
|
-
/**
|
|
559
|
+
/** Keys bound by two or more items (normalized shortcut -> the parsed shortcuts, earliest first) */
|
|
432
560
|
getConflicts() {
|
|
433
|
-
return detectConflicts(this.#bindings.values().map((b) => b.shortcut).toArray());
|
|
561
|
+
return detectConflicts(this.#bindings.values().flatMap((stack) => stack.map((b) => b.shortcut)).toArray());
|
|
434
562
|
}
|
|
435
563
|
/** Enable or disable global shortcut listening */
|
|
436
564
|
setEnabled(enabled) {
|
|
437
565
|
this.#enabled = enabled;
|
|
438
566
|
}
|
|
439
|
-
/**
|
|
567
|
+
/** The winning binding of every key (a snapshot: the later of the bindings that share a key) */
|
|
440
568
|
getBindings() {
|
|
441
|
-
return this.#bindings;
|
|
569
|
+
return new Map(this.#bindings.entries().map(([key, stack]) => [key, stack.at(-1)]));
|
|
442
570
|
}
|
|
443
571
|
#attachGlobalListener() {
|
|
444
572
|
if (typeof document === "undefined") return;
|
|
445
573
|
this.#globalListener = (event) => {
|
|
446
574
|
if (!this.#enabled) return;
|
|
447
|
-
for (const
|
|
448
|
-
|
|
449
|
-
event.
|
|
450
|
-
|
|
451
|
-
|
|
575
|
+
for (const stack of this.#bindings.values()) {
|
|
576
|
+
const binding = stack.at(-1);
|
|
577
|
+
if (matchesShortcut(event, binding.shortcut)) {
|
|
578
|
+
event.preventDefault();
|
|
579
|
+
event.stopPropagation();
|
|
580
|
+
binding.handler();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
452
583
|
}
|
|
453
584
|
};
|
|
454
585
|
document.addEventListener("keydown", this.#globalListener);
|
|
@@ -462,6 +593,7 @@ var KeyboardShortcutRegistry = class {
|
|
|
462
593
|
[Symbol.dispose]() {
|
|
463
594
|
this.#detachGlobalListener();
|
|
464
595
|
this.#bindings.clear();
|
|
596
|
+
this.#keysByItem.clear();
|
|
465
597
|
}
|
|
466
598
|
};
|
|
467
599
|
// src/core/utils/set-ops.ts
|
|
@@ -516,7 +648,6 @@ function setSymmetricDifference(a, b) {
|
|
|
516
648
|
var CommandRegistry = class {
|
|
517
649
|
#items = /* @__PURE__ */ new Map();
|
|
518
650
|
#groups = /* @__PURE__ */ new Map();
|
|
519
|
-
#itemOrder = [];
|
|
520
651
|
#itemOrderSet = /* @__PURE__ */ new Set();
|
|
521
652
|
#cachedItems = null;
|
|
522
653
|
#cachedGroups = null;
|
|
@@ -528,45 +659,32 @@ var CommandRegistry = class {
|
|
|
528
659
|
}
|
|
529
660
|
/** Register an item — returns Disposable for `using` auto-deregister */
|
|
530
661
|
registerItem(item) {
|
|
531
|
-
this
|
|
532
|
-
if (!this.#itemOrderSet.has(item.id)) {
|
|
533
|
-
this.#itemOrder.push(item.id);
|
|
534
|
-
this.#itemOrderSet.add(item.id);
|
|
535
|
-
}
|
|
536
|
-
this.#invalidateCache();
|
|
537
|
-
return { [Symbol.dispose]: () => {
|
|
538
|
-
this.unregisterItem(item.id);
|
|
539
|
-
} };
|
|
662
|
+
return this.registerItems([item]);
|
|
540
663
|
}
|
|
541
664
|
/** Register multiple items at once — single Disposable for batch cleanup. for...of for hot path. */
|
|
542
665
|
registerItems(items) {
|
|
543
|
-
|
|
666
|
+
const owned = [...items];
|
|
667
|
+
for (const item of owned) {
|
|
544
668
|
this.#items.set(item.id, item);
|
|
545
|
-
|
|
546
|
-
this.#itemOrder.push(item.id);
|
|
547
|
-
this.#itemOrderSet.add(item.id);
|
|
548
|
-
}
|
|
669
|
+
this.#itemOrderSet.add(item.id);
|
|
549
670
|
}
|
|
550
671
|
this.#invalidateCache();
|
|
551
|
-
const ids = new Set(items.values().map((i) => i.id));
|
|
552
672
|
return { [Symbol.dispose]: () => {
|
|
553
|
-
this.
|
|
673
|
+
for (const item of owned) if (this.#items.get(item.id) === item) this.unregisterItem(item.id);
|
|
554
674
|
} };
|
|
555
675
|
}
|
|
556
|
-
/** Unregister a single item — O(1) Map/Set delete
|
|
676
|
+
/** Unregister a single item — O(1) Map/Set delete */
|
|
557
677
|
unregisterItem(id) {
|
|
558
|
-
if (!this.#items.
|
|
559
|
-
this.#items.delete(id);
|
|
678
|
+
if (!this.#items.delete(id)) return;
|
|
560
679
|
this.#itemOrderSet.delete(id);
|
|
561
|
-
const idx = this.#itemOrder.indexOf(id);
|
|
562
|
-
if (idx !== -1) this.#itemOrder = this.#itemOrder.toSpliced(idx, 1);
|
|
563
680
|
this.#invalidateCache();
|
|
564
681
|
}
|
|
565
682
|
/** Bulk unregister — for...of for hot path */
|
|
566
683
|
unregisterItems(ids) {
|
|
567
|
-
for (const id of ids)
|
|
568
|
-
|
|
569
|
-
|
|
684
|
+
for (const id of ids) {
|
|
685
|
+
this.#items.delete(id);
|
|
686
|
+
this.#itemOrderSet.delete(id);
|
|
687
|
+
}
|
|
570
688
|
this.#invalidateCache();
|
|
571
689
|
}
|
|
572
690
|
/** Register a group */
|
|
@@ -574,8 +692,7 @@ var CommandRegistry = class {
|
|
|
574
692
|
this.#groups.set(group.id, group);
|
|
575
693
|
this.#invalidateGroupCache();
|
|
576
694
|
return { [Symbol.dispose]: () => {
|
|
577
|
-
this.#groups.
|
|
578
|
-
this.#invalidateGroupCache();
|
|
695
|
+
if (this.#groups.get(group.id) === group) this.unregisterGroup(group.id);
|
|
579
696
|
} };
|
|
580
697
|
}
|
|
581
698
|
/** Unregister a group */
|
|
@@ -594,7 +711,7 @@ var CommandRegistry = class {
|
|
|
594
711
|
/** Get all registered items in insertion order — cached between mutations */
|
|
595
712
|
getItems() {
|
|
596
713
|
if (this.#cachedItems !== null) return this.#cachedItems;
|
|
597
|
-
this.#cachedItems = this.#
|
|
714
|
+
this.#cachedItems = this.#items.values().toArray();
|
|
598
715
|
return this.#cachedItems;
|
|
599
716
|
}
|
|
600
717
|
/** Get all registered groups sorted by priority — cached between group mutations */
|
|
@@ -611,8 +728,8 @@ var CommandRegistry = class {
|
|
|
611
728
|
const groupItems = grouped.get(group.id);
|
|
612
729
|
if (groupItems != null) result.set(group.id, groupItems);
|
|
613
730
|
}
|
|
614
|
-
const ungrouped = grouped.get(
|
|
615
|
-
if (ungrouped) result.set(
|
|
731
|
+
const ungrouped = grouped.get(UNGROUPED_GROUP_ID);
|
|
732
|
+
if (ungrouped) result.set(UNGROUPED_GROUP_ID, ungrouped);
|
|
616
733
|
return result;
|
|
617
734
|
}
|
|
618
735
|
/** Get item IDs as a Set — returns internal set (O(1), no allocation) */
|
|
@@ -659,7 +776,6 @@ var CommandRegistry = class {
|
|
|
659
776
|
clear() {
|
|
660
777
|
this.#items.clear();
|
|
661
778
|
this.#groups.clear();
|
|
662
|
-
this.#itemOrder = [];
|
|
663
779
|
this.#itemOrderSet.clear();
|
|
664
780
|
this.#invalidateCache();
|
|
665
781
|
this.#invalidateGroupCache();
|
|
@@ -693,6 +809,11 @@ function isWordSeparator(code) {
|
|
|
693
809
|
* Multi-strategy: exact → prefix → substring → word-boundary → fuzzy.
|
|
694
810
|
* Returns null if no match. For batch use, prefer the search engine
|
|
695
811
|
* which pre-caches lowercase and normalizes query once.
|
|
812
|
+
*
|
|
813
|
+
* `matches` are half-open ranges into `item.value` itself (UTF-16 indices of the original
|
|
814
|
+
* string, even where lowercasing changes its length, as 'İ' → 'i̇' does). A keyword has no
|
|
815
|
+
* position in the label: when a keyword outscores the value, `matches` keeps the value's own
|
|
816
|
+
* ranges if the value matched and is `[]` otherwise.
|
|
696
817
|
*/
|
|
697
818
|
function scoreItem(query, item) {
|
|
698
819
|
if (query === "") return {
|
|
@@ -700,45 +821,21 @@ function scoreItem(query, item) {
|
|
|
700
821
|
score: 1,
|
|
701
822
|
matches: []
|
|
702
823
|
};
|
|
703
|
-
|
|
704
|
-
let bestScore = 0;
|
|
705
|
-
let bestMatches = null;
|
|
706
|
-
const valueResult = scoreTarget(lowerQuery, item.value.toLowerCase());
|
|
707
|
-
if (valueResult !== null) {
|
|
708
|
-
bestScore = valueResult.score;
|
|
709
|
-
bestMatches = valueResult.matches;
|
|
710
|
-
if (bestScore >= 1) return {
|
|
711
|
-
id: item.id,
|
|
712
|
-
score: 1,
|
|
713
|
-
matches: bestMatches
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
const kw = item.keywords;
|
|
717
|
-
if (kw !== void 0) for (let i = 0; i < kw.length; i++) {
|
|
718
|
-
const result = scoreTarget(lowerQuery, kw[i].toLowerCase());
|
|
719
|
-
if (result !== null && result.score > bestScore) {
|
|
720
|
-
bestScore = result.score;
|
|
721
|
-
bestMatches = result.matches;
|
|
722
|
-
if (bestScore >= 1) break;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
return bestMatches !== null ? {
|
|
726
|
-
id: item.id,
|
|
727
|
-
score: bestScore,
|
|
728
|
-
matches: bestMatches
|
|
729
|
-
} : null;
|
|
824
|
+
return scoreItemPreLowered(query.toLowerCase(), item.id, item.value, item.value.toLowerCase(), item.keywords?.map((keyword) => keyword.toLowerCase()));
|
|
730
825
|
}
|
|
731
826
|
/**
|
|
732
827
|
* Internal fast path: called by search engine with pre-lowered query and targets.
|
|
733
|
-
* Eliminates
|
|
828
|
+
* Eliminates the toLowerCase() calls on the hot path, except per character in toOriginalRanges
|
|
829
|
+
* when lowering changes the length of the value. `value` is the original text, which the
|
|
830
|
+
* ranges index (see {@link scoreItem}).
|
|
734
831
|
*/
|
|
735
|
-
function scoreItemPreLowered(lowerQuery, id, lowerValue, lowerKeywords) {
|
|
832
|
+
function scoreItemPreLowered(lowerQuery, id, value, lowerValue, lowerKeywords) {
|
|
736
833
|
let bestScore = 0;
|
|
737
834
|
let bestMatches = null;
|
|
738
835
|
const valueResult = scoreTarget(lowerQuery, lowerValue);
|
|
739
836
|
if (valueResult !== null) {
|
|
740
837
|
bestScore = valueResult.score;
|
|
741
|
-
bestMatches = valueResult.matches;
|
|
838
|
+
bestMatches = toOriginalRanges(value, lowerValue, valueResult.matches);
|
|
742
839
|
if (bestScore >= 1) return {
|
|
743
840
|
id,
|
|
744
841
|
score: 1,
|
|
@@ -749,7 +846,7 @@ function scoreItemPreLowered(lowerQuery, id, lowerValue, lowerKeywords) {
|
|
|
749
846
|
const result = scoreTarget(lowerQuery, lowerKeywords[i]);
|
|
750
847
|
if (result !== null && result.score > bestScore) {
|
|
751
848
|
bestScore = result.score;
|
|
752
|
-
bestMatches
|
|
849
|
+
bestMatches ??= [];
|
|
753
850
|
if (bestScore >= 1) break;
|
|
754
851
|
}
|
|
755
852
|
}
|
|
@@ -759,6 +856,28 @@ function scoreItemPreLowered(lowerQuery, id, lowerValue, lowerKeywords) {
|
|
|
759
856
|
matches: bestMatches
|
|
760
857
|
} : null;
|
|
761
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Maps ranges found in `lowerValue` back onto `value`. Under toLowerCase() only U+0130
|
|
861
|
+
* ('İ' → 'i' + U+0307) changes the UTF-16 length of any code point, which shifts every later
|
|
862
|
+
* index, so when the lengths differ each lowered unit is traced to the character it came from:
|
|
863
|
+
* a range starts at the start of the character holding its first unit and ends at the end of
|
|
864
|
+
* the character holding its last.
|
|
865
|
+
* Equal lengths (the common case; the final sigma lowers to 'ς' or 'σ', both one unit) need no map.
|
|
866
|
+
*/
|
|
867
|
+
function toOriginalRanges(value, lowerValue, ranges) {
|
|
868
|
+
if (value.length === lowerValue.length) return ranges;
|
|
869
|
+
const locate = (unit, after) => {
|
|
870
|
+
let from = 0;
|
|
871
|
+
let lowered = 0;
|
|
872
|
+
for (const char of value) {
|
|
873
|
+
lowered += char.toLowerCase().length;
|
|
874
|
+
if (lowered > unit) return from + after * char.length;
|
|
875
|
+
from += char.length;
|
|
876
|
+
}
|
|
877
|
+
return from;
|
|
878
|
+
};
|
|
879
|
+
return ranges.map(([start, end]) => [locate(start, 0), locate(end - 1, 1)]);
|
|
880
|
+
}
|
|
762
881
|
function scoreTarget(query, lowerTarget) {
|
|
763
882
|
const tLen = lowerTarget.length;
|
|
764
883
|
if (tLen === 0) return null;
|
|
@@ -790,28 +909,41 @@ function scoreTarget(query, lowerTarget) {
|
|
|
790
909
|
return scoreFuzzy(query, qLen, lowerTarget, tLen);
|
|
791
910
|
}
|
|
792
911
|
function scoreWordBoundary(query, qLen, lowerTarget, tLen) {
|
|
793
|
-
|
|
912
|
+
if (qLen > 512) return null;
|
|
794
913
|
const matches = [];
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const matchStart = i;
|
|
799
|
-
let matchLen = 0;
|
|
800
|
-
while (queryIdx < qLen && i < tLen && !isWordSeparator(lowerTarget.charCodeAt(i)) && lowerTarget.charCodeAt(i) === query.charCodeAt(queryIdx)) {
|
|
801
|
-
queryIdx++;
|
|
802
|
-
i++;
|
|
803
|
-
matchLen++;
|
|
804
|
-
}
|
|
805
|
-
matches.push([matchStart, matchStart + matchLen]);
|
|
806
|
-
totalWeightedLen += matchLen * 2;
|
|
807
|
-
} else i++;
|
|
808
|
-
if (queryIdx !== qLen) return null;
|
|
809
|
-
const maxPossible = qLen * 2;
|
|
914
|
+
failedFrom = void 0;
|
|
915
|
+
squares = 0;
|
|
916
|
+
if (!coverQuery(query, 0, lowerTarget, 0, matches)) return null;
|
|
810
917
|
return {
|
|
811
|
-
score: .
|
|
918
|
+
score: .65 + .03 * (squares / (qLen * qLen)) + .02 * (qLen / tLen),
|
|
812
919
|
matches
|
|
813
920
|
};
|
|
814
921
|
}
|
|
922
|
+
let failedFrom;
|
|
923
|
+
let squares = 0;
|
|
924
|
+
/**
|
|
925
|
+
* Covers query[queryIdx..] with segments that each start at a word start at or after `from`,
|
|
926
|
+
* stay inside one word and match contiguously. Each word start tries its longest run first, then
|
|
927
|
+
* shorter ones, so the first cover found is the one a greedy scan finds whenever that succeeds.
|
|
928
|
+
* Prepends the segments of the cover to `matches`, in order; charCode scan, no regex, no split.
|
|
929
|
+
*/
|
|
930
|
+
function coverQuery(query, queryIdx, lowerTarget, from, matches) {
|
|
931
|
+
const end = failedFrom?.[queryIdx] ?? lowerTarget.length;
|
|
932
|
+
for (let i = from; i < end; i++) if ((i === 0 || isWordSeparator(lowerTarget.charCodeAt(i - 1))) && lowerTarget.charCodeAt(i) === query.charCodeAt(queryIdx)) {
|
|
933
|
+
let run = 0;
|
|
934
|
+
while (!isWordSeparator(lowerTarget.charCodeAt(i + run)) && lowerTarget.charCodeAt(i + run) === query.charCodeAt(queryIdx + run)) run++;
|
|
935
|
+
for (; run > 0; run--) if (queryIdx + run === query.length || coverQuery(query, queryIdx + run, lowerTarget, i + run, matches)) {
|
|
936
|
+
matches.unshift([i, i + run]);
|
|
937
|
+
squares += run * run;
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (queryIdx > 0 && from < end) {
|
|
942
|
+
failedFrom ??= [];
|
|
943
|
+
failedFrom[queryIdx] = from;
|
|
944
|
+
}
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
815
947
|
function scoreFuzzy(query, qLen, lowerTarget, tLen) {
|
|
816
948
|
let queryIdx = 0;
|
|
817
949
|
let targetIdx = 0;
|
|
@@ -852,9 +984,10 @@ if (isDev && typeof window !== "undefined") leakRegistry = new FinalizationRegis
|
|
|
852
984
|
console.warn(`[modern-cmdk] ${label} was garbage collected without being disposed. Use \`using\` or call [Symbol.dispose]() to prevent leaks.`);
|
|
853
985
|
});
|
|
854
986
|
/**
|
|
855
|
-
* Creates a search engine with pluggable scorer
|
|
856
|
-
*
|
|
857
|
-
*
|
|
987
|
+
* Creates a search engine with pluggable scorer.
|
|
988
|
+
* With the default scorer, a query that extends the previous one (compared lowercased) re-scores
|
|
989
|
+
* only the prior matches; a custom scorer is called for every item on every non-empty query.
|
|
990
|
+
* Pre-caches lowercase at index time; indexed items are not re-lowered during search.
|
|
858
991
|
* Uses Iterator Helpers (ES2025) for result pipelines.
|
|
859
992
|
*/
|
|
860
993
|
function createSearchEngine(options) {
|
|
@@ -886,11 +1019,11 @@ function createSearchEngine(options) {
|
|
|
886
1019
|
}));
|
|
887
1020
|
}
|
|
888
1021
|
const lowerQuery = query.toLowerCase();
|
|
889
|
-
const candidateItems =
|
|
1022
|
+
const candidateItems = useDefaultScorer && previousQuery.length > 0 && lowerQuery.startsWith(previousQuery) ? items.values().filter((item) => previousResults.has(item.id)) : items;
|
|
890
1023
|
const results = [];
|
|
891
1024
|
if (useDefaultScorer) for (const item of candidateItems) {
|
|
892
1025
|
const entry = indexedEntries.get(item.id);
|
|
893
|
-
const result = entry ? scoreItemPreLowered(lowerQuery, item.id, entry.lowerValue, entry.lowerKeywords) : scoreItemPreLowered(lowerQuery, item.id, item.value.toLowerCase(), item.keywords?.map((k) => k.toLowerCase()));
|
|
1026
|
+
const result = entry ? scoreItemPreLowered(lowerQuery, item.id, item.value, entry.lowerValue, entry.lowerKeywords) : scoreItemPreLowered(lowerQuery, item.id, item.value, item.value.toLowerCase(), item.keywords?.map((k) => k.toLowerCase()));
|
|
894
1027
|
if (result !== null) results.push(result);
|
|
895
1028
|
}
|
|
896
1029
|
else for (const item of candidateItems) {
|
|
@@ -898,13 +1031,15 @@ function createSearchEngine(options) {
|
|
|
898
1031
|
if (result != null) results.push(result);
|
|
899
1032
|
}
|
|
900
1033
|
results.sort((a, b) => b.score - a.score);
|
|
901
|
-
previousQuery =
|
|
1034
|
+
previousQuery = lowerQuery;
|
|
902
1035
|
previousResults = new Set(results.values().map((r) => r.id));
|
|
903
1036
|
return results.values();
|
|
904
1037
|
},
|
|
905
1038
|
remove(ids) {
|
|
906
|
-
for (const id of ids)
|
|
907
|
-
|
|
1039
|
+
for (const id of ids) {
|
|
1040
|
+
indexedEntries.delete(id);
|
|
1041
|
+
previousResults.delete(id);
|
|
1042
|
+
}
|
|
908
1043
|
},
|
|
909
1044
|
clear() {
|
|
910
1045
|
indexedEntries.clear();
|
|
@@ -921,6 +1056,7 @@ function createSearchEngine(options) {
|
|
|
921
1056
|
// src/core/utils/event-emitter.ts
|
|
922
1057
|
/**
|
|
923
1058
|
* Type-safe event emitter with strong references and Disposable subscriptions.
|
|
1059
|
+
* `T` maps event names to payload types; an interface or a type alias both work (`object`, not an index signature, which an interface lacks).
|
|
924
1060
|
* Returns Disposable from `on()` for automatic unsubscription via `using`.
|
|
925
1061
|
*/
|
|
926
1062
|
var TypedEmitter = class {
|
|
@@ -938,11 +1074,20 @@ var TypedEmitter = class {
|
|
|
938
1074
|
set.delete(fn);
|
|
939
1075
|
} };
|
|
940
1076
|
}
|
|
941
|
-
/**
|
|
1077
|
+
/**
|
|
1078
|
+
* Emit an event to all listeners. Snapshots the set to prevent live-iteration issues.
|
|
1079
|
+
* A throwing listener does not stop the later ones: its error is rethrown from a microtask, as the scheduler does.
|
|
1080
|
+
*/
|
|
942
1081
|
emit(event, data) {
|
|
943
1082
|
const set = this.#listeners.get(event);
|
|
944
1083
|
if (!set || set.size === 0) return;
|
|
945
|
-
for (const fn of [...set])
|
|
1084
|
+
for (const fn of [...set]) try {
|
|
1085
|
+
fn(data);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
queueMicrotask(() => {
|
|
1088
|
+
throw error;
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
946
1091
|
}
|
|
947
1092
|
/** Check if any listeners exist for an event */
|
|
948
1093
|
has(event) {
|
|
@@ -964,13 +1109,9 @@ var TypedEmitter = class {
|
|
|
964
1109
|
// src/core/utils/scheduler.ts
|
|
965
1110
|
/** Whether we're in a browser environment with rAF available */
|
|
966
1111
|
const hasRAF = typeof requestAnimationFrame === "function";
|
|
967
|
-
/** Feature detection for cutting-edge scheduling APIs, read through globalThis: typeof guards only a bare
|
|
968
|
-
* identifier, so `typeof navigator?.scheduling` would throw where navigator is undeclared (runtimes without one). */
|
|
969
|
-
const canYield = typeof globalThis.scheduler?.yield === "function";
|
|
970
|
-
const hasInputPending = typeof globalThis.navigator?.scheduling?.isInputPending === "function";
|
|
971
1112
|
/**
|
|
972
1113
|
* Creates a batched update scheduler. Coalesces rapid state updates into a single
|
|
973
|
-
* execution pass using rAF (browser) or queueMicrotask (Node.js).
|
|
1114
|
+
* execution pass using rAF (browser) or queueMicrotask (where rAF is absent: Node.js, SSR).
|
|
974
1115
|
*/
|
|
975
1116
|
function createScheduler() {
|
|
976
1117
|
let pending = [];
|
|
@@ -981,12 +1122,6 @@ function createScheduler() {
|
|
|
981
1122
|
pending = [];
|
|
982
1123
|
rafId = null;
|
|
983
1124
|
microtaskScheduled = false;
|
|
984
|
-
if (batch.length > 10 && hasInputPending && navigator.scheduling?.isInputPending?.()) {
|
|
985
|
-
pending = [...batch, ...pending];
|
|
986
|
-
if (canYield) globalThis.scheduler?.yield?.().then(() => void executeBatch());
|
|
987
|
-
else rafId ??= requestAnimationFrame(() => void executeBatch());
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
1125
|
for (const update of batch) try {
|
|
991
1126
|
update();
|
|
992
1127
|
} catch (error) {
|
|
@@ -1026,52 +1161,82 @@ function createScheduler() {
|
|
|
1026
1161
|
// src/core/machine.ts
|
|
1027
1162
|
/**
|
|
1028
1163
|
* Creates a headless command palette state machine with search, frecency,
|
|
1029
|
-
* and keyboard shortcut support. Implements Disposable
|
|
1164
|
+
* and keyboard shortcut support. Implements Disposable and AsyncDisposable for
|
|
1165
|
+
* `using` and `await using` cleanup (the latter awaits the pending frecency save).
|
|
1030
1166
|
*
|
|
1031
1167
|
* @example
|
|
1032
1168
|
* ```ts
|
|
1033
|
-
*
|
|
1034
|
-
* machine.subscribe(() => console.log(machine.getState()));
|
|
1169
|
+
* const machine = createCommandMachine({ items, loop: true });
|
|
1170
|
+
* const unsubscribe = machine.subscribe(() => console.log(machine.getState()));
|
|
1171
|
+
* machine.send({ type: 'SEARCH_CHANGE', query: 'app' }); // handled in the next frame
|
|
1172
|
+
*
|
|
1173
|
+
* // on teardown
|
|
1174
|
+
* unsubscribe();
|
|
1175
|
+
* machine[Symbol.dispose]();
|
|
1035
1176
|
* ```
|
|
1036
1177
|
*/
|
|
1037
1178
|
function createCommandMachine(options = {}) {
|
|
1038
1179
|
let state = createInitialState(options);
|
|
1039
1180
|
let filteredIdSet = /* @__PURE__ */ new Set();
|
|
1040
1181
|
let filteredIdIndex = /* @__PURE__ */ new Map();
|
|
1182
|
+
let navIds = [];
|
|
1041
1183
|
const registry = new CommandRegistry();
|
|
1042
1184
|
const searchEngine = options.search ?? createSearchEngine(typeof options.filter === "function" ? { scorer: (query, item) => {
|
|
1043
1185
|
const score = options.filter(item, query);
|
|
1044
|
-
|
|
1045
|
-
return {
|
|
1186
|
+
return score && score > 0 ? {
|
|
1046
1187
|
id: item.id,
|
|
1047
1188
|
score,
|
|
1048
1189
|
matches: []
|
|
1049
|
-
};
|
|
1190
|
+
} : null;
|
|
1050
1191
|
} } : void 0);
|
|
1051
1192
|
const scheduler = createScheduler();
|
|
1052
1193
|
const emitter = new TypedEmitter();
|
|
1053
1194
|
const keyboardRegistry = new KeyboardShortcutRegistry();
|
|
1195
|
+
const pendingSources = /* @__PURE__ */ new Set();
|
|
1054
1196
|
let frecencyEngine = null;
|
|
1055
1197
|
if (options.frecency?.enabled) {
|
|
1056
1198
|
frecencyEngine = new FrecencyEngine(options.frecency);
|
|
1057
|
-
frecencyEngine.load()
|
|
1199
|
+
frecencyEngine.load().then(() => {
|
|
1200
|
+
if (disposed || state.search === "" || options.filter === false) return;
|
|
1201
|
+
dirty = true;
|
|
1202
|
+
if (queued === 0) commit();
|
|
1203
|
+
}, (error) => {
|
|
1204
|
+
console.warn("[modern-cmdk] frecency load failed:", error);
|
|
1205
|
+
});
|
|
1058
1206
|
}
|
|
1207
|
+
let queued = 0;
|
|
1208
|
+
let nextQueued = 0;
|
|
1209
|
+
let inHandler = false;
|
|
1210
|
+
let dirty = false;
|
|
1211
|
+
let changed = false;
|
|
1212
|
+
let disposed = false;
|
|
1059
1213
|
if (options.items) {
|
|
1060
1214
|
registry.registerItems(options.items);
|
|
1061
1215
|
searchEngine.index(options.items);
|
|
1062
|
-
for (const item of options.items)
|
|
1216
|
+
for (const item of options.items) bind(item);
|
|
1063
1217
|
}
|
|
1064
1218
|
if (options.groups) for (const group of options.groups) registry.registerGroup(group);
|
|
1065
1219
|
refilter("");
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1220
|
+
changed = false;
|
|
1221
|
+
function setState(patch) {
|
|
1222
|
+
state = {
|
|
1223
|
+
...state,
|
|
1224
|
+
...patch,
|
|
1225
|
+
lastUpdated: Date.now()
|
|
1226
|
+
};
|
|
1227
|
+
changed = true;
|
|
1228
|
+
}
|
|
1229
|
+
function setOpen(open) {
|
|
1230
|
+
setState({ open });
|
|
1231
|
+
emitter.emit("openChange", open);
|
|
1232
|
+
options.onOpenChange?.(open);
|
|
1069
1233
|
}
|
|
1070
1234
|
function refilter(query) {
|
|
1235
|
+
dirty = false;
|
|
1071
1236
|
const items = registry.getItems();
|
|
1072
1237
|
const disableFilter = options.filter === false;
|
|
1073
1238
|
let filteredIds;
|
|
1074
|
-
if (disableFilter || query === "") filteredIds = items.
|
|
1239
|
+
if (disableFilter || query === "") filteredIds = items.map((i) => i.id);
|
|
1075
1240
|
else {
|
|
1076
1241
|
let results = searchEngine.search(query, items).toArray();
|
|
1077
1242
|
if (frecencyEngine) {
|
|
@@ -1084,68 +1249,76 @@ function createCommandMachine(options = {}) {
|
|
|
1084
1249
|
}
|
|
1085
1250
|
filteredIds = results.map((r) => r.id);
|
|
1086
1251
|
}
|
|
1252
|
+
if (options.disabledItems === "hide") filteredIds = filteredIds.filter(isEnabled);
|
|
1087
1253
|
filteredIdSet = new Set(filteredIds);
|
|
1088
1254
|
filteredIdIndex = new Map(filteredIds.entries().map(([i, id]) => [id, i]));
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const activeId = state.activeId && filteredIdSet.has(state.activeId) ? state.activeId :
|
|
1255
|
+
const priority = (g) => registry.getGroup(g)?.priority ?? 0;
|
|
1256
|
+
const groupedIds = new Map([...mapGroupBy(filteredIds, (id) => registry.getItem(id)?.groupId ?? "__ungrouped")].sort((x, y) => priority(x[0]) - priority(y[0])));
|
|
1257
|
+
navIds = filteredIds.filter(isEnabled);
|
|
1258
|
+
const activeId = state.activeId && filteredIdSet.has(state.activeId) && isEnabled(state.activeId) ? state.activeId : navIds[0] ?? null;
|
|
1093
1259
|
setState({
|
|
1094
|
-
...state,
|
|
1095
1260
|
search: query,
|
|
1096
1261
|
filteredIds,
|
|
1097
1262
|
groupedIds,
|
|
1098
1263
|
filteredCount: filteredIds.length,
|
|
1099
|
-
activeId
|
|
1100
|
-
lastUpdated: Date.now()
|
|
1264
|
+
activeId
|
|
1101
1265
|
});
|
|
1102
1266
|
}
|
|
1267
|
+
function isEnabled(id) {
|
|
1268
|
+
return !registry.getItem(id)?.disabled;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* (Re)binds an item's keyboard shortcut — the one place that does it, for initial items, ITEMS_LOADED,
|
|
1272
|
+
* REGISTER_ITEM and UPDATE_ITEM. The item's bindings on other keys go first; one on the same key is replaced in place, so
|
|
1273
|
+
* it keeps its place in that key's stack (CE-108). A disabled item is not bound at all (its key
|
|
1274
|
+
* stays free for the page and for other items on it); UPDATE_ITEM re-binds, so enabling an item binds it. Under
|
|
1275
|
+
* 'select' the binding dispatches ITEM_SELECT, whose guard checks the item; under 'direct' it only binds an item
|
|
1276
|
+
* with an onSelect and checks the registry entry again before calling it.
|
|
1277
|
+
*/
|
|
1278
|
+
function bind({ id, shortcut, disabled, onSelect }) {
|
|
1279
|
+
const select = options.shortcutAction === "select";
|
|
1280
|
+
const bound = shortcut && !disabled && (select || onSelect) ? shortcut : void 0;
|
|
1281
|
+
keyboardRegistry.unregisterByItem(id, bound);
|
|
1282
|
+
if (!bound) return;
|
|
1283
|
+
const handler = () => {
|
|
1284
|
+
if (select) machine.send({
|
|
1285
|
+
type: "ITEM_SELECT",
|
|
1286
|
+
id
|
|
1287
|
+
});
|
|
1288
|
+
else if (isEnabled(id)) onSelect?.();
|
|
1289
|
+
};
|
|
1290
|
+
keyboardRegistry.register(bound, id, handler);
|
|
1291
|
+
}
|
|
1103
1292
|
function navigate(direction) {
|
|
1104
|
-
const {
|
|
1105
|
-
|
|
1106
|
-
if (
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
if (direction === "last") {
|
|
1119
|
-
const newActiveId = filteredIds[filteredIds.length - 1] ?? null;
|
|
1120
|
-
if (newActiveId !== activeId) {
|
|
1121
|
-
setState({
|
|
1122
|
-
...state,
|
|
1123
|
-
activeId: newActiveId,
|
|
1124
|
-
lastUpdated: Date.now()
|
|
1125
|
-
});
|
|
1126
|
-
options.onActiveChange?.(newActiveId);
|
|
1127
|
-
}
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
const loop = options.loop ?? true;
|
|
1131
|
-
const currentIdx = activeId ? filteredIdIndex.get(activeId) ?? -1 : -1;
|
|
1132
|
-
let newIdx;
|
|
1133
|
-
if (direction === "next") {
|
|
1134
|
-
if (currentIdx === -1 || currentIdx >= filteredIds.length - 1) newIdx = loop ? 0 : filteredIds.length - 1;
|
|
1135
|
-
else newIdx = currentIdx + 1;
|
|
1136
|
-
} else if (currentIdx <= 0) newIdx = loop ? filteredIds.length - 1 : 0;
|
|
1137
|
-
else newIdx = currentIdx - 1;
|
|
1138
|
-
const newActiveId = filteredIds[newIdx] ?? null;
|
|
1293
|
+
const { activeId } = state;
|
|
1294
|
+
const last = navIds.length - 1;
|
|
1295
|
+
if (last < 0) return;
|
|
1296
|
+
const loop = options.loop ?? DEFAULT_MACHINE_OPTIONS.loop;
|
|
1297
|
+
const current = activeId ? navIds.indexOf(activeId) : -1;
|
|
1298
|
+
let index;
|
|
1299
|
+
if (direction === "first") index = 0;
|
|
1300
|
+
else if (direction === "last") index = last;
|
|
1301
|
+
else if (direction === "next") index = current < 0 || current >= last ? loop ? 0 : last : current + 1;
|
|
1302
|
+
else index = current <= 0 ? loop ? last : 0 : current - 1;
|
|
1303
|
+
const newActiveId = navIds[index] ?? null;
|
|
1139
1304
|
if (newActiveId !== activeId) {
|
|
1140
|
-
setState({
|
|
1141
|
-
...state,
|
|
1142
|
-
activeId: newActiveId,
|
|
1143
|
-
lastUpdated: Date.now()
|
|
1144
|
-
});
|
|
1305
|
+
setState({ activeId: newActiveId });
|
|
1145
1306
|
options.onActiveChange?.(newActiveId);
|
|
1146
1307
|
}
|
|
1147
1308
|
}
|
|
1309
|
+
/** Ends a batch: one refilter if a registry event ran, then one stateChange even if the refilter throws. */
|
|
1310
|
+
function commit() {
|
|
1311
|
+
try {
|
|
1312
|
+
if (dirty) refilter(state.search);
|
|
1313
|
+
} finally {
|
|
1314
|
+
if (changed) {
|
|
1315
|
+
changed = false;
|
|
1316
|
+
emitter.emit("stateChange", state);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1148
1320
|
function handleEvent(event) {
|
|
1321
|
+
if (dirty && (event.type === "NAVIGATE" || event.type === "ITEM_ACTIVATE")) refilter(state.search);
|
|
1149
1322
|
switch (event.type) {
|
|
1150
1323
|
case "SEARCH_CHANGE":
|
|
1151
1324
|
refilter(event.query);
|
|
@@ -1162,12 +1335,8 @@ function createCommandMachine(options = {}) {
|
|
|
1162
1335
|
break;
|
|
1163
1336
|
}
|
|
1164
1337
|
case "ITEM_ACTIVATE":
|
|
1165
|
-
if (filteredIdSet.has(event.id)) {
|
|
1166
|
-
setState({
|
|
1167
|
-
...state,
|
|
1168
|
-
activeId: event.id,
|
|
1169
|
-
lastUpdated: Date.now()
|
|
1170
|
-
});
|
|
1338
|
+
if (filteredIdSet.has(event.id) && isEnabled(event.id)) {
|
|
1339
|
+
setState({ activeId: event.id });
|
|
1171
1340
|
options.onActiveChange?.(event.id);
|
|
1172
1341
|
}
|
|
1173
1342
|
break;
|
|
@@ -1176,87 +1345,81 @@ function createCommandMachine(options = {}) {
|
|
|
1176
1345
|
break;
|
|
1177
1346
|
case "PAGE_PUSH":
|
|
1178
1347
|
setState({
|
|
1179
|
-
...state,
|
|
1180
1348
|
page: event.page,
|
|
1181
|
-
pageStack: [...state.pageStack, state.page]
|
|
1182
|
-
lastUpdated: Date.now()
|
|
1349
|
+
pageStack: [...state.pageStack, state.page]
|
|
1183
1350
|
});
|
|
1184
1351
|
break;
|
|
1185
1352
|
case "PAGE_POP": {
|
|
1186
1353
|
const prevPage = state.pageStack.at(-1);
|
|
1187
1354
|
if (prevPage != null) setState({
|
|
1188
|
-
...state,
|
|
1189
1355
|
page: prevPage,
|
|
1190
|
-
pageStack: state.pageStack.toSpliced(-1, 1)
|
|
1191
|
-
lastUpdated: Date.now()
|
|
1356
|
+
pageStack: state.pageStack.toSpliced(-1, 1)
|
|
1192
1357
|
});
|
|
1193
1358
|
break;
|
|
1194
1359
|
}
|
|
1195
1360
|
case "OPEN":
|
|
1196
|
-
|
|
1197
|
-
...state,
|
|
1198
|
-
open: true,
|
|
1199
|
-
lastUpdated: Date.now()
|
|
1200
|
-
});
|
|
1201
|
-
emitter.emit("openChange", true);
|
|
1202
|
-
options.onOpenChange?.(true);
|
|
1361
|
+
setOpen(true);
|
|
1203
1362
|
break;
|
|
1204
1363
|
case "CLOSE":
|
|
1205
|
-
|
|
1206
|
-
...state,
|
|
1207
|
-
open: false,
|
|
1208
|
-
lastUpdated: Date.now()
|
|
1209
|
-
});
|
|
1210
|
-
emitter.emit("openChange", false);
|
|
1211
|
-
options.onOpenChange?.(false);
|
|
1364
|
+
setOpen(false);
|
|
1212
1365
|
break;
|
|
1213
|
-
case "TOGGLE":
|
|
1214
|
-
|
|
1215
|
-
setState({
|
|
1216
|
-
...state,
|
|
1217
|
-
open: newOpen,
|
|
1218
|
-
lastUpdated: Date.now()
|
|
1219
|
-
});
|
|
1220
|
-
emitter.emit("openChange", newOpen);
|
|
1221
|
-
options.onOpenChange?.(newOpen);
|
|
1366
|
+
case "TOGGLE":
|
|
1367
|
+
setOpen(!state.open);
|
|
1222
1368
|
break;
|
|
1223
|
-
}
|
|
1224
1369
|
case "ITEMS_LOADED":
|
|
1225
1370
|
registry.registerItems(event.items);
|
|
1226
1371
|
searchEngine.index(event.items);
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1372
|
+
for (const item of event.items) bind(item);
|
|
1373
|
+
dirty = true;
|
|
1374
|
+
break;
|
|
1375
|
+
case "LOADING": {
|
|
1376
|
+
if (event.pending) pendingSources.add(event.source);
|
|
1377
|
+
else pendingSources.delete(event.source);
|
|
1378
|
+
const loading = pendingSources.size > 0;
|
|
1379
|
+
if (loading !== state.loading) setState({ loading });
|
|
1232
1380
|
break;
|
|
1381
|
+
}
|
|
1233
1382
|
case "REGISTER_ITEM":
|
|
1383
|
+
case "UPDATE_ITEM":
|
|
1234
1384
|
registry.registerItem(event.item);
|
|
1235
1385
|
searchEngine.index([event.item]);
|
|
1236
|
-
|
|
1237
|
-
|
|
1386
|
+
bind(event.item);
|
|
1387
|
+
dirty = true;
|
|
1238
1388
|
break;
|
|
1239
1389
|
case "UNREGISTER_ITEM":
|
|
1240
1390
|
registry.unregisterItem(event.id);
|
|
1241
1391
|
searchEngine.remove(/* @__PURE__ */ new Set([event.id]));
|
|
1242
1392
|
keyboardRegistry.unregisterByItem(event.id);
|
|
1243
|
-
|
|
1393
|
+
dirty = true;
|
|
1244
1394
|
break;
|
|
1245
1395
|
case "REGISTER_GROUP":
|
|
1246
1396
|
registry.registerGroup(event.group);
|
|
1247
|
-
|
|
1397
|
+
dirty = true;
|
|
1248
1398
|
break;
|
|
1249
1399
|
case "UNREGISTER_GROUP":
|
|
1250
1400
|
registry.unregisterGroup(event.id);
|
|
1251
|
-
|
|
1401
|
+
dirty = true;
|
|
1252
1402
|
}
|
|
1253
1403
|
}
|
|
1254
|
-
|
|
1404
|
+
const machine = {
|
|
1255
1405
|
getState() {
|
|
1256
1406
|
return state;
|
|
1257
1407
|
},
|
|
1258
1408
|
send(event) {
|
|
1259
|
-
|
|
1409
|
+
queued++;
|
|
1410
|
+
if (inHandler) nextQueued++;
|
|
1411
|
+
scheduler.schedule(() => {
|
|
1412
|
+
inHandler = true;
|
|
1413
|
+
try {
|
|
1414
|
+
handleEvent(event);
|
|
1415
|
+
} finally {
|
|
1416
|
+
inHandler = false;
|
|
1417
|
+
if (--queued === nextQueued) {
|
|
1418
|
+
nextQueued = 0;
|
|
1419
|
+
commit();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1260
1423
|
},
|
|
1261
1424
|
/** useSyncExternalStore-compatible subscribe — returns cleanup function */
|
|
1262
1425
|
subscribe(listener) {
|
|
@@ -1280,14 +1443,25 @@ function createCommandMachine(options = {}) {
|
|
|
1280
1443
|
return filteredIdIndex;
|
|
1281
1444
|
},
|
|
1282
1445
|
[Symbol.dispose]() {
|
|
1446
|
+
disposed = true;
|
|
1447
|
+
queued = nextQueued = 0;
|
|
1283
1448
|
scheduler[Symbol.dispose]();
|
|
1284
1449
|
registry[Symbol.dispose]();
|
|
1285
1450
|
searchEngine[Symbol.dispose]();
|
|
1286
1451
|
emitter[Symbol.dispose]();
|
|
1287
1452
|
keyboardRegistry[Symbol.dispose]();
|
|
1288
1453
|
frecencyEngine?.[Symbol.dispose]();
|
|
1454
|
+
},
|
|
1455
|
+
/** Awaits the pending frecency save, then disposes like [Symbol.dispose] — for `await using` */
|
|
1456
|
+
async [Symbol.asyncDispose]() {
|
|
1457
|
+
try {
|
|
1458
|
+
await frecencyEngine?.flush();
|
|
1459
|
+
} finally {
|
|
1460
|
+
machine[Symbol.dispose]();
|
|
1461
|
+
}
|
|
1289
1462
|
}
|
|
1290
1463
|
};
|
|
1464
|
+
return machine;
|
|
1291
1465
|
}
|
|
1292
1466
|
// src/core/search/fuzzy-scorer.ts
|
|
1293
1467
|
/**
|
|
@@ -1346,4 +1520,4 @@ function createTelemetryMiddleware(hooks) {
|
|
|
1346
1520
|
}
|
|
1347
1521
|
};
|
|
1348
1522
|
}
|
|
1349
|
-
export { CommandRegistry, DEFAULT_FRECENCY_DECAY, DEFAULT_MACHINE_OPTIONS, FrecencyEngine, IdbFrecencyStorage, KeyboardShortcutRegistry, MemoryFrecencyStorage, TypedEmitter, batchScoreItems, computeFrecencyBonus, createCommandMachine, createInitialState, createScheduler, createSearchEngine, createTelemetryMiddleware, detectConflicts, findMatchingShortcut, formatShortcut, groupId, itemId, matchesShortcut, parseShortcut, scoreItem, scoreItemAsync };
|
|
1523
|
+
export { CommandRegistry, DEFAULT_FRECENCY_DECAY, DEFAULT_MACHINE_OPTIONS, FrecencyEngine, IdbFrecencyStorage, KeyboardShortcutRegistry, MemoryFrecencyStorage, TypedEmitter, UNGROUPED_GROUP_ID, batchScoreItems, computeFrecencyBonus, createCommandMachine, createInitialState, createScheduler, createSearchEngine, createTelemetryMiddleware, detectConflicts, findMatchingShortcut, formatShortcut, groupId, itemId, matchesShortcut, parseShortcut, scoreItem, scoreItemAsync };
|