@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
@@ -15,20 +15,31 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { injectable, inject, optional } from 'inversify';
18
- import type { ILinuxKeyMapping, IMacKeyMapping, IWindowsKeyMapping } from 'native-keymap';
18
+ import type { IKeyboardLayoutInfo, ILinuxKeyMapping, IMacKeyMapping, IWindowsKeyMapping } from 'native-keymap';
19
19
  import { isOSX, isWindows } from '../../common/os';
20
20
  import {
21
21
  NativeKeyboardLayout, KeyboardLayoutProvider, KeyboardLayoutChangeNotifier, KeyValidator
22
22
  } from '../../common/keyboard/keyboard-layout-provider';
23
23
  import { Emitter, Event } from '../../common/event';
24
- import { KeyCode, Key } from './keys';
24
+ import {
25
+ KeyCode, Key, LayoutModifiers, NormalizedKeyboardInput, isPrintableCharacter, isPrintableKey,
26
+ layoutModifiersIncludeAltGraph, layoutModifiersIncludeShift
27
+ } from './keys';
28
+
29
+ /**
30
+ * A physical key and keyboard-layout modifier layer that can produce a logical character.
31
+ */
32
+ export interface KeyboardLayoutCandidate {
33
+ readonly key: Key;
34
+ readonly character: string;
35
+ readonly layoutModifiers: LayoutModifiers;
36
+ }
25
37
 
