@opentui/keymap 0.0.0-20260423-618ea9b1 → 0.1.106

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/index.js CHANGED
@@ -18,41 +18,49 @@ function forEachActivationTarget(host, focused, visit) {
18
18
  isFocusedTarget = false;
19
19
  }
20
20
  }
21
- function getActiveLayersForFocused(targetLayers, host, focused) {
21
+ function getActivationPath(host, focused) {
22
+ const path = new Set;
23
+ forEachActivationTarget(host, focused, (current) => {
24
+ path.add(current);
25
+ });
26
+ return path;
27
+ }
28
+ function getActiveLayersForFocused(state, host, focused) {
29
+ if (state.activeLayersCacheVersion === state.activeLayersVersion && state.activeLayersCacheFocused === focused) {
30
+ return state.activeLayersCache;
31
+ }
22
32
  const activeLayers = [];
23
- forEachActivationTarget(host, focused, (current, isFocusedTarget) => {
24
- const bucket = targetLayers.get(current);
25
- if (!bucket) {
26
- return;
27
- }
28
- if (isFocusedTarget) {
29
- activeLayers.push(...bucket.focusLayers);
33
+ const activationPath = getActivationPath(host, focused);
34
+ for (const layer of state.sortedLayers) {
35
+ if (isLayerActiveForFocused(host, layer, focused, activationPath)) {
36
+ activeLayers.push(layer);
30
37
  }
31
- activeLayers.push(...bucket.focusWithinLayers);
32
- });
38
+ }
39
+ state.activeLayersCacheVersion = state.activeLayersVersion;
40
+ state.activeLayersCacheFocused = focused;
41
+ state.activeLayersCache = activeLayers;
33
42
  return activeLayers;
34
43
  }
35
- function isLayerActiveForFocused(host, layer, focused) {
36
- const target = layer.indexTarget;
44
+ function invalidateCachedActiveLayers(state) {
45
+ state.activeLayersCacheVersion = -1;
46
+ state.activeLayersCacheFocused = undefined;
47
+ state.activeLayersCache = [];
48
+ }
49
+ function isLayerActiveForFocused(host, layer, focused, activationPath = getActivationPath(host, focused)) {
50
+ const target = layer.target;
51
+ if (!target) {
52
+ return true;
53
+ }
37
54
  if (host.isTargetDestroyed(target)) {
38
55
  return false;
39
56
  }
40
- if (layer.scope === "focus") {
57
+ if (layer.targetMode === "focus") {
41
58
  return target === focused;
42
59
  }
43
- let isActive = false;
44
- forEachActivationTarget(host, focused, (current) => {
45
- if (current === target) {
46
- isActive = true;
47
- return false;
48
- }
49
- return true;
50
- });
51
- return isActive;
60
+ return activationPath.has(target);
52
61
  }
53
62
 
54
63
  // src/services/keys.ts
55
- var keyMatches = new Map;
56
64
  function normalizeBindingTokenName(token) {
57
65
  const normalized = token.trim().toLowerCase();
58
66
  if (!normalized) {
@@ -117,14 +125,14 @@ function resolveKeyMatch(input) {
117
125
  return createKeyMatch(input);
118
126
  }
119
127
  function createKeyMatch(input) {
120
- return getOrCreateKeyMatch(buildKeyMatchId(normalizeKeyStroke(input)));
128
+ return `key:${buildKeyMatchId(normalizeKeyStroke(input))}`;
121
129
  }
122
130
  function createTextKeyMatch(id) {
123
131
  const normalized = id.trim();
124
132
  if (!normalized) {
125
133
  throw new Error("Invalid keymap match id: id cannot be empty");
126
134
  }
127
- return getOrCreateKeyMatch(`text:${normalized}`);
135
+ return `text:${normalized}`;
128
136
  }
129
137
  function stringifyKeyStroke(input, options) {
130
138
  if ("stroke" in input) {
@@ -161,15 +169,6 @@ function stringifyCanonicalStroke(stroke) {
161
169
  function buildKeyMatchId(stroke) {
162
170
  return `${stroke.name}:${stroke.ctrl ? 1 : 0}:${stroke.shift ? 1 : 0}:${stroke.meta ? 1 : 0}:${stroke.super ? 1 : 0}:${stroke.hyper ? 1 : 0}`;
163
171
  }
164
- function getOrCreateKeyMatch(id) {
165
- const existing = keyMatches.get(id);
166
- if (existing) {
167
- return existing;
168
- }
169
- const match = Symbol(id);
170
- keyMatches.set(id, match);
171
- return match;
172
- }
173
172
 
174
173
  // src/services/activation.ts
175
174
  function getLiveHost(host) {
@@ -205,13 +204,15 @@ class ActivationService {
205
204
  notify;
206
205
  conditions;
207
206
  catalog;
208
- constructor(state, host, hooks, notify, conditions, catalog) {
207
+ options;
208
+ constructor(state, host, hooks, notify, conditions, catalog, options = {}) {
209
209
  this.state = state;
210
210
  this.host = host;
211
211
  this.hooks = hooks;
212
212
  this.notify = notify;
213
213
  this.conditions = conditions;
214
214
  this.catalog = catalog;
215
+ this.options = options;
215
216
  }
216
217
  getFocusedTarget() {
217
218
  return getLiveHost(this.host).getFocusedTarget();
@@ -220,10 +221,12 @@ class ActivationService {
220
221
  return getFocusedTargetIfAvailable(this.host);
221
222
  }
222
223
  setPendingSequence(next) {
223
- if (isSamePendingSequence(this.state.projection.pendingSequence, next)) {
224
+ const previous = this.state.projection.pendingSequence;
225
+ if (isSamePendingSequence(previous, next)) {
224
226
  return;
225
227
  }
226
228
  this.state.projection.pendingSequence = next;
229
+ this.options.onPendingSequenceChanged?.(previous, next);
227
230
  this.invalidateCaches();
228
231
  this.notifyPendingSequenceChange();
229
232
  this.notify.queueStateChange();
@@ -246,6 +249,15 @@ class ActivationService {
246
249
  }
247
250
  return this.state.projection.pendingSequence ?? undefined;
248
251
  }
252
+ revalidatePendingSequenceIfNeeded() {
253
+ if (this.host.isDestroyed || !this.state.projection.pendingSequence) {
254
+ return;
255
+ }
256
+ this.ensureValidPendingSequence();
257
+ }
258
+ hasPendingSequenceState() {
259
+ return !this.host.isDestroyed && this.state.projection.pendingSequence !== null;
260
+ }
249
261
  getPendingSequence() {
250
262
  const projections = this.state.projection;
251
263
  const derivedStateVersion = this.state.notify.derivedStateVersion;
@@ -335,11 +347,24 @@ class ActivationService {
335
347
  }
336
348
  return activeKeys;
337
349
  }
350
+ getActiveKeysForCaptures(captures, options) {
351
+ const includeBindings = options?.includeBindings === true;
352
+ const includeMetadata = options?.includeMetadata === true;
353
+ const focused = this.getFocusedTarget();
354
+ const activeView = this.catalog.getActiveCommandView(focused);
355
+ return this.collectActiveKeysFromPending(captures, includeBindings, includeMetadata, focused, activeView);
356
+ }
338
357
  nodeHasReachableBindings(node, focused) {
339
358
  return this.hasMatchingBindings(node.reachableBindings, focused, this.catalog.getActiveCommandView(focused));
340
359
  }
341
360
  getActiveLayers(focused) {
342
- return getActiveLayersForFocused(this.state.layers.targetLayers, this.host, focused);
361
+ return getActiveLayersForFocused(this.state.layers, this.host, focused);
362
+ }
363
+ refreshActiveLayers(focused = this.getFocusedTargetIfAvailable()) {
364
+ getActiveLayersForFocused(this.state.layers, this.host, focused);
365
+ }
366
+ invalidateActiveLayers() {
367
+ invalidateCachedActiveLayers(this.state.layers);
343
368
  }
344
369
  isLayerActiveForFocused(layer, focused) {
345
370
  return isLayerActiveForFocused(this.host, layer, focused);
@@ -415,7 +440,7 @@ class ActivationService {
415
440
  }
416
441
  return parts;
417
442
  }
418
- getMatchingBindings(bindings, focused, activeView) {
443
+ collectMatchingBindings(bindings, focused, activeView) {
419
444
  const matches = [];
420
445
  for (const binding of bindings) {
421
446
  if (this.conditions.matchesConditions(binding) && this.catalog.isBindingVisible(binding, focused, activeView)) {
@@ -432,7 +457,7 @@ class ActivationService {
432
457
  }
433
458
  return false;
434
459
  }
435
- getNodePresentation(node, focused, activeView, reachableBindings = this.getMatchingBindings(node.reachableBindings, focused, activeView)) {
460
+ getNodePresentation(node, focused, activeView, reachableBindings = this.collectMatchingBindings(node.reachableBindings, focused, activeView)) {
436
461
  if (!node.stroke) {
437
462
  return { display: "" };
438
463
  }
@@ -561,16 +586,17 @@ class ActivationService {
561
586
  if (!node.stroke) {
562
587
  return;
563
588
  }
564
- const reachableBindings = this.getMatchingBindings(node.reachableBindings, focused, activeView);
589
+ const reachableBindings = this.collectMatchingBindings(node.reachableBindings, focused, activeView);
565
590
  if (reachableBindings.length === 0) {
566
591
  return;
567
592
  }
568
- const prefixBindings = this.getMatchingBindings(node.bindings, focused, activeView);
593
+ const prefixBindings = this.selectActiveBindings(node.bindings, focused, activeView);
569
594
  return {
570
595
  ...this.getNodePresentation(node, focused, activeView, reachableBindings),
571
596
  continues: true,
572
- firstBinding: prefixBindings[0],
573
- bindings: includeBindings && prefixBindings.length > 0 ? prefixBindings : undefined,
597
+ firstBinding: prefixBindings?.bindings[0],
598
+ commandBinding: prefixBindings?.commandBinding,
599
+ bindings: includeBindings && prefixBindings && prefixBindings.bindings.length > 0 ? [...prefixBindings.bindings] : undefined,
574
600
  stop: true
575
601
  };
576
602
  }
@@ -701,7 +727,7 @@ class ActivationService {
701
727
  // src/schema.ts
702
728
  var RESERVED_COMMAND_FIELDS = new Set(["name", "run"]);
703
729
  var RESERVED_BINDING_FIELDS = new Set(["key", "cmd", "event", "preventDefault", "fallthrough"]);
704
- var RESERVED_LAYER_FIELDS = new Set(["target", "scope", "priority", "bindings", "commands"]);
730
+ var RESERVED_LAYER_FIELDS = new Set(["target", "targetMode", "priority", "bindings", "commands"]);
705
731
 
706
732
  // src/services/primitives/field-invariants.ts
707
733
  function mergeRequirement(target, name, value, source) {
@@ -770,6 +796,16 @@ var SNAPSHOT_FROZEN_COMMAND_METADATA_OPTIONS = Object.freeze({
770
796
  preserveNonPlainObjects: true
771
797
  });
772
798
  var EMPTY_COMMAND_FIELDS = Object.freeze({});
799
+ function createCommandChainCacheState() {
800
+ return {
801
+ resolvedWithoutRecordChains: new Map,
802
+ resolvedWithRecordChains: new Map,
803
+ fallbackWithoutRecord: new Map,
804
+ fallbackWithRecord: new Map,
805
+ fallbackWithoutRecordErrors: new Set,
806
+ fallbackWithRecordErrors: new Set
807
+ };
808
+ }
773
809
  function normalizeBindingCommand(command) {
774
810
  if (command === undefined || typeof command === "function") {
775
811
  return command;
@@ -864,10 +900,41 @@ class CommandCatalogService {
864
900
  }
865
901
  getResolvedCommandChain(command, focused, includeRecord) {
866
902
  const view = this.getActiveCommandView(focused);
867
- const entries = this.getResolvedCommandChainFromView(view, command, focused, includeRecord);
903
+ const entries = this.getResolvedCommandChainFromView(view, command, focused, includeRecord, "active", view.chainsByName.get(command));
868
904
  const hadError = (includeRecord ? view.fallbackWithRecordErrors : view.fallbackWithoutRecordErrors).has(command);
869
905
  return { entries, hadError };
870
906
  }
907
+ getRegisteredResolvedEntries(command, includeRecord) {
908
+ const view = this.getRegisteredCommandView();
909
+ const cache = includeRecord ? view.resolvedWithRecordChains : view.resolvedWithoutRecordChains;
910
+ const cached = cache.get(command);
911
+ if (cached) {
912
+ return cached.length > 0 ? cached : undefined;
913
+ }
914
+ const chain = view.chainsByName.get(command);
915
+ if (!chain || chain.length === 0) {
916
+ cache.set(command, []);
917
+ return;
918
+ }
919
+ const resolved = [];
920
+ for (const entry of chain) {
921
+ resolved.push({
922
+ target: entry.layer.target,
923
+ resolved: resolveRegisteredCommand(entry.command, { includeRecord })
924
+ });
925
+ }
926
+ cache.set(command, resolved);
927
+ return resolved;
928
+ }
929
+ getRegisteredResolverFallback(command, includeRecord) {
930
+ const view = this.getRegisteredCommandView();
931
+ const fallback = this.getFallbackResolvedCommand(view, command, null, includeRecord, "registered");
932
+ const hadError = (includeRecord ? view.fallbackWithRecordErrors : view.fallbackWithoutRecordErrors).has(command);
933
+ return {
934
+ resolved: fallback?.resolved,
935
+ hadError
936
+ };
937
+ }
871
938
  getCommandAttrs(command, focused) {
872
939
  const top = this.getTopResolvedCommand(command, focused, false);
873
940
  return top?.resolved.attrs;
@@ -876,6 +943,36 @@ class CommandCatalogService {
876
943
  const top = this.getTopResolvedCommand(command, focused, true);
877
944
  return top?.resolved.record;
878
945
  }
946
+ getTopRegisteredCommandRecord(command) {
947
+ const top = this.getTopRegisteredCommand(command);
948
+ return top ? getRegisteredCommandRecord(top.command) : undefined;
949
+ }
950
+ getDispatchUnavailableCommandState(command, focused, includeRecord) {
951
+ const view = this.getRegisteredCommandView();
952
+ const chain = view.chainsByName.get(command);
953
+ if (!chain || chain.length === 0) {
954
+ return;
955
+ }
956
+ let inactiveEntry;
957
+ let disabledEntry;
958
+ for (const entry of chain) {
959
+ if (!isLayerActiveForFocused(this.host, entry.layer, focused)) {
960
+ inactiveEntry ??= entry;
961
+ continue;
962
+ }
963
+ if (!this.conditions.layerMatchesRuntimeState(entry.layer) || !this.conditions.matchesConditions(entry.command)) {
964
+ disabledEntry ??= entry;
965
+ }
966
+ }
967
+ const unavailableEntry = disabledEntry ?? inactiveEntry;
968
+ if (!unavailableEntry) {
969
+ return;
970
+ }
971
+ return {
972
+ reason: disabledEntry ? "disabled" : "inactive",
973
+ command: includeRecord ? getRegisteredCommandRecord(unavailableEntry.command) : undefined
974
+ };
975
+ }
879
976
  getActiveCommandView(focused) {
880
977
  const currentFocused = getFocusedTargetIfAvailable(this.host);
881
978
  const derivedStateVersion = this.state.notify.derivedStateVersion;
@@ -888,7 +985,7 @@ class CommandCatalogService {
888
985
  const chainsByName = new Map;
889
986
  let cacheable = true;
890
987
  if (this.state.layers.layersWithCommands > 0) {
891
- for (const layer of getActiveLayersForFocused(this.state.layers.targetLayers, this.host, focused)) {
988
+ for (const layer of getActiveLayersForFocused(this.state.layers, this.host, focused)) {
892
989
  if (layer.commands.length === 0 || !this.conditions.layerMatchesRuntimeState(layer)) {
893
990
  continue;
894
991
  }
@@ -923,12 +1020,7 @@ class CommandCatalogService {
923
1020
  reachable,
924
1021
  reachableByName,
925
1022
  chainsByName,
926
- resolvedWithoutRecordChains: new Map,
927
- resolvedWithRecordChains: new Map,
928
- fallbackWithoutRecord: new Map,
929
- fallbackWithRecord: new Map,
930
- fallbackWithoutRecordErrors: new Set,
931
- fallbackWithRecordErrors: new Set
1023
+ ...createCommandChainCacheState()
932
1024
  };
933
1025
  if (focused === currentFocused && view.cacheable) {
934
1026
  this.state.commands.activeCommandViewVersion = derivedStateVersion;
@@ -936,6 +1028,37 @@ class CommandCatalogService {
936
1028
  }
937
1029
  return view;
938
1030
  }
1031
+ getRegisteredCommandView() {
1032
+ const cacheVersion = this.state.commands.commandMetadataVersion;
1033
+ if (this.state.commands.registeredCommandViewVersion === cacheVersion && this.state.commands.registeredCommandView) {
1034
+ return this.state.commands.registeredCommandView;
1035
+ }
1036
+ const entries = [];
1037
+ const chainsByName = new Map;
1038
+ for (const layer of this.state.layers.sortedLayers) {
1039
+ if (layer.commands.length === 0) {
1040
+ continue;
1041
+ }
1042
+ for (const command of layer.commands) {
1043
+ const entry = { layer, command };
1044
+ entries.push(entry);
1045
+ const existing = chainsByName.get(command.name);
1046
+ if (existing) {
1047
+ existing.push(entry);
1048
+ } else {
1049
+ chainsByName.set(command.name, [entry]);
1050
+ }
1051
+ }
1052
+ }
1053
+ const view = {
1054
+ entries,
1055
+ chainsByName,
1056
+ ...createCommandChainCacheState()
1057
+ };
1058
+ this.state.commands.registeredCommandViewVersion = cacheVersion;
1059
+ this.state.commands.registeredCommandView = view;
1060
+ return view;
1061
+ }
939
1062
  isBindingVisible(binding, focused, activeView) {
940
1063
  if (binding.command === undefined || binding.run) {
941
1064
  return true;
@@ -946,7 +1069,7 @@ class CommandCatalogService {
946
1069
  if (activeView.reachableByName.has(binding.command)) {
947
1070
  return true;
948
1071
  }
949
- return this.getFallbackResolvedCommand(activeView, binding.command, focused, false) !== undefined;
1072
+ return this.getFallbackResolvedCommand(activeView, binding.command, focused, false, "active") !== undefined;
950
1073
  }
951
1074
  getBindingCommandAttrs(binding, focused, activeView) {
952
1075
  if (typeof binding.command !== "string") {
@@ -956,7 +1079,7 @@ class CommandCatalogService {
956
1079
  if (active) {
957
1080
  return active.command.attrs;
958
1081
  }
959
- const fallback = this.getFallbackResolvedCommand(activeView, binding.command, focused, false);
1082
+ const fallback = this.getFallbackResolvedCommand(activeView, binding.command, focused, false, "active");
960
1083
  return fallback?.resolved.attrs;
961
1084
  }
962
1085
  getCommandResolutionStatus(command, layerCommands) {
@@ -997,16 +1120,20 @@ class CommandCatalogService {
997
1120
  resolved: resolveRegisteredCommand(active.command, { includeRecord })
998
1121
  };
999
1122
  }
1000
- return this.getFallbackResolvedCommand(activeView, command, focused, includeRecord);
1123
+ return this.getFallbackResolvedCommand(activeView, command, focused, includeRecord, "active");
1124
+ }
1125
+ getTopRegisteredCommand(command) {
1126
+ const view = this.getRegisteredCommandView();
1127
+ return view.chainsByName.get(command)?.[0];
1001
1128
  }
1002
- getFallbackResolvedCommand(activeView, command, focused, includeRecord) {
1003
- const cache = includeRecord ? activeView.fallbackWithRecord : activeView.fallbackWithoutRecord;
1004
- const errorCache = includeRecord ? activeView.fallbackWithRecordErrors : activeView.fallbackWithoutRecordErrors;
1129
+ getFallbackResolvedCommand(view, command, focused, includeRecord, mode) {
1130
+ const cache = includeRecord ? view.fallbackWithRecord : view.fallbackWithoutRecord;
1131
+ const errorCache = includeRecord ? view.fallbackWithRecordErrors : view.fallbackWithoutRecordErrors;
1005
1132
  if (cache.has(command)) {
1006
1133
  const cached = cache.get(command);
1007
1134
  return cached ? { resolved: cached } : undefined;
1008
1135
  }
1009
- const lookup = this.resolveCommandWithResolvers(command, focused, { includeRecord });
1136
+ const lookup = this.resolveCommandWithResolvers(command, focused, { includeRecord, mode });
1010
1137
  cache.set(command, lookup.resolved ?? null);
1011
1138
  if (lookup.hadError) {
1012
1139
  errorCache.add(command);
@@ -1016,23 +1143,23 @@ class CommandCatalogService {
1016
1143
  }
1017
1144
  return { resolved: lookup.resolved };
1018
1145
  }
1019
- getResolvedCommandChainFromView(activeView, command, focused, includeRecord) {
1020
- const cache = includeRecord ? activeView.resolvedWithRecordChains : activeView.resolvedWithoutRecordChains;
1146
+ getResolvedCommandChainFromView(view, command, focused, includeRecord, mode, activeChain) {
1147
+ const cache = includeRecord ? view.resolvedWithRecordChains : view.resolvedWithoutRecordChains;
1021
1148
  const cached = cache.get(command);
1022
1149
  if (cached) {
1023
1150
  return cached.length > 0 ? cached : undefined;
1024
1151
  }
1025
1152
  const resolved = [];
1026
- const activeChain = activeView.chainsByName.get(command);
1027
- if (activeChain) {
1028
- for (const entry of activeChain) {
1153
+ const chain = activeChain;
1154
+ if (chain) {
1155
+ for (const entry of chain) {
1029
1156
  resolved.push({
1030
1157
  target: entry.layer.target,
1031
1158
  resolved: resolveRegisteredCommand(entry.command, { includeRecord })
1032
1159
  });
1033
1160
  }
1034
1161
  }
1035
- const fallback = this.getFallbackResolvedCommand(activeView, command, focused, includeRecord);
1162
+ const fallback = this.getFallbackResolvedCommand(view, command, focused, includeRecord, mode);
1036
1163
  if (fallback) {
1037
1164
  resolved.push(fallback);
1038
1165
  }
@@ -1101,7 +1228,7 @@ class CommandCatalogService {
1101
1228
  if (!activeView) {
1102
1229
  return;
1103
1230
  }
1104
- for (const layer of getActiveLayersForFocused(this.state.layers.targetLayers, this.host, context.focused)) {
1231
+ for (const layer of getActiveLayersForFocused(this.state.layers, this.host, context.focused)) {
1105
1232
  if (layer.compiledBindings.length === 0 || !this.conditions.layerMatchesRuntimeState(layer)) {
1106
1233
  continue;
1107
1234
  }
@@ -1139,20 +1266,26 @@ class CommandCatalogService {
1139
1266
  }
1140
1267
  resolveCommandWithResolvers(command, focused, options) {
1141
1268
  const includeRecord = options?.includeRecord === true;
1142
- const context = this.createCommandResolverContext(includeRecord, focused);
1269
+ const context = this.createCommandResolverContext(includeRecord, focused, options?.mode ?? "active");
1143
1270
  return resolveCommandWithResolvers(command, this.state.commands.commandResolvers.values(), context, (error) => {
1144
1271
  this.notify.emitError("command-resolver-error", error, `[Keymap] Error in command resolver for "${command}":`);
1145
1272
  });
1146
1273
  }
1147
- createCommandResolverContext(includeRecord, focused) {
1274
+ createCommandResolverContext(includeRecord, focused, mode) {
1148
1275
  return {
1149
1276
  getCommandAttrs: (name) => {
1277
+ if (mode === "registered") {
1278
+ return this.getTopRegisteredCommand(name)?.command.attrs;
1279
+ }
1150
1280
  return this.getCommandAttrs(name, focused);
1151
1281
  },
1152
1282
  getCommandRecord: (name) => {
1153
1283
  if (!includeRecord) {
1154
1284
  return;
1155
1285
  }
1286
+ if (mode === "registered") {
1287
+ return this.getTopRegisteredCommandRecord(name);
1288
+ }
1156
1289
  return this.getTopCommandRecord(name, focused);
1157
1290
  }
1158
1291
  };
@@ -1523,6 +1656,72 @@ class CommandExecutorService {
1523
1656
  this.options = options;
1524
1657
  }
1525
1658
  runCommand(cmd, options) {
1659
+ let normalized;
1660
+ try {
1661
+ normalized = normalizeBindingCommand(cmd);
1662
+ } catch {
1663
+ return { ok: false, reason: "invalid-args" };
1664
+ }
1665
+ if (typeof normalized !== "string") {
1666
+ return { ok: false, reason: "not-found" };
1667
+ }
1668
+ const includeRecord = options?.includeCommand === true;
1669
+ const focused = options?.focused ?? this.activation.getFocusedTargetIfAvailable();
1670
+ const event = options?.event ?? this.options.createCommandEvent();
1671
+ const data = this.runtime.getReadonlyData();
1672
+ const chain = this.catalog.getRegisteredResolvedEntries(normalized, includeRecord);
1673
+ let rejectedResult;
1674
+ if (chain?.length === 1) {
1675
+ const [entry] = chain;
1676
+ if (entry) {
1677
+ const execution = this.executeResolvedCommand(normalized, entry.resolved, {
1678
+ keymap: this.options.keymap,
1679
+ event,
1680
+ focused,
1681
+ target: options?.target ?? entry.target ?? null,
1682
+ data
1683
+ });
1684
+ if (execution.status === "handled" || execution.status === "error") {
1685
+ return execution.result;
1686
+ }
1687
+ rejectedResult = execution.result;
1688
+ }
1689
+ } else if (chain) {
1690
+ for (const entry of chain) {
1691
+ const context = {
1692
+ keymap: this.options.keymap,
1693
+ event,
1694
+ focused,
1695
+ target: options?.target ?? entry.target ?? null,
1696
+ data
1697
+ };
1698
+ const execution = this.executeResolvedCommand(normalized, entry.resolved, context);
1699
+ if (execution.status === "handled" || execution.status === "error") {
1700
+ return execution.result;
1701
+ }
1702
+ rejectedResult = execution.result;
1703
+ }
1704
+ }
1705
+ const fallback = this.catalog.getRegisteredResolverFallback(normalized, includeRecord);
1706
+ if (fallback.resolved) {
1707
+ const execution = this.executeResolvedCommand(normalized, fallback.resolved, {
1708
+ keymap: this.options.keymap,
1709
+ event,
1710
+ focused,
1711
+ target: options?.target ?? null,
1712
+ data
1713
+ });
1714
+ if (execution.status === "handled" || execution.status === "error") {
1715
+ return execution.result;
1716
+ }
1717
+ rejectedResult = execution.result;
1718
+ }
1719
+ if (fallback.hadError) {
1720
+ return { ok: false, reason: "error" };
1721
+ }
1722
+ return rejectedResult ?? { ok: false, reason: "not-found" };
1723
+ }
1724
+ dispatchCommand(cmd, options) {
1526
1725
  let normalized;
1527
1726
  try {
1528
1727
  normalized = normalizeBindingCommand(cmd);
@@ -1539,7 +1738,22 @@ class CommandExecutorService {
1539
1738
  const chainLookup = this.catalog.getResolvedCommandChain(normalized, focused, includeRecord);
1540
1739
  const chain = chainLookup.entries;
1541
1740
  let rejectedResult;
1542
- if (chain) {
1741
+ if (chain?.length === 1) {
1742
+ const [entry] = chain;
1743
+ if (entry) {
1744
+ const execution = this.executeResolvedCommand(normalized, entry.resolved, {
1745
+ keymap: this.options.keymap,
1746
+ event,
1747
+ focused,
1748
+ target: options?.target ?? entry.target ?? null,
1749
+ data
1750
+ });
1751
+ if (execution.status === "handled" || execution.status === "error") {
1752
+ return execution.result;
1753
+ }
1754
+ rejectedResult = execution.result;
1755
+ }
1756
+ } else if (chain) {
1543
1757
  for (const entry of chain) {
1544
1758
  const context = {
1545
1759
  keymap: this.options.keymap,
@@ -1558,6 +1772,10 @@ class CommandExecutorService {
1558
1772
  if (chainLookup.hadError) {
1559
1773
  return { ok: false, reason: "error" };
1560
1774
  }
1775
+ const unavailable = this.catalog.getDispatchUnavailableCommandState(normalized, focused, includeRecord);
1776
+ if (unavailable) {
1777
+ return unavailable.command ? { ok: false, reason: unavailable.reason, command: unavailable.command } : { ok: false, reason: unavailable.reason };
1778
+ }
1561
1779
  return rejectedResult ?? { ok: false, reason: "not-found" };
1562
1780
  }
1563
1781
  runBinding(bindingLayer, binding, event, focused) {
@@ -1580,7 +1798,23 @@ class CommandExecutorService {
1580
1798
  return false;
1581
1799
  }
1582
1800
  const chain = this.catalog.getResolvedCommandChain(binding.command, focused, false).entries;
1583
- if (chain) {
1801
+ if (chain?.length === 1) {
1802
+ const [entry] = chain;
1803
+ if (entry) {
1804
+ const execution = this.executeResolvedCommand(binding.command, entry.resolved, {
1805
+ keymap: this.options.keymap,
1806
+ event,
1807
+ focused,
1808
+ target: entry.target ?? bindingLayer.target ?? null,
1809
+ data
1810
+ });
1811
+ if (execution.status === "rejected") {
1812
+ return false;
1813
+ }
1814
+ applyBindingEventEffects(binding, event);
1815
+ return true;
1816
+ }
1817
+ } else if (chain) {
1584
1818
  for (const entry of chain) {
1585
1819
  const context = {
1586
1820
  keymap: this.options.keymap,
@@ -1714,13 +1948,14 @@ class CompilerService {
1714
1948
  parseObjectKey: (value, options) => this.parseObjectKeyPart(value, options)
1715
1949
  });
1716
1950
  }
1717
- compileBindings(bindings, tokens, sourceScope, sourceTarget, sourceLayerOrder, compileFields) {
1951
+ compileBindings(bindings, tokens, sourceTarget, sourceLayerOrder, compileFields) {
1718
1952
  const root = createSequenceNode(null, null, null);
1719
1953
  const compiledBindings = [];
1720
1954
  let hasTokenBindings = false;
1721
1955
  const bindingExpanders = this.state.environment.bindingExpanders.values();
1722
1956
  const bindingParsers = this.state.environment.bindingParsers.values();
1723
1957
  const bindingFieldCompilers = this.state.environment.bindingFields;
1958
+ const allowExactPrefixAmbiguity = this.state.dispatch.disambiguationResolvers.has();
1724
1959
  const warnUnknownField = this.options.warnUnknownField;
1725
1960
  const warnUnknownToken = this.options.warnUnknownToken;
1726
1961
  const conditions = this.conditions;
@@ -1817,7 +2052,6 @@ class CompilerService {
1817
2052
  command,
1818
2053
  event,
1819
2054
  sourceBinding: snapshotParsedBindingInput(compiledInput),
1820
- sourceScope,
1821
2055
  sourceTarget,
1822
2056
  sourceLayerOrder,
1823
2057
  sourceBindingIndex: bindingIndex,
@@ -1842,7 +2076,7 @@ class CompilerService {
1842
2076
  throw new Error("Keymap release bindings only support a single key stroke");
1843
2077
  }
1844
2078
  if (event === "press") {
1845
- this.insertBinding(root, compiledBinding);
2079
+ this.insertBinding(root, compiledBinding, allowExactPrefixAmbiguity);
1846
2080
  }
1847
2081
  compiledBindings.push(compiledBinding);
1848
2082
  } catch (error) {
@@ -1911,13 +2145,13 @@ class CompilerService {
1911
2145
  }
1912
2146
  return [parsedBinding, ...extraBindings];
1913
2147
  }
1914
- insertBinding(root, binding) {
2148
+ insertBinding(root, binding, allowExactPrefixAmbiguity) {
1915
2149
  let node = root;
1916
2150
  const touchedNodes = [];
1917
2151
  const createdNodes = [];
1918
2152
  try {
1919
2153
  for (const part of binding.sequence) {
1920
- if (node.bindings.some((candidate) => candidate.command !== undefined)) {
2154
+ if (!allowExactPrefixAmbiguity && node.bindings.some((candidate) => candidate.command !== undefined)) {
1921
2155
  throw new Error("Keymap bindings cannot use the same sequence as both an exact match and a prefix in the same layer");
1922
2156
  }
1923
2157
  const bindingKey = part.match;
@@ -1931,7 +2165,7 @@ class CompilerService {
1931
2165
  touchedNodes.push(child);
1932
2166
  node = child;
1933
2167
  }
1934
- if (binding.command !== undefined && node.children.size > 0) {
2168
+ if (!allowExactPrefixAmbiguity && binding.command !== undefined && node.children.size > 0) {
1935
2169
  throw new Error("Keymap bindings cannot use the same sequence as both an exact match and a prefix in the same layer");
1936
2170
  }
1937
2171
  node.bindings = [...node.bindings, binding];
@@ -2096,20 +2330,7 @@ class ConditionService {
2096
2330
  hasNoConditions(target) {
2097
2331
  return target.requires.length === 0 && target.matchers.length === 0;
2098
2332
  }
2099
- registerRuntimeMatchable(target) {
2100
- for (const matcher of target.matchers) {
2101
- if (!matcher.subscribe) {
2102
- continue;
2103
- }
2104
- try {
2105
- matcher.dispose = matcher.subscribe(() => {
2106
- target.matchCacheDirty = true;
2107
- this.notify.queueStateChange();
2108
- });
2109
- } catch (error) {
2110
- this.notify.emitError("reactive-matcher-subscribe-error", error, getErrorMessage(error, `Failed to subscribe to reactive matcher from ${matcher.source}`));
2111
- }
2112
- }
2333
+ indexRuntimeMatchable(target) {
2113
2334
  if (target.conditionKeys.length > 0) {
2114
2335
  for (const key of target.conditionKeys) {
2115
2336
  const dependents = this.state.conditions.runtimeKeyDependents.get(key);
@@ -2124,18 +2345,7 @@ class ConditionService {
2124
2345
  target.matchCacheDirty = true;
2125
2346
  }
2126
2347
  }
2127
- unregisterRuntimeMatchable(target) {
2128
- for (const matcher of target.matchers) {
2129
- if (!matcher.dispose) {
2130
- continue;
2131
- }
2132
- try {
2133
- matcher.dispose();
2134
- } catch (error) {
2135
- this.notify.emitError("reactive-matcher-dispose-error", error, getErrorMessage(error, `Failed to dispose reactive matcher from ${matcher.source}`));
2136
- }
2137
- matcher.dispose = undefined;
2138
- }
2348
+ unindexRuntimeMatchable(target) {
2139
2349
  if (target.conditionKeys.length === 0) {
2140
2350
  return;
2141
2351
  }
@@ -2225,7 +2435,31 @@ class ConditionService {
2225
2435
  }
2226
2436
  }
2227
2437
 
2438
+ // src/types.ts
2439
+ var KEY_DISAMBIGUATION_DECISION = Symbol("keymap-disambiguation-decision");
2440
+ var KEY_DEFERRED_DISAMBIGUATION_DECISION = Symbol("keymap-deferred-disambiguation-decision");
2441
+
2228
2442
  // src/services/dispatch.ts
2443
+ function createSyncDecision(action, handler) {
2444
+ return {
2445
+ [KEY_DISAMBIGUATION_DECISION]: true,
2446
+ action,
2447
+ handler
2448
+ };
2449
+ }
2450
+ function createDeferredDecision(action) {
2451
+ return {
2452
+ [KEY_DEFERRED_DISAMBIGUATION_DECISION]: true,
2453
+ action
2454
+ };
2455
+ }
2456
+ function isSyncDecision(value) {
2457
+ return !!value && typeof value === "object" && value[KEY_DISAMBIGUATION_DECISION] === true;
2458
+ }
2459
+ function isDeferredDecision(value) {
2460
+ return !!value && typeof value === "object" && value[KEY_DEFERRED_DISAMBIGUATION_DECISION] === true;
2461
+ }
2462
+
2229
2463
  class DispatchService {
2230
2464
  state;
2231
2465
  notify;
@@ -2234,8 +2468,12 @@ class DispatchService {
2234
2468
  conditions;
2235
2469
  executor;
2236
2470
  compiler;
2471
+ catalog;
2472
+ layers;
2237
2473
  eventMatchResolverContext;
2238
- constructor(state, notify, runtime, activation, conditions, executor, compiler) {
2474
+ pendingDisambiguation = null;
2475
+ nextPendingDisambiguationId = 0;
2476
+ constructor(state, notify, runtime, activation, conditions, executor, compiler, catalog, layers) {
2239
2477
  this.state = state;
2240
2478
  this.notify = notify;
2241
2479
  this.runtime = runtime;
@@ -2243,6 +2481,8 @@ class DispatchService {
2243
2481
  this.conditions = conditions;
2244
2482
  this.executor = executor;
2245
2483
  this.compiler = compiler;
2484
+ this.catalog = catalog;
2485
+ this.layers = layers;
2246
2486
  this.eventMatchResolverContext = {
2247
2487
  resolveKey: (key) => {
2248
2488
  return this.compiler.parseTokenKey(key).match;
@@ -2271,6 +2511,27 @@ class DispatchService {
2271
2511
  clearEventMatchResolvers() {
2272
2512
  this.state.dispatch.eventMatchResolvers.clear();
2273
2513
  }
2514
+ prependDisambiguationResolver(resolver) {
2515
+ return this.mutateDisambiguationResolvers(() => this.state.dispatch.disambiguationResolvers.prepend(resolver), resolver);
2516
+ }
2517
+ appendDisambiguationResolver(resolver) {
2518
+ return this.mutateDisambiguationResolvers(() => this.state.dispatch.disambiguationResolvers.append(resolver), resolver);
2519
+ }
2520
+ clearDisambiguationResolvers() {
2521
+ if (!this.state.dispatch.disambiguationResolvers.has()) {
2522
+ return;
2523
+ }
2524
+ this.notify.runWithStateChangeBatch(() => {
2525
+ this.state.dispatch.disambiguationResolvers.clear();
2526
+ this.layers.recompileBindings();
2527
+ });
2528
+ }
2529
+ handlePendingSequenceChange(_previous, _next) {
2530
+ if (!this.pendingDisambiguation) {
2531
+ return;
2532
+ }
2533
+ this.cancelPendingDisambiguation();
2534
+ }
2274
2535
  handleRawSequence(sequence) {
2275
2536
  const hooks = this.state.dispatch.rawHooks.entries();
2276
2537
  if (hooks.length === 0) {
@@ -2296,6 +2557,9 @@ class DispatchService {
2296
2557
  return false;
2297
2558
  }
2298
2559
  handleKeyEvent(event, release) {
2560
+ if (!release) {
2561
+ this.cancelPendingDisambiguation();
2562
+ }
2299
2563
  const hooks = this.state.dispatch.keyHooks.entries();
2300
2564
  const context = {
2301
2565
  event,
@@ -2335,6 +2599,27 @@ class DispatchService {
2335
2599
  }
2336
2600
  this.dispatchLayers(event);
2337
2601
  }
2602
+ mutateDisambiguationResolvers(register, resolver) {
2603
+ return this.notify.runWithStateChangeBatch(() => {
2604
+ const hadResolvers = this.state.dispatch.disambiguationResolvers.has();
2605
+ const off = register();
2606
+ if (!hadResolvers && this.state.dispatch.disambiguationResolvers.has()) {
2607
+ this.layers.recompileBindings();
2608
+ }
2609
+ return () => {
2610
+ this.notify.runWithStateChangeBatch(() => {
2611
+ const hadBeforeRemoval = this.state.dispatch.disambiguationResolvers.has();
2612
+ off();
2613
+ if (this.state.dispatch.disambiguationResolvers.values().includes(resolver)) {
2614
+ return;
2615
+ }
2616
+ if (hadBeforeRemoval && !this.state.dispatch.disambiguationResolvers.has()) {
2617
+ this.layers.recompileBindings();
2618
+ }
2619
+ });
2620
+ };
2621
+ });
2622
+ }
2338
2623
  dispatchReleaseLayers(event) {
2339
2624
  const focused = this.activation.getFocusedTarget();
2340
2625
  const activeLayers = this.activation.getActiveLayers(focused);
@@ -2387,74 +2672,357 @@ class DispatchService {
2387
2672
  this.activation.setPendingSequence(null);
2388
2673
  return;
2389
2674
  }
2390
- let handledExact = false;
2391
- captureLoop:
2392
- for (let index = 0;index < advancedCaptures.length; index += 1) {
2393
- const capture = advancedCaptures[index];
2394
- if (!capture) {
2395
- continue;
2396
- }
2397
- if (capture.node.children.size > 0) {
2398
- if (handledExact) {
2399
- continue;
2400
- }
2401
- this.activation.setPendingSequence({
2402
- captures: advancedCaptures.filter((candidate, candidateIndex) => {
2403
- return candidateIndex >= index && candidate.node.children.size > 0;
2404
- })
2405
- });
2406
- event.preventDefault();
2407
- event.stopPropagation();
2408
- return;
2409
- }
2410
- const result = this.runBindings(capture.layer, capture.node.bindings, event, focused);
2411
- if (!result.handled) {
2675
+ this.dispatchPendingCapturesFromIndex(advancedCaptures, 0, false, event, focused);
2676
+ }
2677
+ dispatchPendingCapturesFromIndex(advancedCaptures, startIndex, handledExact, event, focused) {
2678
+ let hasHandledExact = handledExact;
2679
+ for (let index = startIndex;index < advancedCaptures.length; index += 1) {
2680
+ const capture = advancedCaptures[index];
2681
+ if (!capture) {
2682
+ continue;
2683
+ }
2684
+ if (capture.node.children.size > 0) {
2685
+ if (hasHandledExact) {
2412
2686
  continue;
2413
2687
  }
2414
- handledExact = true;
2415
- if (result.stop) {
2416
- this.activation.setPendingSequence(null);
2688
+ const continuationCaptures = this.collectPendingCapturesFromAdvanced(advancedCaptures, index);
2689
+ if (this.tryResolvePendingAmbiguity(advancedCaptures, index, continuationCaptures, capture, event, focused, hasHandledExact)) {
2417
2690
  return;
2418
2691
  }
2419
- continue captureLoop;
2692
+ this.activation.setPendingSequence({ captures: continuationCaptures });
2693
+ event.preventDefault();
2694
+ event.stopPropagation();
2695
+ return;
2420
2696
  }
2697
+ const result = this.runBindings(capture.layer, capture.node.bindings, event, focused);
2698
+ if (!result.handled) {
2699
+ continue;
2700
+ }
2701
+ hasHandledExact = true;
2702
+ if (result.stop) {
2703
+ this.activation.setPendingSequence(null);
2704
+ return;
2705
+ }
2706
+ }
2421
2707
  this.activation.setPendingSequence(null);
2422
2708
  }
2423
2709
  dispatchFromRoot(activeLayers, matchKeys, event, focused) {
2710
+ this.dispatchFromRootAtIndex(activeLayers, 0, matchKeys, event, focused);
2711
+ }
2712
+ dispatchFromRootAtIndex(activeLayers, startIndex, matchKeys, event, focused) {
2424
2713
  const hasLayerConditions = this.state.layers.layersWithConditions > 0;
2425
- layerLoop:
2426
- for (let index = 0;index < activeLayers.length; index += 1) {
2427
- const layer = activeLayers[index];
2428
- if (!layer) {
2429
- continue;
2430
- }
2431
- if (layer.root.children.size === 0) {
2432
- continue;
2433
- }
2434
- if (hasLayerConditions && !this.conditions.hasNoConditions(layer) && !this.conditions.matchesConditions(layer)) {
2435
- continue;
2436
- }
2437
- const nextNode = this.getReachableChild(layer.root, matchKeys, focused);
2438
- if (!nextNode) {
2439
- continue;
2440
- }
2441
- if (nextNode.children.size > 0) {
2442
- this.activation.setPendingSequence({
2443
- captures: this.collectPendingCapturesFromRoot(activeLayers, index, matchKeys, focused)
2444
- });
2445
- event.preventDefault();
2446
- event.stopPropagation();
2714
+ for (let index = startIndex;index < activeLayers.length; index += 1) {
2715
+ const layer = activeLayers[index];
2716
+ if (!layer) {
2717
+ continue;
2718
+ }
2719
+ if (layer.root.children.size === 0) {
2720
+ continue;
2721
+ }
2722
+ if (hasLayerConditions && !this.conditions.hasNoConditions(layer) && !this.conditions.matchesConditions(layer)) {
2723
+ continue;
2724
+ }
2725
+ const nextNode = this.getReachableChild(layer.root, matchKeys, focused);
2726
+ if (!nextNode) {
2727
+ continue;
2728
+ }
2729
+ if (nextNode.children.size > 0) {
2730
+ const continuationCaptures = this.collectPendingCapturesFromRoot(activeLayers, index, matchKeys, focused);
2731
+ if (this.tryResolveRootAmbiguity(activeLayers, index, matchKeys, continuationCaptures, layer, nextNode, event, focused)) {
2447
2732
  return;
2448
2733
  }
2449
- const result = this.runBindings(layer, nextNode.bindings, event, focused);
2450
- if (!result.handled) {
2451
- continue;
2734
+ this.activation.setPendingSequence({ captures: continuationCaptures });
2735
+ event.preventDefault();
2736
+ event.stopPropagation();
2737
+ return;
2738
+ }
2739
+ const result = this.runBindings(layer, nextNode.bindings, event, focused);
2740
+ if (!result.handled) {
2741
+ continue;
2742
+ }
2743
+ if (result.stop) {
2744
+ return;
2745
+ }
2746
+ }
2747
+ }
2748
+ tryResolveRootAmbiguity(activeLayers, layerIndex, matchKeys, continuationCaptures, layer, node, event, focused) {
2749
+ const applyExact = () => {
2750
+ this.activation.setPendingSequence(null);
2751
+ const result = this.runBindings(layer, node.bindings, event, focused);
2752
+ if (!result.stop) {
2753
+ this.dispatchFromRootAtIndex(activeLayers, layerIndex + 1, matchKeys, event, focused);
2754
+ }
2755
+ };
2756
+ return this.tryResolveAmbiguity({
2757
+ event,
2758
+ focused,
2759
+ continuationCaptures,
2760
+ exactBindingsSource: node.bindings,
2761
+ runExact: applyExact
2762
+ });
2763
+ }
2764
+ tryResolvePendingAmbiguity(advancedCaptures, captureIndex, continuationCaptures, capture, event, focused, handledExact) {
2765
+ const applyExact = () => {
2766
+ this.activation.setPendingSequence(null);
2767
+ const result = this.runBindings(capture.layer, capture.node.bindings, event, focused);
2768
+ if (result.stop) {
2769
+ return;
2770
+ }
2771
+ this.dispatchPendingCapturesFromIndex(advancedCaptures, captureIndex + 1, handledExact || result.handled, event, focused);
2772
+ };
2773
+ return this.tryResolveAmbiguity({
2774
+ event,
2775
+ focused,
2776
+ continuationCaptures,
2777
+ exactBindingsSource: capture.node.bindings,
2778
+ runExact: applyExact
2779
+ });
2780
+ }
2781
+ tryResolveAmbiguity(options) {
2782
+ const { event, focused, continuationCaptures, exactBindingsSource, runExact } = options;
2783
+ if (!this.state.dispatch.disambiguationResolvers.has() || continuationCaptures.length === 0) {
2784
+ return false;
2785
+ }
2786
+ const activeView = this.catalog.getActiveCommandView(focused);
2787
+ const exactBindings = this.activation.collectMatchingBindings(exactBindingsSource, focused, activeView);
2788
+ if (!exactBindings.some((binding) => binding.command !== undefined)) {
2789
+ return false;
2790
+ }
2791
+ const continueSequence = () => {
2792
+ this.activation.setPendingSequence({ captures: continuationCaptures });
2793
+ event.preventDefault();
2794
+ event.stopPropagation();
2795
+ };
2796
+ const clear = () => {
2797
+ this.activation.setPendingSequence(null);
2798
+ event.preventDefault();
2799
+ event.stopPropagation();
2800
+ };
2801
+ let sequence;
2802
+ const getSequence = () => {
2803
+ sequence ??= this.activation.collectSequencePartsFromPending({ captures: continuationCaptures });
2804
+ return sequence;
2805
+ };
2806
+ const decision = this.resolveDisambiguation({
2807
+ event,
2808
+ focused,
2809
+ getSequence,
2810
+ exactBindings,
2811
+ continuationCaptures,
2812
+ activeView
2813
+ });
2814
+ if (!decision) {
2815
+ this.warnUnresolvedAmbiguity(getSequence());
2816
+ continueSequence();
2817
+ return true;
2818
+ }
2819
+ return this.applySyncDecision(decision, continuationCaptures, runExact, continueSequence, clear, focused, getSequence);
2820
+ }
2821
+ applySyncDecision(decision, continuationCaptures, runExact, continueSequence, clear, focused, getSequence) {
2822
+ if (decision.action === "run-exact") {
2823
+ runExact();
2824
+ return true;
2825
+ }
2826
+ if (decision.action === "continue-sequence") {
2827
+ continueSequence();
2828
+ return true;
2829
+ }
2830
+ if (decision.action === "clear") {
2831
+ clear();
2832
+ return true;
2833
+ }
2834
+ continueSequence();
2835
+ this.scheduleDeferredDisambiguation(continuationCaptures, decision.handler, focused, getSequence(), (nextDecision) => {
2836
+ if (!nextDecision) {
2837
+ return;
2838
+ }
2839
+ if (nextDecision.action === "run-exact") {
2840
+ runExact();
2841
+ return;
2842
+ }
2843
+ if (nextDecision.action === "continue-sequence") {
2844
+ continueSequence();
2845
+ return;
2846
+ }
2847
+ clear();
2848
+ });
2849
+ return true;
2850
+ }
2851
+ resolveDisambiguation(options) {
2852
+ const activation = this.activation;
2853
+ const runtime = this.runtime;
2854
+ let sequence;
2855
+ let exact;
2856
+ let continuations;
2857
+ let strokePart;
2858
+ const ctx = {
2859
+ event: options.event,
2860
+ focused: options.focused,
2861
+ get sequence() {
2862
+ sequence ??= cloneKeySequence(options.getSequence());
2863
+ return sequence;
2864
+ },
2865
+ get stroke() {
2866
+ const stroke = options.getSequence().at(-1);
2867
+ if (!stroke) {
2868
+ throw new Error("Disambiguation context expected a non-empty sequence");
2452
2869
  }
2453
- if (result.stop) {
2870
+ strokePart ??= {
2871
+ ...stroke,
2872
+ stroke: cloneKeyStroke(stroke.stroke)
2873
+ };
2874
+ return strokePart;
2875
+ },
2876
+ get exact() {
2877
+ exact ??= activation.collectActiveBindings(options.exactBindings, options.focused, options.activeView).map((binding) => ({
2878
+ ...binding,
2879
+ sequence: cloneKeySequence(binding.sequence)
2880
+ }));
2881
+ return exact;
2882
+ },
2883
+ get continuations() {
2884
+ continuations ??= activation.getActiveKeysForCaptures(options.continuationCaptures, {
2885
+ includeBindings: true,
2886
+ includeMetadata: true
2887
+ });
2888
+ return continuations;
2889
+ },
2890
+ getData: (name) => {
2891
+ return runtime.getData(name);
2892
+ },
2893
+ setData: (name, value) => {
2894
+ runtime.setData(name, value);
2895
+ },
2896
+ runExact: () => createSyncDecision("run-exact"),
2897
+ continueSequence: () => createSyncDecision("continue-sequence"),
2898
+ clear: () => createSyncDecision("clear"),
2899
+ defer: (run) => createSyncDecision("defer", run)
2900
+ };
2901
+ for (const resolver of this.state.dispatch.disambiguationResolvers.values()) {
2902
+ let result;
2903
+ try {
2904
+ result = resolver(ctx);
2905
+ } catch (error) {
2906
+ this.notify.emitError("disambiguation-resolver-error", error, "[Keymap] Error in disambiguation resolver:");
2907
+ continue;
2908
+ }
2909
+ if (result === undefined) {
2910
+ continue;
2911
+ }
2912
+ if (isPromiseLike(result)) {
2913
+ this.notify.emitError("invalid-disambiguation-resolver-return", result, "[Keymap] Disambiguation resolvers must return synchronously; use ctx.defer(...) for async handling");
2914
+ continue;
2915
+ }
2916
+ if (!isSyncDecision(result)) {
2917
+ this.notify.emitError("invalid-disambiguation-decision", result, "[Keymap] Invalid disambiguation decision returned by resolver:");
2918
+ continue;
2919
+ }
2920
+ return result;
2921
+ }
2922
+ return;
2923
+ }
2924
+ scheduleDeferredDisambiguation(captures, handler, focused, sequence, apply) {
2925
+ this.cancelPendingDisambiguation();
2926
+ const controller = new AbortController;
2927
+ const pending = {
2928
+ id: this.nextPendingDisambiguationId++,
2929
+ controller,
2930
+ captures,
2931
+ apply
2932
+ };
2933
+ this.pendingDisambiguation = pending;
2934
+ queueMicrotask(() => {
2935
+ this.executeDeferredDisambiguation(pending, handler, focused, sequence);
2936
+ });
2937
+ }
2938
+ executeDeferredDisambiguation(pending, handler, focused, sequence) {
2939
+ if (!this.isPendingDisambiguationCurrent(pending)) {
2940
+ return;
2941
+ }
2942
+ const ctx = {
2943
+ signal: pending.controller.signal,
2944
+ sequence: cloneKeySequence(sequence),
2945
+ focused,
2946
+ sleep: (ms) => {
2947
+ return this.sleepWithSignal(ms, pending.controller.signal);
2948
+ },
2949
+ runExact: () => createDeferredDecision("run-exact"),
2950
+ continueSequence: () => createDeferredDecision("continue-sequence"),
2951
+ clear: () => createDeferredDecision("clear")
2952
+ };
2953
+ let result;
2954
+ try {
2955
+ result = handler(ctx);
2956
+ } catch (error) {
2957
+ if (this.isPendingDisambiguationCurrent(pending)) {
2958
+ this.notify.emitError("deferred-disambiguation-error", error, "[Keymap] Error in deferred disambiguation handler:");
2959
+ this.finishPendingDisambiguation(pending);
2960
+ }
2961
+ return;
2962
+ }
2963
+ if (isPromiseLike(result)) {
2964
+ result.then((resolved) => {
2965
+ this.applyDeferredDisambiguationResult(pending, resolved);
2966
+ }).catch((error) => {
2967
+ if (!this.isPendingDisambiguationCurrent(pending)) {
2454
2968
  return;
2455
2969
  }
2456
- continue layerLoop;
2457
- }
2970
+ this.notify.emitError("deferred-disambiguation-error", error, "[Keymap] Error in deferred disambiguation handler:");
2971
+ this.finishPendingDisambiguation(pending);
2972
+ });
2973
+ return;
2974
+ }
2975
+ this.applyDeferredDisambiguationResult(pending, result);
2976
+ }
2977
+ applyDeferredDisambiguationResult(pending, result) {
2978
+ if (!this.isPendingDisambiguationCurrent(pending)) {
2979
+ return;
2980
+ }
2981
+ if (result !== undefined && !isDeferredDecision(result)) {
2982
+ this.notify.emitError("invalid-deferred-disambiguation-decision", result, "[Keymap] Invalid deferred disambiguation decision returned by handler:");
2983
+ this.finishPendingDisambiguation(pending);
2984
+ return;
2985
+ }
2986
+ this.finishPendingDisambiguation(pending);
2987
+ pending.apply(result);
2988
+ }
2989
+ finishPendingDisambiguation(pending) {
2990
+ if (!this.isPendingDisambiguationCurrent(pending)) {
2991
+ return;
2992
+ }
2993
+ this.pendingDisambiguation = null;
2994
+ }
2995
+ cancelPendingDisambiguation() {
2996
+ const pending = this.pendingDisambiguation;
2997
+ if (!pending) {
2998
+ return;
2999
+ }
3000
+ this.pendingDisambiguation = null;
3001
+ pending.controller.abort();
3002
+ }
3003
+ isPendingDisambiguationCurrent(pending) {
3004
+ return this.pendingDisambiguation === pending;
3005
+ }
3006
+ sleepWithSignal(ms, signal) {
3007
+ if (signal.aborted) {
3008
+ return Promise.resolve(false);
3009
+ }
3010
+ return new Promise((resolve) => {
3011
+ const timeout = setTimeout(() => {
3012
+ signal.removeEventListener("abort", onAbort);
3013
+ resolve(true);
3014
+ }, Math.max(0, ms));
3015
+ const onAbort = () => {
3016
+ clearTimeout(timeout);
3017
+ signal.removeEventListener("abort", onAbort);
3018
+ resolve(false);
3019
+ };
3020
+ signal.addEventListener("abort", onAbort, { once: true });
3021
+ });
3022
+ }
3023
+ warnUnresolvedAmbiguity(sequence) {
3024
+ const display = stringifyKeySequence(sequence, { preferDisplay: true });
3025
+ this.notify.warnOnce(`unresolved-disambiguation:${display}`, "unresolved-disambiguation", { sequence: display }, `[Keymap] Ambiguous exact/prefix sequence "${display}" fell back to prefix handling because no disambiguation resolver resolved it`);
2458
3026
  }
2459
3027
  collectPendingCapturesFromRoot(activeLayers, startIndex, matchKeys, focused) {
2460
3028
  const captures = [];
@@ -2478,6 +3046,11 @@ class DispatchService {
2478
3046
  }
2479
3047
  return captures;
2480
3048
  }
3049
+ collectPendingCapturesFromAdvanced(advancedCaptures, startIndex) {
3050
+ return advancedCaptures.filter((candidate, candidateIndex) => {
3051
+ return candidateIndex >= startIndex && candidate.node.children.size > 0;
3052
+ });
3053
+ }
2481
3054
  resolveEventMatchKeys(event) {
2482
3055
  const resolvers = this.state.dispatch.eventMatchResolvers.values();
2483
3056
  if (resolvers.length === 0) {
@@ -2500,7 +3073,7 @@ class DispatchService {
2500
3073
  continue;
2501
3074
  }
2502
3075
  for (const candidate of resolved) {
2503
- if (typeof candidate !== "symbol") {
3076
+ if (typeof candidate !== "string") {
2504
3077
  this.notify.emitError("invalid-event-match-resolver-candidate", candidate, "[Keymap] Invalid event match resolver candidate:");
2505
3078
  continue;
2506
3079
  }
@@ -2578,7 +3151,7 @@ function resolveSingleEventMatchKeys(resolver, event, ctx, notify) {
2578
3151
  }
2579
3152
  if (resolved.length === 1) {
2580
3153
  const [candidate] = resolved;
2581
- if (typeof candidate !== "symbol") {
3154
+ if (typeof candidate !== "string") {
2582
3155
  notify.emitError("invalid-event-match-resolver-candidate", candidate, "[Keymap] Invalid event match resolver candidate:");
2583
3156
  return [];
2584
3157
  }
@@ -2587,7 +3160,7 @@ function resolveSingleEventMatchKeys(resolver, event, ctx, notify) {
2587
3160
  const keys = [];
2588
3161
  const seen = new Set;
2589
3162
  for (const candidate of resolved) {
2590
- if (typeof candidate !== "symbol") {
3163
+ if (typeof candidate !== "string") {
2591
3164
  notify.emitError("invalid-event-match-resolver-candidate", candidate, "[Keymap] Invalid event match resolver candidate:");
2592
3165
  continue;
2593
3166
  }
@@ -2749,14 +3322,13 @@ class EnvironmentService {
2749
3322
 
2750
3323
  // src/services/layers.ts
2751
3324
  var NOOP2 = () => {};
2752
- function sortByPriorityAndOrder(items, options) {
2753
- const orderDirection = options?.order ?? "asc";
2754
- return [...items].sort((a, b) => {
2755
- const priorityDiff = b.priority - a.priority;
3325
+ function sortLayers(layers) {
3326
+ return [...layers].sort((left, right) => {
3327
+ const priorityDiff = right.priority - left.priority;
2756
3328
  if (priorityDiff !== 0) {
2757
3329
  return priorityDiff;
2758
3330
  }
2759
- return orderDirection === "desc" ? b.order - a.order : a.order - b.order;
3331
+ return right.order - left.order;
2760
3332
  });
2761
3333
  }
2762
3334
  function createCommandLookup(commands) {
@@ -2805,7 +3377,6 @@ function buildLayerBindingAnalyses(root, compiledBindings) {
2805
3377
  preventDefault: binding.preventDefault,
2806
3378
  fallthrough: binding.fallthrough,
2807
3379
  sourceBinding: snapshotParsedBindingInput(binding.sourceBinding),
2808
- sourceScope: binding.sourceScope,
2809
3380
  sourceTarget: binding.sourceTarget,
2810
3381
  sourceLayerOrder: binding.sourceLayerOrder,
2811
3382
  sourceBindingIndex: binding.sourceBindingIndex,
@@ -2835,7 +3406,6 @@ class LayerService {
2835
3406
  this.notify.emitError("destroyed-layer-target", { target }, "Cannot register a keymap layer for a destroyed keymap target");
2836
3407
  return NOOP2;
2837
3408
  }
2838
- let scope;
2839
3409
  let bindingInputs;
2840
3410
  let requires;
2841
3411
  let matchers;
@@ -2844,10 +3414,9 @@ class LayerService {
2844
3414
  let compileFields;
2845
3415
  let commands;
2846
3416
  let commandLookup;
2847
- let indexTarget;
3417
+ let targetMode;
2848
3418
  try {
2849
- scope = this.normalizeScope(layer);
2850
- indexTarget = layer.target ?? this.options.host.rootTarget;
3419
+ targetMode = this.normalizeTargetMode(layer);
2851
3420
  bindingInputs = snapshotBindingInputs(layer.bindings ?? []);
2852
3421
  commands = !layer.commands || layer.commands.length === 0 ? [] : this.options.commands.normalizeCommands(layer.commands);
2853
3422
  commandLookup = createCommandLookup(commands);
@@ -2857,12 +3426,11 @@ class LayerService {
2857
3426
  return NOOP2;
2858
3427
  }
2859
3428
  const order = this.state.core.order++;
2860
- const compiledBindings = this.options.compiler.compileBindings(bindingInputs, this.state.environment.tokens, scope, target, order, compileFields);
3429
+ const compiledBindings = this.options.compiler.compileBindings(bindingInputs, this.state.environment.tokens, target, order, compileFields);
2861
3430
  if (compiledBindings.bindings.length === 0 && !compiledBindings.hasTokenBindings && commands.length === 0) {
2862
3431
  return NOOP2;
2863
3432
  }
2864
3433
  this.runLayerAnalyzers({
2865
- scope,
2866
3434
  target,
2867
3435
  order,
2868
3436
  commandLookup,
@@ -2874,8 +3442,7 @@ class LayerService {
2874
3442
  const registeredLayer = {
2875
3443
  order,
2876
3444
  target,
2877
- indexTarget,
2878
- scope,
3445
+ targetMode,
2879
3446
  priority: layer.priority ?? 0,
2880
3447
  requires,
2881
3448
  matchers,
@@ -2901,14 +3468,16 @@ class LayerService {
2901
3468
  if (registeredLayer.requires.length > 0 || registeredLayer.matchers.length > 0) {
2902
3469
  this.state.layers.layersWithConditions += 1;
2903
3470
  }
2904
- this.conditions.registerRuntimeMatchable(registeredLayer);
3471
+ this.connectRuntimeMatchable(registeredLayer);
2905
3472
  for (const command of registeredLayer.commands) {
2906
- this.conditions.registerRuntimeMatchable(command);
3473
+ this.connectRuntimeMatchable(command);
2907
3474
  }
2908
3475
  for (const binding of registeredLayer.compiledBindings) {
2909
- this.conditions.registerRuntimeMatchable(binding);
3476
+ this.connectRuntimeMatchable(binding);
2910
3477
  }
2911
3478
  this.indexLayer(registeredLayer);
3479
+ this.activation.invalidateActiveLayers();
3480
+ this.activation.refreshActiveLayers();
2912
3481
  if (target) {
2913
3482
  const onTargetDestroy = () => {
2914
3483
  this.unregisterLayer(registeredLayer);
@@ -2931,37 +3500,41 @@ class LayerService {
2931
3500
  if (!layer.hasTokenBindings) {
2932
3501
  continue;
2933
3502
  }
2934
- nextCompilations.set(layer, this.options.compiler.compileBindings(layer.bindingInputs, nextTokens, layer.scope, layer.target, layer.order, layer.compileFields));
3503
+ nextCompilations.set(layer, this.compileLayerBindings(layer, nextTokens));
2935
3504
  }
2936
3505
  this.state.environment.tokens = nextTokens;
2937
3506
  let shouldClearPending = false;
2938
3507
  for (const [layer, compilation] of nextCompilations) {
2939
- this.runLayerAnalyzers({
2940
- scope: layer.scope,
2941
- target: layer.target,
2942
- order: layer.order,
2943
- commandLookup: layer.commandLookup,
2944
- bindingInputs: layer.bindingInputs,
2945
- compiledBindings: compilation.bindings,
2946
- root: compilation.root,
2947
- hasTokenBindings: compilation.hasTokenBindings
2948
- });
2949
- for (const binding of layer.compiledBindings) {
2950
- this.conditions.unregisterRuntimeMatchable(binding);
3508
+ if (this.applyCompiledBindings(layer, compilation)) {
3509
+ shouldClearPending = true;
2951
3510
  }
2952
- layer.root = compilation.root;
2953
- layer.compiledBindings = compilation.bindings;
2954
- for (const binding of layer.compiledBindings) {
2955
- this.conditions.registerRuntimeMatchable(binding);
3511
+ }
3512
+ if (shouldClearPending) {
3513
+ this.activation.setPendingSequence(null);
3514
+ }
3515
+ if (nextCompilations.size > 0) {
3516
+ this.notify.queueStateChange();
3517
+ }
3518
+ });
3519
+ }
3520
+ recompileBindings() {
3521
+ this.notify.runWithStateChangeBatch(() => {
3522
+ let recompiledLayers = 0;
3523
+ let shouldClearPending = false;
3524
+ for (const layer of this.state.layers.layers) {
3525
+ if (layer.bindingInputs.length === 0) {
3526
+ continue;
2956
3527
  }
2957
- if (this.state.projection.pendingSequence?.captures.some((capture) => capture.layer === layer)) {
3528
+ const compilation = this.compileLayerBindings(layer, this.state.environment.tokens);
3529
+ if (this.applyCompiledBindings(layer, compilation)) {
2958
3530
  shouldClearPending = true;
2959
3531
  }
3532
+ recompiledLayers += 1;
2960
3533
  }
2961
3534
  if (shouldClearPending) {
2962
3535
  this.activation.setPendingSequence(null);
2963
3536
  }
2964
- if (nextCompilations.size > 0) {
3537
+ if (recompiledLayers > 0) {
2965
3538
  this.notify.queueStateChange();
2966
3539
  }
2967
3540
  });
@@ -2975,17 +3548,27 @@ class LayerService {
2975
3548
  clearLayerAnalyzers() {
2976
3549
  this.state.layers.layerAnalyzers.clear();
2977
3550
  }
2978
- normalizeScope(layer) {
2979
- if (layer.scope) {
2980
- if (layer.scope !== "global" && !layer.target) {
2981
- throw new Error(`Keymap scope "${layer.scope}" requires a target`);
3551
+ cleanup() {
3552
+ for (const layer of this.state.layers.layers) {
3553
+ this.disconnectRuntimeMatchable(layer);
3554
+ for (const command of layer.commands) {
3555
+ this.disconnectRuntimeMatchable(command);
3556
+ }
3557
+ for (const binding of layer.compiledBindings) {
3558
+ this.disconnectRuntimeMatchable(binding);
2982
3559
  }
2983
- return layer.scope;
3560
+ layer.offTargetDestroy?.();
3561
+ layer.offTargetDestroy = undefined;
2984
3562
  }
2985
- if (layer.target) {
2986
- return "focus-within";
3563
+ }
3564
+ normalizeTargetMode(layer) {
3565
+ if (layer.targetMode) {
3566
+ if (!layer.target) {
3567
+ throw new Error(`Keymap targetMode "${layer.targetMode}" requires a target`);
3568
+ }
3569
+ return layer.targetMode;
2987
3570
  }
2988
- return "global";
3571
+ return layer.target ? "focus-within" : undefined;
2989
3572
  }
2990
3573
  runLayerAnalyzers(options) {
2991
3574
  const analyzers = this.state.layers.layerAnalyzers.values();
@@ -2994,7 +3577,6 @@ class LayerService {
2994
3577
  }
2995
3578
  const bindings = buildLayerBindingAnalyses(options.root, options.compiledBindings);
2996
3579
  const ctx = {
2997
- scope: options.scope,
2998
3580
  target: options.target,
2999
3581
  order: options.order,
3000
3582
  bindingInputs: options.bindingInputs,
@@ -3062,41 +3644,38 @@ class LayerService {
3062
3644
  compileFields: Object.keys(compileFields).length > 0 ? Object.freeze(compileFields) : undefined
3063
3645
  };
3064
3646
  }
3065
- getOrCreateTargetBucket(target) {
3066
- const existing = this.state.layers.targetLayers.get(target);
3067
- if (existing) {
3068
- return existing;
3647
+ compileLayerBindings(layer, tokens) {
3648
+ return this.options.compiler.compileBindings(layer.bindingInputs, tokens, layer.target, layer.order, layer.compileFields);
3649
+ }
3650
+ applyCompiledBindings(layer, compilation) {
3651
+ this.runLayerAnalyzers({
3652
+ target: layer.target,
3653
+ order: layer.order,
3654
+ commandLookup: layer.commandLookup,
3655
+ bindingInputs: layer.bindingInputs,
3656
+ compiledBindings: compilation.bindings,
3657
+ root: compilation.root,
3658
+ hasTokenBindings: compilation.hasTokenBindings
3659
+ });
3660
+ for (const binding of layer.compiledBindings) {
3661
+ this.disconnectRuntimeMatchable(binding);
3069
3662
  }
3070
- const bucket = {
3071
- focusLayers: [],
3072
- focusWithinLayers: []
3073
- };
3074
- this.state.layers.targetLayers.set(target, bucket);
3075
- return bucket;
3663
+ layer.root = compilation.root;
3664
+ layer.compiledBindings = compilation.bindings;
3665
+ layer.hasUnkeyedBindings = compilation.bindings.some((binding) => binding.hasUnkeyedMatchers);
3666
+ layer.hasTokenBindings = compilation.hasTokenBindings;
3667
+ for (const binding of layer.compiledBindings) {
3668
+ this.connectRuntimeMatchable(binding);
3669
+ }
3670
+ return this.state.projection.pendingSequence?.captures.some((capture) => capture.layer === layer) ?? false;
3076
3671
  }
3077
3672
  indexLayer(layer) {
3078
- const bucket = this.getOrCreateTargetBucket(layer.indexTarget);
3079
- if (layer.scope === "focus") {
3080
- bucket.focusLayers = sortByPriorityAndOrder([...bucket.focusLayers, layer], { order: "desc" });
3081
- } else {
3082
- bucket.focusWithinLayers = sortByPriorityAndOrder([...bucket.focusWithinLayers, layer], { order: "desc" });
3083
- }
3084
- layer.bucket = bucket;
3673
+ this.state.layers.sortedLayers = sortLayers([...this.state.layers.sortedLayers, layer]);
3674
+ this.state.layers.activeLayersVersion += 1;
3085
3675
  }
3086
3676
  removeLayerFromIndex(layer) {
3087
- const bucket = layer.bucket;
3088
- if (!bucket) {
3089
- return;
3090
- }
3091
- if (layer.scope === "focus") {
3092
- bucket.focusLayers = bucket.focusLayers.filter((candidate) => candidate !== layer);
3093
- } else {
3094
- bucket.focusWithinLayers = bucket.focusWithinLayers.filter((candidate) => candidate !== layer);
3095
- }
3096
- if (bucket.focusLayers.length === 0 && bucket.focusWithinLayers.length === 0) {
3097
- this.state.layers.targetLayers.delete(layer.indexTarget);
3098
- }
3099
- layer.bucket = undefined;
3677
+ this.state.layers.sortedLayers = this.state.layers.sortedLayers.filter((candidate) => candidate !== layer);
3678
+ this.state.layers.activeLayersVersion += 1;
3100
3679
  }
3101
3680
  unregisterLayer(layer) {
3102
3681
  this.notify.runWithStateChangeBatch(() => {
@@ -3111,14 +3690,16 @@ class LayerService {
3111
3690
  this.state.commands.commandMetadataVersion += 1;
3112
3691
  removeRegisteredCommandNames(this.state.commands.registeredNames, layer.commands);
3113
3692
  }
3114
- this.conditions.unregisterRuntimeMatchable(layer);
3693
+ this.disconnectRuntimeMatchable(layer);
3115
3694
  for (const command of layer.commands) {
3116
- this.conditions.unregisterRuntimeMatchable(command);
3695
+ this.disconnectRuntimeMatchable(command);
3117
3696
  }
3118
3697
  for (const binding of layer.compiledBindings) {
3119
- this.conditions.unregisterRuntimeMatchable(binding);
3698
+ this.disconnectRuntimeMatchable(binding);
3120
3699
  }
3121
3700
  this.removeLayerFromIndex(layer);
3701
+ this.activation.invalidateActiveLayers();
3702
+ this.activation.refreshActiveLayers();
3122
3703
  layer.offTargetDestroy?.();
3123
3704
  layer.offTargetDestroy = undefined;
3124
3705
  if (this.state.projection.pendingSequence?.captures.some((capture) => capture.layer === layer)) {
@@ -3129,6 +3710,49 @@ class LayerService {
3129
3710
  this.notify.queueStateChange();
3130
3711
  });
3131
3712
  }
3713
+ connectRuntimeMatchable(target) {
3714
+ this.attachReactiveMatchers(target);
3715
+ this.conditions.indexRuntimeMatchable(target);
3716
+ }
3717
+ disconnectRuntimeMatchable(target) {
3718
+ this.detachReactiveMatchers(target);
3719
+ this.conditions.unindexRuntimeMatchable(target);
3720
+ }
3721
+ attachReactiveMatchers(target) {
3722
+ for (const matcher of target.matchers) {
3723
+ if (!matcher.subscribe) {
3724
+ continue;
3725
+ }
3726
+ try {
3727
+ matcher.dispose = matcher.subscribe(() => {
3728
+ target.matchCacheDirty = true;
3729
+ if (!this.activation.hasPendingSequenceState()) {
3730
+ this.notify.queueStateChange();
3731
+ return;
3732
+ }
3733
+ this.notify.runWithStateChangeBatch(() => {
3734
+ this.activation.revalidatePendingSequenceIfNeeded();
3735
+ this.notify.queueStateChange();
3736
+ });
3737
+ });
3738
+ } catch (error) {
3739
+ this.notify.emitError("reactive-matcher-subscribe-error", error, getErrorMessage(error, `Failed to subscribe to reactive matcher from ${matcher.source}`));
3740
+ }
3741
+ }
3742
+ }
3743
+ detachReactiveMatchers(target) {
3744
+ for (const matcher of target.matchers) {
3745
+ if (!matcher.dispose) {
3746
+ continue;
3747
+ }
3748
+ try {
3749
+ matcher.dispose();
3750
+ } catch (error) {
3751
+ this.notify.emitError("reactive-matcher-dispose-error", error, getErrorMessage(error, `Failed to dispose reactive matcher from ${matcher.source}`));
3752
+ }
3753
+ matcher.dispose = undefined;
3754
+ }
3755
+ }
3132
3756
  }
3133
3757
 
3134
3758
  // src/lib/emitter.ts
@@ -3195,6 +3819,8 @@ class Emitter {
3195
3819
  }
3196
3820
 
3197
3821
  // src/services/notify.ts
3822
+ var MAX_STATE_CHANGE_FLUSH_ITERATIONS = 1000;
3823
+
3198
3824
  class NotificationService {
3199
3825
  state;
3200
3826
  events;
@@ -3227,10 +3853,11 @@ class NotificationService {
3227
3853
  }
3228
3854
  emitWarning(code, warning, message) {
3229
3855
  if (!this.events.has("warning")) {
3856
+ const consoleMessage = `[${code}] ${message}`;
3230
3857
  if (warning instanceof Error) {
3231
- console.warn(message, warning);
3858
+ console.warn(consoleMessage, warning);
3232
3859
  } else {
3233
- console.warn(message);
3860
+ console.warn(consoleMessage);
3234
3861
  }
3235
3862
  return;
3236
3863
  }
@@ -3238,10 +3865,11 @@ class NotificationService {
3238
3865
  }
3239
3866
  emitError(code, error, message) {
3240
3867
  if (!this.events.has("error")) {
3868
+ const consoleMessage = `[${code}] ${message}`;
3241
3869
  if (error instanceof Error) {
3242
- console.error(message, error);
3870
+ console.error(consoleMessage, error);
3243
3871
  } else {
3244
- console.error(message);
3872
+ console.error(consoleMessage);
3245
3873
  }
3246
3874
  return;
3247
3875
  }
@@ -3270,7 +3898,14 @@ class NotificationService {
3270
3898
  }
3271
3899
  this.state.notify.flushingStateChange = true;
3272
3900
  try {
3901
+ let iterations = 0;
3273
3902
  while (this.state.notify.stateChangePending && this.state.notify.stateChangeDepth === 0) {
3903
+ if (iterations >= MAX_STATE_CHANGE_FLUSH_ITERATIONS) {
3904
+ this.state.notify.stateChangePending = false;
3905
+ this.emitError("state-change-feedback-loop", { iterations: MAX_STATE_CHANGE_FLUSH_ITERATIONS }, `[Keymap] Possible infinite state listener feedback loop detected after ${MAX_STATE_CHANGE_FLUSH_ITERATIONS} iterations; pending state notifications were dropped`);
3906
+ break;
3907
+ }
3908
+ iterations += 1;
3274
3909
  this.state.notify.stateChangePending = false;
3275
3910
  this.hooks.emit("state");
3276
3911
  }
@@ -3416,12 +4051,17 @@ function createKeymapState() {
3416
4051
  },
3417
4052
  dispatch: {
3418
4053
  eventMatchResolvers: new OrderedRegistry,
4054
+ disambiguationResolvers: new OrderedRegistry,
3419
4055
  keyHooks: new PriorityRegistry,
3420
4056
  rawHooks: new PriorityRegistry
3421
4057
  },
3422
4058
  layers: {
3423
4059
  layers: new Set,
3424
- targetLayers: new WeakMap,
4060
+ sortedLayers: [],
4061
+ activeLayersVersion: 0,
4062
+ activeLayersCacheVersion: -1,
4063
+ activeLayersCacheFocused: undefined,
4064
+ activeLayersCache: [],
3425
4065
  layersWithConditions: 0,
3426
4066
  layersWithCommands: 0,
3427
4067
  layerAnalyzers: new OrderedRegistry
@@ -3432,6 +4072,8 @@ function createKeymapState() {
3432
4072
  commandResolvers: new OrderedRegistry,
3433
4073
  activeCommandViewVersion: -1,
3434
4074
  activeCommandView: undefined,
4075
+ registeredCommandViewVersion: -1,
4076
+ registeredCommandView: undefined,
3435
4077
  registeredCommandEntriesCacheVersion: -1,
3436
4078
  registeredCommandEntriesCache: []
3437
4079
  },
@@ -3509,7 +4151,11 @@ class Keymap {
3509
4151
  this.activation.ensureValidPendingSequence();
3510
4152
  }
3511
4153
  });
3512
- this.activation = new ActivationService(this.state, this.host, this.hooks, this.notify, this.conditions, this.catalog);
4154
+ this.activation = new ActivationService(this.state, this.host, this.hooks, this.notify, this.conditions, this.catalog, {
4155
+ onPendingSequenceChanged: (previous, next) => {
4156
+ this.dispatch?.handlePendingSequenceChange(previous, next);
4157
+ }
4158
+ });
3513
4159
  this.runtime = new RuntimeService(this.state, this.notify, this.conditions, this.activation);
3514
4160
  this.executor = new CommandExecutorService(this.notify, this.runtime, this.activation, this.catalog, {
3515
4161
  keymap: this,
@@ -3532,7 +4178,7 @@ class Keymap {
3532
4178
  }
3533
4179
  });
3534
4180
  this.environment = new EnvironmentService(this.state, this.notify, this.compiler, this.layers);
3535
- this.dispatch = new DispatchService(this.state, this.notify, this.runtime, this.activation, this.conditions, this.executor, this.compiler);
4181
+ this.dispatch = new DispatchService(this.state, this.notify, this.runtime, this.activation, this.conditions, this.executor, this.compiler, this.catalog, this.layers);
3536
4182
  this.keypressListener = (event) => {
3537
4183
  this.dispatch.handleKeyEvent(event, false);
3538
4184
  };
@@ -3567,18 +4213,7 @@ class Keymap {
3567
4213
  resource.dispose();
3568
4214
  }
3569
4215
  this.resources.clear();
3570
- for (const layer of this.state.layers.layers) {
3571
- this.conditions.unregisterRuntimeMatchable(layer);
3572
- for (const command of layer.commands) {
3573
- this.conditions.unregisterRuntimeMatchable(command);
3574
- }
3575
- for (const binding of layer.compiledBindings) {
3576
- this.conditions.unregisterRuntimeMatchable(binding);
3577
- }
3578
- layer.offTargetDestroy?.();
3579
- layer.offTargetDestroy = undefined;
3580
- layer.bucket = undefined;
3581
- }
4216
+ this.layers.cleanup();
3582
4217
  for (const cleanupListener of this.cleanupListeners.splice(0)) {
3583
4218
  cleanupListener();
3584
4219
  }
@@ -3646,6 +4281,9 @@ class Keymap {
3646
4281
  runCommand(cmd, options) {
3647
4282
  return this.executor.runCommand(cmd, options);
3648
4283
  }
4284
+ dispatchCommand(cmd, options) {
4285
+ return this.executor.dispatchCommand(cmd, options);
4286
+ }
3649
4287
  on(name, fn) {
3650
4288
  if (name === "warning") {
3651
4289
  return this.events.hook(name, fn);
@@ -3730,9 +4368,20 @@ class Keymap {
3730
4368
  clearEventMatchResolvers() {
3731
4369
  this.dispatch.clearEventMatchResolvers();
3732
4370
  }
4371
+ prependDisambiguationResolver(resolver) {
4372
+ return this.dispatch.prependDisambiguationResolver(resolver);
4373
+ }
4374
+ appendDisambiguationResolver(resolver) {
4375
+ return this.dispatch.appendDisambiguationResolver(resolver);
4376
+ }
4377
+ clearDisambiguationResolvers() {
4378
+ this.dispatch.clearDisambiguationResolvers();
4379
+ }
3733
4380
  handleFocusedTargetChange(_focused) {
3734
4381
  this.notify.runWithStateChangeBatch(() => {
3735
4382
  this.activation.setPendingSequence(null);
4383
+ this.activation.invalidateActiveLayers();
4384
+ this.activation.refreshActiveLayers(_focused);
3736
4385
  this.notify.queueStateChange();
3737
4386
  });
3738
4387
  }