@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
package/src/common/keys.ts
CHANGED
|
@@ -75,7 +75,7 @@ export namespace KeySequence {
|
|
|
75
75
|
|
|
76
76
|
export function parse(keybinding: string): KeySequence {
|
|
77
77
|
const keyCodes = [];
|
|
78
|
-
const rawKeyCodes = keybinding.trim()
|
|
78
|
+
const rawKeyCodes = splitKeySequence(keybinding.trim());
|
|
79
79
|
for (const rawKeyCode of rawKeyCodes) {
|
|
80
80
|
const keyCode = KeyCode.parse(rawKeyCode);
|
|
81
81
|
if (keyCode !== undefined) {
|
|
@@ -97,6 +97,35 @@ export interface Keystroke {
|
|
|
97
97
|
readonly modifiers?: KeyModifier[];
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The keyboard-layout modifier layer used to produce a logical character, ordered by resolution cost.
|
|
102
|
+
*/
|
|
103
|
+
export type LayoutModifiers = 'none' | 'shift' | 'altGraph' | 'shiftAltGraph';
|
|
104
|
+
|
|
105
|
+
export function layoutModifiersIncludeShift(layoutModifiers: LayoutModifiers): boolean {
|
|
106
|
+
return layoutModifiers === 'shift' || layoutModifiers === 'shiftAltGraph';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function layoutModifiersIncludeAltGraph(layoutModifiers: LayoutModifiers): boolean {
|
|
110
|
+
return layoutModifiers === 'altGraph' || layoutModifiers === 'shiftAltGraph';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isPrintableKey(key: Key): boolean {
|
|
114
|
+
return isPrintableCharacter(key.easyString);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isPrintableCharacter(character: string | undefined): character is string {
|
|
118
|
+
return !!character && character !== ' ' && isSingleCharacter(character);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Identifies how a key code was derived for runtime matching.
|
|
123
|
+
* `authored` is the constructor default and represents an authored binding, even for manually constructed instances.
|
|
124
|
+
* `commandModifiers` preserves the event's modifiers as command modifiers, while `layoutModifiers` attributes them to
|
|
125
|
+
* the keyboard layout's character-producing layer.
|
|
126
|
+
*/
|
|
127
|
+
export type KeyCodeInterpretation = 'authored' | 'commandModifiers' | 'layoutModifiers';
|
|
128
|
+
|
|
100
129
|
export interface KeyCodeSchema {
|
|
101
130
|
key?: Partial<Key>;
|
|
102
131
|
ctrl?: boolean;
|
|
@@ -104,6 +133,180 @@ export interface KeyCodeSchema {
|
|
|
104
133
|
alt?: boolean;
|
|
105
134
|
meta?: boolean;
|
|
106
135
|
character?: string;
|
|
136
|
+
layoutModifiers?: LayoutModifiers;
|
|
137
|
+
interpretation?: KeyCodeInterpretation;
|
|
138
|
+
supportedByLayout?: boolean;
|
|
139
|
+
authoredToken?: string;
|
|
140
|
+
physical?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Plain keyboard input data used by keybinding dispatch and diagnostics.
|
|
145
|
+
*/
|
|
146
|
+
export interface NormalizedKeyboardInput {
|
|
147
|
+
key?: string;
|
|
148
|
+
code?: string;
|
|
149
|
+
keyCode?: number;
|
|
150
|
+
charCode?: number;
|
|
151
|
+
ctrlKey?: boolean;
|
|
152
|
+
shiftKey?: boolean;
|
|
153
|
+
altKey?: boolean;
|
|
154
|
+
metaKey?: boolean;
|
|
155
|
+
altGraph?: boolean;
|
|
156
|
+
repeat?: boolean;
|
|
157
|
+
isComposing?: boolean;
|
|
158
|
+
location?: number;
|
|
159
|
+
timeStamp?: number;
|
|
160
|
+
keyIdentifier?: string;
|
|
161
|
+
getModifierState?(keyArg: string): boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Read AltGraph state, preferring explicitly transported state over native event state.
|
|
166
|
+
*/
|
|
167
|
+
export function readAltGraph(input: NormalizedKeyboardInput): boolean {
|
|
168
|
+
if (input.altGraph !== undefined) {
|
|
169
|
+
return input.altGraph;
|
|
170
|
+
}
|
|
171
|
+
return input.getModifierState?.('AltGraph') ?? false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Copy native or transported keyboard input into a normalized, transport-independent representation.
|
|
176
|
+
* The parameter and result share this type because a native `KeyboardEvent` structurally satisfies the permissive input
|
|
177
|
+
* shape. The returned data always has AltGraph resolved even though the field is optional in the input type.
|
|
178
|
+
* `getModifierState` is retained for local diagnostics only and must not be relied on after transport.
|
|
179
|
+
*/
|
|
180
|
+
export function normalizeKeyboardInput(input: NormalizedKeyboardInput): NormalizedKeyboardInput {
|
|
181
|
+
return {
|
|
182
|
+
key: input.key,
|
|
183
|
+
code: input.code,
|
|
184
|
+
keyCode: input.keyCode,
|
|
185
|
+
charCode: input.charCode,
|
|
186
|
+
ctrlKey: input.ctrlKey,
|
|
187
|
+
shiftKey: input.shiftKey,
|
|
188
|
+
altKey: input.altKey,
|
|
189
|
+
metaKey: input.metaKey,
|
|
190
|
+
altGraph: readAltGraph(input),
|
|
191
|
+
repeat: input.repeat,
|
|
192
|
+
isComposing: input.isComposing,
|
|
193
|
+
location: input.location,
|
|
194
|
+
timeStamp: input.timeStamp,
|
|
195
|
+
keyIdentifier: input.keyIdentifier,
|
|
196
|
+
getModifierState: input.getModifierState?.bind(input)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function splitKeySequence(keybinding: string): string[] {
|
|
201
|
+
const result: string[] = [];
|
|
202
|
+
let current = '';
|
|
203
|
+
let bracketed = false;
|
|
204
|
+
for (let index = 0; index < keybinding.length; index++) {
|
|
205
|
+
const character = keybinding[index];
|
|
206
|
+
if (character === '[') {
|
|
207
|
+
const closingBracket = keybinding.indexOf(']', index + 1);
|
|
208
|
+
const nextWhitespace = keybinding.slice(index + 1).search(/\s/);
|
|
209
|
+
bracketed = closingBracket !== -1 && (nextWhitespace === -1 || closingBracket < index + 1 + nextWhitespace);
|
|
210
|
+
} else if (character === ']' && bracketed) {
|
|
211
|
+
bracketed = false;
|
|
212
|
+
}
|
|
213
|
+
if (/\s/.test(character) && !bracketed) {
|
|
214
|
+
if (current) {
|
|
215
|
+
result.push(current);
|
|
216
|
+
current = '';
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
current += character;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (current) {
|
|
223
|
+
result.push(current);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function splitKeyCode(keybinding: string): string[] {
|
|
229
|
+
const result: string[] = [];
|
|
230
|
+
let current = '';
|
|
231
|
+
let bracketed = false;
|
|
232
|
+
for (let index = 0; index < keybinding.length; index++) {
|
|
233
|
+
const character = keybinding[index];
|
|
234
|
+
if (character === '[' && keybinding.indexOf(']', index + 1) !== -1) {
|
|
235
|
+
bracketed = true;
|
|
236
|
+
} else if (character === ']' && bracketed) {
|
|
237
|
+
bracketed = false;
|
|
238
|
+
}
|
|
239
|
+
if (!bracketed && current && (character === '+' || character === '-')) {
|
|
240
|
+
result.push(current);
|
|
241
|
+
current = '';
|
|
242
|
+
} else if (!bracketed && character === '+') {
|
|
243
|
+
continue;
|
|
244
|
+
} else {
|
|
245
|
+
current += character;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (current) {
|
|
249
|
+
result.push(current);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseScanCode(token: string): Key | undefined {
|
|
255
|
+
const match = /^\[([^\]]+)\]$/.exec(token);
|
|
256
|
+
if (!match || match[1].toLocaleLowerCase().startsWith('char:')) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
const key = Key.getKey(match[1]) ?? Object.values(CODE_TO_KEY).find(candidate => candidate.code.toLocaleLowerCase() === match[1].toLocaleLowerCase());
|
|
260
|
+
if (!key) {
|
|
261
|
+
throw new Error(nls.localize('theia/core/keybinding/unknownScanCode', 'Unknown scan code {0}. Use a supported KeyboardEvent.code name.', match[1]));
|
|
262
|
+
}
|
|
263
|
+
return key;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Parse a logical-character token, rejecting bare grammar delimiters. */
|
|
267
|
+
function parseCharacterToken(token: string): string | undefined {
|
|
268
|
+
const match = /^\[char:(.*)\]$/i.exec(token);
|
|
269
|
+
if (match) {
|
|
270
|
+
const payload = match[1];
|
|
271
|
+
let character: string;
|
|
272
|
+
if (/^0x[0-9a-f]+$/i.test(payload)) {
|
|
273
|
+
const value = Number.parseInt(payload.slice(2), 16);
|
|
274
|
+
if (value > 0x10ffff || value >= 0xd800 && value <= 0xdfff) {
|
|
275
|
+
throw new Error(nls.localize('theia/core/keybinding/invalidCharacter', 'Invalid Unicode character {0}.', payload));
|
|
276
|
+
}
|
|
277
|
+
character = String.fromCodePoint(value);
|
|
278
|
+
} else {
|
|
279
|
+
character = payload;
|
|
280
|
+
}
|
|
281
|
+
if (!isSingleCharacter(character) || !/^0x/i.test(payload) && RESERVED_BARE_CHARACTERS.has(character)) {
|
|
282
|
+
throw new Error(nls.localize('theia/core/keybinding/invalidCharacter', 'Invalid Unicode character {0}.', payload));
|
|
283
|
+
}
|
|
284
|
+
return character;
|
|
285
|
+
}
|
|
286
|
+
return isSingleCharacter(token) && !RESERVED_BARE_CHARACTERS.has(token) ? token : undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isSingleCharacter(value: string): boolean {
|
|
290
|
+
return Array.from(value).length === 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Characters reserved by the keybinding grammar and therefore unavailable as bare logical-character tokens. */
|
|
294
|
+
const RESERVED_BARE_CHARACTERS = new Set([' ', '+', '-', '[', ']']);
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Serialize a logical character for the keybinding grammar.
|
|
298
|
+
* `+` and `-` are excluded from legacy bare spelling because they delimit key-code tokens. Uppercase ASCII letters are
|
|
299
|
+
* escaped so their case is not lost by case-insensitive legacy key lookup.
|
|
300
|
+
*/
|
|
301
|
+
export function characterToken(character: string): string {
|
|
302
|
+
if (!isSingleCharacter(character)) {
|
|
303
|
+
throw new Error(nls.localize('theia/core/keybinding/invalidCharacter', 'Invalid Unicode character {0}.', character));
|
|
304
|
+
}
|
|
305
|
+
const legacyCharacter = !!EASY_TO_KEY[character] && character !== '+' && character !== '-';
|
|
306
|
+
const requiresEscape = /^[A-Z]$/.test(character) || RESERVED_BARE_CHARACTERS.has(character) && !legacyCharacter;
|
|
307
|
+
return requiresEscape
|
|
308
|
+
? `[char:0x${character.codePointAt(0)!.toString(16).toUpperCase()}]`
|
|
309
|
+
: character;
|
|
107
310
|
}
|
|
108
311
|
|
|
109
312
|
/**
|
|
@@ -117,6 +320,13 @@ export class KeyCode {
|
|
|
117
320
|
public readonly alt: boolean;
|
|
118
321
|
public readonly meta: boolean;
|
|
119
322
|
public readonly character: string | undefined;
|
|
323
|
+
public readonly layoutModifiers: LayoutModifiers;
|
|
324
|
+
public readonly interpretation: KeyCodeInterpretation;
|
|
325
|
+
/** Whether the authored logical character is available on the active keyboard layout. */
|
|
326
|
+
public readonly supportedByLayout: boolean;
|
|
327
|
+
public readonly authoredToken: string | undefined;
|
|
328
|
+
/** Whether the authored token denotes a physical scan code rather than a logical character. */
|
|
329
|
+
public readonly physical: boolean;
|
|
120
330
|
|
|
121
331
|
public constructor(schema: KeyCodeSchema) {
|
|
122
332
|
const key = schema.key;
|
|
@@ -134,13 +344,18 @@ export class KeyCode {
|
|
|
134
344
|
this.alt = !!schema.alt;
|
|
135
345
|
this.meta = !!schema.meta;
|
|
136
346
|
this.character = schema.character;
|
|
347
|
+
this.layoutModifiers = schema.layoutModifiers ?? 'none';
|
|
348
|
+
this.interpretation = schema.interpretation ?? 'authored';
|
|
349
|
+
this.supportedByLayout = schema.supportedByLayout !== false;
|
|
350
|
+
this.authoredToken = schema.authoredToken;
|
|
351
|
+
this.physical = !!schema.physical;
|
|
137
352
|
}
|
|
138
353
|
|
|
139
354
|
/**
|
|
140
355
|
* Return true if this KeyCode only contains modifiers.
|
|
141
356
|
*/
|
|
142
357
|
public isModifierOnly(): boolean {
|
|
143
|
-
return this.key === undefined;
|
|
358
|
+
return this.key === undefined && this.character === undefined;
|
|
144
359
|
}
|
|
145
360
|
|
|
146
361
|
/**
|
|
@@ -150,11 +365,11 @@ export class KeyCode {
|
|
|
150
365
|
if (this.key && (!other.key || this.key.code !== other.key.code) || !this.key && other.key) {
|
|
151
366
|
return false;
|
|
152
367
|
}
|
|
153
|
-
return this.
|
|
368
|
+
return this.dispatchString() === other.dispatchString();
|
|
154
369
|
}
|
|
155
370
|
|
|
156
|
-
|
|
157
|
-
* Return a keybinding string
|
|
371
|
+
/**
|
|
372
|
+
* Return a keybinding string that round-trips the authored spelling through {@link authoredToken}.
|
|
158
373
|
*/
|
|
159
374
|
toString(): string {
|
|
160
375
|
const result = [];
|
|
@@ -170,8 +385,69 @@ export class KeyCode {
|
|
|
170
385
|
if (this.ctrl) {
|
|
171
386
|
result.push(Key.CONTROL_LEFT.easyString);
|
|
172
387
|
}
|
|
388
|
+
if (this.authoredToken) {
|
|
389
|
+
result.push(this.authoredToken);
|
|
390
|
+
} else if (this.key) {
|
|
391
|
+
result.push(this.key.easyString);
|
|
392
|
+
}
|
|
393
|
+
return result.join('+');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Return the runtime match identity used for keybinding dispatch.
|
|
398
|
+
* The interpretation is diagnostic metadata and does not affect this identity.
|
|
399
|
+
*/
|
|
400
|
+
dispatchString(): string {
|
|
401
|
+
const result: string[] = [];
|
|
402
|
+
if (this.meta) {
|
|
403
|
+
result.push(SpecialCases.META);
|
|
404
|
+
}
|
|
405
|
+
if (this.shift) {
|
|
406
|
+
result.push(Key.SHIFT_LEFT.easyString);
|
|
407
|
+
}
|
|
408
|
+
if (this.alt) {
|
|
409
|
+
result.push(Key.ALT_LEFT.easyString);
|
|
410
|
+
}
|
|
411
|
+
if (this.ctrl) {
|
|
412
|
+
result.push(Key.CONTROL_LEFT.easyString);
|
|
413
|
+
}
|
|
173
414
|
if (this.key) {
|
|
174
415
|
result.push(this.key.easyString);
|
|
416
|
+
} else if (this.character) {
|
|
417
|
+
result.push(characterToken(this.character));
|
|
418
|
+
}
|
|
419
|
+
const suffix = this.layoutModifiers === 'shift' ? '@shift'
|
|
420
|
+
: this.layoutModifiers === 'altGraph' ? '@altgraph'
|
|
421
|
+
: this.layoutModifiers === 'shiftAltGraph' ? '@shift@altgraph' : '';
|
|
422
|
+
return result.join('+') + suffix;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Derive an authored keybinding stroke from a key event for recorder persistence.
|
|
427
|
+
* This cannot use {@link toString}: event-derived key codes have no {@link authoredToken}, so `toString` would persist
|
|
428
|
+
* the physical US key name instead of the committed logical character. Non-printable keys, Space and numpad keys are
|
|
429
|
+
* persisted by name; printable keys without a committed character are persisted as scan codes.
|
|
430
|
+
*/
|
|
431
|
+
toAuthoredKeybindingString(): string {
|
|
432
|
+
const result: string[] = [];
|
|
433
|
+
if (this.meta) {
|
|
434
|
+
result.push(SpecialCases.META);
|
|
435
|
+
}
|
|
436
|
+
const character = isPrintableCharacter(this.character) && !this.key?.code.startsWith('Numpad') ? this.character : undefined;
|
|
437
|
+
const shiftedLetter = !!character && /^[A-Z]$/.test(character);
|
|
438
|
+
if (this.shift && (!character || shiftedLetter)) {
|
|
439
|
+
result.push(Key.SHIFT_LEFT.easyString);
|
|
440
|
+
}
|
|
441
|
+
if (this.alt) {
|
|
442
|
+
result.push(Key.ALT_LEFT.easyString);
|
|
443
|
+
}
|
|
444
|
+
if (this.ctrl) {
|
|
445
|
+
result.push(Key.CONTROL_LEFT.easyString);
|
|
446
|
+
}
|
|
447
|
+
if (character) {
|
|
448
|
+
result.push(characterToken(shiftedLetter ? character.toLocaleLowerCase() : character));
|
|
449
|
+
} else if (this.key) {
|
|
450
|
+
result.push(isPrintableKey(this.key) ? `[${this.key.code}]` : this.key.easyString);
|
|
175
451
|
}
|
|
176
452
|
return result.join('+');
|
|
177
453
|
}
|
|
@@ -179,7 +455,7 @@ export class KeyCode {
|
|
|
179
455
|
/**
|
|
180
456
|
* Create a KeyCode from one of several input types.
|
|
181
457
|
*/
|
|
182
|
-
public static createKeyCode(input: KeyboardEvent | Keystroke | KeyCodeSchema | string, eventDispatch: 'code' | 'keyCode' = 'code'): KeyCode {
|
|
458
|
+
public static createKeyCode(input: KeyboardEvent | NormalizedKeyboardInput | Keystroke | KeyCodeSchema | string, eventDispatch: 'code' | 'keyCode' = 'code'): KeyCode {
|
|
183
459
|
if (typeof input === 'string') {
|
|
184
460
|
const parts = input.split('+');
|
|
185
461
|
if (!KeyCode.isModifierString(parts[0])) {
|
|
@@ -189,7 +465,7 @@ export class KeyCode {
|
|
|
189
465
|
});
|
|
190
466
|
}
|
|
191
467
|
return KeyCode.createKeyCode({ modifiers: parts as KeyModifier[] });
|
|
192
|
-
} else if (KeyCode.isKeyboardEvent(input)) {
|
|
468
|
+
} else if (KeyCode.isKeyboardEvent(input) || KeyCode.isNormalizedKeyboardInput(input)) {
|
|
193
469
|
const key = KeyCode.toKey(input, eventDispatch);
|
|
194
470
|
return new KeyCode({
|
|
195
471
|
key: Key.isModifier(key.code) ? undefined : key,
|
|
@@ -238,25 +514,14 @@ export class KeyCode {
|
|
|
238
514
|
}
|
|
239
515
|
|
|
240
516
|
const schema: KeyCodeSchema = {};
|
|
241
|
-
const keys =
|
|
242
|
-
let currentKey = '';
|
|
243
|
-
for (const character of keybinding.trim().toLowerCase()) {
|
|
244
|
-
if (currentKey && (character === '-' || character === '+')) {
|
|
245
|
-
keys.push(currentKey);
|
|
246
|
-
currentKey = '';
|
|
247
|
-
} else if (character !== '+') {
|
|
248
|
-
currentKey += character;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (currentKey) {
|
|
252
|
-
keys.push(currentKey);
|
|
253
|
-
}
|
|
517
|
+
const keys = splitKeyCode(keybinding.trim());
|
|
254
518
|
/* If duplicates i.e ctrl+ctrl+a or alt+alt+b or b+alt+b it is invalid */
|
|
255
|
-
if (keys.length !== new Set(keys).size) {
|
|
519
|
+
if (keys.length !== new Set(keys.map(key => key.toLocaleLowerCase())).size) {
|
|
256
520
|
throw new Error(nls.localize('theia/core/keybinding/duplicateModifierError', "Can't parse keybinding {0} Duplicate modifiers", keybinding));
|
|
257
521
|
}
|
|
258
522
|
|
|
259
|
-
for (
|
|
523
|
+
for (const token of keys) {
|
|
524
|
+
let keyString = token.toLocaleLowerCase();
|
|
260
525
|
if (SPECIAL_ALIASES[keyString] !== undefined) {
|
|
261
526
|
keyString = SPECIAL_ALIASES[keyString];
|
|
262
527
|
}
|
|
@@ -289,7 +554,20 @@ export class KeyCode {
|
|
|
289
554
|
schema.key = key;
|
|
290
555
|
}
|
|
291
556
|
} else {
|
|
292
|
-
|
|
557
|
+
const scanCode = parseScanCode(token);
|
|
558
|
+
if (scanCode) {
|
|
559
|
+
schema.key = scanCode;
|
|
560
|
+
schema.authoredToken = `[${scanCode.code}]`;
|
|
561
|
+
schema.physical = true;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const character = parseCharacterToken(token);
|
|
565
|
+
if (character) {
|
|
566
|
+
schema.character = character;
|
|
567
|
+
schema.authoredToken = characterToken(character);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
throw new Error(nls.localize('theia/core/keybinding/unrecognizedKeyError', 'Unrecognized key {0} in {1}', token, keybinding));
|
|
293
571
|
}
|
|
294
572
|
}
|
|
295
573
|
|
|
@@ -339,6 +617,13 @@ export namespace KeyCode {
|
|
|
339
617
|
return false;
|
|
340
618
|
}
|
|
341
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Return true if the given object carries normalized keyboard input fields.
|
|
622
|
+
*/
|
|
623
|
+
export function isNormalizedKeyboardInput(input: object): input is NormalizedKeyboardInput {
|
|
624
|
+
return 'code' in input || 'keyCode' in input || 'charCode' in input || 'keyIdentifier' in input;
|
|
625
|
+
}
|
|
626
|
+
|
|
342
627
|
/**
|
|
343
628
|
* Determine the pressed key of a keyboard event. This key should correspond to the physical key according
|
|
344
629
|
* to a standard US keyboard layout. International keyboard layouts are handled by `KeyboardLayoutService`.
|
|
@@ -346,7 +631,7 @@ export namespace KeyCode {
|
|
|
346
631
|
* `keyIdentifier` is used to access this deprecated field:
|
|
347
632
|
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyIdentifier
|
|
348
633
|
*/
|
|
349
|
-
export function toKey(event:
|
|
634
|
+
export function toKey(event: NormalizedKeyboardInput, dispatch: 'code' | 'keyCode' = 'code'): Key {
|
|
350
635
|
const code = event.code;
|
|
351
636
|
if (code && dispatch === 'code') {
|
|
352
637
|
if (isOSX) {
|
|
@@ -382,7 +667,7 @@ export namespace KeyCode {
|
|
|
382
667
|
}
|
|
383
668
|
}
|
|
384
669
|
|
|
385
|
-
const keyIdentifier =
|
|
670
|
+
const keyIdentifier = event.keyIdentifier;
|
|
386
671
|
if (keyIdentifier) {
|
|
387
672
|
const key = Key.getKey(keyIdentifier);
|
|
388
673
|
if (key) {
|
|
@@ -397,7 +682,7 @@ export namespace KeyCode {
|
|
|
397
682
|
* If the key does not correspond to a printable character, `undefined` is returned.
|
|
398
683
|
* The result may be altered by modifier keys.
|
|
399
684
|
*/
|
|
400
|
-
export function toCharacter(event:
|
|
685
|
+
export function toCharacter(event: NormalizedKeyboardInput): string | undefined {
|
|
401
686
|
const key = event.key;
|
|
402
687
|
// Use the key property if it contains exactly one unicode character
|
|
403
688
|
if (key && Array.from(key).length === 1) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { expect } from 'chai';
|
|
18
|
+
import * as sinon from 'sinon';
|
|
18
19
|
import { TernarySearchTree, KeySequenceIterator } from './ternary-search-tree';
|
|
19
20
|
import { KeyCode, KeySequence, Key, KeyModifier } from './keys';
|
|
20
21
|
|
|
@@ -69,6 +70,19 @@ describe('KeySequenceIterator', () => {
|
|
|
69
70
|
// Different value should return non-zero
|
|
70
71
|
expect(iterator.cmp('different')).to.not.equal(0);
|
|
71
72
|
});
|
|
73
|
+
|
|
74
|
+
it('should use dispatch identity for values and comparisons', () => {
|
|
75
|
+
const keyCode = KeyCode.createKeyCode({ first: Key.KEY_A, modifiers: [KeyModifier.CtrlCmd] });
|
|
76
|
+
const dispatchString = sinon.stub(keyCode, 'dispatchString').returns('dispatch-a');
|
|
77
|
+
const toString = sinon.stub(keyCode, 'toString').returns('serialized-a');
|
|
78
|
+
|
|
79
|
+
iterator.reset([keyCode]);
|
|
80
|
+
|
|
81
|
+
expect(iterator.value()).to.equal('dispatch-a');
|
|
82
|
+
expect(iterator.cmp('dispatch-a')).to.equal(0);
|
|
83
|
+
expect(dispatchString.called).to.be.true;
|
|
84
|
+
expect(toString.called).to.be.false;
|
|
85
|
+
});
|
|
72
86
|
});
|
|
73
87
|
|
|
74
88
|
describe('TernarySearchTree for KeySequences', () => {
|
|
@@ -208,12 +208,12 @@ export class KeySequenceIterator implements IKeyIterator<KeySequence> {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
cmp(a: string): number {
|
|
211
|
-
const current = this._value[this._index]?.
|
|
211
|
+
const current = this._value[this._index]?.dispatchString() ?? '';
|
|
212
212
|
return compare(a, current);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
value(): string {
|
|
216
|
-
return this._value[this._index]?.
|
|
216
|
+
return this._value[this._index]?.dispatchString() ?? '';
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { NativeKeyboardLayout } from '../../common/keyboard/keyboard-layout-provider';
|
|
19
|
+
import { createElectronKeyboardLayoutProvider } from './electron-keyboard-module';
|
|
20
|
+
|
|
21
|
+
describe('Electron keyboard layout provider', () => {
|
|
22
|
+
it('reports the native-keymap source and delegates layout retrieval', async () => {
|
|
23
|
+
const layout: NativeKeyboardLayout = { info: { id: 'test', lang: 'en' }, mapping: {} };
|
|
24
|
+
const provider = createElectronKeyboardLayoutProvider({ getNativeLayout: async () => layout });
|
|
25
|
+
|
|
26
|
+
expect(provider.layoutSource).to.equal('native-keymap');
|
|
27
|
+
expect(await provider.getNativeLayout()).to.equal(layout);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -19,10 +19,17 @@ import { KeyboardLayoutProvider, keyboardPath, KeyboardLayoutChangeNotifier } fr
|
|
|
19
19
|
import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider';
|
|
20
20
|
import { ElectronKeyboardLayoutChangeNotifier } from './electron-keyboard-layout-change-notifier';
|
|
21
21
|
|
|
22
|
+
export function createElectronKeyboardLayoutProvider(delegate: KeyboardLayoutProvider): KeyboardLayoutProvider {
|
|
23
|
+
return {
|
|
24
|
+
layoutSource: 'native-keymap',
|
|
25
|
+
getNativeLayout: () => delegate.getNativeLayout()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|
23
|
-
bind(KeyboardLayoutProvider).toDynamicValue(ctx =>
|
|
30
|
+
bind(KeyboardLayoutProvider).toDynamicValue(ctx => createElectronKeyboardLayoutProvider(
|
|
24
31
|
WebSocketConnectionProvider.createLocalProxy<KeyboardLayoutProvider>(ctx.container, keyboardPath)
|
|
25
|
-
).inSingletonScope();
|
|
32
|
+
)).inSingletonScope();
|
|
26
33
|
bind(ElectronKeyboardLayoutChangeNotifier).toSelf().inSingletonScope();
|
|
27
34
|
bind(KeyboardLayoutChangeNotifier).toService(ElectronKeyboardLayoutChangeNotifier);
|
|
28
35
|
});
|
|
@@ -22,7 +22,7 @@ import { CommonCommands } from '../../browser';
|
|
|
22
22
|
import debounce = require('lodash.debounce');
|
|
23
23
|
import { BrowserMainMenuFactory } from '../../browser/menu/browser-menu-plugin';
|
|
24
24
|
import { ContextMatcher } from '../../browser/context-key-service';
|
|
25
|
-
import { MenuDto, MenuRole } from '../../electron-common/electron-api';
|
|
25
|
+
import { electronMenuAcceleratorMetadata, MenuDto, MenuRole } from '../../electron-common/electron-api';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Representation of possible electron menu options.
|
|
@@ -249,7 +249,7 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
|
|
|
249
249
|
return parentItems;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
const
|
|
252
|
+
const acceleratorSequence = AcceleratorSource.is(menu) ? menu.getAccelerator(options.context, 'physical') : [];
|
|
253
253
|
|
|
254
254
|
const menuItem: MenuDto = {
|
|
255
255
|
id: menu.id,
|
|
@@ -258,7 +258,7 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
|
|
|
258
258
|
checked: menu.isToggled(effectivePath, ...args),
|
|
259
259
|
enabled: !honorDisabled || menu.isEnabled(effectivePath, ...args), // see https://github.com/eclipse-theia/theia/issues/446
|
|
260
260
|
visible: true,
|
|
261
|
-
|
|
261
|
+
...electronMenuAcceleratorMetadata(acceleratorSequence),
|
|
262
262
|
execute: async () => {
|
|
263
263
|
const wasToggled = menuItem.checked;
|
|
264
264
|
await menu.run(effectivePath, ...args);
|
|
@@ -362,6 +362,10 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
|
|
|
362
362
|
command: ElectronCommands.RELOAD.id,
|
|
363
363
|
keybinding: 'ctrlcmd+r'
|
|
364
364
|
},
|
|
365
|
+
{
|
|
366
|
+
command: ElectronCommands.ZOOM_IN.id,
|
|
367
|
+
keybinding: 'ctrlcmd+[char:0x2B]'
|
|
368
|
+
},
|
|
365
369
|
{
|
|
366
370
|
command: ElectronCommands.ZOOM_IN.id,
|
|
367
371
|
keybinding: 'ctrlcmd+='
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { electronMenuAccelerator, electronMenuAcceleratorMetadata, isElectronAcceleratorRepresentable } from './electron-api';
|
|
19
|
+
|
|
20
|
+
describe('Electron menu accelerators', () => {
|
|
21
|
+
it('accepts only ASCII accelerators without AltGr', () => {
|
|
22
|
+
expect(isElectronAcceleratorRepresentable('Ctrl+8')).to.be.true;
|
|
23
|
+
expect(isElectronAcceleratorRepresentable('Ctrl+AltGr+8')).to.be.false;
|
|
24
|
+
expect(isElectronAcceleratorRepresentable('Ctrl+ä')).to.be.false;
|
|
25
|
+
expect(isElectronAcceleratorRepresentable('Ctrl++')).to.be.false;
|
|
26
|
+
expect(isElectronAcceleratorRepresentable('Ctrl+K Ctrl+S')).to.be.false;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('forwards renderer accelerator text without precomputing representability', () => {
|
|
30
|
+
expect(electronMenuAcceleratorMetadata(['Ctrl+8'])).to.deep.equal({ accelerator: 'Ctrl+8' });
|
|
31
|
+
expect(electronMenuAcceleratorMetadata(['Ctrl+K', 'Ctrl+S'])).to.deep.equal({ accelerator: 'Ctrl+K Ctrl+S' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('omits unrepresentable accelerators', () => {
|
|
35
|
+
expect(electronMenuAccelerator({ accelerator: 'Ctrl+AltGr+8' })).to.deep.equal({});
|
|
36
|
+
expect(electronMenuAccelerator({ accelerator: 'Ctrl+K Ctrl+S' })).to.deep.equal({});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns representable accelerators', () => {
|
|
40
|
+
expect(electronMenuAccelerator({ accelerator: 'Ctrl+8' })).to.deep.equal({ accelerator: 'Ctrl+8' });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -22,6 +22,29 @@ import { LaunchArguments } from '../common/launch-arguments';
|
|
|
22
22
|
|
|
23
23
|
export type MenuRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'selectAll' | 'about' | 'services' | 'hide' | 'hideOthers' | 'unhide' | 'quit');
|
|
24
24
|
|
|
25
|
+
export function isElectronAcceleratorRepresentable(accelerator: string | undefined): accelerator is string {
|
|
26
|
+
if (!accelerator || /altgr/i.test(accelerator) || /\s/.test(accelerator) || accelerator.split('+').some(token => !token)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
for (let i = 0; i < accelerator.length; i++) {
|
|
30
|
+
if (accelerator.charCodeAt(i) > 127) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ElectronMenuAccelerator = Pick<MenuDto, 'accelerator'>;
|
|
38
|
+
|
|
39
|
+
export function electronMenuAcceleratorMetadata(acceleratorSequence: string[]): ElectronMenuAccelerator {
|
|
40
|
+
return { accelerator: acceleratorSequence.join(' ') || undefined };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Return an Electron accelerator only when the rendered single chord is representable. */
|
|
44
|
+
export function electronMenuAccelerator(dto: ElectronMenuAccelerator): ElectronMenuAccelerator {
|
|
45
|
+
return isElectronAcceleratorRepresentable(dto.accelerator) ? { accelerator: dto.accelerator } : {};
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
export interface MenuDto {
|
|
26
49
|
id?: string,
|
|
27
50
|
label?: string,
|