@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.
- package/lib/browser/catalog.json +66 -0
- package/lib/browser/common-frontend-contribution.d.ts +6 -1
- package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/common-frontend-contribution.js +18 -1
- package/lib/browser/common-frontend-contribution.js.map +1 -1
- package/lib/browser/keybinding.d.ts +95 -17
- package/lib/browser/keybinding.d.ts.map +1 -1
- package/lib/browser/keybinding.js +405 -75
- package/lib/browser/keybinding.js.map +1 -1
- package/lib/browser/keybinding.spec.js +786 -30
- package/lib/browser/keybinding.spec.js.map +1 -1
- package/lib/browser/keyboard/browser-keyboard-frontend-contribution.js +1 -1
- package/lib/browser/keyboard/browser-keyboard-frontend-contribution.js.map +1 -1
- package/lib/browser/keyboard/browser-keyboard-layout-provider.d.ts +3 -4
- package/lib/browser/keyboard/browser-keyboard-layout-provider.d.ts.map +1 -1
- package/lib/browser/keyboard/browser-keyboard-layout-provider.js +3 -3
- package/lib/browser/keyboard/browser-keyboard-layout-provider.js.map +1 -1
- package/lib/browser/keyboard/browser-keyboard-layout-provider.spec.js +39 -0
- package/lib/browser/keyboard/browser-keyboard-layout-provider.spec.js.map +1 -1
- package/lib/browser/keyboard/keyboard-layout-service.d.ts +67 -15
- package/lib/browser/keyboard/keyboard-layout-service.d.ts.map +1 -1
- package/lib/browser/keyboard/keyboard-layout-service.js +309 -98
- package/lib/browser/keyboard/keyboard-layout-service.js.map +1 -1
- package/lib/browser/keyboard/keyboard-layout-service.spec.js +538 -12
- package/lib/browser/keyboard/keyboard-layout-service.spec.js.map +1 -1
- package/lib/browser/keyboard/keys.spec.js +183 -0
- package/lib/browser/keyboard/keys.spec.js.map +1 -1
- package/lib/browser/menu/action-menu-node.d.ts +5 -3
- package/lib/browser/menu/action-menu-node.d.ts.map +1 -1
- package/lib/browser/menu/action-menu-node.js +2 -11
- package/lib/browser/menu/action-menu-node.js.map +1 -1
- package/lib/browser/menu/action-menu-node.spec.d.ts +2 -0
- package/lib/browser/menu/action-menu-node.spec.d.ts.map +1 -0
- package/lib/browser/menu/action-menu-node.spec.js +54 -0
- package/lib/browser/menu/action-menu-node.spec.js.map +1 -0
- package/lib/browser/menu/browser-menu-plugin.js +1 -1
- package/lib/browser/menu/browser-menu-plugin.js.map +1 -1
- package/lib/browser/test/jsdom.d.ts.map +1 -1
- package/lib/browser/test/jsdom.js +11 -0
- package/lib/browser/test/jsdom.js.map +1 -1
- package/lib/common/keyboard/keyboard-layout-provider.d.ts +7 -0
- package/lib/common/keyboard/keyboard-layout-provider.d.ts.map +1 -1
- package/lib/common/keyboard/keyboard-layout-provider.js.map +1 -1
- package/lib/common/keys.d.ts +86 -3
- package/lib/common/keys.d.ts.map +1 -1
- package/lib/common/keys.js +262 -23
- package/lib/common/keys.js.map +1 -1
- package/lib/common/ternary-search-tree.js +2 -2
- package/lib/common/ternary-search-tree.js.map +1 -1
- package/lib/common/ternary-search-tree.spec.js +12 -0
- package/lib/common/ternary-search-tree.spec.js.map +1 -1
- package/lib/electron-browser/keyboard/electron-keyboard-module.d.ts +2 -0
- package/lib/electron-browser/keyboard/electron-keyboard-module.d.ts.map +1 -1
- package/lib/electron-browser/keyboard/electron-keyboard-module.js +8 -1
- package/lib/electron-browser/keyboard/electron-keyboard-module.js.map +1 -1
- package/lib/electron-browser/keyboard/electron-keyboard-module.spec.d.ts +2 -0
- package/lib/electron-browser/keyboard/electron-keyboard-module.spec.d.ts.map +1 -0
- package/lib/electron-browser/keyboard/electron-keyboard-module.spec.js +28 -0
- package/lib/electron-browser/keyboard/electron-keyboard-module.spec.js.map +1 -0
- package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
- package/lib/electron-browser/menu/electron-main-menu-factory.js +3 -2
- package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.d.ts.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.js +3 -0
- package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
- package/lib/electron-common/electron-api.d.ts +5 -0
- package/lib/electron-common/electron-api.d.ts.map +1 -1
- package/lib/electron-common/electron-api.js +21 -0
- package/lib/electron-common/electron-api.js.map +1 -1
- package/lib/electron-common/electron-api.spec.d.ts +2 -0
- package/lib/electron-common/electron-api.spec.d.ts.map +1 -0
- package/lib/electron-common/electron-api.spec.js +40 -0
- package/lib/electron-common/electron-api.spec.js.map +1 -0
- package/lib/electron-main/electron-api-main.d.ts +0 -1
- package/lib/electron-main/electron-api-main.d.ts.map +1 -1
- package/lib/electron-main/electron-api-main.js +1 -12
- package/lib/electron-main/electron-api-main.js.map +1 -1
- package/package.json +4 -4
- package/src/browser/common-frontend-contribution.ts +16 -0
- package/src/browser/keybinding.spec.ts +878 -34
- package/src/browser/keybinding.ts +470 -85
- package/src/browser/keyboard/browser-keyboard-frontend-contribution.ts +1 -1
- package/src/browser/keyboard/browser-keyboard-layout-provider.spec.ts +52 -1
- package/src/browser/keyboard/browser-keyboard-layout-provider.ts +5 -5
- package/src/browser/keyboard/keyboard-layout-service.spec.ts +640 -15
- package/src/browser/keyboard/keyboard-layout-service.ts +358 -106
- package/src/browser/keyboard/keys.spec.ts +206 -1
- package/src/browser/menu/action-menu-node.spec.ts +60 -0
- package/src/browser/menu/action-menu-node.ts +8 -14
- package/src/browser/menu/browser-menu-plugin.ts +1 -1
- package/src/browser/test/jsdom.ts +11 -0
- package/src/common/keyboard/keyboard-layout-provider.ts +8 -0
- package/src/common/keys.ts +311 -26
- package/src/common/ternary-search-tree.spec.ts +14 -0
- package/src/common/ternary-search-tree.ts +2 -2
- package/src/electron-browser/keyboard/electron-keyboard-module.spec.ts +29 -0
- package/src/electron-browser/keyboard/electron-keyboard-module.ts +9 -2
- package/src/electron-browser/menu/electron-main-menu-factory.ts +3 -3
- package/src/electron-browser/menu/electron-menu-contribution.ts +4 -0
- package/src/electron-common/electron-api.spec.ts +42 -0
- package/src/electron-common/electron-api.ts +23 -0
- 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 {
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
*
|
|
75
|
-
* key
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
shiftKey:
|
|
129
|
-
ctrlKey:
|
|
130
|
-
altKey:
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
// compose a glyph and
|
|
202
|
-
//
|
|
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(
|
|
464
|
+
this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.value, 'none');
|
|
205
465
|
}
|
|
206
466
|
if (keyMapping.withShift && !keyMapping.withShiftIsDeadKey) {
|
|
207
|
-
this.addKeyMapping(
|
|
467
|
+
this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.withShift, 'shift');
|
|
208
468
|
}
|
|
209
469
|
if (keyMapping.withAltGr && !keyMapping.withAltGrIsDeadKey) {
|
|
210
|
-
this.addKeyMapping(
|
|
470
|
+
this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.withAltGr, 'altGraph');
|
|
211
471
|
}
|
|
212
472
|
if (keyMapping.withShiftAltGr && !keyMapping.withShiftAltGrIsDeadKey) {
|
|
213
|
-
this.addKeyMapping(
|
|
473
|
+
this.addKeyMapping(candidatesByCharacter, mappedKey, keyMapping.withShiftAltGr, 'shiftAltGraph');
|
|
214
474
|
}
|
|
215
475
|
}
|
|
216
476
|
|
|
217
|
-
private addKeyMapping(
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
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
|
*/
|