@theia/core 1.77.0-next.56 → 1.77.0-next.57

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.
Files changed (102) hide show
  1. package/lib/browser/catalog.json +66 -0
  2. package/lib/browser/common-frontend-contribution.d.ts +6 -1
  3. package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
  4. package/lib/browser/common-frontend-contribution.js +18 -1
  5. package/lib/browser/common-frontend-contribution.js.map +1 -1
  6. package/lib/browser/keybinding.d.ts +95 -17
  7. package/lib/browser/keybinding.d.ts.map +1 -1
  8. package/lib/browser/keybinding.js +405 -75
  9. package/lib/browser/keybinding.js.map +1 -1
  10. package/lib/browser/keybinding.spec.js +786 -30
  11. package/lib/browser/keybinding.spec.js.map +1 -1
  12. package/lib/browser/keyboard/browser-keyboard-frontend-contribution.js +1 -1
  13. package/lib/browser/keyboard/browser-keyboard-frontend-contribution.js.map +1 -1
  14. package/lib/browser/keyboard/browser-keyboard-layout-provider.d.ts +3 -4
  15. package/lib/browser/keyboard/browser-keyboard-layout-provider.d.ts.map +1 -1
  16. package/lib/browser/keyboard/browser-keyboard-layout-provider.js +3 -3
  17. package/lib/browser/keyboard/browser-keyboard-layout-provider.js.map +1 -1
  18. package/lib/browser/keyboard/browser-keyboard-layout-provider.spec.js +39 -0
  19. package/lib/browser/keyboard/browser-keyboard-layout-provider.spec.js.map +1 -1
  20. package/lib/browser/keyboard/keyboard-layout-service.d.ts +67 -15
  21. package/lib/browser/keyboard/keyboard-layout-service.d.ts.map +1 -1
  22. package/lib/browser/keyboard/keyboard-layout-service.js +309 -98
  23. package/lib/browser/keyboard/keyboard-layout-service.js.map +1 -1
  24. package/lib/browser/keyboard/keyboard-layout-service.spec.js +538 -12
  25. package/lib/browser/keyboard/keyboard-layout-service.spec.js.map +1 -1
  26. package/lib/browser/keyboard/keys.spec.js +183 -0
  27. package/lib/browser/keyboard/keys.spec.js.map +1 -1
  28. package/lib/browser/menu/action-menu-node.d.ts +5 -3
  29. package/lib/browser/menu/action-menu-node.d.ts.map +1 -1
  30. package/lib/browser/menu/action-menu-node.js +2 -11
  31. package/lib/browser/menu/action-menu-node.js.map +1 -1
  32. package/lib/browser/menu/action-menu-node.spec.d.ts +2 -0
  33. package/lib/browser/menu/action-menu-node.spec.d.ts.map +1 -0
  34. package/lib/browser/menu/action-menu-node.spec.js +54 -0
  35. package/lib/browser/menu/action-menu-node.spec.js.map +1 -0
  36. package/lib/browser/menu/browser-menu-plugin.js +1 -1
  37. package/lib/browser/menu/browser-menu-plugin.js.map +1 -1
  38. package/lib/browser/test/jsdom.d.ts.map +1 -1
  39. package/lib/browser/test/jsdom.js +11 -0
  40. package/lib/browser/test/jsdom.js.map +1 -1
  41. package/lib/common/keyboard/keyboard-layout-provider.d.ts +7 -0
  42. package/lib/common/keyboard/keyboard-layout-provider.d.ts.map +1 -1
  43. package/lib/common/keyboard/keyboard-layout-provider.js.map +1 -1
  44. package/lib/common/keys.d.ts +86 -3
  45. package/lib/common/keys.d.ts.map +1 -1
  46. package/lib/common/keys.js +262 -23
  47. package/lib/common/keys.js.map +1 -1
  48. package/lib/common/ternary-search-tree.js +2 -2
  49. package/lib/common/ternary-search-tree.js.map +1 -1
  50. package/lib/common/ternary-search-tree.spec.js +12 -0
  51. package/lib/common/ternary-search-tree.spec.js.map +1 -1
  52. package/lib/electron-browser/keyboard/electron-keyboard-module.d.ts +2 -0
  53. package/lib/electron-browser/keyboard/electron-keyboard-module.d.ts.map +1 -1
  54. package/lib/electron-browser/keyboard/electron-keyboard-module.js +8 -1
  55. package/lib/electron-browser/keyboard/electron-keyboard-module.js.map +1 -1
  56. package/lib/electron-browser/keyboard/electron-keyboard-module.spec.d.ts +2 -0
  57. package/lib/electron-browser/keyboard/electron-keyboard-module.spec.d.ts.map +1 -0
  58. package/lib/electron-browser/keyboard/electron-keyboard-module.spec.js +28 -0
  59. package/lib/electron-browser/keyboard/electron-keyboard-module.spec.js.map +1 -0
  60. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
  61. package/lib/electron-browser/menu/electron-main-menu-factory.js +3 -2
  62. package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
  63. package/lib/electron-browser/menu/electron-menu-contribution.d.ts.map +1 -1
  64. package/lib/electron-browser/menu/electron-menu-contribution.js +3 -0
  65. package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
  66. package/lib/electron-common/electron-api.d.ts +5 -0
  67. package/lib/electron-common/electron-api.d.ts.map +1 -1
  68. package/lib/electron-common/electron-api.js +21 -0
  69. package/lib/electron-common/electron-api.js.map +1 -1
  70. package/lib/electron-common/electron-api.spec.d.ts +2 -0
  71. package/lib/electron-common/electron-api.spec.d.ts.map +1 -0
  72. package/lib/electron-common/electron-api.spec.js +40 -0
  73. package/lib/electron-common/electron-api.spec.js.map +1 -0
  74. package/lib/electron-main/electron-api-main.d.ts +0 -1
  75. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  76. package/lib/electron-main/electron-api-main.js +1 -12
  77. package/lib/electron-main/electron-api-main.js.map +1 -1
  78. package/package.json +4 -4
  79. package/src/browser/common-frontend-contribution.ts +16 -0
  80. package/src/browser/keybinding.spec.ts +878 -34
  81. package/src/browser/keybinding.ts +470 -85
  82. package/src/browser/keyboard/browser-keyboard-frontend-contribution.ts +1 -1
  83. package/src/browser/keyboard/browser-keyboard-layout-provider.spec.ts +52 -1
  84. package/src/browser/keyboard/browser-keyboard-layout-provider.ts +5 -5
  85. package/src/browser/keyboard/keyboard-layout-service.spec.ts +640 -15
  86. package/src/browser/keyboard/keyboard-layout-service.ts +358 -106
  87. package/src/browser/keyboard/keys.spec.ts +206 -1
  88. package/src/browser/menu/action-menu-node.spec.ts +60 -0
  89. package/src/browser/menu/action-menu-node.ts +8 -14
  90. package/src/browser/menu/browser-menu-plugin.ts +1 -1
  91. package/src/browser/test/jsdom.ts +11 -0
  92. package/src/common/keyboard/keyboard-layout-provider.ts +8 -0
  93. package/src/common/keys.ts +311 -26
  94. package/src/common/ternary-search-tree.spec.ts +14 -0
  95. package/src/common/ternary-search-tree.ts +2 -2
  96. package/src/electron-browser/keyboard/electron-keyboard-module.spec.ts +29 -0
  97. package/src/electron-browser/keyboard/electron-keyboard-module.ts +9 -2
  98. package/src/electron-browser/menu/electron-main-menu-factory.ts +3 -3
  99. package/src/electron-browser/menu/electron-menu-contribution.ts +4 -0
  100. package/src/electron-common/electron-api.spec.ts +42 -0
  101. package/src/electron-common/electron-api.ts +23 -0
  102. package/src/electron-main/electron-api-main.ts +2 -13
