@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
|
@@ -18,7 +18,11 @@ import { enableJSDOM } from './test/jsdom';
|
|
|
18
18
|
let disableJSDOM = enableJSDOM();
|
|
19
19
|
|
|
20
20
|
import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
|
|
21
|
-
|
|
21
|
+
try {
|
|
22
|
+
FrontendApplicationConfigProvider.get();
|
|
23
|
+
} catch {
|
|
24
|
+
FrontendApplicationConfigProvider.set({});
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
import { Container, injectable, ContainerModule } from 'inversify';
|
|
24
28
|
import { bindContributionProvider } from '../common/contribution-provider';
|
|
@@ -26,7 +30,7 @@ import { KeyboardLayoutProvider, NativeKeyboardLayout, KeyboardLayoutChangeNotif
|
|
|
26
30
|
import { ILogger } from '../common/logger';
|
|
27
31
|
import { KeybindingRegistry, KeybindingContext, KeybindingContribution, KeybindingScope } from './keybinding';
|
|
28
32
|
import { Keybinding } from '../common/keybinding';
|
|
29
|
-
import { KeyCode, Key, KeyModifier, KeySequence } from './keyboard/keys';
|
|
33
|
+
import { KeyCode, Key, KeyModifier, KeySequence, NormalizedKeyboardInput } from './keyboard/keys';
|
|
30
34
|
import { KeyboardLayoutService } from './keyboard/keyboard-layout-service';
|
|
31
35
|
import { CommandRegistry, CommandService, CommandContribution, Command } from '../common/command';
|
|
32
36
|
import { LabelParser } from './label-parser';
|
|
@@ -46,15 +50,53 @@ disableJSDOM();
|
|
|
46
50
|
|
|
47
51
|
const expect = chai.expect;
|
|
48
52
|
|
|
53
|
+
function testKeyboardEvent(input: Partial<KeyboardEvent> = {}): KeyboardEvent {
|
|
54
|
+
let defaultPrevented = false;
|
|
55
|
+
return {
|
|
56
|
+
key: input.key,
|
|
57
|
+
code: input.code,
|
|
58
|
+
keyCode: input.keyCode,
|
|
59
|
+
ctrlKey: input.ctrlKey,
|
|
60
|
+
shiftKey: input.shiftKey,
|
|
61
|
+
altKey: input.altKey,
|
|
62
|
+
metaKey: input.metaKey,
|
|
63
|
+
repeat: input.repeat,
|
|
64
|
+
isComposing: input.isComposing,
|
|
65
|
+
location: input.location,
|
|
66
|
+
timeStamp: input.timeStamp,
|
|
67
|
+
target: input.target,
|
|
68
|
+
get defaultPrevented(): boolean { return defaultPrevented; },
|
|
69
|
+
preventDefault(): void { defaultPrevented = true; },
|
|
70
|
+
stopPropagation(): void { }
|
|
71
|
+
} as KeyboardEvent;
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
let keybindingRegistry: KeybindingRegistry;
|
|
50
75
|
let commandRegistry: CommandRegistry;
|
|
51
76
|
let testContainer: Container;
|
|
52
77
|
|
|
53
78
|
let stub: sinon.SinonStub;
|
|
79
|
+
const preferenceChanged = new Emitter<{ preferenceName: string }>();
|
|
80
|
+
const corePreferences = {
|
|
81
|
+
'keyboard.dispatch': 'code',
|
|
82
|
+
onPreferenceChanged: preferenceChanged.event
|
|
83
|
+
} as unknown as CorePreferences;
|
|
84
|
+
|
|
85
|
+
const RUSSIAN_LAYOUT: NativeKeyboardLayout = {
|
|
86
|
+
info: { id: 'ru', lang: 'ru' },
|
|
87
|
+
mapping: {
|
|
88
|
+
KeyA: { value: 'ф', withShift: 'Ф', withAltGr: '', withShiftAltGr: '' },
|
|
89
|
+
KeyC: { value: 'с', withShift: 'С', withAltGr: '', withShiftAltGr: '' },
|
|
90
|
+
KeyE: { value: 'у', withShift: 'У', withAltGr: '€', withShiftAltGr: '' },
|
|
91
|
+
KeyV: { value: 'м', withShift: 'М', withAltGr: '', withShiftAltGr: '' },
|
|
92
|
+
KeyS: { value: 'ы', withShift: 'Ы', withAltGr: '', withShiftAltGr: '' },
|
|
93
|
+
Digit8: { value: '8', withShift: '*', withAltGr: '', withShiftAltGr: '' },
|
|
94
|
+
BracketLeft: { value: 'х', withShift: 'Х', withAltGr: '', withShiftAltGr: '' },
|
|
95
|
+
Comma: { value: 'б', withShift: 'Б', withAltGr: '', withShiftAltGr: '' }
|
|
96
|
+
}
|
|
97
|
+
};
|
|
54
98
|
|
|
55
99
|
before(async () => {
|
|
56
|
-
disableJSDOM = enableJSDOM();
|
|
57
|
-
|
|
58
100
|
testContainer = new Container();
|
|
59
101
|
const module = new ContainerModule((bind, unbind, isBound, rebind) => {
|
|
60
102
|
|
|
@@ -63,7 +105,14 @@ before(async () => {
|
|
|
63
105
|
|
|
64
106
|
bind(KeyboardLayoutService).toSelf().inSingletonScope();
|
|
65
107
|
bind(MockKeyboardLayoutProvider).toSelf().inSingletonScope();
|
|
66
|
-
bind(KeyboardLayoutProvider).
|
|
108
|
+
bind(KeyboardLayoutProvider).toDynamicValue(ctx => {
|
|
109
|
+
const provider = ctx.container.get(MockKeyboardLayoutProvider);
|
|
110
|
+
return new Proxy(provider, {
|
|
111
|
+
get: (target, property) => property === 'then' ? undefined
|
|
112
|
+
: property === 'layoutSource' ? 'navigator.keyboard'
|
|
113
|
+
: property in target ? Reflect.get(target, property) : () => undefined
|
|
114
|
+
});
|
|
115
|
+
});
|
|
67
116
|
bind(MockKeyboardLayoutChangeNotifier).toSelf().inSingletonScope();
|
|
68
117
|
bind(KeyboardLayoutChangeNotifier).toService(MockKeyboardLayoutChangeNotifier);
|
|
69
118
|
|
|
@@ -87,7 +136,10 @@ before(async () => {
|
|
|
87
136
|
}
|
|
88
137
|
});
|
|
89
138
|
|
|
90
|
-
bind(StatusBar).toConstantValue({
|
|
139
|
+
bind(StatusBar).toConstantValue({
|
|
140
|
+
setElement: () => Promise.resolve(),
|
|
141
|
+
removeElement: () => Promise.resolve()
|
|
142
|
+
} as unknown as StatusBar);
|
|
91
143
|
bind(MarkdownRendererImpl).toSelf().inSingletonScope();
|
|
92
144
|
bind(MarkdownRenderer).toService(MarkdownRendererImpl);
|
|
93
145
|
bind(MarkdownRendererFactory).toFactory(({ container }) => () => container.get(MarkdownRenderer));
|
|
@@ -96,7 +148,7 @@ before(async () => {
|
|
|
96
148
|
bind(LabelParser).toSelf().inSingletonScope();
|
|
97
149
|
bind(ContextKeyService).to(MockContextKeyService).inSingletonScope();
|
|
98
150
|
bind(FrontendApplicationStateService).toSelf().inSingletonScope();
|
|
99
|
-
bind(CorePreferences).toConstantValue(
|
|
151
|
+
bind(CorePreferences).toConstantValue(corePreferences);
|
|
100
152
|
bindPreferenceService(bind);
|
|
101
153
|
});
|
|
102
154
|
|
|
@@ -107,18 +159,22 @@ before(async () => {
|
|
|
107
159
|
|
|
108
160
|
});
|
|
109
161
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
162
|
+
beforeEach(async function (): Promise<void> {
|
|
163
|
+
if (this.currentTest?.file?.endsWith('keybinding.spec.js')) {
|
|
164
|
+
disableJSDOM();
|
|
165
|
+
disableJSDOM = enableJSDOM();
|
|
166
|
+
}
|
|
115
167
|
stub = sinon.stub(os, 'isOSX').value(false);
|
|
116
168
|
keybindingRegistry = testContainer.get<KeybindingRegistry>(KeybindingRegistry);
|
|
169
|
+
(corePreferences as unknown as Record<string, unknown>)['keyboard.dispatch'] = 'code';
|
|
117
170
|
await keybindingRegistry.onStart();
|
|
118
171
|
});
|
|
119
172
|
|
|
120
|
-
afterEach(()
|
|
173
|
+
afterEach(function (): void {
|
|
121
174
|
stub.restore();
|
|
175
|
+
if (this.currentTest?.file?.endsWith('keybinding.spec.js')) {
|
|
176
|
+
disableJSDOM();
|
|
177
|
+
}
|
|
122
178
|
});
|
|
123
179
|
|
|
124
180
|
describe('keybindings', () => {
|
|
@@ -353,7 +409,7 @@ describe('keybindings', () => {
|
|
|
353
409
|
expect(match && match.kind).to.be.equal('full');
|
|
354
410
|
|
|
355
411
|
match = keybindingRegistry.matchKeybinding([KeyCode.createKeyCode({ first: Key.KEY_C, modifiers: [KeyModifier.CtrlCmd] })]);
|
|
356
|
-
const keyCode = match && KeyCode.parse(match.binding.keybinding);
|
|
412
|
+
const keyCode = match && KeyCode.parse(match.runtime.binding.keybinding);
|
|
357
413
|
expect(keyCode?.key).to.be.equal(validKeyCode.key);
|
|
358
414
|
});
|
|
359
415
|
|
|
@@ -423,23 +479,23 @@ describe('keybindings', () => {
|
|
|
423
479
|
keybindingRegistry.setKeymap(KeybindingScope.WORKSPACE, [workspaceBinding]);
|
|
424
480
|
// now WORKSPACE bindings are overriding the other scopes
|
|
425
481
|
|
|
426
|
-
let match = keybindingRegistry.matchKeybinding(
|
|
482
|
+
let match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(workspaceBinding));
|
|
427
483
|
expect(match?.kind).to.be.equal('full');
|
|
428
|
-
expect(match?.binding?.command).to.be.equal(workspaceBinding.command);
|
|
484
|
+
expect(match?.runtime.binding?.command).to.be.equal(workspaceBinding.command);
|
|
429
485
|
|
|
430
486
|
keybindingRegistry.resetKeybindingsForScope(KeybindingScope.WORKSPACE);
|
|
431
487
|
// now it should find USER bindings
|
|
432
488
|
|
|
433
|
-
match = keybindingRegistry.matchKeybinding(
|
|
489
|
+
match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(userBinding));
|
|
434
490
|
expect(match?.kind).to.be.equal('full');
|
|
435
|
-
expect(match?.binding?.command).to.be.equal(userBinding.command);
|
|
491
|
+
expect(match?.runtime.binding?.command).to.be.equal(userBinding.command);
|
|
436
492
|
|
|
437
493
|
keybindingRegistry.resetKeybindingsForScope(KeybindingScope.USER);
|
|
438
494
|
// and finally it should fallback to DEFAULT bindings.
|
|
439
495
|
|
|
440
|
-
match = keybindingRegistry.matchKeybinding(
|
|
496
|
+
match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(defaultBinding));
|
|
441
497
|
expect(match?.kind).to.be.equal('full');
|
|
442
|
-
expect(match?.binding?.command).to.be.equal(defaultBinding.command);
|
|
498
|
+
expect(match?.runtime.binding?.command).to.be.equal(defaultBinding.command);
|
|
443
499
|
|
|
444
500
|
keybindingRegistry.resetKeybindingsForScope(KeybindingScope.DEFAULT);
|
|
445
501
|
// now the registry should be empty
|
|
@@ -462,18 +518,760 @@ describe('keybindings', () => {
|
|
|
462
518
|
};
|
|
463
519
|
|
|
464
520
|
keybindingRegistry.setKeymap(KeybindingScope.DEFAULT, [defaultBinding]);
|
|
465
|
-
let match = keybindingRegistry.matchKeybinding(
|
|
521
|
+
let match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(defaultBinding));
|
|
466
522
|
expect(match?.kind).to.be.equal('full');
|
|
467
|
-
expect(match?.binding?.command).to.be.equal(defaultBinding.command);
|
|
523
|
+
expect(match?.runtime.binding?.command).to.be.equal(defaultBinding.command);
|
|
468
524
|
|
|
469
525
|
keybindingRegistry.setKeymap(KeybindingScope.USER, [disableDefaultBinding]);
|
|
470
|
-
match = keybindingRegistry.matchKeybinding(
|
|
526
|
+
match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(defaultBinding));
|
|
471
527
|
expect(match).to.be.undefined;
|
|
472
528
|
|
|
473
529
|
keybindingRegistry.resetKeybindingsForScope(KeybindingScope.USER);
|
|
474
|
-
match = keybindingRegistry.matchKeybinding(
|
|
530
|
+
match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(defaultBinding));
|
|
475
531
|
expect(match?.kind).to.be.equal('full');
|
|
476
|
-
expect(match?.binding?.command).to.be.equal(defaultBinding.command);
|
|
532
|
+
expect(match?.runtime.binding?.command).to.be.equal(defaultBinding.command);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should dispatch already-handled compatibility events to capture listeners', () => {
|
|
536
|
+
const target = document.createElement('iframe');
|
|
537
|
+
document.body.appendChild(target);
|
|
538
|
+
const captured = sinon.spy();
|
|
539
|
+
document.addEventListener('keydown', captured, true);
|
|
540
|
+
const match = sinon.spy(keybindingRegistry, 'matchKeybinding');
|
|
541
|
+
|
|
542
|
+
keybindingRegistry.dispatchKeyDown({ key: 'a', code: 'KeyA' }, target, true);
|
|
543
|
+
|
|
544
|
+
expect(captured.calledOnce).to.be.true;
|
|
545
|
+
expect(captured.firstCall.args[0].defaultPrevented).to.be.false;
|
|
546
|
+
expect(match.called).to.be.false;
|
|
547
|
+
document.removeEventListener('keydown', captured, true);
|
|
548
|
+
target.remove();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should dispatch normalized keyboard data directly', () => {
|
|
552
|
+
const match = sinon.spy(keybindingRegistry, 'matchKeybinding');
|
|
553
|
+
|
|
554
|
+
keybindingRegistry.dispatchNormalizedKeyDown({
|
|
555
|
+
key: 'a',
|
|
556
|
+
code: 'KeyA',
|
|
557
|
+
keyCode: 65,
|
|
558
|
+
ctrlKey: true
|
|
559
|
+
}, new EventTarget());
|
|
560
|
+
|
|
561
|
+
expect(match.calledOnce).to.be.true;
|
|
562
|
+
expect(match.firstCall.args[0][0].dispatchString()).to.equal('ctrl+a');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should dispatch a DOM keydown for dispatched commands without executing them twice', async () => {
|
|
566
|
+
const target = document.createElement('div');
|
|
567
|
+
document.body.appendChild(target);
|
|
568
|
+
const captured = sinon.spy();
|
|
569
|
+
const run = (event: KeyboardEvent) => keybindingRegistry.run(event);
|
|
570
|
+
document.addEventListener('keydown', captured, true);
|
|
571
|
+
document.addEventListener('keydown', run, true);
|
|
572
|
+
const match = sinon.spy(keybindingRegistry, 'matchKeybinding');
|
|
573
|
+
const execute = sinon.spy();
|
|
574
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND.id, { execute });
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
keybindingRegistry.dispatchCommand(TEST_COMMAND.id, target);
|
|
578
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
579
|
+
|
|
580
|
+
expect(captured.calledOnce).to.be.true;
|
|
581
|
+
expect(captured.firstCall.args[0].ctrlKey).to.be.true;
|
|
582
|
+
expect(execute.calledOnce).to.be.true;
|
|
583
|
+
expect(match.calledOnce).to.be.true;
|
|
584
|
+
} finally {
|
|
585
|
+
handler.dispose();
|
|
586
|
+
document.removeEventListener('keydown', captured, true);
|
|
587
|
+
document.removeEventListener('keydown', run, true);
|
|
588
|
+
target.remove();
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('should preserve authored modifiers when dispatching commands through normalized Windows layout modifier data', async () => {
|
|
593
|
+
const windows = sinon.stub(os, 'isWindows').value(true);
|
|
594
|
+
testContainer.get(MockKeyboardLayoutChangeNotifier).emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
595
|
+
const dispatch = sinon.spy(keybindingRegistry, 'dispatchNormalizedKeyDown');
|
|
596
|
+
const target = document.createElement('div');
|
|
597
|
+
const cases = [
|
|
598
|
+
{ keybinding: '[', ctrlKey: false, altKey: false },
|
|
599
|
+
{ keybinding: 'ctrl+[', ctrlKey: true, altKey: false, inactive: true },
|
|
600
|
+
{ keybinding: 'alt+[', ctrlKey: false, altKey: true, inactive: true },
|
|
601
|
+
{ keybinding: 'ctrl+alt+[', ctrlKey: true, altKey: true, inactive: true }
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
for (const testCase of cases) {
|
|
606
|
+
const binding = { command: TEST_COMMAND_SHADOW.id, keybinding: testCase.keybinding };
|
|
607
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
608
|
+
const execute = sinon.spy();
|
|
609
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
610
|
+
dispatch.resetHistory();
|
|
611
|
+
try {
|
|
612
|
+
keybindingRegistry.dispatchCommand(TEST_COMMAND_SHADOW.id, target);
|
|
613
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
614
|
+
|
|
615
|
+
if (testCase.inactive) {
|
|
616
|
+
// Windows reports AltGr as Ctrl+Alt, so neither command Ctrl nor Alt can accompany an AltGraph character.
|
|
617
|
+
expect(keybindingRegistry.isKeybindingInactive(binding)).to.be.true;
|
|
618
|
+
expect(dispatch.called).to.be.false;
|
|
619
|
+
expect(execute.called).to.be.false;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
expect(dispatch.calledOnce).to.be.true;
|
|
623
|
+
expect(dispatch.firstCall.args[0]).to.include({
|
|
624
|
+
key: '[',
|
|
625
|
+
code: 'Digit8',
|
|
626
|
+
keyCode: 56,
|
|
627
|
+
ctrlKey: testCase.ctrlKey,
|
|
628
|
+
altKey: testCase.altKey,
|
|
629
|
+
altGraph: true
|
|
630
|
+
});
|
|
631
|
+
expect(execute.calledOnce).to.be.true;
|
|
632
|
+
} finally {
|
|
633
|
+
handler.dispose();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} finally {
|
|
637
|
+
windows.restore();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should interpret native and transported AltGraph input identically', () => {
|
|
642
|
+
testContainer.get(MockKeyboardLayoutChangeNotifier).emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
643
|
+
const native = keybindingRegistry.getKeyCodeInterpretations({
|
|
644
|
+
key: '[',
|
|
645
|
+
code: 'Digit8',
|
|
646
|
+
keyCode: 56,
|
|
647
|
+
ctrlKey: true,
|
|
648
|
+
getModifierState: modifier => modifier === 'AltGraph'
|
|
649
|
+
});
|
|
650
|
+
const transported = keybindingRegistry.getKeyCodeInterpretations({
|
|
651
|
+
key: '[',
|
|
652
|
+
code: 'Digit8',
|
|
653
|
+
keyCode: 56,
|
|
654
|
+
ctrlKey: true,
|
|
655
|
+
altGraph: true
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
expect(transported.map(code => code.dispatchString())).to.deep.equal(native.map(code => code.dispatchString()));
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should log normalized keyboard troubleshooting data when enabled', () => {
|
|
662
|
+
const logger = (keybindingRegistry as unknown as { logger: ILogger }).logger;
|
|
663
|
+
const info = sinon.spy(logger, 'info');
|
|
664
|
+
keybindingRegistry.toggleKeyboardShortcutsTroubleshooting();
|
|
665
|
+
|
|
666
|
+
keybindingRegistry.dispatchNormalizedKeyDown({
|
|
667
|
+
key: '[',
|
|
668
|
+
code: 'Digit8',
|
|
669
|
+
keyCode: 56,
|
|
670
|
+
getModifierState: modifier => modifier === 'AltGraph',
|
|
671
|
+
timeStamp: 42
|
|
672
|
+
}, new EventTarget());
|
|
673
|
+
|
|
674
|
+
expect(info.calledTwice).to.be.true;
|
|
675
|
+
expect(info.secondCall.args[1].input).to.include({
|
|
676
|
+
key: '[',
|
|
677
|
+
code: 'Digit8',
|
|
678
|
+
altGraph: true,
|
|
679
|
+
timeStamp: 42
|
|
680
|
+
});
|
|
681
|
+
expect(info.secondCall.args[1].layout.source).to.equal('navigator.keyboard');
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should skip composing normalized keyboard data', () => {
|
|
685
|
+
const match = sinon.spy(keybindingRegistry, 'matchKeybinding');
|
|
686
|
+
|
|
687
|
+
keybindingRegistry.dispatchNormalizedKeyDown({
|
|
688
|
+
key: 'a',
|
|
689
|
+
code: 'KeyA',
|
|
690
|
+
keyCode: 65,
|
|
691
|
+
isComposing: true
|
|
692
|
+
}, new EventTarget());
|
|
693
|
+
|
|
694
|
+
expect(match.called).to.be.false;
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should exclude inactive bindings and reactivate them after a layout change', () => {
|
|
698
|
+
const binding = { command: TEST_COMMAND.id, keybinding: 'ctrl+[' };
|
|
699
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
700
|
+
expect(keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(binding))?.kind).to.equal('full');
|
|
701
|
+
|
|
702
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
703
|
+
notifier.emitter.fire({
|
|
704
|
+
info: { id: 'missing-bracket', lang: 'en' },
|
|
705
|
+
mapping: { KeyA: { value: 'a', withShift: 'A', withAltGr: '', withShiftAltGr: '' } }
|
|
706
|
+
});
|
|
707
|
+
expect(keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND.id).some(candidate => candidate.keybinding === 'ctrl+[')).to.be.true;
|
|
708
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.a('string');
|
|
709
|
+
expect(keybindingRegistry.matchKeybinding(KeySequence.parse('ctrl+['))).to.be.undefined;
|
|
710
|
+
expect(keybindingRegistry.containsKeybindingInScope({ command: TEST_COMMAND2.id, keybinding: 'ctrl+[' })).to.be.false;
|
|
711
|
+
|
|
712
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
713
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.undefined;
|
|
714
|
+
expect(keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(binding))?.kind).to.equal('full');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should keep default Latin-letter bindings active on a non-Latin layout', async () => {
|
|
718
|
+
const binding = { command: TEST_COMMAND_SHADOW.id, keybinding: 'ctrl+c' };
|
|
719
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
720
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
721
|
+
notifier.emitter.fire(RUSSIAN_LAYOUT);
|
|
722
|
+
const execute = sinon.spy();
|
|
723
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
expect(keybindingRegistry.isKeybindingInactive(binding)).to.be.false;
|
|
727
|
+
keybindingRegistry.dispatchNormalizedKeyDown({
|
|
728
|
+
key: 'с', code: 'KeyC', ctrlKey: true
|
|
729
|
+
}, new EventTarget());
|
|
730
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
731
|
+
|
|
732
|
+
expect(execute.calledOnce).to.be.true;
|
|
733
|
+
} finally {
|
|
734
|
+
handler.dispose();
|
|
735
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should dispatch a shifted Latin-letter binding on a non-Latin layout', async () => {
|
|
740
|
+
const binding = { command: TEST_COMMAND_SHADOW.id, keybinding: 'ctrl+shift+a' };
|
|
741
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
742
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
743
|
+
notifier.emitter.fire(RUSSIAN_LAYOUT);
|
|
744
|
+
const execute = sinon.spy();
|
|
745
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
keybindingRegistry.dispatchNormalizedKeyDown({
|
|
749
|
+
key: 'Ф', code: 'KeyA', ctrlKey: true, shiftKey: true
|
|
750
|
+
}, new EventTarget());
|
|
751
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
752
|
+
|
|
753
|
+
expect(execute.calledOnce).to.be.true;
|
|
754
|
+
} finally {
|
|
755
|
+
handler.dispose();
|
|
756
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it('should keep punctuation bindings inactive on a non-Latin layout', () => {
|
|
761
|
+
const binding = { command: TEST_COMMAND.id, keybinding: 'ctrl+[' };
|
|
762
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
763
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
764
|
+
notifier.emitter.fire(RUSSIAN_LAYOUT);
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
expect(keybindingRegistry.isKeybindingInactive(binding)).to.be.true;
|
|
768
|
+
} finally {
|
|
769
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('should execute an authored Shift command modifier on a non-US logical character', async () => {
|
|
774
|
+
testContainer.get(MockKeyboardLayoutChangeNotifier).emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
775
|
+
const binding = { command: TEST_COMMAND_SHADOW.id, keybinding: 'ctrl+shift+ü' };
|
|
776
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
777
|
+
const execute = sinon.spy();
|
|
778
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
keybindingRegistry.dispatchNormalizedKeyDown({
|
|
782
|
+
key: 'Ü', code: 'BracketLeft', ctrlKey: true, shiftKey: true
|
|
783
|
+
}, new EventTarget());
|
|
784
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
785
|
+
|
|
786
|
+
expect(execute.calledOnce).to.be.true;
|
|
787
|
+
} finally {
|
|
788
|
+
handler.dispose();
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('should mark an unmatchable Shift plus AltGraph character binding inactive', async () => {
|
|
793
|
+
testContainer.get(MockKeyboardLayoutChangeNotifier).emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
794
|
+
const binding = { command: TEST_COMMAND_SHADOW.id, keybinding: 'ctrl+shift+[char:0x7B]' };
|
|
795
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
796
|
+
const execute = sinon.spy();
|
|
797
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.a('string');
|
|
801
|
+
keybindingRegistry.dispatchNormalizedKeyDown({
|
|
802
|
+
key: '{', code: 'Digit7', ctrlKey: true, shiftKey: true, altGraph: true
|
|
803
|
+
}, new EventTarget());
|
|
804
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
805
|
+
|
|
806
|
+
expect(execute.called).to.be.false;
|
|
807
|
+
} finally {
|
|
808
|
+
handler.dispose();
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('should mark command Alt plus AltGraph character bindings inactive on macOS and Windows', async () => {
|
|
813
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
814
|
+
const binding = { command: TEST_COMMAND_SHADOW.id, keybinding: 'alt+[' };
|
|
815
|
+
const execute = sinon.spy();
|
|
816
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
817
|
+
const windows = sinon.stub(os, 'isWindows').value(false);
|
|
818
|
+
const dispatch = async (input: NormalizedKeyboardInput) => {
|
|
819
|
+
keybindingRegistry.dispatchNormalizedKeyDown(input, new EventTarget());
|
|
820
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
try {
|
|
824
|
+
stub.value(true);
|
|
825
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/de-German-mac.json'));
|
|
826
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
827
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.a('string');
|
|
828
|
+
expect(keybindingRegistry.acceleratorForCommand(TEST_COMMAND_SHADOW.id, undefined, 'physical')).to.deep.equal([]);
|
|
829
|
+
await dispatch({ key: '[', code: 'Digit5', altKey: true });
|
|
830
|
+
expect(execute.called).to.be.false;
|
|
831
|
+
|
|
832
|
+
stub.value(false);
|
|
833
|
+
windows.value(true);
|
|
834
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
835
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
836
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.a('string');
|
|
837
|
+
await dispatch({ key: '[', code: 'Digit8', ctrlKey: true, altKey: true });
|
|
838
|
+
expect(execute.called).to.be.false;
|
|
839
|
+
|
|
840
|
+
windows.value(false);
|
|
841
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
842
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
843
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.undefined;
|
|
844
|
+
await dispatch({ key: '[', code: 'Digit8', altKey: true, altGraph: true });
|
|
845
|
+
expect(execute.calledOnce).to.be.true;
|
|
846
|
+
} finally {
|
|
847
|
+
windows.restore();
|
|
848
|
+
handler.dispose();
|
|
849
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('should mark command Ctrl plus AltGraph character bindings inactive on Windows', async () => {
|
|
854
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
855
|
+
const binding = { command: TEST_COMMAND_SHADOW.id, keybinding: 'ctrl+[' };
|
|
856
|
+
const typed = { command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND, keybinding: '[' };
|
|
857
|
+
const execute = sinon.spy();
|
|
858
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
859
|
+
const windows = sinon.stub(os, 'isWindows').value(true);
|
|
860
|
+
const match = sinon.spy(keybindingRegistry, 'matchKeybinding');
|
|
861
|
+
const dispatch = async (input: NormalizedKeyboardInput) => {
|
|
862
|
+
keybindingRegistry.dispatchNormalizedKeyDown(input, new EventTarget());
|
|
863
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
868
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding, typed]);
|
|
869
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.a('string');
|
|
870
|
+
expect(keybindingRegistry.acceleratorForCommand(TEST_COMMAND_SHADOW.id, undefined, 'physical')).to.deep.equal([]);
|
|
871
|
+
// Chromium reports Ctrl+AltGr+8 on Windows exactly like AltGr+8, so the input types `[`.
|
|
872
|
+
await dispatch({ key: '[', code: 'Digit8', ctrlKey: true, altKey: true, altGraph: true });
|
|
873
|
+
expect(execute.called).to.be.false;
|
|
874
|
+
const fullMatches = match.returnValues.filter(result => result?.kind === 'full').map(result => result!.runtime.binding.keybinding);
|
|
875
|
+
expect(fullMatches).to.deep.equal(['[']);
|
|
876
|
+
|
|
877
|
+
windows.value(false);
|
|
878
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
879
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
880
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.undefined;
|
|
881
|
+
await dispatch({ key: '[', code: 'Digit8', ctrlKey: true, altGraph: true });
|
|
882
|
+
expect(execute.calledOnce).to.be.true;
|
|
883
|
+
} finally {
|
|
884
|
+
windows.restore();
|
|
885
|
+
handler.dispose();
|
|
886
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('should route Windows Ctrl+Alt candidates through the registry with and without reported AltGraph', () => {
|
|
891
|
+
const windows = sinon.stub(os, 'isWindows').value(true);
|
|
892
|
+
try {
|
|
893
|
+
testContainer.get(MockKeyboardLayoutChangeNotifier).emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
894
|
+
const match = sinon.spy(keybindingRegistry, 'matchKeybinding');
|
|
895
|
+
const logicalBinding = {
|
|
896
|
+
command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND,
|
|
897
|
+
keybinding: '[',
|
|
898
|
+
resolved: [new KeyCode({ key: Key.DIGIT8, character: '[', layoutModifiers: 'altGraph' })]
|
|
899
|
+
};
|
|
900
|
+
for (const altGraph of [true, false]) {
|
|
901
|
+
const input = { key: '[', code: 'Digit8', ctrlKey: true, altKey: true, altGraph };
|
|
902
|
+
match.resetHistory();
|
|
903
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [logicalBinding]);
|
|
904
|
+
keybindingRegistry.dispatchNormalizedKeyDown(input, new EventTarget());
|
|
905
|
+
|
|
906
|
+
expect(match.callCount).to.equal(2);
|
|
907
|
+
expect(match.secondCall.returnValue?.kind).to.equal('full');
|
|
908
|
+
expect(match.secondCall.returnValue?.runtime.binding.keybinding).to.equal('[');
|
|
909
|
+
|
|
910
|
+
match.resetHistory();
|
|
911
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [
|
|
912
|
+
logicalBinding,
|
|
913
|
+
{ command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND, keybinding: 'ctrl+alt+8' }
|
|
914
|
+
]);
|
|
915
|
+
keybindingRegistry.dispatchNormalizedKeyDown(input, new EventTarget());
|
|
916
|
+
|
|
917
|
+
expect(match.callCount).to.equal(2);
|
|
918
|
+
expect(match.firstCall.returnValue?.runtime.binding.keybinding).to.equal('ctrl+alt+8');
|
|
919
|
+
}
|
|
920
|
+
} finally {
|
|
921
|
+
windows.restore();
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('should prefer a command chord prefix over a layout modifier full match', () => {
|
|
926
|
+
const interpretations = sinon.stub(keybindingRegistry, 'getKeyCodeInterpretations').returns([
|
|
927
|
+
new KeyCode({ key: Key.KEY_A, ctrl: true, interpretation: 'commandModifiers' }),
|
|
928
|
+
new KeyCode({ key: Key.DIGIT8, ctrl: true, layoutModifiers: 'altGraph', interpretation: 'layoutModifiers' })
|
|
929
|
+
]);
|
|
930
|
+
const commandChord = {
|
|
931
|
+
command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND,
|
|
932
|
+
keybinding: 'ctrl+a b'
|
|
933
|
+
};
|
|
934
|
+
const layoutFull = {
|
|
935
|
+
command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND,
|
|
936
|
+
keybinding: 'ctrl+['
|
|
937
|
+
};
|
|
938
|
+
keybindingRegistry.resolveKeybinding(commandChord).splice(0, 2, ...KeySequence.parse('ctrl+a b'));
|
|
939
|
+
keybindingRegistry.resolveKeybinding(layoutFull).splice(0, 1,
|
|
940
|
+
new KeyCode({ key: Key.DIGIT8, ctrl: true, character: '[', layoutModifiers: 'altGraph' }));
|
|
941
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [commandChord, layoutFull]);
|
|
942
|
+
|
|
943
|
+
const event = testKeyboardEvent({ key: '[', code: 'Digit8' });
|
|
944
|
+
keybindingRegistry.run(event);
|
|
945
|
+
|
|
946
|
+
expect(event.defaultPrevented).to.be.true;
|
|
947
|
+
expect((keybindingRegistry as unknown as { keySequenceCandidates: KeySequence[] }).keySequenceCandidates).to.have.length(1);
|
|
948
|
+
expect((keybindingRegistry as unknown as { keySequenceCandidates: KeySequence[] }).keySequenceCandidates[0][0].interpretation).to.equal('commandModifiers');
|
|
949
|
+
interpretations.restore();
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('should retain alternative interpretation prefixes across a chord', () => {
|
|
953
|
+
const commandPrefix = new KeyCode({ key: Key.KEY_A, ctrl: true, interpretation: 'commandModifiers' });
|
|
954
|
+
const layoutPrefix = new KeyCode({ key: Key.DIGIT8, ctrl: true, layoutModifiers: 'altGraph', interpretation: 'layoutModifiers' });
|
|
955
|
+
const layoutSecond = new KeyCode({ key: Key.KEY_C, interpretation: 'layoutModifiers' });
|
|
956
|
+
const interpretations = sinon.stub(keybindingRegistry, 'getKeyCodeInterpretations');
|
|
957
|
+
interpretations.onFirstCall().returns([commandPrefix, layoutPrefix]);
|
|
958
|
+
interpretations.onSecondCall().returns([
|
|
959
|
+
new KeyCode({ key: Key.KEY_X, interpretation: 'commandModifiers' }),
|
|
960
|
+
layoutSecond
|
|
961
|
+
]);
|
|
962
|
+
const commandBinding = {
|
|
963
|
+
command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND,
|
|
964
|
+
keybinding: 'ctrl+a b'
|
|
965
|
+
};
|
|
966
|
+
const layoutBinding = {
|
|
967
|
+
command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND,
|
|
968
|
+
keybinding: 'ctrl+[ c'
|
|
969
|
+
};
|
|
970
|
+
keybindingRegistry.resolveKeybinding(commandBinding).splice(0, 2, commandPrefix, new KeyCode({ key: Key.KEY_B }));
|
|
971
|
+
keybindingRegistry.resolveKeybinding(layoutBinding).splice(0, 2, layoutPrefix, layoutSecond);
|
|
972
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [commandBinding, layoutBinding]);
|
|
973
|
+
|
|
974
|
+
keybindingRegistry.run(testKeyboardEvent({ key: '[', code: 'Digit8' }));
|
|
975
|
+
expect((keybindingRegistry as unknown as { keySequenceCandidates: KeySequence[] }).keySequenceCandidates).to.have.length(2);
|
|
976
|
+
const second = testKeyboardEvent({ key: 'c', code: 'KeyC' });
|
|
977
|
+
keybindingRegistry.run(second);
|
|
978
|
+
|
|
979
|
+
expect(second.defaultPrevented).to.be.false;
|
|
980
|
+
expect((keybindingRegistry as unknown as { keySequenceCandidates: KeySequence[] }).keySequenceCandidates).to.deep.equal([[]]);
|
|
981
|
+
interpretations.restore();
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('should decline equal-priority full matches from different interpretations', () => {
|
|
985
|
+
const first = new KeyCode({ key: Key.KEY_A, ctrl: true, interpretation: 'commandModifiers' });
|
|
986
|
+
const second = new KeyCode({ key: Key.KEY_B, ctrl: true, interpretation: 'commandModifiers' });
|
|
987
|
+
const interpretations = sinon.stub(keybindingRegistry, 'getKeyCodeInterpretations').returns([first, second]);
|
|
988
|
+
const troubleshooting = sinon.spy(keybindingRegistry as unknown as {
|
|
989
|
+
logKeyboardTroubleshooting: (input: unknown, keyCode: KeyCode, match: unknown, skipped?: string) => void
|
|
990
|
+
}, 'logKeyboardTroubleshooting');
|
|
991
|
+
const logger = (keybindingRegistry as unknown as { logger: ILogger }).logger;
|
|
992
|
+
const warning = sinon.spy(logger, 'warn');
|
|
993
|
+
const firstBinding = { command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND, keybinding: 'ctrl+a' };
|
|
994
|
+
const secondBinding = { command: KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND, keybinding: 'ctrl+b' };
|
|
995
|
+
keybindingRegistry.resolveKeybinding(firstBinding).splice(0, 1, first);
|
|
996
|
+
keybindingRegistry.resolveKeybinding(secondBinding).splice(0, 1, second);
|
|
997
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [firstBinding, secondBinding]);
|
|
998
|
+
|
|
999
|
+
keybindingRegistry.run(testKeyboardEvent({ key: 'a', code: 'KeyA' }));
|
|
1000
|
+
|
|
1001
|
+
expect(troubleshooting.lastCall.args[3]).to.equal('ambiguous');
|
|
1002
|
+
expect(troubleshooting.lastCall.args[2]).to.be.undefined;
|
|
1003
|
+
expect(warning.calledOnce).to.be.true;
|
|
1004
|
+
keybindingRegistry.run(testKeyboardEvent({ key: 'a', code: 'KeyA' }));
|
|
1005
|
+
expect(warning.calledOnce).to.be.true;
|
|
1006
|
+
interpretations.restore();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it('should canonicalize layout modifier input for recorder persistence', () => {
|
|
1010
|
+
const interpretations = sinon.stub(keybindingRegistry, 'getKeyCodeInterpretations').returns([
|
|
1011
|
+
new KeyCode({ key: Key.DIGIT8, ctrl: true, alt: true, interpretation: 'commandModifiers' }),
|
|
1012
|
+
new KeyCode({ key: Key.DIGIT8, ctrl: true, character: '[', layoutModifiers: 'altGraph', interpretation: 'layoutModifiers' })
|
|
1013
|
+
]);
|
|
1014
|
+
|
|
1015
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({ key: '[', code: 'Digit8' })?.toString()).to.equal('ctrl+[');
|
|
1016
|
+
interpretations.restore();
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it('should record the Latin fallback letter on a non-Latin layout', () => {
|
|
1020
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
1021
|
+
notifier.emitter.fire(RUSSIAN_LAYOUT);
|
|
1022
|
+
|
|
1023
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({
|
|
1024
|
+
key: 'с', code: 'KeyC', ctrlKey: true
|
|
1025
|
+
})?.toAuthoredKeybindingString()).to.equal('ctrl+c');
|
|
1026
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({
|
|
1027
|
+
key: 'С', code: 'KeyC', ctrlKey: true, shiftKey: true
|
|
1028
|
+
})?.toAuthoredKeybindingString()).to.equal('shift+ctrl+c');
|
|
1029
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({
|
|
1030
|
+
key: '€', code: 'KeyE', ctrlKey: true, altGraph: true
|
|
1031
|
+
})?.toAuthoredKeybindingString()).to.equal('ctrl+€');
|
|
1032
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({
|
|
1033
|
+
key: 'у', code: 'KeyE', ctrlKey: true
|
|
1034
|
+
})?.toAuthoredKeybindingString()).to.equal('ctrl+e');
|
|
1035
|
+
|
|
1036
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
1037
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({
|
|
1038
|
+
key: 'c', code: 'KeyC', ctrlKey: true
|
|
1039
|
+
})?.toAuthoredKeybindingString()).to.equal('ctrl+c');
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('should record legacy Windows AltGraph input as its logical character', () => {
|
|
1043
|
+
const windows = sinon.stub(os, 'isWindows').value(true);
|
|
1044
|
+
try {
|
|
1045
|
+
testContainer.get(MockKeyboardLayoutChangeNotifier).emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
1046
|
+
|
|
1047
|
+
for (const altGraph of [true, false]) {
|
|
1048
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({
|
|
1049
|
+
key: '[', code: 'Digit8', ctrlKey: true, altKey: true, altGraph
|
|
1050
|
+
})?.toString()).to.equal('[');
|
|
1051
|
+
}
|
|
1052
|
+
} finally {
|
|
1053
|
+
windows.restore();
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('should retain command Shift when layout modifier Shift is not the logical binding', () => {
|
|
1058
|
+
const interpretations = sinon.stub(keybindingRegistry, 'getKeyCodeInterpretations').returns([
|
|
1059
|
+
new KeyCode({ key: Key.KEY_P, ctrl: true, character: 'P', layoutModifiers: 'shift', interpretation: 'layoutModifiers' })
|
|
1060
|
+
]);
|
|
1061
|
+
|
|
1062
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({ key: 'P', code: 'KeyP' })?.toString()).to.equal('shift+ctrl+p');
|
|
1063
|
+
interpretations.restore();
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('should persist a parseable logical character produced by Shift', () => {
|
|
1067
|
+
const interpretations = sinon.stub(keybindingRegistry, 'getKeyCodeInterpretations').returns([
|
|
1068
|
+
new KeyCode({ key: Key.DIGIT7, character: '/', layoutModifiers: 'shift', interpretation: 'layoutModifiers' })
|
|
1069
|
+
]);
|
|
1070
|
+
const resolve = sinon.stub(testContainer.get(KeyboardLayoutService), 'resolveKeyCode').callsFake(code => code.key === Key.SLASH
|
|
1071
|
+
? new KeyCode({ key: Key.DIGIT7, character: '/', layoutModifiers: 'shift' })
|
|
1072
|
+
: code);
|
|
1073
|
+
|
|
1074
|
+
expect(keybindingRegistry.authoredKeyCodeForKeyboardInput({ key: '/', code: 'Digit7' })?.toString()).to.equal('/');
|
|
1075
|
+
resolve.restore();
|
|
1076
|
+
interpretations.restore();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it('should record newly authorable printable characters logically', () => {
|
|
1080
|
+
const layoutCode = new KeyCode({ key: Key.SLASH, ctrl: true, character: '?', layoutModifiers: 'shift', interpretation: 'layoutModifiers' });
|
|
1081
|
+
const interpretations = sinon.stub(keybindingRegistry, 'getKeyCodeInterpretations').returns([layoutCode]);
|
|
1082
|
+
|
|
1083
|
+
const recorded = keybindingRegistry.authoredKeyCodeForKeyboardInput({ key: '?', code: 'Slash' });
|
|
1084
|
+
expect(recorded?.toString()).to.equal('ctrl+?');
|
|
1085
|
+
expect(keybindingRegistry.resolveKeybinding({ command: TEST_COMMAND.id, keybinding: recorded!.toString() })[0].dispatchString())
|
|
1086
|
+
.to.equal(layoutCode.dispatchString());
|
|
1087
|
+
interpretations.restore();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it('should record logical characters, physical non-printables, and ignore modifier-only input', () => {
|
|
1091
|
+
const registry = Object.create(KeybindingRegistry.prototype) as KeybindingRegistry;
|
|
1092
|
+
registry.authoredKeyCodeForKeyboardInput = input => {
|
|
1093
|
+
if (input.code === 'ControlLeft') {
|
|
1094
|
+
return new KeyCode({ ctrl: true });
|
|
1095
|
+
}
|
|
1096
|
+
if (input.code === 'F1') {
|
|
1097
|
+
return new KeyCode({ key: Key.F1, ctrl: true });
|
|
1098
|
+
}
|
|
1099
|
+
if (input.code === 'Delete') {
|
|
1100
|
+
return new KeyCode({ key: Key.DELETE, ctrl: true });
|
|
1101
|
+
}
|
|
1102
|
+
if (input.code === 'KeyP') {
|
|
1103
|
+
return new KeyCode({ key: Key.KEY_P, ctrl: true, shift: true, character: 'P' });
|
|
1104
|
+
}
|
|
1105
|
+
return new KeyCode({ key: input.code === 'Equal' ? Key.EQUAL : Key.BRACKET_LEFT, ctrl: true, character: input.key });
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
expect(registry.authoredKeybindingStringForKeyboardInput({ key: '[', code: 'Digit8', ctrlKey: true })).to.equal('ctrl+[');
|
|
1109
|
+
expect(registry.authoredKeybindingStringForKeyboardInput({ key: '+', code: 'Equal', ctrlKey: true })).to.equal('ctrl+[char:0x2B]');
|
|
1110
|
+
expect(registry.authoredKeybindingStringForKeyboardInput({ key: 'F1', code: 'F1', ctrlKey: true })).to.equal('ctrl+f1');
|
|
1111
|
+
expect(registry.authoredKeybindingStringForKeyboardInput({ key: 'Delete', code: 'Delete', ctrlKey: true })).to.equal('ctrl+delete');
|
|
1112
|
+
const shifted = registry.authoredKeybindingStringForKeyboardInput({ key: 'P', code: 'KeyP', ctrlKey: true, shiftKey: true });
|
|
1113
|
+
expect(shifted).to.equal('shift+ctrl+p');
|
|
1114
|
+
expect(KeyCode.parse(shifted!).dispatchString()).to.equal('shift+ctrl+p');
|
|
1115
|
+
expect(registry.authoredKeybindingStringForKeyboardInput({ key: 'Control', code: 'ControlLeft', ctrlKey: true })).to.be.undefined;
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
it('should record Space and numpad keys by name', () => {
|
|
1119
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: ' ', code: 'Space', ctrlKey: true, shiftKey: true }))
|
|
1120
|
+
.to.equal('shift+ctrl+space');
|
|
1121
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: '+', code: 'NumpadAdd', ctrlKey: true })).to.equal('ctrl+add');
|
|
1122
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: '-', code: 'NumpadSubtract', ctrlKey: true })).to.equal('ctrl+subtract');
|
|
1123
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: '.', code: 'NumpadDecimal', ctrlKey: true })).to.equal('ctrl+decimal');
|
|
1124
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: '1', code: 'Numpad1', ctrlKey: true })).to.equal('ctrl+1');
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it('should dispatch and record Option bindings as command modifiers on macOS', async () => {
|
|
1128
|
+
stub.value(true);
|
|
1129
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
1130
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-mac.json'));
|
|
1131
|
+
const execute = sinon.spy();
|
|
1132
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
1133
|
+
try {
|
|
1134
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [{ command: TEST_COMMAND_SHADOW.id, keybinding: 'shift+alt+p' }]);
|
|
1135
|
+
keybindingRegistry.run(testKeyboardEvent({ key: '∏', code: 'KeyP', shiftKey: true, altKey: true }));
|
|
1136
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1137
|
+
|
|
1138
|
+
expect(execute.calledOnce).to.be.true;
|
|
1139
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: '∏', code: 'KeyP', shiftKey: true, altKey: true }))
|
|
1140
|
+
.to.equal('shift+alt+p');
|
|
1141
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'π', code: 'KeyP', altKey: true })).to.equal('alt+p');
|
|
1142
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'P', code: 'KeyP', shiftKey: true })).to.equal('shift+p');
|
|
1143
|
+
} finally {
|
|
1144
|
+
handler.dispose();
|
|
1145
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('should dispatch and record dead Option keys as their base character on macOS', async () => {
|
|
1150
|
+
stub.value(true);
|
|
1151
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
1152
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-mac.json'));
|
|
1153
|
+
const execute = sinon.spy();
|
|
1154
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
1155
|
+
try {
|
|
1156
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [{ command: TEST_COMMAND_SHADOW.id, keybinding: 'alt+e' }]);
|
|
1157
|
+
keybindingRegistry.run(testKeyboardEvent({ key: 'Dead', code: 'KeyE', altKey: true }));
|
|
1158
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1159
|
+
|
|
1160
|
+
expect(execute.calledOnce).to.be.true;
|
|
1161
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'Dead', code: 'KeyE', altKey: true })).to.equal('alt+e');
|
|
1162
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'Dead', code: 'KeyE', altKey: true, shiftKey: true }))
|
|
1163
|
+
.to.equal('shift+alt+e');
|
|
1164
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'Dead', code: 'KeyE', altKey: true, metaKey: true }))
|
|
1165
|
+
.to.equal('meta+alt+e');
|
|
1166
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'F1', code: 'F1', altKey: true })).to.equal('alt+f1');
|
|
1167
|
+
|
|
1168
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [{ command: TEST_COMMAND_SHADOW.id, keybinding: 'shift+alt+e' }]);
|
|
1169
|
+
keybindingRegistry.run(testKeyboardEvent({ key: 'Dead', code: 'KeyE', altKey: true, shiftKey: true }));
|
|
1170
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1171
|
+
expect(execute.calledTwice).to.be.true;
|
|
1172
|
+
} finally {
|
|
1173
|
+
handler.dispose();
|
|
1174
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it('should record Caps Lock input with the layout layer case', async () => {
|
|
1179
|
+
const notifier = testContainer.get(MockKeyboardLayoutChangeNotifier);
|
|
1180
|
+
const execute = sinon.spy();
|
|
1181
|
+
const handler = commandRegistry.registerHandler(TEST_COMMAND_SHADOW.id, { execute });
|
|
1182
|
+
try {
|
|
1183
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'p', code: 'KeyP', ctrlKey: true, shiftKey: true }))
|
|
1184
|
+
.to.equal('shift+ctrl+p');
|
|
1185
|
+
|
|
1186
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/de-German-pc.json'));
|
|
1187
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'ü', code: 'BracketLeft', ctrlKey: true, shiftKey: true }))
|
|
1188
|
+
.to.equal('ctrl+Ü');
|
|
1189
|
+
const recorded = keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'Ü', code: 'BracketLeft', ctrlKey: true });
|
|
1190
|
+
expect(recorded).to.equal('ctrl+ü');
|
|
1191
|
+
|
|
1192
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [{ command: TEST_COMMAND_SHADOW.id, keybinding: recorded! }]);
|
|
1193
|
+
keybindingRegistry.dispatchNormalizedKeyDown({ key: 'ü', code: 'BracketLeft', ctrlKey: true }, new EventTarget());
|
|
1194
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1195
|
+
expect(execute.calledOnce).to.be.true;
|
|
1196
|
+
|
|
1197
|
+
notifier.emitter.fire({
|
|
1198
|
+
info: { id: 'altgraph-letters', lang: 'pl' },
|
|
1199
|
+
mapping: { KeyL: { value: 'l', withShift: 'L', withAltGr: 'ł', withShiftAltGr: 'Ł' } }
|
|
1200
|
+
});
|
|
1201
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'Ł', code: 'KeyL', ctrlKey: true, altGraph: true }))
|
|
1202
|
+
.to.equal('ctrl+ł');
|
|
1203
|
+
|
|
1204
|
+
stub.value(true);
|
|
1205
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/de-German-mac.json'));
|
|
1206
|
+
expect(keybindingRegistry.authoredKeybindingStringForKeyboardInput({ key: 'Å', code: 'KeyA', altKey: true })).to.equal('alt+a');
|
|
1207
|
+
} finally {
|
|
1208
|
+
handler.dispose();
|
|
1209
|
+
notifier.emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it('should classify interpretation shadowing separately from canonical collisions', () => {
|
|
1214
|
+
const logical = {
|
|
1215
|
+
command: TEST_COMMAND.id,
|
|
1216
|
+
keybinding: 'ctrl+[',
|
|
1217
|
+
resolved: [new KeyCode({ key: Key.DIGIT8, ctrl: true, character: '[', layoutModifiers: 'altGraph' })]
|
|
1218
|
+
};
|
|
1219
|
+
const command = { command: TEST_COMMAND2.id, keybinding: 'ctrl+alt+8' };
|
|
1220
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [logical, command]);
|
|
1221
|
+
|
|
1222
|
+
const diagnostics = keybindingRegistry.getKeybindingCollisionDiagnostics(logical);
|
|
1223
|
+
expect(diagnostics.shadowedBy.map(binding => binding.command)).to.include(TEST_COMMAND2.id);
|
|
1224
|
+
expect(diagnostics.authored.full).to.be.empty;
|
|
1225
|
+
expect(diagnostics.layoutDerived.full).to.be.empty;
|
|
1226
|
+
expect(keybindingRegistry.containsKeybindingInScope(logical)).to.be.false;
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it('should keep shifted printable bindings symmetric in keyCode dispatch mode', () => {
|
|
1230
|
+
(corePreferences as unknown as Record<string, unknown>)['keyboard.dispatch'] = 'keyCode';
|
|
1231
|
+
const commandPalette = { command: TEST_COMMAND.id, keybinding: 'ctrl+shift+p' };
|
|
1232
|
+
const shiftedPrintable = { command: TEST_COMMAND2.id, keybinding: 'shift+/' };
|
|
1233
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [commandPalette, shiftedPrintable]);
|
|
1234
|
+
|
|
1235
|
+
expect(keybindingRegistry.matchKeybinding(KeySequence.parse('ctrl+shift+p'))?.runtime.binding.command).to.equal(TEST_COMMAND.id);
|
|
1236
|
+
expect(keybindingRegistry.matchKeybinding(KeySequence.parse('shift+/'))?.runtime.binding.command).to.equal(TEST_COMMAND2.id);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it('should mark character-only bindings inactive in keyCode dispatch mode', () => {
|
|
1240
|
+
(corePreferences as unknown as Record<string, unknown>)['keyboard.dispatch'] = 'keyCode';
|
|
1241
|
+
const characterOnly = { command: TEST_COMMAND.id, keybinding: 'ctrl+ü' };
|
|
1242
|
+
const positional = { command: TEST_COMMAND2.id, keybinding: 'ctrl+[' };
|
|
1243
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [characterOnly, positional]);
|
|
1244
|
+
|
|
1245
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(characterOnly)).to.be.a('string');
|
|
1246
|
+
expect(keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(characterOnly))).to.be.undefined;
|
|
1247
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(positional)).to.be.undefined;
|
|
1248
|
+
expect(keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(positional))?.kind).to.equal('full');
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it('should require resolved sequences for logical matching in code dispatch mode', () => {
|
|
1252
|
+
testContainer.get(MockKeyboardLayoutChangeNotifier).emitter.fire(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
1253
|
+
const binding = { command: TEST_COMMAND.id, keybinding: 'ctrl+shift+a' };
|
|
1254
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
1255
|
+
|
|
1256
|
+
expect(KeySequence.parse('ctrl+shift+a')[0].dispatchString()).to.equal('shift+ctrl+a');
|
|
1257
|
+
expect(keybindingRegistry.resolveKeybinding(binding)[0].dispatchString()).to.equal('ctrl+a@shift');
|
|
1258
|
+
expect(keybindingRegistry.matchKeybinding(KeySequence.parse('ctrl+shift+a'))).to.be.undefined;
|
|
1259
|
+
expect(keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(binding))?.kind).to.equal('full');
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it('should invalidate resolved bindings when keyboard dispatch changes', () => {
|
|
1263
|
+
const binding = { command: TEST_COMMAND.id, keybinding: 'ctrl+[' };
|
|
1264
|
+
keybindingRegistry.setKeymap(KeybindingScope.USER, [binding]);
|
|
1265
|
+
expect(keybindingRegistry.getKeybindingInactiveReason(binding)).to.be.undefined;
|
|
1266
|
+
const changed = sinon.spy();
|
|
1267
|
+
keybindingRegistry.onKeybindingsChanged(changed);
|
|
1268
|
+
|
|
1269
|
+
(corePreferences as unknown as Record<string, unknown>)['keyboard.dispatch'] = 'keyCode';
|
|
1270
|
+
preferenceChanged.fire({ preferenceName: 'keyboard.dispatch' });
|
|
1271
|
+
|
|
1272
|
+
expect(changed.calledOnce).to.be.true;
|
|
1273
|
+
expect(keybindingRegistry.resolveKeybinding(binding)[0].dispatchString()).to.equal('ctrl+[');
|
|
1274
|
+
expect(keybindingRegistry.matchKeybinding(KeySequence.parse('ctrl+['))?.kind).to.equal('full');
|
|
477
1275
|
});
|
|
478
1276
|
|
|
479
1277
|
it('should prioritize bindings that use local context keys', () => {
|
|
@@ -519,11 +1317,11 @@ describe('keybindings', () => {
|
|
|
519
1317
|
});
|
|
520
1318
|
Object.defineProperty(mockEvent, 'target', { value: targetElement });
|
|
521
1319
|
|
|
522
|
-
const match = keybindingRegistry.matchKeybinding(
|
|
1320
|
+
const match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(localBinding), mockEvent);
|
|
523
1321
|
|
|
524
1322
|
// The localBinding should be selected because it uses a local context key
|
|
525
1323
|
expect(match?.kind).to.be.equal('full');
|
|
526
|
-
expect(match?.binding?.command).to.be.equal('test.local-command');
|
|
1324
|
+
expect(match?.runtime.binding?.command).to.be.equal('test.local-command');
|
|
527
1325
|
|
|
528
1326
|
disable();
|
|
529
1327
|
});
|
|
@@ -566,11 +1364,11 @@ describe('keybindings', () => {
|
|
|
566
1364
|
});
|
|
567
1365
|
Object.defineProperty(mockEvent, 'target', { value: targetElement });
|
|
568
1366
|
|
|
569
|
-
const match = keybindingRegistry.matchKeybinding(
|
|
1367
|
+
const match = keybindingRegistry.matchKeybinding(keybindingRegistry.resolveKeybinding(secondBinding), mockEvent);
|
|
570
1368
|
|
|
571
1369
|
// Should fall back to first enabled binding in priority order (secondBinding was registered later, so it wins)
|
|
572
1370
|
expect(match?.kind).to.be.equal('full');
|
|
573
|
-
expect(match?.binding?.command).to.be.equal('test.second-command');
|
|
1371
|
+
expect(match?.runtime.binding?.command).to.be.equal('test.second-command');
|
|
574
1372
|
|
|
575
1373
|
disable();
|
|
576
1374
|
});
|
|
@@ -593,6 +1391,55 @@ describe('acceleratorForKeyCode', () => {
|
|
|
593
1391
|
expect(accelerator).to.equal('⌘V');
|
|
594
1392
|
});
|
|
595
1393
|
|
|
1394
|
+
it('separates logical labels from physical layout modifier realizations', () => {
|
|
1395
|
+
const logicalBracket = new KeyCode({
|
|
1396
|
+
key: Key.DIGIT8,
|
|
1397
|
+
ctrl: true,
|
|
1398
|
+
character: '[',
|
|
1399
|
+
layoutModifiers: 'altGraph'
|
|
1400
|
+
});
|
|
1401
|
+
const logicalSlash = new KeyCode({
|
|
1402
|
+
key: Key.DIGIT7,
|
|
1403
|
+
character: '/',
|
|
1404
|
+
layoutModifiers: 'shift'
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
expect(keybindingRegistry.componentsForKeyCode(logicalBracket)).to.deep.equal(['Ctrl', '[']);
|
|
1408
|
+
expect(keybindingRegistry.componentsForKeyCode(logicalBracket, false, 'physical')).to.deep.equal(['Ctrl', 'AltGr', '8']);
|
|
1409
|
+
expect(keybindingRegistry.acceleratorForKeyCode(logicalBracket, '+', true, 'physical')).to.equal('Ctrl+AltGr+8');
|
|
1410
|
+
expect(keybindingRegistry.componentsForKeyCode(logicalSlash)).to.deep.equal(['/']);
|
|
1411
|
+
expect(keybindingRegistry.componentsForKeyCode(logicalSlash, false, 'physical')).to.deep.equal(['Shift', '7']);
|
|
1412
|
+
expect(keybindingRegistry.acceleratorForKeyCode(logicalSlash, '+', true, 'physical')).to.equal('Shift+7');
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
it('keeps command Shift in physical realizations', () => {
|
|
1416
|
+
const commandShift = new KeyCode({ key: Key.TAB, ctrl: true, shift: true });
|
|
1417
|
+
expect(keybindingRegistry.componentsForKeyCode(commandShift, false, 'physical')).to.deep.equal(['Ctrl', 'Shift', 'Tab']);
|
|
1418
|
+
|
|
1419
|
+
const shiftedLetter = new KeyCode({ key: Key.KEY_P, ctrl: true, character: 'P', layoutModifiers: 'shift' });
|
|
1420
|
+
expect(keybindingRegistry.componentsForKeyCode(shiftedLetter)).to.deep.equal(['Ctrl', 'Shift', 'P']);
|
|
1421
|
+
|
|
1422
|
+
const unresolvedCharacter = new KeyCode({ ctrl: true, character: '§', supportedByLayout: false });
|
|
1423
|
+
expect(keybindingRegistry.componentsForKeyCode(unresolvedCharacter)).to.deep.equal(['Ctrl', '§']);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it('deduplicates command Alt and layout modifier Option on macOS', () => {
|
|
1427
|
+
stub.value(true);
|
|
1428
|
+
const commandAndLayoutOption = new KeyCode({
|
|
1429
|
+
key: Key.DIGIT6,
|
|
1430
|
+
meta: true,
|
|
1431
|
+
ctrl: true,
|
|
1432
|
+
alt: true,
|
|
1433
|
+
layoutModifiers: 'altGraph'
|
|
1434
|
+
});
|
|
1435
|
+
const layoutOption = new KeyCode({ key: Key.DIGIT6, layoutModifiers: 'altGraph' });
|
|
1436
|
+
|
|
1437
|
+
expect(keybindingRegistry.componentsForKeyCode(commandAndLayoutOption, false, 'physical')).to.deep.equal(['⌘', '⌃', '⌥', '6']);
|
|
1438
|
+
expect(keybindingRegistry.componentsForKeyCode(commandAndLayoutOption, true, 'physical')).to.deep.equal(['Cmd', 'Ctrl', 'Alt', '6']);
|
|
1439
|
+
expect(keybindingRegistry.componentsForKeyCode(layoutOption, false, 'physical')).to.deep.equal(['⌥', '6']);
|
|
1440
|
+
expect(keybindingRegistry.componentsForKeyCode(layoutOption, true, 'physical')).to.deep.equal(['Option', '6']);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
596
1443
|
it('uses ASCII names with the separator on macOS Electron (asciiOnly)', () => {
|
|
597
1444
|
stub.value(true);
|
|
598
1445
|
const accelerator = keybindingRegistry.acceleratorForKeyCode(metaV, '+', true);
|
|
@@ -655,16 +1502,13 @@ class MockContextKeyService extends ContextKeyServiceDummyImpl {
|
|
|
655
1502
|
@injectable()
|
|
656
1503
|
class MockKeyboardLayoutProvider implements KeyboardLayoutProvider {
|
|
657
1504
|
getNativeLayout(): Promise<NativeKeyboardLayout> {
|
|
658
|
-
return Promise.resolve(
|
|
659
|
-
info: { id: 'mock', lang: 'en' },
|
|
660
|
-
mapping: {}
|
|
661
|
-
});
|
|
1505
|
+
return Promise.resolve(require('../../src/common/keyboard/layouts/en-US-pc.json'));
|
|
662
1506
|
}
|
|
663
1507
|
}
|
|
664
1508
|
|
|
665
1509
|
@injectable()
|
|
666
1510
|
class MockKeyboardLayoutChangeNotifier implements KeyboardLayoutChangeNotifier {
|
|
667
|
-
|
|
1511
|
+
readonly emitter = new Emitter<NativeKeyboardLayout>();
|
|
668
1512
|
get onDidChangeNativeLayout(): Event<NativeKeyboardLayout> {
|
|
669
1513
|
return this.emitter.event;
|
|
670
1514
|
}
|