26
38
  export interface KeyboardLayout {
27
- /**
28
- * Mapping of standard US keyboard keys to the actual key codes to use.
29
- * See `KeyboardLayoutService.getCharacterIndex` for the index computation.
30
- */
31
- readonly key2KeyCode: KeyCode[];
39
+ readonly candidatesByCharacter: ReadonlyMap<string, readonly KeyboardLayoutCandidate[]>;
40
+ readonly candidatesByFoldedCharacter: ReadonlyMap<string, readonly KeyboardLayoutCandidate[]>;
41
+ /** Physical KeyboardEvent code to the lowercase Latin letter registered as a fallback. */
42
+ readonly latinFallbackByCode: ReadonlyMap<string, string>;
32
43
  /**
33
44
  * Mapping of KeyboardEvent codes to the characters shown on the user's keyboard
34
45
  * for the respective keys.
@@ -48,11 +59,25 @@ export class KeyboardLayoutService {
48
59
  @inject(KeyValidator) @optional()
49
60
  protected readonly keyValidator?: KeyValidator;
50
61
 
51
- private currentLayout?: KeyboardLayout;
62
+ // The transformed layout used for logical-character resolution and display.
63
+ protected currentLayout?: KeyboardLayout;
64
+ // The provider's original layout data used to verify characters produced by key events.
65
+ protected currentNativeLayout?: NativeKeyboardLayout;
66
+ protected currentLayoutInfo?: IKeyboardLayoutInfo;
67
+
68
+ get layoutInfo(): IKeyboardLayoutInfo | undefined {
69
+ return this.currentLayoutInfo;
70
+ }
71
+
72
+ get layoutSource(): string {
73
+ return this.layoutProvider.layoutSource ?? 'unknown';
74
+ }
52
75
 
53
76
  protected updateLayout(newLayout: NativeKeyboardLayout): KeyboardLayout {
54
77
  const transformed = this.transformNativeLayout(newLayout);
55
78
  this.currentLayout = transformed;
79
+ this.currentNativeLayout = newLayout;
80
+ this.currentLayoutInfo = newLayout.info;
56
81
  this.keyboardLayoutChanged.fire(transformed);
57
82
  return transformed;
58
83
  }
@@ -70,25 +95,238 @@ export class KeyboardLayoutService {
70
95
  }
71
96
 
72
97
  /**
73
- * Resolve a KeyCode of a keybinding using the current keyboard layout.
74
- * If no keyboard layout has been detected or the layout does not contain the
75
- * key used in the KeyCode, the KeyCode is returned unchanged.
98
+ * Resolve an authored keybinding stroke against the active layout.
99
+ * Physical and non-printable strokes, and strokes resolved without an active layout, are returned unchanged. Latin letters
100
+ * missing from the active layout resolve to their US key position; any other missing logical character is returned with
101
+ * {@link KeyCode.supportedByLayout} set to `false`. Authored Shift is absorbed when it produces the character (US-key tokens
102
+ * or shift-layer candidates); otherwise it remains a command modifier.
76
103
  */
77
104
  resolveKeyCode(inCode: KeyCode): KeyCode {
78
- const layout = this.currentLayout;
79
- if (layout && inCode.key) {
80
- for (let shift = 0; shift <= 1; shift++) {
81
- const index = this.getCharacterIndex(inCode.key, !!shift);
82
- const mappedCode = layout.key2KeyCode[index];
83
- if (mappedCode) {
84
- const transformed = this.transformKeyCode(inCode, mappedCode, !!shift);
85
- if (transformed) {
86
- return transformed;
87
- }
88
- }
105
+ if (inCode.physical || !this.currentLayout || inCode.key && !isPrintableKey(inCode.key) && !inCode.character) {
106
+ return inCode;
107
+ }
108
+ const usCharacter = inCode.key ? US_CHARACTER_BY_KEY.get(usCharacterIndex(inCode.key, inCode.shift)) : undefined;
109
+ const character = inCode.character ?? usCharacter ?? inCode.key?.easyString;
110
+ if (!character) {
111
+ return inCode;
112
+ }
113
+ const exactCandidates = this.currentLayout.candidatesByCharacter.get(character);
114
+ const candidate = exactCandidates?.[0] ?? this.currentLayout.candidatesByFoldedCharacter.get(character.toLocaleLowerCase())?.[0];
115
+ if (!candidate) {
116
+ return new KeyCode({
117
+ key: inCode.key,
118
+ meta: inCode.meta,
119
+ ctrl: inCode.ctrl,
120
+ shift: inCode.shift,
121
+ alt: inCode.alt,
122
+ character,
123
+ authoredToken: inCode.authoredToken,
124
+ supportedByLayout: false
125
+ });
126
+ }
127
+ return new KeyCode({
128
+ key: candidate.key,
129
+ meta: inCode.meta,
130
+ ctrl: inCode.ctrl,
131
+ shift: inCode.shift && !usCharacter && !layoutModifiersIncludeShift(candidate.layoutModifiers),
132
+ alt: inCode.alt,
133
+ character,
134
+ layoutModifiers: candidate.layoutModifiers,
135
+ interpretation: 'authored',
136
+ authoredToken: inCode.authoredToken
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Whether the platform's AltGraph layer is active: AltGraph on Linux, Option on macOS, and AltGraph or Ctrl+Alt on Windows.
142
+ */
143
+ protected isAltGraphActive(input: NormalizedKeyboardInput): boolean {
144
+ return isOSX ? !!input.altKey : isWindows ? !!input.altGraph || !!(input.ctrlKey && input.altKey) : !!input.altGraph;
145
+ }
146
+
147
+ /**
148
+ * Detect the keyboard-layout modifier layer that produced an input character.
149
+ * `undefined` means either that no layout modifiers were needed or that the input represents a dead key.
150
+ */
151
+ detectLayoutModifiers(input: NormalizedKeyboardInput): LayoutModifiers | undefined {
152
+ if (input.isComposing || !input.code || !this.shouldIncludeKey(input.code) || !isPrintableCharacter(input.key)) {
153
+ return undefined;
154
+ }
155
+ const mapping = this.getNativeMapping(input.code);
156
+ if (!mapping) {
157
+ return undefined;
158
+ }
159
+ const altGraphActive = this.isAltGraphActive(input);
160
+ // Caps Lock can invert the case of the produced character, so layers are compared case-insensitively.
161
+ const key = input.key.toLocaleLowerCase();
162
+ const producedBy = (character: string | undefined, isDeadKey: boolean | undefined): boolean =>
163
+ !!character && !isDeadKey && character.toLocaleLowerCase() === key;
164
+ if (input.shiftKey && altGraphActive && producedBy(mapping.withShiftAltGr, mapping.withShiftAltGrIsDeadKey)) {
165
+ return 'shiftAltGraph';
166
+ }
167
+ if (input.shiftKey && altGraphActive && !mapping.withShiftAltGr && producedBy(mapping.withAltGr, mapping.withAltGrIsDeadKey)) {
168
+ return 'shiftAltGraph';
169
+ }
170
+ if (!input.shiftKey && altGraphActive && producedBy(mapping.withAltGr, mapping.withAltGrIsDeadKey)) {
171
+ return 'altGraph';
172
+ }
173
+ if (input.shiftKey && producedBy(mapping.withShift, mapping.withShiftIsDeadKey)) {
174
+ return 'shift';
175
+ }
176
+ return undefined;
177
+ }
178
+
179
+ protected getNativeMapping(code: string): (ILinuxKeyMapping & Partial<IMacKeyMapping>) | undefined {
180
+ return this.currentNativeLayout?.mapping[code] as (ILinuxKeyMapping & Partial<IMacKeyMapping>) | undefined;
181
+ }
182
+
183
+ /**
184
+ * Return the character committed by the detected layout layer, which can differ from the input by case, e.g. because
185
+ * Caps Lock inverted the case of the produced character. Returns `undefined` when the layer character is unknown.
186
+ */
187
+ protected layerCharacter(input: NormalizedKeyboardInput, layoutModifiers: LayoutModifiers | undefined): string | undefined {
188
+ if (!input.code || !this.shouldIncludeKey(input.code) || !isPrintableCharacter(input.key)) {
189
+ return undefined;
190
+ }
191
+ const mapping = this.getNativeMapping(input.code);
192
+ if (!mapping) {
193
+ return undefined;
194
+ }
195
+ if (layoutModifiers === 'shift') {
196
+ return mapping.withShift && !mapping.withShiftIsDeadKey ? mapping.withShift : undefined;
197
+ }
198
+ if (layoutModifiers === 'shiftAltGraph' && mapping.withShiftAltGr) {
199
+ return !mapping.withShiftAltGrIsDeadKey ? mapping.withShiftAltGr : undefined;
200
+ }
201
+ if (layoutModifiers === 'altGraph' || layoutModifiers === 'shiftAltGraph') {
202
+ return mapping.withAltGr && !mapping.withAltGrIsDeadKey ? mapping.withAltGr : undefined;
203
+ }
204
+ if (layoutModifiers === undefined && !input.shiftKey && !this.isAltGraphActive(input) && mapping.value && !mapping.valueIsDeadKey
205
+ && mapping.value.toLocaleLowerCase() === input.key.toLocaleLowerCase()) {
206
+ return mapping.value;
207
+ }
208
+ return undefined;
209
+ }
210
+
211
+ /**
212
+ * Return all command- and layout-modifier interpretations of a normalized keyboard input.
213
+ * The committed character follows the detected layout layer, so Caps Lock does not change the case of the character.
214
+ */
215
+ getKeyCodeInterpretations(input: NormalizedKeyboardInput, eventDispatch: 'code' | 'keyCode'): KeyCode[] {
216
+ let keyCode = KeyCode.createKeyCode(input, eventDispatch);
217
+ if (eventDispatch === 'keyCode' || keyCode.isModifierOnly()) {
218
+ return [keyCode];
219
+ }
220
+ const layoutModifiers = this.detectLayoutModifiers(input);
221
+ const character = this.layerCharacter(input, layoutModifiers);
222
+ if (character !== undefined && character !== keyCode.character) {
223
+ keyCode = new KeyCode({ ...keyCode, character });
224
+ }
225
+ if (isOSX) {
226
+ return this.getMacKeyCodeInterpretations(keyCode, input, layoutModifiers);
227
+ }
228
+ if (isWindows) {
229
+ return this.getWindowsKeyCodeInterpretations(keyCode, input, layoutModifiers);
230
+ }
231
+ return this.getLinuxKeyCodeInterpretations(keyCode, input, layoutModifiers);
232
+ }
233
+
234
+ protected getLinuxKeyCodeInterpretations(keyCode: KeyCode, input: NormalizedKeyboardInput, layoutModifiers: LayoutModifiers | undefined): KeyCode[] {
235
+ if (!layoutModifiers || layoutModifiersIncludeAltGraph(layoutModifiers) && !input.altGraph) {
236
+ return [keyCode];
237
+ }
238
+ return layoutModifiers === 'shift'
239
+ ? this.shiftLayerInterpretations(keyCode, layoutModifiers)
240
+ : [this.toLayoutModifiersKeyCode(keyCode, layoutModifiers)];
241
+ }
242
+
243
+ protected getWindowsKeyCodeInterpretations(keyCode: KeyCode, input: NormalizedKeyboardInput, layoutModifiers: LayoutModifiers | undefined): KeyCode[] {
244
+ if (!layoutModifiers) {
245
+ return [keyCode];
246
+ }
247
+ if (!layoutModifiersIncludeAltGraph(layoutModifiers)) {
248
+ return this.shiftLayerInterpretations(keyCode, layoutModifiers);
249
+ }
250
+ if (input.ctrlKey && input.altKey) {
251
+ // Windows reports AltGr as Ctrl+Alt. A real Ctrl cannot be distinguished from it, so both are layout artifacts.
252
+ return [
253
+ new KeyCode({ ...keyCode, interpretation: 'commandModifiers' }),
254
+ this.toLayoutModifiersKeyCode(keyCode, layoutModifiers, { ctrl: true, alt: true })
255
+ ];
256
+ }
257
+ if (input.altGraph) {
258
+ return [this.toLayoutModifiersKeyCode(keyCode, layoutModifiers)];
259
+ }
260
+ return [keyCode];
261
+ }
262
+
263
+ /**
264
+ * macOS has no AltGraph key: Option selects the layout's AltGraph layer but is also the `alt` command modifier used by
265
+ * the default macOS bindings. Option layers therefore yield interpretations that keep Option as a command modifier and
266
+ * carry the character the key produces without Option, followed by the interpretation that attributes Option to the layout.
267
+ */
268
+ protected getMacKeyCodeInterpretations(keyCode: KeyCode, input: NormalizedKeyboardInput, layoutModifiers: LayoutModifiers | undefined): KeyCode[] {
269
+ const mapping = input.code && this.shouldIncludeKey(input.code) ? this.getNativeMapping(input.code) : undefined;
270
+ if (!layoutModifiers) {
271
+ if (!input.altKey || keyCode.character) {
272
+ return [keyCode];
89
273
  }
274
+ // Dead Option keys commit no character. Option is a command modifier on macOS, so the key's base character is used.
275
+ const deadKeyBaseCharacter = input.shiftKey
276
+ ? mapping?.withShift && !mapping.withShiftIsDeadKey ? mapping.withShift : undefined
277
+ : mapping?.value && !mapping.valueIsDeadKey ? mapping.value : undefined;
278
+ if (!isPrintableCharacter(deadKeyBaseCharacter)) {
279
+ return [keyCode];
280
+ }
281
+ const optionCommand = new KeyCode({ ...keyCode, character: deadKeyBaseCharacter, interpretation: 'commandModifiers' });
282
+ return input.shiftKey ? this.shiftLayerInterpretations(optionCommand, 'shift') : [optionCommand];
283
+ }
284
+ if (!layoutModifiersIncludeAltGraph(layoutModifiers)) {
285
+ return this.shiftLayerInterpretations(keyCode, layoutModifiers);
286
+ }
287
+ if (!input.altKey) {
288
+ return [keyCode];
90
289
  }
91
- return inCode;
290
+ const optionLayer = this.toLayoutModifiersKeyCode(keyCode, layoutModifiers, { alt: true });
291
+ if (layoutModifiers === 'shiftAltGraph') {
292
+ const shiftCharacter = mapping?.withShift && !mapping.withShiftIsDeadKey ? mapping.withShift : keyCode.character;
293
+ const optionCommand = new KeyCode({ ...keyCode, character: shiftCharacter, interpretation: 'commandModifiers' });
294
+ return [
295
+ optionCommand,
296
+ this.toLayoutModifiersKeyCode(optionCommand, 'shift'),
297
+ optionLayer
298
+ ];
299
+ }
300
+ const baseCharacter = mapping?.value && !mapping.valueIsDeadKey ? mapping.value : keyCode.character;
301
+ return [
302
+ new KeyCode({ ...keyCode, character: baseCharacter, interpretation: 'commandModifiers' }),
303
+ optionLayer
304
+ ];
305
+ }
306
+
307
+ /** Interpret Shift either as a command modifier or as part of the keyboard layout. */
308
+ protected shiftLayerInterpretations(keyCode: KeyCode, layoutModifiers: LayoutModifiers): KeyCode[] {
309
+ if (!layoutModifiersIncludeShift(layoutModifiers)) {
310
+ return [keyCode];
311
+ }
312
+ return [
313
+ new KeyCode({ ...keyCode, interpretation: 'commandModifiers' }),
314
+ this.toLayoutModifiersKeyCode(keyCode, layoutModifiers)
315
+ ];
316
+ }
317
+
318
+ protected toLayoutModifiersKeyCode(keyCode: KeyCode, layoutModifiers: LayoutModifiers,
319
+ consumed: { ctrl?: boolean, alt?: boolean } = {}): KeyCode {
320
+ return new KeyCode({
321
+ key: keyCode.key,
322
+ character: keyCode.character,
323
+ ctrl: consumed.ctrl ? false : keyCode.ctrl,
324
+ meta: keyCode.meta,
325
+ shift: false,
326
+ alt: consumed.alt ? false : keyCode.alt,
327
+ layoutModifiers,
328
+ interpretation: 'layoutModifiers'
329
+ });
92
330
  }
93
331
 
94
332
  /**
@@ -116,40 +354,31 @@ export class KeyboardLayoutService {
116
354
  return key.easyString;
117
355
  }
118
356
 
357
+ /** Return the Latin letter registered for a physical key because the active layout does not produce it, or `undefined`. */
358
+ getLatinLetterFallback(key: Key): string | undefined {
359
+ return this.currentLayout?.latinFallbackByCode.get(key.code);
360
+ }
361
+
119
362
  /**
120
363
  * Called when a KeyboardEvent is processed by the KeybindingRegistry.
121
364
  * The KeyValidator may trigger a keyboard layout change.
122
365
  */
123
- validateKeyCode(keyCode: KeyCode): void {
124
- if (this.keyValidator && keyCode.key && keyCode.character) {
366
+ validateKeyCode(keyCode: KeyCode, input: NormalizedKeyboardInput): void {
367
+ const character = input.key;
368
+ if (this.keyValidator && keyCode.key && isPrintableCharacter(character)) {
125
369
  this.keyValidator.validateKey({
126
- code: keyCode.key.code,
127
- character: keyCode.character,
128
- shiftKey: keyCode.shift,
129
- ctrlKey: keyCode.ctrl,
130
- altKey: keyCode.alt
370
+ code: input.code ?? keyCode.key.code,
371
+ character,
372
+ shiftKey: input.shiftKey,
373
+ ctrlKey: input.ctrlKey,
374
+ altKey: input.altKey,
375
+ altGraph: this.isAltGraphActive(input)
131
376
  });
132
377
  }
133
378
  }
134
379
 
135
- protected transformKeyCode(inCode: KeyCode, mappedCode: KeyCode, keyNeedsShift: boolean): KeyCode | undefined {
136
- if (!inCode.shift && keyNeedsShift) {
137
- return undefined;
138
- }
139
- if (mappedCode.alt && (inCode.alt || inCode.ctrl || inCode.shift && !keyNeedsShift)) {
140
- return undefined;
141
- }
142
- return new KeyCode({
143
- key: mappedCode.key,
144
- meta: inCode.meta,
145
- ctrl: inCode.ctrl || mappedCode.alt,
146
- shift: inCode.shift && !keyNeedsShift || mappedCode.shift,
147
- alt: inCode.alt || mappedCode.alt
148
- });
149
- }
150
-
151
380
  protected transformNativeLayout(nativeLayout: NativeKeyboardLayout): KeyboardLayout {
152
- const key2KeyCode: KeyCode[] = new Array(2 * (Key.MAX_KEY_CODE + 1));
381
+ const candidatesByCharacter = new Map<string, KeyboardLayoutCandidate[]>();
153
382
  const code2Character: { [code: string]: string } = {};
154
383
  const mapping = nativeLayout.mapping;
155
384
  for (const code in mapping) {
@@ -158,19 +387,33 @@ export class KeyboardLayoutService {
158
387
  const mappedKey = Key.getKey(code);
159
388
  if (mappedKey && this.shouldIncludeKey(code)) {
160
389
  if (isWindows) {
161
- this.addWindowsKeyMapping(key2KeyCode, mappedKey, (keyMapping as IWindowsKeyMapping).vkey, keyMapping.value);
162
- } else if (isOSX) {
163
- this.addMacKeyMappings(key2KeyCode, mappedKey, keyMapping as IMacKeyMapping);
164
- } else {
165
- this.addKeyMappings(key2KeyCode, mappedKey, keyMapping);
390
+ const windowsMapping = keyMapping as IWindowsKeyMapping & Partial<ILinuxKeyMapping>;
391
+ if (!windowsMapping.value && VKEY_TO_KEY[windowsMapping.vkey]) {
392
+ this.addKeyMapping(candidatesByCharacter, mappedKey, VKEY_TO_KEY[windowsMapping.vkey].easyString, 'none');
393
+ }
166
394
  }
395
+ this.addKeyMappings(candidatesByCharacter, mappedKey, keyMapping as ILinuxKeyMapping & Partial<IMacKeyMapping>);
167
396
  }
168
397
  if (keyMapping.value) {
169
398
  code2Character[code] = keyMapping.value;
170
399
  }
171
400
  }
172
401
  }
173
- return { key2KeyCode, code2Character };
402
+ const latinFallbackByCode = this.addMissingLatinLetterCandidates(candidatesByCharacter);
403
+ for (const candidates of candidatesByCharacter.values()) {
404
+ candidates.sort((left, right) => this.layoutModifierCost(left.layoutModifiers) - this.layoutModifierCost(right.layoutModifiers)
405
+ || left.key.code.localeCompare(right.key.code));
406
+ }
407
+ const candidatesByFoldedCharacter = new Map<string, KeyboardLayoutCandidate[]>();
408
+ for (const [character, candidates] of candidatesByCharacter) {
409
+ const folded = character.toLocaleLowerCase();
410
+ const existing = candidatesByFoldedCharacter.get(folded) ?? [];
411
+ existing.push(...candidates);
412
+ existing.sort((left, right) => this.layoutModifierCost(left.layoutModifiers) - this.layoutModifierCost(right.layoutModifiers)
413
+ || left.key.code.localeCompare(right.key.code));
414
+ candidatesByFoldedCharacter.set(folded, existing);
415
+ }
416
+ return { candidatesByCharacter, candidatesByFoldedCharacter, latinFallbackByCode, code2Character };
174
417
  }
175
418
 
176
419
  protected shouldIncludeKey(code: string): boolean {
@@ -181,73 +424,64 @@ export class KeyboardLayoutService {
181
424
  return !code.startsWith('Numpad');
182
425
  }
183
426
 
184
- private addKeyMappings(key2KeyCode: KeyCode[], mappedKey: Key, keyMapping: ILinuxKeyMapping): void {
185
- if (keyMapping.value) {
186
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.value, false, false);
187
- }
188
- if (keyMapping.withShift) {
189
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.withShift, true, false);
190
- }
191
- if (keyMapping.withAltGr) {
192
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.withAltGr, false, true);
193
- }
194
- if (keyMapping.withShiftAltGr) {
195
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.withShiftAltGr, true, true);
427
+ protected layoutModifierCost(layoutModifiers: LayoutModifiers): number {
428
+ return LAYOUT_MODIFIER_COST[layoutModifiers];
429
+ }
430
+
431
+ /**
432
+ * Register VS Code-compatible US-position fallbacks for Latin letters that the layout does not produce
433
+ * on its base or Shift layer. This is called once whenever the active keyboard layout is transformed.
434
+ */
435
+ protected addMissingLatinLetterCandidates(candidatesByCharacter: Map<string, KeyboardLayoutCandidate[]>): Map<string, string> {
436
+ const result = new Map<string, string>();
437
+ const produced = (lower: string, upper: string): boolean => [lower, upper].some(character =>
438
+ candidatesByCharacter.get(character)?.some(candidate =>
439
+ candidate.layoutModifiers === 'none' || candidate.layoutModifiers === 'shift'));
440
+
441
+ for (const lower of LATIN_LETTERS) {
442
+ const upper = lower.toUpperCase();
443
+ if (produced(lower, upper)) {
444
+ continue;
445
+ }
446
+ const key = VALUE_TO_KEY[lower].key;
447
+ const lowercaseCandidates = candidatesByCharacter.get(lower) ?? [];
448
+ lowercaseCandidates.push({ key, character: lower, layoutModifiers: 'none' });
449
+ candidatesByCharacter.set(lower, lowercaseCandidates);
450
+
451
+ const uppercaseCandidates = candidatesByCharacter.get(upper) ?? [];
452
+ uppercaseCandidates.push({ key, character: upper, layoutModifiers: 'shift' });
453
+ candidatesByCharacter.set(upper, uppercaseCandidates);
454
+ result.set(key.code, lower);
196
455
  }
456
+ return result;
197
457
  }
198
458
 
199
- private addMacKeyMappings(key2KeyCode: KeyCode[], mappedKey: Key, keyMapping: IMacKeyMapping): void {
200
- // Dead keys (e.g. the combining-accent layer of Option+E on macOS US) await a following keystroke to
201
- // compose a glyph and so never produce a committed character. They must not be registered as if they
202
- // produced their display glyph, which would otherwise collide with the real key for that glyph.
459
+ private addKeyMappings(candidatesByCharacter: Map<string, KeyboardLayoutCandidate[]>, mappedKey: Key,
460
+ keyMapping: ILinuxKeyMapping & Partial<IMacKeyMapping>): void {
461
+ // Dead keys await a following keystroke to compose a glyph and never produce a committed character. The flags are
462
+ // currently reported only on macOS, but applying the guards uniformly keeps resolution and detection symmetric.
203
463
  if (keyMapping.value && !keyMapping.valueIsDeadKey) {
204
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.value, false, false);
464
+ this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.value, 'none');
205
465
  }
206
466
  if (keyMapping.withShift && !keyMapping.withShiftIsDeadKey) {
207
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.withShift, true, false);
467
+ this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.withShift, 'shift');
208
468
  }
209
469
  if (keyMapping.withAltGr && !keyMapping.withAltGrIsDeadKey) {
210
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.withAltGr, false, true);
470
+ this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.withAltGr, 'altGraph');
211
471
  }
212
472
  if (keyMapping.withShiftAltGr && !keyMapping.withShiftAltGrIsDeadKey) {
213
- this.addKeyMapping(key2KeyCode, mappedKey, keyMapping.withShiftAltGr, true, true);
473
+ this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.withShiftAltGr, 'shiftAltGraph');
214
474
  }
215
475
  }
216
476
 
217
- private addKeyMapping(key2KeyCode: KeyCode[], mappedKey: Key, value: string, shift: boolean, alt: boolean): void {
218
- const key = VALUE_TO_KEY[value];
219
- if (key) {
220
- const index = this.getCharacterIndex(key.key, key.shift);
221
- if (key2KeyCode[index] === undefined) {
222
- key2KeyCode[index] = new KeyCode({
223
- key: mappedKey,
224
- shift,
225
- alt,
226
- character: value
227
- });
228
- }
229
- }
230
- }
231
-
232
- private addWindowsKeyMapping(key2KeyCode: KeyCode[], mappedKey: Key, vkey: string, value: string): void {
233
- const key = VKEY_TO_KEY[vkey];
234
- if (key) {
235
- const index = this.getCharacterIndex(key);
236
- if (key2KeyCode[index] === undefined) {
237
- key2KeyCode[index] = new KeyCode({
238
- key: mappedKey,
239
- character: value
240
- });
241
- }
242
- }
243
- }
244
-
245
- protected getCharacterIndex(key: Key, shift?: boolean): number {
246
- if (shift) {
247
- return Key.MAX_KEY_CODE + key.keyCode + 1;
248
- } else {
249
- return key.keyCode;
477
+ private addKeyMapping(candidatesByCharacter: Map<string, KeyboardLayoutCandidate[]>, mappedKey: Key,
478
+ value: string, layoutModifiers: LayoutModifiers): void {
479
+ if (Array.from(value).length !== 1) {
480
+ return;
250
481
  }
482
+ const candidates = candidatesByCharacter.get(value) ?? [];
483
+ candidates.push({ key: mappedKey, character: value, layoutModifiers });
484
+ candidatesByCharacter.set(value, candidates);
251
485
  }
252
486
 
253
487
  }
@@ -359,6 +593,24 @@ const VALUE_TO_KEY: { [value: string]: { key: Key, shift?: boolean } } = {
359
593
  ' ': { key: Key.SPACE },
360
594
  };
361
595
 
596
+ function usCharacterIndex(key: Key, shift = false): string {
597
+ return `${key.code}:${shift}`;
598
+ }
599
+
600
+ const LATIN_LETTERS = 'abcdefghijklmnopqrstuvwxyz';
601
+
602
+ const LAYOUT_MODIFIER_COST: Record<LayoutModifiers, number> = {
603
+ none: 0,
604
+ shift: 1,
605
+ altGraph: 2,
606
+ shiftAltGraph: 3
607
+ };
608
+
609
+ const US_CHARACTER_BY_KEY = new Map<string, string>();
610
+ for (const [character, value] of Object.entries(VALUE_TO_KEY)) {
611
+ US_CHARACTER_BY_KEY.set(usCharacterIndex(value.key, !!value.shift), character);
612
+ }
613
+
362
614
  /**
363
615
  * Mapping of Windows Virtual Keys to the corresponding keys on a standard US keyboard layout.
364
616
  */