@@ -59,9 +59,13 @@ var KeybindingContexts;
59
59
  })(KeybindingContexts || (exports.KeybindingContexts = KeybindingContexts = {}));
60
60
  let KeybindingRegistry = class KeybindingRegistry {
61
61
  constructor() {
62
- this.keySequence = [];
62
+ this.keySequenceCandidates = [[]];
63
+ /** Whether keyboard shortcut troubleshooting logs are enabled. */
64
+ this.keyboardShortcutsTroubleshooting = false;
63
65
  this.contexts = {};
64
66
  this.keymaps = [...Array(KeybindingScope.length)].map(() => []);
67
+ this.handledKeyboardEvents = new WeakSet();
68
+ this.warnedAmbiguousSequences = new Set();
65
69
  this.keybindingsChanged = new event_1.Emitter();
66
70
  this.toResetKeymap = new Map();
67
71
  }
@@ -69,10 +73,11 @@ let KeybindingRegistry = class KeybindingRegistry {
69
73
  static { this.PASSTHROUGH_PSEUDO_COMMAND = 'passthrough'; }
70
74
  async onStart() {
71
75
  await this.keyboardLayoutService.initialize();
72
- this.keyboardLayoutService.onKeyboardLayoutChanged(newLayout => {
73
- this.clearResolvedKeybindings();
74
- this.invalidateKeybindingTree();
75
- this.keybindingsChanged.fire(undefined);
76
+ this.keyboardLayoutService.onKeyboardLayoutChanged(() => this.invalidateResolvedKeybindings());
77
+ this.corePreferences.onPreferenceChanged(change => {
78
+ if (change.preferenceName === 'keyboard.dispatch') {
79
+ this.invalidateResolvedKeybindings();
80
+ }
76
81
  });
77
82
  this.registerContext(KeybindingContexts.NOOP_CONTEXT);
78
83
  this.registerContext(KeybindingContexts.DEFAULT_CONTEXT);
@@ -190,13 +195,54 @@ let KeybindingRegistry = class KeybindingRegistry {
190
195
  resolveKeybinding(binding) {
191
196
  if (!binding.resolved) {
192
197
  const sequence = keys_1.KeySequence.parse(binding.keybinding);
193
- binding.resolved = sequence.map(code => this.keyboardLayoutService.resolveKeyCode(code));
198
+ const resolved = this.corePreferences['keyboard.dispatch'] === 'keyCode'
199
+ ? sequence
200
+ : sequence.map(code => this.keyboardLayoutService.resolveKeyCode(code));
201
+ binding.inactiveReason = this.getInactiveReason(resolved, this.corePreferences['keyboard.dispatch']);
202
+ binding.inactive = binding.inactiveReason !== undefined;
203
+ binding.resolved = resolved;
194
204
  }
195
205
  return binding.resolved;
196
206
  }
207
+ getInactiveReason(sequence, dispatch) {
208
+ if (dispatch === 'keyCode' && sequence.some(code => code.character !== undefined && !code.key)) {
209
+ return nls_1.nls.localize('theia/core/keybinding/characterRequiresCodeDispatch', 'Character keybindings require the "keyboard.dispatch" preference to be set to "code".');
210
+ }
211
+ if (sequence.some(code => code.shift && (0, keys_1.layoutModifiersIncludeAltGraph)(code.layoutModifiers)
212
+ && !(0, keys_1.layoutModifiersIncludeShift)(code.layoutModifiers))) {
213
+ return nls_1.nls.localize('theia/core/keybinding/shiftedAltGraphCharacterUnavailable', 'The logical character and Shift command modifier cannot be produced together on the current keyboard layout.');
214
+ }
215
+ // On macOS, Option selects the AltGraph layer. On Windows, AltGr is reported as Ctrl+Alt, so both Ctrl and Alt are
216
+ // consumed by the layout layer. Linux keeps Alt and AltGraph distinct.
217
+ const altGraphCodes = os_1.isOSX || os_1.isWindows ? sequence.filter(code => (0, keys_1.layoutModifiersIncludeAltGraph)(code.layoutModifiers)) : [];
218
+ const consumedModifier = altGraphCodes.some(code => code.alt) ? (os_1.isOSX ? 'Option' : 'Alt')
219
+ : os_1.isWindows && altGraphCodes.some(code => code.ctrl) ? 'Ctrl' : undefined;
220
+ if (consumedModifier) {
221
+ return nls_1.nls.localize('theia/core/keybinding/commandModifierAltGraphCharacterUnavailable', 'The logical character and the {0} command modifier cannot be produced together on the current keyboard layout.', consumedModifier);
222
+ }
223
+ if (sequence.some(code => !code.supportedByLayout)) {
224
+ return nls_1.nls.localize('theia/core/keybinding/unavailableCharacter', 'The key is not available on the current keyboard layout.');
225
+ }
226
+ return undefined;
227
+ }
228
+ /** Return the localized explanation when a binding is inactive on the current layout. */
229
+ getKeybindingInactiveReason(binding) {
230
+ this.resolveKeybinding(binding);
231
+ return binding.inactiveReason;
232
+ }
233
+ /** Return whether a binding is inactive on the current keyboard layout. */
234
+ isKeybindingInactive(binding) {
235
+ this.resolveKeybinding(binding);
236
+ return !!binding.inactive;
237
+ }
238
+ /** Clear cached resolutions, invalidate the lookup tree, and notify listeners. */
239
+ invalidateResolvedKeybindings() {
240
+ this.clearResolvedKeybindings();
241
+ this.invalidateKeybindingTree();
242
+ this.keybindingsChanged.fire(undefined);
243
+ }
197
244
  /**
198
- * Clear all `resolved` properties of registered keybindings so the KeyboardLayoutService is called
199
- * again to resolve them. This is necessary when the user's keyboard layout has changed.
245
+ * Clear all `resolved` properties of registered keybindings so the keyboard layout service resolves them again.
200
246
  */
201
247
  clearResolvedKeybindings() {
202
248
  for (let i = KeybindingScope.DEFAULT; i < KeybindingScope.END; i++) {
@@ -204,6 +250,8 @@ let KeybindingRegistry = class KeybindingRegistry {
204
250
  for (let j = 0; j < bindings.length; j++) {
205
251
  const binding = bindings[j];
206
252
  binding.resolved = undefined;
253
+ binding.inactive = undefined;
254
+ binding.inactiveReason = undefined;
207
255
  }
208
256
  }
209
257
  }
@@ -212,6 +260,7 @@ let KeybindingRegistry = class KeybindingRegistry {
212
260
  */
213
261
  invalidateKeybindingTree() {
214
262
  this.keybindingTree = undefined;
263
+ this.warnedAmbiguousSequences.clear();
215
264
  }
216
265
  /**
217
266
  * Gets or builds the keybinding tree for efficient lookup.
@@ -237,13 +286,16 @@ let KeybindingRegistry = class KeybindingRegistry {
237
286
  for (const binding of this.keymaps[scope]) {
238
287
  try {
239
288
  const resolved = this.resolveKeybinding(binding);
289
+ if (this.isKeybindingInactive(binding)) {
290
+ continue;
291
+ }
292
+ const runtime = { binding, sequence: resolved };
240
293
  const existing = tree.get(resolved);
241
294
  if (existing) {
242
- // Append to maintain correct order: higher scopes first, then keymaps order within scope
243
- existing.push(binding);
295
+ existing.push(runtime);
244
296
  }
245
297
  else {
246
- tree.set(resolved, [binding]);
298
+ tree.set(resolved, [runtime]);
247
299
  }
248
300
  }
249
301
  catch (error) {
@@ -261,38 +313,78 @@ let KeybindingRegistry = class KeybindingRegistry {
261
313
  * @returns true if there is a colliding keybinding
262
314
  */
263
315
  containsKeybindingInScope(binding, scope = KeybindingScope.USER) {
264
- const bindingKeySequence = this.resolveKeybinding(binding);
265
- const collisions = this.getKeySequenceCollisions(this.getUsableBindings(this.keymaps[scope]), bindingKeySequence)
266
- .filter(b => b.context === binding.context && !b.when && !binding.when);
267
- if (collisions.full.length > 0) {
268
- return true;
316
+ const diagnostics = this.getKeybindingCollisionDiagnostics(binding, scope);
317
+ return [diagnostics.authored, diagnostics.layoutDerived].some(result => result.full.length > 0 || result.partial.length > 0 || result.shadow.length > 0);
318
+ }
319
+ /** Return authored, layout-resolved, and shadowing collision diagnostics for a binding. */
320
+ getKeybindingCollisionDiagnostics(binding, scope = KeybindingScope.USER) {
321
+ const diagnostics = {
322
+ authored: new KeybindingRegistry_1.KeybindingsResult(),
323
+ layoutDerived: new KeybindingRegistry_1.KeybindingsResult(),
324
+ shadowedBy: []
325
+ };
326
+ const sequence = this.resolveKeybinding(binding);
327
+ if (this.isKeybindingInactive(binding)) {
328
+ return diagnostics;
269
329
  }
270
- if (collisions.partial.length > 0) {
271
- return true;
330
+ const candidates = this.getUsableBindings(this.keymaps[scope]).filter(candidate => candidate !== binding
331
+ && candidate.context === binding.context && !candidate.when && !binding.when);
332
+ for (const candidate of candidates) {
333
+ const resolved = this.resolveKeybinding(candidate);
334
+ const target = resolved.some(code => code.layoutModifiers !== 'none')
335
+ ? diagnostics.layoutDerived
336
+ : diagnostics.authored;
337
+ target.merge(this.getKeySequenceCollisions([candidate], sequence));
272
338
  }
273
- if (collisions.shadow.length > 0) {
274
- return true;
339
+ diagnostics.shadowedBy.push(...this.getShadowingKeybindings(binding, scope));
340
+ return diagnostics;
341
+ }
342
+ /** Return bindings whose command-modifier interpretation takes the keystroke instead of the supplied binding. */
343
+ getShadowingKeybindings(binding, scope) {
344
+ const sequence = this.resolveKeybinding(binding);
345
+ if (!sequence.some(code => code.layoutModifiers !== 'none')) {
346
+ return [];
275
347
  }
276
- return false;
348
+ const commandSequence = sequence.map(code => code.layoutModifiers !== 'none' ? new keys_1.KeyCode({
349
+ key: code.key,
350
+ ctrl: code.ctrl,
351
+ shift: code.shift || (0, keys_1.layoutModifiersIncludeShift)(code.layoutModifiers),
352
+ alt: code.alt || (0, keys_1.layoutModifiersIncludeAltGraph)(code.layoutModifiers),
353
+ meta: code.meta,
354
+ character: code.key?.easyString,
355
+ interpretation: 'commandModifiers'
356
+ }) : code);
357
+ const result = [];
358
+ const matchesContext = (candidate) => candidate !== binding
359
+ && candidate.context === binding.context && candidate.when === binding.when;
360
+ const firstScope = scope ?? KeybindingScope.END - 1;
361
+ const lastScope = scope ?? KeybindingScope.DEFAULT;
362
+ for (let currentScope = firstScope; currentScope >= lastScope; currentScope--) {
363
+ const collisions = this.getKeySequenceCollisions(this.getUsableBindings(this.keymaps[currentScope]), commandSequence);
364
+ result.push(...collisions.full.filter(matchesContext));
365
+ }
366
+ return result;
277
367
  }
278
368
  /**
279
369
  * Get a user visible representation of a {@link common.Keybinding}.
280
370
  * @returns an array of strings representing all elements of the {@link KeySequence} defined by the {@link common.Keybinding}
281
371
  * @param keybinding the keybinding
282
372
  * @param separator the separator to be used to stringify {@link KeyCode}s that are part of the {@link KeySequence}
373
+ * @param form whether to render logical characters or their physical realization
283
374
  */
284
- acceleratorFor(keybinding, separator = ' ', asciiOnly = false) {
375
+ acceleratorFor(keybinding, separator = ' ', asciiOnly = false, form = 'logical') {
285
376
  const bindingKeySequence = this.resolveKeybinding(keybinding);
286
- return this.acceleratorForSequence(bindingKeySequence, separator, asciiOnly);
377
+ return this.acceleratorForSequence(bindingKeySequence, separator, asciiOnly, form);
287
378
  }
288
379
  /**
289
380
  * Get a user visible representation of a {@link KeySequence}.
290
381
  * @returns an array of strings representing all elements of the {@link KeySequence}
291
382
  * @param keySequence the keysequence
292
383
  * @param separator the separator to be used to stringify {@link KeyCode}s that are part of the {@link KeySequence}
384
+ * @param form whether to render logical characters or their physical realization
293
385
  */
294
- acceleratorForSequence(keySequence, separator = ' ', asciiOnly = false) {
295
- return keySequence.map(keyCode => this.acceleratorForKeyCode(keyCode, separator, asciiOnly));
386
+ acceleratorForSequence(keySequence, separator = ' ', asciiOnly = false, form = 'logical') {
387
+ return keySequence.map(keyCode => this.acceleratorForKeyCode(keyCode, separator, asciiOnly, form));
296
388
  }
297
389
  /**
298
390
  * Get a user visible representation of a key code (a key with modifiers).
@@ -302,12 +394,31 @@ let KeybindingRegistry = class KeybindingRegistry {
302
394
  * Ignored when rendering macOS symbols (i.e. on macOS with `asciiOnly=false`), as the convention is
303
395
  * to juxtapose modifier symbols without a separator (e.g. `⌃⌘V`).
304
396
  * @param asciiOnly if `true`, no special characters will be substituted into the string returned. Ensures correct keyboard shortcuts in Electron menus.
397
+ * @param form whether to render the logical character or its physical realization
305
398
  */
306
- acceleratorForKeyCode(keyCode, separator = ' ', asciiOnly = false) {
399
+ acceleratorForKeyCode(keyCode, separator = ' ', asciiOnly = false, form = 'logical') {
307
400
  const useSymbols = os_1.isOSX && !asciiOnly;
308
- return this.componentsForKeyCode(keyCode, asciiOnly).join(useSymbols ? '' : separator);
401
+ return this.componentsForKeyCode(keyCode, asciiOnly, form).join(useSymbols ? '' : separator);
309
402
  }
310
- componentsForKeyCode(keyCode, asciiOnly = false) {
403
+ componentsForKeyCode(keyCode, asciiOnly = false, form = 'logical') {
404
+ if (form === 'physical') {
405
+ const physical = new keys_1.KeyCode({
406
+ key: keyCode.key,
407
+ meta: keyCode.meta,
408
+ ctrl: keyCode.ctrl,
409
+ alt: keyCode.alt,
410
+ shift: keyCode.shift || (0, keys_1.layoutModifiersIncludeShift)(keyCode.layoutModifiers)
411
+ });
412
+ const result = this.componentsForKeyCode(physical, asciiOnly);
413
+ if ((0, keys_1.layoutModifiersIncludeAltGraph)(keyCode.layoutModifiers)) {
414
+ const layoutModifier = os_1.isOSX ? (asciiOnly ? 'Option' : '⌥') : 'AltGr';
415
+ const keyIndex = result.length - (physical.key ? 1 : 0);
416
+ if (!(os_1.isOSX && keyCode.alt)) {
417
+ result.splice(keyIndex, 0, layoutModifier);
418
+ }
419
+ }
420
+ return result;
421
+ }
311
422
  const keyCodeResult = [];
312
423
  const useSymbols = os_1.isOSX && !asciiOnly;
313
424
  if (keyCode.meta && os_1.isOSX) {
@@ -319,14 +430,27 @@ let KeybindingRegistry = class KeybindingRegistry {
319
430
  if (keyCode.alt) {
320
431
  keyCodeResult.push(useSymbols ? '⌥' : 'Alt');
321
432
  }
322
- if (keyCode.shift) {
433
+ if (this.displaysShift(keyCode)) {
323
434
  keyCodeResult.push(useSymbols ? '⇧' : 'Shift');
324
435
  }
325
- if (keyCode.key) {
436
+ if (keyCode.character) {
437
+ keyCodeResult.push(this.acceleratorForCharacter(keyCode.character));
438
+ }
439
+ else if (keyCode.key) {
326
440
  keyCodeResult.push(this.acceleratorForKey(keyCode.key, asciiOnly));
327
441
  }
328
442
  return keyCodeResult;
329
443
  }
444
+ /**
445
+ * Whether the logical label of a key code shows Shift: when Shift is a command modifier or produces an uppercase letter.
446
+ * Shift that only selects a layout layer for other characters (e.g. `/` on German layouts) is not shown.
447
+ */
448
+ displaysShift(keyCode) {
449
+ return keyCode.shift || (0, keys_1.layoutModifiersIncludeShift)(keyCode.layoutModifiers) && !!keyCode.character && /^[A-Z]$/.test(keyCode.character);
450
+ }
451
+ acceleratorForCharacter(character) {
452
+ return /^[a-z]$/i.test(character) ? character.toUpperCase() : character;
453
+ }
330
454
  /**
331
455
  * @param asciiOnly if `true`, no special characters will be substituted into the string returned. Ensures correct keyboard shortcuts in Electron menus.
332
456
  *
@@ -370,6 +494,9 @@ let KeybindingRegistry = class KeybindingRegistry {
370
494
  for (const binding of bindings) {
371
495
  try {
372
496
  const bindingKeySequence = this.resolveKeybinding(binding);
497
+ if (this.isKeybindingInactive(binding)) {
498
+ continue;
499
+ }
373
500
  const compareResult = keys_1.KeySequence.compare(candidate, bindingKeySequence);
374
501
  switch (compareResult) {
375
502
  case keys_1.KeySequence.CompareResult.FULL: {
@@ -418,6 +545,20 @@ let KeybindingRegistry = class KeybindingRegistry {
418
545
  }
419
546
  return result;
420
547
  }
548
+ /**
549
+ * Get the accelerator of the first keybinding of a command that is enabled in the given context.
550
+ * Physical accelerators (for native menus) are omitted for inactive keybindings.
551
+ */
552
+ acceleratorForCommand(commandId, context, form = 'logical') {
553
+ const binding = this.getKeybindingsForCommand(commandId).find(candidate => this.isEnabledInScope(candidate, context));
554
+ if (!binding) {
555
+ return [];
556
+ }
557
+ if (form === 'physical') {
558
+ return this.isKeybindingInactive(binding) ? [] : this.acceleratorFor(binding, '+', true, 'physical');
559
+ }
560
+ return this.acceleratorFor(binding, '+');
561
+ }
421
562
  isActive(binding) {
422
563
  /* Pseudo commands like "passthrough" are always active (and not found
423
564
  in the command registry). */
@@ -470,19 +611,60 @@ let KeybindingRegistry = class KeybindingRegistry {
470
611
  }
471
612
  return true;
472
613
  }
614
+ /**
615
+ * Executes the command by dispatching its first keybinding: the resolved key codes are matched directly,
616
+ * and a DOM keydown marked as already handled is dispatched to the target for other listeners to observe.
617
+ */
473
618
  dispatchCommand(id, target) {
474
619
  const keybindings = this.getKeybindingsForCommand(id);
475
- if (keybindings.length) {
620
+ if (keybindings.length && !this.isKeybindingInactive(keybindings[0])) {
476
621
  for (const keyCode of this.resolveKeybinding(keybindings[0])) {
477
- this.dispatchKeyDown(keyCode, target);
622
+ const input = {
623
+ key: keyCode.character ?? keyCode.key?.code,
624
+ code: keyCode.key?.code,
625
+ keyCode: keyCode.key?.keyCode,
626
+ ctrlKey: keyCode.ctrl,
627
+ shiftKey: keyCode.shift || (0, keys_1.layoutModifiersIncludeShift)(keyCode.layoutModifiers),
628
+ altKey: keyCode.alt,
629
+ metaKey: keyCode.meta,
630
+ altGraph: (0, keys_1.layoutModifiersIncludeAltGraph)(keyCode.layoutModifiers)
631
+ };
632
+ this.dispatchNormalizedKeyDown(input, target, [keyCode]);
633
+ this.dispatchKeyDown({ ...input, modifierAltGraph: input.altGraph }, target, true);
478
634
  }
479
635
  }
480
636
  }
481
- dispatchKeyDown(input, target = document.activeElement || window) {
637
+ /** Dispatch a DOM keydown, optionally marking it as already handled by direct normalized dispatch. */
638
+ dispatchKeyDown(input, target = document.activeElement || window, alreadyHandled = false) {
482
639
  const eventInit = this.asKeyboardEventInit(input);
483
- const emulatedKeyboardEvent = new KeyboardEvent('keydown', eventInit);
640
+ const emulatedKeyboardEvent = new KeyboardEvent('keydown', { ...eventInit, cancelable: true });
641
+ if (alreadyHandled) {
642
+ this.handledKeyboardEvents.add(emulatedKeyboardEvent);
643
+ }
484
644
  target.dispatchEvent(emulatedKeyboardEvent);
485
645
  }
646
+ /**
647
+ * Runs normalized keyboard data directly through keybinding matching without dispatching a DOM event.
648
+ */
649
+ dispatchNormalizedKeyDown(input, target, keyCodes) {
650
+ const normalizedInput = (0, keys_1.normalizeKeyboardInput)(input);
651
+ const eventTarget = target ?? (typeof document === 'undefined' ? new EventTarget() : document.activeElement || window);
652
+ const event = new KeyboardEvent('keydown', this.asKeyboardEventInit(normalizedInput));
653
+ Object.defineProperty(event, 'target', { value: eventTarget });
654
+ this.runNormalizedKeyboardInput(normalizedInput, event, keyCodes);
655
+ }
656
+ isKeyboardShortcutsTroubleshooting() {
657
+ return this.keyboardShortcutsTroubleshooting;
658
+ }
659
+ /**
660
+ * Mirrors VS Code's `workbench.action.toggleKeybindingsLog` command.
661
+ * It logs how each key press was interpreted so keyboard-layout problems can be diagnosed on other machines.
662
+ */
663
+ toggleKeyboardShortcutsTroubleshooting() {
664
+ this.keyboardShortcutsTroubleshooting = !this.keyboardShortcutsTroubleshooting;
665
+ this.logger.info(`Keyboard shortcuts troubleshooting is now ${this.keyboardShortcutsTroubleshooting ? 'active' : 'inactive'}.`);
666
+ return this.keyboardShortcutsTroubleshooting;
667
+ }
486
668
  asKeyboardEventInit(input) {
487
669
  if (typeof input === 'string') {
488
670
  return this.asKeyboardEventInit(keys_1.KeyCode.createKeyCode(input));
@@ -514,17 +696,7 @@ let KeybindingRegistry = class KeybindingRegistry {
514
696
  inComposition = false;
515
697
  };
516
698
  win.document.addEventListener('compositionend', compositionEnd);
517
- const keydown = (event) => {
518
- // Skip IME-related keydowns. `inComposition` covers the textarea input path;
519
- // `isComposing` and `keyCode === 229` cover the native EditContext path where
520
- // composition events don't reach the DOM. The keyCode check catches keys that
521
- // finalize a composition (e.g. arrow keys) where isComposing is already false.
522
- // eslint-disable-next-line @typescript-eslint/no-deprecated
523
- if (inComposition || event.isComposing || event.keyCode === 229) {
524
- return;
525
- }
526
- this.run(event);
527
- };
699
+ const keydown = (event) => this.run(event, inComposition);
528
700
  win.document.addEventListener('keydown', keydown, true);
529
701
  return disposable_1.Disposable.create(() => {
530
702
  win.document.removeEventListener('compositionstart', compositionStart);
@@ -535,38 +707,196 @@ let KeybindingRegistry = class KeybindingRegistry {
535
707
  /**
536
708
  * Run the command matching to the given keyboard event.
537
709
  */
538
- run(event) {
539
- if (event.defaultPrevented) {
710
+ run(event, inComposition = false) {
711
+ const input = (0, keys_1.normalizeKeyboardInput)(event);
712
+ // The textarea input path reports composition through separate DOM events.
713
+ if (inComposition) {
714
+ this.logKeyboardTroubleshooting(input, undefined, undefined, 'composition');
540
715
  return;
541
716
  }
542
- const eventDispatch = this.corePreferences['keyboard.dispatch'];
543
- const keyCode = keys_1.KeyCode.createKeyCode(event, eventDispatch);
717
+ this.runNormalizedKeyboardInput(input, event);
718
+ }
719
+ /** Return all runtime interpretations of normalized keyboard input. */
720
+ getKeyCodeInterpretations(input) {
721
+ return this.keyboardLayoutService.getKeyCodeInterpretations((0, keys_1.normalizeKeyboardInput)(input), this.corePreferences['keyboard.dispatch']);
722
+ }
723
+ /** Return the authored representation that should be persisted for normalized keyboard input. */
724
+ authoredKeyCodeForKeyboardInput(input) {
725
+ const interpretations = this.getKeyCodeInterpretations(input);
726
+ // macOS Option is recorded as the `alt` command modifier rather than as the character it produces.
727
+ const optionAsCommandModifier = os_1.isOSX && !!input.altKey;
728
+ const keyCode = interpretations.find(candidate => candidate.interpretation === 'layoutModifiers' && (!optionAsCommandModifier || candidate.alt))
729
+ ?? interpretations[0];
730
+ if (!keyCode || keyCode.isModifierOnly()) {
731
+ return keyCode;
732
+ }
733
+ const fallbackLetter = keyCode.key && this.keyboardLayoutService.getLatinLetterFallback(keyCode.key);
734
+ if (fallbackLetter && keyCode.character && !(0, keys_1.layoutModifiersIncludeAltGraph)(keyCode.layoutModifiers)
735
+ && keyCode.character.toLocaleLowerCase() !== fallbackLetter) {
736
+ const shift = keyCode.shift || (0, keys_1.layoutModifiersIncludeShift)(keyCode.layoutModifiers);
737
+ return new keys_1.KeyCode({
738
+ key: keyCode.key,
739
+ ctrl: keyCode.ctrl,
740
+ alt: keyCode.alt,
741
+ meta: keyCode.meta,
742
+ shift,
743
+ character: shift ? fallbackLetter.toLocaleUpperCase() : fallbackLetter
744
+ });
745
+ }
746
+ if (keyCode.interpretation !== 'layoutModifiers' || !keyCode.character) {
747
+ return keyCode;
748
+ }
749
+ try {
750
+ const parsedCharacter = keys_1.KeyCode.parse(keyCode.character);
751
+ const logical = new keys_1.KeyCode({
752
+ key: parsedCharacter.key,
753
+ ctrl: keyCode.ctrl,
754
+ shift: keyCode.shift,
755
+ alt: keyCode.alt,
756
+ meta: keyCode.meta,
757
+ character: keyCode.character,
758
+ authoredToken: parsedCharacter.authoredToken
759
+ });
760
+ const persisted = keys_1.KeyCode.parse(logical.toString());
761
+ if (this.keyboardLayoutService.resolveKeyCode(persisted).dispatchString() === keyCode.dispatchString()) {
762
+ return logical;
763
+ }
764
+ return new keys_1.KeyCode({
765
+ key: logical.key,
766
+ ctrl: logical.ctrl,
767
+ shift: logical.shift || (0, keys_1.layoutModifiersIncludeShift)(keyCode.layoutModifiers),
768
+ alt: logical.alt,
769
+ meta: logical.meta,
770
+ character: logical.character
771
+ });
772
+ }
773
+ catch {
774
+ return new keys_1.KeyCode({
775
+ key: keyCode.key,
776
+ ctrl: keyCode.ctrl,
777
+ shift: keyCode.shift || (0, keys_1.layoutModifiersIncludeShift)(keyCode.layoutModifiers),
778
+ alt: keyCode.alt,
779
+ meta: keyCode.meta,
780
+ character: keyCode.key?.easyString
781
+ });
782
+ }
783
+ }
784
+ /** Return the keybinding stroke the keybinding recorder persists for normalized keyboard input, or `undefined` for modifier-only input. */
785
+ authoredKeybindingStringForKeyboardInput(input) {
786
+ const keyCode = this.authoredKeyCodeForKeyboardInput(input);
787
+ return keyCode && !keyCode.isModifierOnly() ? keyCode.toAuthoredKeybindingString() : undefined;
788
+ }
789
+ runNormalizedKeyboardInput(input, event, keyCodesOverride) {
790
+ // Skip IME-related keydowns. `isComposing` and `keyCode === 229` cover the native EditContext path where
791
+ // composition events do not reach the DOM. The keyCode check catches keys that finalize a composition,
792
+ // such as arrow keys, when isComposing is already false.
793
+ if (input.isComposing || input.keyCode === 229) {
794
+ this.logKeyboardTroubleshooting(input, undefined, undefined, 'composition');
795
+ return;
796
+ }
797
+ if (this.handledKeyboardEvents.has(event) || event.defaultPrevented) {
798
+ this.logKeyboardTroubleshooting(input, undefined, undefined, this.handledKeyboardEvents.has(event) ? 'alreadyHandled' : 'defaultPrevented');
799
+ return;
800
+ }
801
+ const keyCodes = keyCodesOverride ?? this.getKeyCodeInterpretations(input);
802
+ const keyCode = keyCodes[0];
544
803
  /* Keycode is only a modifier, next keycode will be modifier + key.
545
804
  Ignore this one. */
546
805
  if (keyCode.isModifierOnly()) {
806
+ this.logKeyboardTroubleshooting(input, keyCode, undefined, 'modifierOnly');
547
807
  return;
548
808
  }
549
- this.keyboardLayoutService.validateKeyCode(keyCode);
550
- this.keySequence.push(keyCode);
551
- const match = this.matchKeybinding(this.keySequence, event);
552
- if (match && match.kind === 'partial') {
553
- /* Accumulate the keysequence */
809
+ this.keyboardLayoutService.validateKeyCode(keyCode, input);
810
+ const candidates = this.keySequenceCandidates.flatMap(sequence => keyCodes.flatMap(keyCodeCandidate => {
811
+ const candidateSequence = [...sequence, keyCodeCandidate];
812
+ const candidateMatch = this.matchKeybinding(candidateSequence, event);
813
+ return candidateMatch ? [{ sequence: candidateSequence, keyCode: keyCodeCandidate, match: candidateMatch }] : [];
814
+ }));
815
+ const prioritized = candidates.map(candidate => ({ candidate, priority: this.matchPriority(candidate) }))
816
+ .sort((left, right) => right.priority - left.priority);
817
+ const selectedEntry = prioritized[0];
818
+ const selected = selectedEntry?.candidate;
819
+ const equallyPreferred = prioritized.filter(entry => entry.priority === selectedEntry?.priority).map(entry => entry.candidate);
820
+ const ambiguous = selected?.match.kind === 'full'
821
+ && equallyPreferred.some(candidate => candidate.match.kind === 'full'
822
+ && candidate.match.runtime.binding !== selected.match.runtime.binding);
823
+ const match = ambiguous ? undefined : selected?.match;
824
+ if (ambiguous) {
825
+ const sequence = selected.sequence.map(code => code.dispatchString()).join(' ');
826
+ if (!this.warnedAmbiguousSequences.has(sequence)) {
827
+ this.warnedAmbiguousSequences.add(sequence);
828
+ this.logger.warn(`Ambiguous keybinding sequence ignored: ${sequence}`);
829
+ }
830
+ }
831
+ this.logKeyboardTroubleshooting(input, selected?.keyCode ?? keyCode, match, ambiguous ? 'ambiguous' : undefined);
832
+ if (match?.kind === 'partial') {
833
+ this.keySequenceCandidates = candidates.filter(candidate => candidate.match.kind === 'partial').map(candidate => candidate.sequence);
554
834
  event.preventDefault();
555
835
  event.stopPropagation();
556
836
  this.statusBar.setElement('keybinding-status', {
557
- text: nls_1.nls.localize('theia/core/keybindingStatus', '{0} was pressed, waiting for more keys', `(${this.acceleratorForSequence(this.keySequence, '+')})`),
837
+ text: nls_1.nls.localize('theia/core/keybindingStatus', '{0} was pressed, waiting for more keys', `(${this.acceleratorForSequence(selected.sequence, '+')})`),
558
838
  alignment: status_bar_1.StatusBarAlignment.LEFT,
559
839
  priority: 2
560
840
  });
561
841
  }
562
842
  else {
563
- if (match && match.kind === 'full') {
564
- this.executeKeyBinding(match.binding, event);
843
+ if (match?.kind === 'full') {
844
+ this.executeKeyBinding(match.runtime.binding, event);
565
845
  }
566
- this.keySequence = [];
846
+ this.keySequenceCandidates = [[]];
567
847
  this.statusBar.removeElement('keybinding-status');
568
848
  }
569
849
  }
850
+ /** Higher values win; explicit command modifiers take precedence over layout-modifier interpretations. */
851
+ matchPriority(candidate) {
852
+ const usesLayoutModifiers = candidate.keyCode.interpretation === 'layoutModifiers';
853
+ return (usesLayoutModifiers ? 0 : 2) + (candidate.match.kind === 'full' ? 1 : 0);
854
+ }
855
+ logKeyboardTroubleshooting(input, keyCode, match, skipped) {
856
+ if (!this.keyboardShortcutsTroubleshooting) {
857
+ return;
858
+ }
859
+ this.logger.info('Keyboard shortcut troubleshooting', {
860
+ input: {
861
+ key: input.key,
862
+ code: input.code,
863
+ keyCode: input.keyCode,
864
+ ctrlKey: input.ctrlKey,
865
+ shiftKey: input.shiftKey,
866
+ altKey: input.altKey,
867
+ metaKey: input.metaKey,
868
+ altGraph: input.altGraph,
869
+ controlModifier: input.getModifierState?.('Control'),
870
+ altModifier: input.getModifierState?.('Alt'),
871
+ repeat: input.repeat,
872
+ isComposing: input.isComposing,
873
+ location: input.location,
874
+ timeStamp: input.timeStamp
875
+ },
876
+ normalized: keyCode && {
877
+ key: keyCode.key?.code,
878
+ character: keyCode.character,
879
+ ctrl: keyCode.ctrl,
880
+ shift: keyCode.shift,
881
+ alt: keyCode.alt,
882
+ meta: keyCode.meta,
883
+ layoutModifiers: keyCode.layoutModifiers,
884
+ interpretation: keyCode.interpretation,
885
+ dispatch: keyCode.dispatchString()
886
+ },
887
+ layout: {
888
+ info: this.keyboardLayoutService.layoutInfo,
889
+ source: this.keyboardLayoutService.layoutSource
890
+ },
891
+ match: match && {
892
+ kind: match.kind,
893
+ command: match.runtime.binding.command,
894
+ keybinding: match.runtime.binding.keybinding,
895
+ interpretations: match.runtime.sequence.map(code => code.interpretation)
896
+ },
897
+ skipped
898
+ });
899
+ }
570
900
  /**
571
901
  * Match first binding in the current context.
572
902
  * Keybindings ordered by a scope and by a registration order within the scope.
@@ -574,6 +904,10 @@ let KeybindingRegistry = class KeybindingRegistry {
574
904
  * Uses a ternary search tree for efficient O(log n) lookup instead of O(n) iteration.
575
905
  * The tree is lazily built and invalidated when keybindings change.
576
906
  *
907
+ * `keySequence` must be a resolved sequence from {@link resolveKeybinding} or a runtime interpretation from
908
+ * {@link getKeyCodeInterpretations}. Lookup uses {@link KeyCode.dispatchString} identity, so raw
909
+ * {@link KeySequence.parse} output only matches when parsing and resolution coincide, such as `keyboard.dispatch: 'keyCode'`.
910
+ *
577
911
  * When multiple bindings match, bindings that use context keys local to the focused
578
912
  * element are given priority over bindings that only use global context keys.
579
913
  */
@@ -595,32 +929,28 @@ let KeybindingRegistry = class KeybindingRegistry {
595
929
  // Check for exact (FULL) matches first
596
930
  const fullMatches = tree.get(keySequence);
597
931
  if (fullMatches) {
598
- // Collect all enabled bindings
599
- const enabledBindings = fullMatches.filter(isEnabled);
600
- if (enabledBindings.length > 0) {
601
- // If we have multiple enabled bindings, prioritize those using local context keys
602
- const selectedBinding = this.selectBindingByLocalContext(enabledBindings, event);
603
- return { kind: 'full', binding: selectedBinding };
932
+ const enabledForms = fullMatches.filter(runtimeForm => isEnabled(runtimeForm.binding));
933
+ if (enabledForms.length > 0) {
934
+ const selectedBinding = this.selectBindingByLocalContext(enabledForms.map(candidate => candidate.binding), event);
935
+ const runtimeForm = enabledForms.find(candidate => candidate.binding === selectedBinding);
936
+ return { kind: 'full', runtime: runtimeForm };
604
937
  }
605
938
  }
606
939
  // Check for PARTIAL matches (bindings that start with keySequence but are longer)
607
940
  const partialIterator = tree.findSuperstr(keySequence);
608
941
  if (partialIterator) {
609
- // Collect all partial match bindings and sort by scope
610
- const partialBindings = [];
942
+ const partialForms = [];
611
943
  let result = partialIterator.next();
612
944
  while (!result.done) {
613
- partialBindings.push(...result.value);
945
+ partialForms.push(...result.value);
614
946
  result = partialIterator.next();
615
947
  }
616
- // Sort by scope descending (highest scope first)
617
- partialBindings.sort((a, b) => b.scope - a.scope);
618
- // Collect all enabled bindings
619
- const enabledBindings = partialBindings.filter(isEnabled);
620
- if (enabledBindings.length > 0) {
621
- // If we have multiple enabled bindings, prioritize those using local context keys
622
- const selectedBinding = this.selectBindingByLocalContext(enabledBindings, event);
623
- return { kind: 'partial', binding: selectedBinding };
948
+ partialForms.sort((a, b) => b.binding.scope - a.binding.scope);
949
+ const enabledForms = partialForms.filter(runtimeForm => isEnabled(runtimeForm.binding));
950
+ if (enabledForms.length > 0) {
951
+ const selectedBinding = this.selectBindingByLocalContext(enabledForms.map(candidate => candidate.binding), event);
952
+ const runtimeForm = enabledForms.find(candidate => candidate.binding === selectedBinding);
953
+ return { kind: 'partial', runtime: runtimeForm };
624
954
  }
625
955
  }
626
956
  return undefined;