Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/keybinding/browser/keybindingService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as nls from '../../../../nls.js';
7
8
// base
9
import * as browser from '../../../../base/browser/browser.js';
10
import { BrowserFeatures, KeyboardSupport } from '../../../../base/browser/canIUse.js';
11
import * as dom from '../../../../base/browser/dom.js';
12
import { printKeyboardEvent, printStandardKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
13
import { mainWindow } from '../../../../base/browser/window.js';
14
import { DeferredPromise, RunOnceScheduler } from '../../../../base/common/async.js';
15
import { Emitter, Event } from '../../../../base/common/event.js';
16
import { parse } from '../../../../base/common/json.js';
17
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
18
import { UserSettingsLabelProvider } from '../../../../base/common/keybindingLabels.js';
19
import { KeybindingParser } from '../../../../base/common/keybindingParser.js';
20
import { Keybinding, KeyCodeChord, ResolvedKeybinding, ScanCodeChord } from '../../../../base/common/keybindings.js';
21
import { IMMUTABLE_CODE_TO_KEY_CODE, KeyCode, KeyCodeUtils, KeyMod, ScanCode, ScanCodeUtils } from '../../../../base/common/keyCodes.js';
22
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
23
import * as objects from '../../../../base/common/objects.js';
24
import { isMacintosh, OperatingSystem, OS } from '../../../../base/common/platform.js';
25
import { dirname } from '../../../../base/common/resources.js';
26
27
// platform
28
import { ILocalizedString, isLocalizedString } from '../../../../platform/action/common/action.js';
29
import { MenuRegistry } from '../../../../platform/actions/common/actions.js';
30
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
31
import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
32
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
33
import { FileOperation, IFileService } from '../../../../platform/files/common/files.js';
34
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
35
import { Extensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
36
import { AbstractKeybindingService } from '../../../../platform/keybinding/common/abstractKeybindingService.js';
37
import { IKeybindingService, IKeyboardEvent, KeybindingsSchemaContribution } from '../../../../platform/keybinding/common/keybinding.js';
38
import { KeybindingResolver } from '../../../../platform/keybinding/common/keybindingResolver.js';
39
import { IExtensionKeybindingRule, IKeybindingItem, KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
40
import { ResolvedKeybindingItem } from '../../../../platform/keybinding/common/resolvedKeybindingItem.js';
41
import { IKeyboardLayoutService } from '../../../../platform/keyboardLayout/common/keyboardLayout.js';
42
import { IKeyboardMapper } from '../../../../platform/keyboardLayout/common/keyboardMapper.js';
43
import { ILogService } from '../../../../platform/log/common/log.js';
44
import { INotificationService } from '../../../../platform/notification/common/notification.js';
45
import { Registry } from '../../../../platform/registry/common/platform.js';
46
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
47
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
48
49
// workbench
50
import { remove } from '../../../../base/common/arrays.js';
51
import { commandsExtensionPoint } from '../../actions/common/menusExtensionPoint.js';
52
import { IExtensionService } from '../../extensions/common/extensions.js';
53
import { ExtensionMessageCollector, ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js';
54
import { IHostService } from '../../host/browser/host.js';
55
import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';
56
import { IUserKeybindingItem, KeybindingIO, OutputBuilder } from '../common/keybindingIO.js';
57
import { IKeyboard, INavigatorWithKeyboard } from './navigatorKeyboard.js';
58
import { getAllUnboundCommands } from './unboundCommands.js';
59
60
interface ContributedKeyBinding {
61
command: string;
62
args?: any;
63
key: string;
64
when?: string;
65
mac?: string;
66
linux?: string;
67
win?: string;
68
}
69
70
function isValidContributedKeyBinding(keyBinding: ContributedKeyBinding, rejects: string[]): boolean {
71
if (!keyBinding) {
72
rejects.push(nls.localize('nonempty', "expected non-empty value."));
73
return false;
74
}
75
if (typeof keyBinding.command !== 'string') {
76
rejects.push(nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
77
return false;
78
}
79
if (keyBinding.key && typeof keyBinding.key !== 'string') {
80
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'key'));
81
return false;
82
}
83
if (keyBinding.when && typeof keyBinding.when !== 'string') {
84
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
85
return false;
86
}
87
if (keyBinding.mac && typeof keyBinding.mac !== 'string') {
88
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'mac'));
89
return false;
90
}
91
if (keyBinding.linux && typeof keyBinding.linux !== 'string') {
92
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'linux'));
93
return false;
94
}
95
if (keyBinding.win && typeof keyBinding.win !== 'string') {
96
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'win'));
97
return false;
98
}
99
return true;
100
}
101
102
const keybindingType: IJSONSchema = {
103
type: 'object',
104
default: { command: '', key: '' },
105
properties: {
106
command: {
107
description: nls.localize('vscode.extension.contributes.keybindings.command', 'Identifier of the command to run when keybinding is triggered.'),
108
type: 'string'
109
},
110
args: {
111
description: nls.localize('vscode.extension.contributes.keybindings.args', "Arguments to pass to the command to execute.")
112
},
113
key: {
114
description: nls.localize('vscode.extension.contributes.keybindings.key', 'Key or key sequence (separate keys with plus-sign and sequences with space, e.g. Ctrl+O and Ctrl+L L for a chord).'),
115
type: 'string'
116
},
117
mac: {
118
description: nls.localize('vscode.extension.contributes.keybindings.mac', 'Mac specific key or key sequence.'),
119
type: 'string'
120
},
121
linux: {
122
description: nls.localize('vscode.extension.contributes.keybindings.linux', 'Linux specific key or key sequence.'),
123
type: 'string'
124
},
125
win: {
126
description: nls.localize('vscode.extension.contributes.keybindings.win', 'Windows specific key or key sequence.'),
127
type: 'string'
128
},
129
when: {
130
description: nls.localize('vscode.extension.contributes.keybindings.when', 'Condition when the key is active.'),
131
type: 'string'
132
},
133
}
134
};
135
136
const keybindingsExtPoint = ExtensionsRegistry.registerExtensionPoint<ContributedKeyBinding | ContributedKeyBinding[]>({
137
extensionPoint: 'keybindings',
138
deps: [commandsExtensionPoint],
139
jsonSchema: {
140
description: nls.localize('vscode.extension.contributes.keybindings', "Contributes keybindings."),
141
oneOf: [
142
keybindingType,
143
{
144
type: 'array',
145
items: keybindingType
146
}
147
]
148
}
149
});
150
151
const NUMPAD_PRINTABLE_SCANCODES = [
152
ScanCode.NumpadDivide,
153
ScanCode.NumpadMultiply,
154
ScanCode.NumpadSubtract,
155
ScanCode.NumpadAdd,
156
ScanCode.Numpad1,
157
ScanCode.Numpad2,
158
ScanCode.Numpad3,
159
ScanCode.Numpad4,
160
ScanCode.Numpad5,
161
ScanCode.Numpad6,
162
ScanCode.Numpad7,
163
ScanCode.Numpad8,
164
ScanCode.Numpad9,
165
ScanCode.Numpad0,
166
ScanCode.NumpadDecimal
167
];
168
169
const otherMacNumpadMapping = new Map<ScanCode, KeyCode>();
170
otherMacNumpadMapping.set(ScanCode.Numpad1, KeyCode.Digit1);
171
otherMacNumpadMapping.set(ScanCode.Numpad2, KeyCode.Digit2);
172
otherMacNumpadMapping.set(ScanCode.Numpad3, KeyCode.Digit3);
173
otherMacNumpadMapping.set(ScanCode.Numpad4, KeyCode.Digit4);
174
otherMacNumpadMapping.set(ScanCode.Numpad5, KeyCode.Digit5);
175
otherMacNumpadMapping.set(ScanCode.Numpad6, KeyCode.Digit6);
176
otherMacNumpadMapping.set(ScanCode.Numpad7, KeyCode.Digit7);
177
otherMacNumpadMapping.set(ScanCode.Numpad8, KeyCode.Digit8);
178
otherMacNumpadMapping.set(ScanCode.Numpad9, KeyCode.Digit9);
179
otherMacNumpadMapping.set(ScanCode.Numpad0, KeyCode.Digit0);
180
181
export class WorkbenchKeybindingService extends AbstractKeybindingService {
182
183
private _keyboardMapper: IKeyboardMapper;
184
private _cachedResolver: KeybindingResolver | null;
185
private userKeybindings: UserKeybindings;
186
private isComposingGlobalContextKey: IContextKey<boolean>;
187
private _keybindingHoldMode: DeferredPromise<void> | null;
188
private readonly _contributions: Array<{
189
readonly listener?: IDisposable;
190
readonly contribution: KeybindingsSchemaContribution;
191
}> = [];
192
private readonly kbsJsonSchema: KeybindingsJsonSchema;
193
194
constructor(
195
@IContextKeyService contextKeyService: IContextKeyService,
196
@ICommandService commandService: ICommandService,
197
@ITelemetryService telemetryService: ITelemetryService,
198
@INotificationService notificationService: INotificationService,
199
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
200
@IHostService private readonly hostService: IHostService,
201
@IExtensionService extensionService: IExtensionService,
202
@IFileService fileService: IFileService,
203
@IUriIdentityService uriIdentityService: IUriIdentityService,
204
@ILogService logService: ILogService,
205
@IKeyboardLayoutService private readonly keyboardLayoutService: IKeyboardLayoutService
206
) {
207
super(contextKeyService, commandService, telemetryService, notificationService, logService);
208
209
this.isComposingGlobalContextKey = contextKeyService.createKey('isComposing', false);
210
211
this.kbsJsonSchema = new KeybindingsJsonSchema();
212
this.updateKeybindingsJsonSchema();
213
214
this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper();
215
this._register(this.keyboardLayoutService.onDidChangeKeyboardLayout(() => {
216
this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper();
217
this.updateResolver();
218
}));
219
220
this._keybindingHoldMode = null;
221
this._cachedResolver = null;
222
223
this.userKeybindings = this._register(new UserKeybindings(userDataProfileService, uriIdentityService, fileService, logService));
224
this.userKeybindings.initialize().then(() => {
225
if (this.userKeybindings.keybindings.length) {
226
this.updateResolver();
227
}
228
});
229
this._register(this.userKeybindings.onDidChange(() => {
230
logService.debug('User keybindings changed');
231
this.updateResolver();
232
}));
233
234
keybindingsExtPoint.setHandler((extensions) => {
235
236
const keybindings: IExtensionKeybindingRule[] = [];
237
for (const extension of extensions) {
238
this._handleKeybindingsExtensionPointUser(extension.description.identifier, extension.description.isBuiltin, extension.value, extension.collector, keybindings);
239
}
240
241
KeybindingsRegistry.setExtensionKeybindings(keybindings);
242
this.updateResolver();
243
});
244
245
this.updateKeybindingsJsonSchema();
246
this._register(extensionService.onDidRegisterExtensions(() => this.updateKeybindingsJsonSchema()));
247
248
this._register(Event.runAndSubscribe(dom.onDidRegisterWindow, ({ window, disposables }) => disposables.add(this._registerKeyListeners(window)), { window: mainWindow, disposables: this._store }));
249
250
this._register(browser.onDidChangeFullscreen(windowId => {
251
if (windowId !== mainWindow.vscodeWindowId) {
252
return;
253
}
254
255
const keyboard: IKeyboard | null = (<INavigatorWithKeyboard>navigator).keyboard;
256
257
if (BrowserFeatures.keyboard === KeyboardSupport.None) {
258
return;
259
}
260
261
if (browser.isFullscreen(mainWindow)) {
262
keyboard?.lock(['Escape']);
263
} else {
264
keyboard?.unlock();
265
}
266
267
// update resolver which will bring back all unbound keyboard shortcuts
268
this._cachedResolver = null;
269
this._onDidUpdateKeybindings.fire();
270
}));
271
}
272
273
public override dispose(): void {
274
this._contributions.forEach(c => c.listener?.dispose());
275
this._contributions.length = 0;
276
277
super.dispose();
278
}
279
280
private _registerKeyListeners(window: Window): IDisposable {
281
const disposables = new DisposableStore();
282
283
// for standard keybindings
284
disposables.add(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
285
if (this._keybindingHoldMode) {
286
return;
287
}
288
this.isComposingGlobalContextKey.set(e.isComposing);
289
const keyEvent = new StandardKeyboardEvent(e);
290
this._log(`/ Received keydown event - ${printKeyboardEvent(e)}`);
291
this._log(`| Converted keydown event - ${printStandardKeyboardEvent(keyEvent)}`);
292
const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
293
if (shouldPreventDefault) {
294
keyEvent.preventDefault();
295
}
296
this.isComposingGlobalContextKey.set(false);
297
}));
298
299
// for single modifier chord keybindings (e.g. shift shift)
300
disposables.add(dom.addDisposableListener(window, dom.EventType.KEY_UP, (e: KeyboardEvent) => {
301
this._resetKeybindingHoldMode();
302
this.isComposingGlobalContextKey.set(e.isComposing);
303
const keyEvent = new StandardKeyboardEvent(e);
304
const shouldPreventDefault = this._singleModifierDispatch(keyEvent, keyEvent.target);
305
if (shouldPreventDefault) {
306
keyEvent.preventDefault();
307
}
308
this.isComposingGlobalContextKey.set(false);
309
}));
310
311
return disposables;
312
}
313
314
public registerSchemaContribution(contribution: KeybindingsSchemaContribution): IDisposable {
315
const listener = contribution.onDidChange?.(() => this.updateKeybindingsJsonSchema());
316
const entry = { listener, contribution };
317
this._contributions.push(entry);
318
319
this.updateKeybindingsJsonSchema();
320
321
return toDisposable(() => {
322
listener?.dispose();
323
remove(this._contributions, entry);
324
this.updateKeybindingsJsonSchema();
325
});
326
}
327
328
private updateKeybindingsJsonSchema() {
329
this.kbsJsonSchema.updateSchema(this._contributions.flatMap(x => x.contribution.getSchemaAdditions()));
330
}
331
332
private _printKeybinding(keybinding: Keybinding): string {
333
return UserSettingsLabelProvider.toLabel(OS, keybinding.chords, (chord) => {
334
if (chord instanceof KeyCodeChord) {
335
return KeyCodeUtils.toString(chord.keyCode);
336
}
337
return ScanCodeUtils.toString(chord.scanCode);
338
}) || '[null]';
339
}
340
341
private _printResolvedKeybinding(resolvedKeybinding: ResolvedKeybinding): string {
342
return resolvedKeybinding.getDispatchChords().map(x => x || '[null]').join(' ');
343
}
344
345
private _printResolvedKeybindings(output: string[], input: string, resolvedKeybindings: ResolvedKeybinding[]): void {
346
const padLength = 35;
347
const firstRow = `${input.padStart(padLength, ' ')} => `;
348
if (resolvedKeybindings.length === 0) {
349
// no binding found
350
output.push(`${firstRow}${'[NO BINDING]'.padStart(padLength, ' ')}`);
351
return;
352
}
353
354
const firstRowIndentation = firstRow.length;
355
const isFirst = true;
356
for (const resolvedKeybinding of resolvedKeybindings) {
357
if (isFirst) {
358
output.push(`${firstRow}${this._printResolvedKeybinding(resolvedKeybinding).padStart(padLength, ' ')}`);
359
} else {
360
output.push(`${' '.repeat(firstRowIndentation)}${this._printResolvedKeybinding(resolvedKeybinding).padStart(padLength, ' ')}`);
361
}
362
}
363
}
364
365
private _dumpResolveKeybindingDebugInfo(): string {
366
367
const seenBindings = new Set<string>();
368
const result: string[] = [];
369
370
result.push(`Default Resolved Keybindings (unique only):`);
371
for (const item of KeybindingsRegistry.getDefaultKeybindings()) {
372
if (!item.keybinding) {
373
continue;
374
}
375
const input = this._printKeybinding(item.keybinding);
376
if (seenBindings.has(input)) {
377
continue;
378
}
379
seenBindings.add(input);
380
const resolvedKeybindings = this._keyboardMapper.resolveKeybinding(item.keybinding);
381
this._printResolvedKeybindings(result, input, resolvedKeybindings);
382
}
383
384
result.push(`User Resolved Keybindings (unique only):`);
385
for (const item of this.userKeybindings.keybindings) {
386
if (!item.keybinding) {
387
continue;
388
}
389
const input = item._sourceKey ?? 'Impossible: missing source key, but has keybinding';
390
if (seenBindings.has(input)) {
391
continue;
392
}
393
seenBindings.add(input);
394
const resolvedKeybindings = this._keyboardMapper.resolveKeybinding(item.keybinding);
395
this._printResolvedKeybindings(result, input, resolvedKeybindings);
396
}
397
398
return result.join('\n');
399
}
400
401
public _dumpDebugInfo(): string {
402
const layoutInfo = JSON.stringify(this.keyboardLayoutService.getCurrentKeyboardLayout(), null, '\t');
403
const mapperInfo = this._keyboardMapper.dumpDebugInfo();
404
const resolvedKeybindings = this._dumpResolveKeybindingDebugInfo();
405
const rawMapping = JSON.stringify(this.keyboardLayoutService.getRawKeyboardMapping(), null, '\t');
406
return `Layout info:\n${layoutInfo}\n\n${resolvedKeybindings}\n\n${mapperInfo}\n\nRaw mapping:\n${rawMapping}`;
407
}
408
409
public _dumpDebugInfoJSON(): string {
410
const info = {
411
layout: this.keyboardLayoutService.getCurrentKeyboardLayout(),
412
rawMapping: this.keyboardLayoutService.getRawKeyboardMapping()
413
};
414
return JSON.stringify(info, null, '\t');
415
}
416
417
public override enableKeybindingHoldMode(commandId: string): Promise<void> | undefined {
418
if (this._currentlyDispatchingCommandId !== commandId) {
419
return undefined;
420
}
421
this._keybindingHoldMode = new DeferredPromise<void>();
422
const focusTracker = dom.trackFocus(dom.getWindow(undefined));
423
const listener = focusTracker.onDidBlur(() => this._resetKeybindingHoldMode());
424
this._keybindingHoldMode.p.finally(() => {
425
listener.dispose();
426
focusTracker.dispose();
427
});
428
this._log(`+ Enabled hold-mode for ${commandId}.`);
429
return this._keybindingHoldMode.p;
430
}
431
432
private _resetKeybindingHoldMode(): void {
433
if (this._keybindingHoldMode) {
434
this._keybindingHoldMode?.complete();
435
this._keybindingHoldMode = null;
436
}
437
}
438
439
public override customKeybindingsCount(): number {
440
return this.userKeybindings.keybindings.length;
441
}
442
443
private updateResolver(): void {
444
this._cachedResolver = null;
445
this._onDidUpdateKeybindings.fire();
446
}
447
448
protected _getResolver(): KeybindingResolver {
449
if (!this._cachedResolver) {
450
const defaults = this._resolveKeybindingItems(KeybindingsRegistry.getDefaultKeybindings(), true);
451
const overrides = this._resolveUserKeybindingItems(this.userKeybindings.keybindings, false);
452
this._cachedResolver = new KeybindingResolver(defaults, overrides, (str) => this._log(str));
453
}
454
return this._cachedResolver;
455
}
456
457
protected _documentHasFocus(): boolean {
458
// it is possible that the document has lost focus, but the
459
// window is still focused, e.g. when a <webview> element
460
// has focus
461
return this.hostService.hasFocus;
462
}
463
464
private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
465
const result: ResolvedKeybindingItem[] = [];
466
let resultLen = 0;
467
for (const item of items) {
468
const when = item.when || undefined;
469
const keybinding = item.keybinding;
470
if (!keybinding) {
471
// This might be a removal keybinding item in user settings => accept it
472
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, item.extensionId, item.isBuiltinExtension);
473
} else {
474
if (this._assertBrowserConflicts(keybinding)) {
475
continue;
476
}
477
478
const resolvedKeybindings = this._keyboardMapper.resolveKeybinding(keybinding);
479
for (let i = resolvedKeybindings.length - 1; i >= 0; i--) {
480
const resolvedKeybinding = resolvedKeybindings[i];
481
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, item.extensionId, item.isBuiltinExtension);
482
}
483
}
484
}
485
486
return result;
487
}
488
489
private _resolveUserKeybindingItems(items: IUserKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
490
const result: ResolvedKeybindingItem[] = [];
491
let resultLen = 0;
492
for (const item of items) {
493
const when = item.when || undefined;
494
if (!item.keybinding) {
495
// This might be a removal keybinding item in user settings => accept it
496
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, null, false);
497
} else {
498
const resolvedKeybindings = this._keyboardMapper.resolveKeybinding(item.keybinding);
499
for (const resolvedKeybinding of resolvedKeybindings) {
500
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, null, false);
501
}
502
}
503
}
504
505
return result;
506
}
507
508
private _assertBrowserConflicts(keybinding: Keybinding): boolean {
509
if (BrowserFeatures.keyboard === KeyboardSupport.Always) {
510
return false;
511
}
512
513
if (BrowserFeatures.keyboard === KeyboardSupport.FullScreen && browser.isFullscreen(mainWindow)) {
514
return false;
515
}
516
517
for (const chord of keybinding.chords) {
518
if (!chord.metaKey && !chord.altKey && !chord.ctrlKey && !chord.shiftKey) {
519
continue;
520
}
521
522
const modifiersMask = KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift;
523
524
let partModifiersMask = 0;
525
if (chord.metaKey) {
526
partModifiersMask |= KeyMod.CtrlCmd;
527
}
528
529
if (chord.shiftKey) {
530
partModifiersMask |= KeyMod.Shift;
531
}
532
533
if (chord.altKey) {
534
partModifiersMask |= KeyMod.Alt;
535
}
536
537
if (chord.ctrlKey && OS === OperatingSystem.Macintosh) {
538
partModifiersMask |= KeyMod.WinCtrl;
539
}
540
541
if ((partModifiersMask & modifiersMask) === (KeyMod.CtrlCmd | KeyMod.Alt)) {
542
if (chord instanceof ScanCodeChord && (chord.scanCode === ScanCode.ArrowLeft || chord.scanCode === ScanCode.ArrowRight)) {
543
// console.warn('Ctrl/Cmd+Arrow keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
544
return true;
545
}
546
if (chord instanceof KeyCodeChord && (chord.keyCode === KeyCode.LeftArrow || chord.keyCode === KeyCode.RightArrow)) {
547
// console.warn('Ctrl/Cmd+Arrow keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
548
return true;
549
}
550
}
551
552
if ((partModifiersMask & modifiersMask) === KeyMod.CtrlCmd) {
553
if (chord instanceof ScanCodeChord && (chord.scanCode >= ScanCode.Digit1 && chord.scanCode <= ScanCode.Digit0)) {
554
// console.warn('Ctrl/Cmd+Num keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
555
return true;
556
}
557
if (chord instanceof KeyCodeChord && (chord.keyCode >= KeyCode.Digit0 && chord.keyCode <= KeyCode.Digit9)) {
558
// console.warn('Ctrl/Cmd+Num keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
559
return true;
560
}
561
}
562
}
563
564
return false;
565
}
566
567
public resolveKeybinding(kb: Keybinding): ResolvedKeybinding[] {
568
return this._keyboardMapper.resolveKeybinding(kb);
569
}
570
571
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
572
this.keyboardLayoutService.validateCurrentKeyboardMapping(keyboardEvent);
573
return this._keyboardMapper.resolveKeyboardEvent(keyboardEvent);
574
}
575
576
public resolveUserBinding(userBinding: string): ResolvedKeybinding[] {
577
const keybinding = KeybindingParser.parseKeybinding(userBinding);
578
return (keybinding ? this._keyboardMapper.resolveKeybinding(keybinding) : []);
579
}
580
581
private _handleKeybindingsExtensionPointUser(extensionId: ExtensionIdentifier, isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: ExtensionMessageCollector, result: IExtensionKeybindingRule[]): void {
582
if (Array.isArray(keybindings)) {
583
for (let i = 0, len = keybindings.length; i < len; i++) {
584
this._handleKeybinding(extensionId, isBuiltin, i + 1, keybindings[i], collector, result);
585
}
586
} else {
587
this._handleKeybinding(extensionId, isBuiltin, 1, keybindings, collector, result);
588
}
589
}
590
591
private _handleKeybinding(extensionId: ExtensionIdentifier, isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: ExtensionMessageCollector, result: IExtensionKeybindingRule[]): void {
592
593
const rejects: string[] = [];
594
595
if (isValidContributedKeyBinding(keybindings, rejects)) {
596
const rule = this._asCommandRule(extensionId, isBuiltin, idx++, keybindings);
597
if (rule) {
598
result.push(rule);
599
}
600
}
601
602
if (rejects.length > 0) {
603
collector.error(nls.localize(
604
'invalid.keybindings',
605
"Invalid `contributes.{0}`: {1}",
606
keybindingsExtPoint.name,
607
rejects.join('\n')
608
));
609
}
610
}
611
612
private static bindToCurrentPlatform(key: string | undefined, mac: string | undefined, linux: string | undefined, win: string | undefined): string | undefined {
613
if (OS === OperatingSystem.Windows && win) {
614
if (win) {
615
return win;
616
}
617
} else if (OS === OperatingSystem.Macintosh) {
618
if (mac) {
619
return mac;
620
}
621
} else {
622
if (linux) {
623
return linux;
624
}
625
}
626
return key;
627
}
628
629
private _asCommandRule(extensionId: ExtensionIdentifier, isBuiltin: boolean, idx: number, binding: ContributedKeyBinding): IExtensionKeybindingRule | undefined {
630
631
const { command, args, when, key, mac, linux, win } = binding;
632
const keybinding = WorkbenchKeybindingService.bindToCurrentPlatform(key, mac, linux, win);
633
if (!keybinding) {
634
return undefined;
635
}
636
637
let weight: number;
638
if (isBuiltin) {
639
weight = KeybindingWeight.BuiltinExtension + idx;
640
} else {
641
weight = KeybindingWeight.ExternalExtension + idx;
642
}
643
644
const commandAction = MenuRegistry.getCommand(command);
645
const precondition = commandAction && commandAction.precondition;
646
let fullWhen: ContextKeyExpression | undefined;
647
if (when && precondition) {
648
fullWhen = ContextKeyExpr.and(precondition, ContextKeyExpr.deserialize(when));
649
} else if (when) {
650
fullWhen = ContextKeyExpr.deserialize(when);
651
} else if (precondition) {
652
fullWhen = precondition;
653
}
654
655
const desc: IExtensionKeybindingRule = {
656
id: command,
657
args,
658
when: fullWhen,
659
weight: weight,
660
keybinding: KeybindingParser.parseKeybinding(keybinding),
661
extensionId: extensionId.value,
662
isBuiltinExtension: isBuiltin
663
};
664
return desc;
665
}
666
667
public override getDefaultKeybindingsContent(): string {
668
const resolver = this._getResolver();
669
const defaultKeybindings = resolver.getDefaultKeybindings();
670
const boundCommands = resolver.getDefaultBoundCommands();
671
return (
672
WorkbenchKeybindingService._getDefaultKeybindings(defaultKeybindings)
673
+ '\n\n'
674
+ WorkbenchKeybindingService._getAllCommandsAsComment(boundCommands)
675
);
676
}
677
678
private static _getDefaultKeybindings(defaultKeybindings: readonly ResolvedKeybindingItem[]): string {
679
const out = new OutputBuilder();
680
out.writeLine('[');
681
682
const lastIndex = defaultKeybindings.length - 1;
683
defaultKeybindings.forEach((k, index) => {
684
KeybindingIO.writeKeybindingItem(out, k);
685
if (index !== lastIndex) {
686
out.writeLine(',');
687
} else {
688
out.writeLine();
689
}
690
});
691
out.writeLine(']');
692
return out.toString();
693
}
694
695
private static _getAllCommandsAsComment(boundCommands: Map<string, boolean>): string {
696
const unboundCommands = getAllUnboundCommands(boundCommands);
697
const pretty = unboundCommands.sort().join('\n// - ');
698
return '// ' + nls.localize('unboundCommands', "Here are other available commands: ") + '\n// - ' + pretty;
699
}
700
701
override mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
702
if (event.ctrlKey || event.metaKey || event.altKey) {
703
// ignore ctrl/cmd/alt-combination but not shift-combinatios
704
return false;
705
}
706
const code = ScanCodeUtils.toEnum(event.code);
707
708
if (NUMPAD_PRINTABLE_SCANCODES.indexOf(code) !== -1) {
709
// This is a numpad key that might produce a printable character based on NumLock.
710
// Let's check if NumLock is on or off based on the event's keyCode.
711
// e.g.
712
// - when NumLock is off, ScanCode.Numpad4 produces KeyCode.LeftArrow
713
// - when NumLock is on, ScanCode.Numpad4 produces KeyCode.NUMPAD_4
714
// However, ScanCode.NumpadAdd always produces KeyCode.NUMPAD_ADD
715
if (event.keyCode === IMMUTABLE_CODE_TO_KEY_CODE[code]) {
716
// NumLock is on or this is /, *, -, + on the numpad
717
return true;
718
}
719
if (isMacintosh && event.keyCode === otherMacNumpadMapping.get(code)) {
720
// on macOS, the numpad keys can also map to keys 1 - 0.
721
return true;
722
}
723
return false;
724
}
725
726
const keycode = IMMUTABLE_CODE_TO_KEY_CODE[code];
727
if (keycode !== -1) {
728
// https://github.com/microsoft/vscode/issues/74934
729
return false;
730
}
731
// consult the KeyboardMapperFactory to check the given event for
732
// a printable value.
733
const mapping = this.keyboardLayoutService.getRawKeyboardMapping();
734
if (!mapping) {
735
return false;
736
}
737
const keyInfo = mapping[event.code];
738
if (!keyInfo) {
739
return false;
740
}
741
if (!keyInfo.value || /\s/.test(keyInfo.value)) {
742
return false;
743
}
744
return true;
745
}
746
}
747
748
class UserKeybindings extends Disposable {
749
750
private _rawKeybindings: Object[] = [];
751
private _keybindings: IUserKeybindingItem[] = [];
752
get keybindings(): IUserKeybindingItem[] { return this._keybindings; }
753
754
private readonly reloadConfigurationScheduler: RunOnceScheduler;
755
756
private readonly watchDisposables = this._register(new DisposableStore());
757
758
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
759
readonly onDidChange: Event<void> = this._onDidChange.event;
760
761
constructor(
762
private readonly userDataProfileService: IUserDataProfileService,
763
private readonly uriIdentityService: IUriIdentityService,
764
private readonly fileService: IFileService,
765
logService: ILogService,
766
) {
767
super();
768
769
this.watch();
770
771
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(changed => {
772
if (changed) {
773
this._onDidChange.fire();
774
}
775
}), 50));
776
777
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userDataProfileService.currentProfile.keybindingsResource))(() => {
778
logService.debug('Keybindings file changed');
779
this.reloadConfigurationScheduler.schedule();
780
}));
781
782
this._register(this.fileService.onDidRunOperation((e) => {
783
if (e.operation === FileOperation.WRITE && e.resource.toString() === this.userDataProfileService.currentProfile.keybindingsResource.toString()) {
784
logService.debug('Keybindings file written');
785
this.reloadConfigurationScheduler.schedule();
786
}
787
}));
788
789
this._register(userDataProfileService.onDidChangeCurrentProfile(e => {
790
if (!this.uriIdentityService.extUri.isEqual(e.previous.keybindingsResource, e.profile.keybindingsResource)) {
791
e.join(this.whenCurrentProfileChanged());
792
}
793
}));
794
}
795
796
private async whenCurrentProfileChanged(): Promise<void> {
797
this.watch();
798
this.reloadConfigurationScheduler.schedule();
799
}
800
801
private watch(): void {
802
this.watchDisposables.clear();
803
this.watchDisposables.add(this.fileService.watch(dirname(this.userDataProfileService.currentProfile.keybindingsResource)));
804
// Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134
805
this.watchDisposables.add(this.fileService.watch(this.userDataProfileService.currentProfile.keybindingsResource));
806
}
807
808
async initialize(): Promise<void> {
809
await this.reload();
810
}
811
812
private async reload(): Promise<boolean> {
813
const newKeybindings = await this.readUserKeybindings();
814
if (objects.equals(this._rawKeybindings, newKeybindings)) {
815
// no change
816
return false;
817
}
818
819
this._rawKeybindings = newKeybindings;
820
this._keybindings = this._rawKeybindings.map((k) => KeybindingIO.readUserKeybindingItem(k));
821
return true;
822
}
823
824
private async readUserKeybindings(): Promise<Object[]> {
825
try {
826
const content = await this.fileService.readFile(this.userDataProfileService.currentProfile.keybindingsResource);
827
const value = parse(content.value.toString());
828
return Array.isArray(value)
829
? value.filter(v => v && typeof v === 'object' /* just typeof === object doesn't catch `null` */)
830
: [];
831
} catch (e) {
832
return [];
833
}
834
}
835
}
836
837
/**
838
* Registers the `keybindings.json`'s schema with the JSON schema registry. Allows updating the schema, e.g., when new commands are registered (e.g., by extensions).
839
*
840
* Lifecycle owned by `WorkbenchKeybindingService`. Must be instantiated only once.
841
*/
842
class KeybindingsJsonSchema {
843
844
private static readonly schemaId = 'vscode://schemas/keybindings';
845
846
private readonly commandsSchemas: IJSONSchema[] = [];
847
private readonly commandsEnum: string[] = [];
848
private readonly removalCommandsEnum: string[] = [];
849
private readonly commandsEnumDescriptions: (string | undefined)[] = [];
850
private readonly schema: IJSONSchema = {
851
id: KeybindingsJsonSchema.schemaId,
852
type: 'array',
853
title: nls.localize('keybindings.json.title', "Keybindings configuration"),
854
allowTrailingCommas: true,
855
allowComments: true,
856
definitions: {
857
'editorGroupsSchema': {
858
'type': 'array',
859
'items': {
860
'type': 'object',
861
'properties': {
862
'groups': {
863
'$ref': '#/definitions/editorGroupsSchema',
864
'default': [{}, {}]
865
},
866
'size': {
867
'type': 'number',
868
'default': 0.5
869
}
870
}
871
}
872
},
873
'commandNames': {
874
'type': 'string',
875
'enum': this.commandsEnum,
876
'enumDescriptions': <any>this.commandsEnumDescriptions,
877
'description': nls.localize('keybindings.json.command', "Name of the command to execute"),
878
},
879
'commandType': {
880
'anyOf': [ // repetition of this clause here and below is intentional: one is for nice diagnostics & one is for code completion
881
{
882
$ref: '#/definitions/commandNames'
883
},
884
{
885
'type': 'string',
886
'enum': this.removalCommandsEnum,
887
'enumDescriptions': <any>this.commandsEnumDescriptions,
888
'description': nls.localize('keybindings.json.removalCommand', "Name of the command to remove keyboard shortcut for"),
889
},
890
{
891
'type': 'string'
892
},
893
]
894
},
895
'commandsSchemas': {
896
'allOf': this.commandsSchemas
897
}
898
},
899
items: {
900
'required': ['key'],
901
'type': 'object',
902
'defaultSnippets': [{ 'body': { 'key': '$1', 'command': '$2', 'when': '$3' } }],
903
'properties': {
904
'key': {
905
'type': 'string',
906
'description': nls.localize('keybindings.json.key', "Key or key sequence (separated by space)"),
907
},
908
'command': {
909
'anyOf': [
910
{
911
'if': {
912
'type': 'array'
913
},
914
'then': {
915
'not': {
916
'type': 'array'
917
},
918
'errorMessage': nls.localize('keybindings.commandsIsArray', "Incorrect type. Expected \"{0}\". The field 'command' does not support running multiple commands. Use command 'runCommands' to pass it multiple commands to run.", 'string')
919
},
920
'else': {
921
'$ref': '#/definitions/commandType'
922
}
923
},
924
{
925
'$ref': '#/definitions/commandType'
926
}
927
]
928
},
929
'when': {
930
'type': 'string',
931
'description': nls.localize('keybindings.json.when', "Condition when the key is active.")
932
},
933
'args': {
934
'description': nls.localize('keybindings.json.args', "Arguments to pass to the command to execute.")
935
}
936
},
937
'$ref': '#/definitions/commandsSchemas'
938
}
939
};
940
941
private readonly schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
942
943
constructor() {
944
this.schemaRegistry.registerSchema(KeybindingsJsonSchema.schemaId, this.schema);
945
}
946
947
// TODO@ulugbekna: can updates happen incrementally rather than rebuilding; concerns:
948
// - is just appending additional schemas enough for the registry to pick them up?
949
// - can `CommandsRegistry.getCommands` and `MenuRegistry.getCommands` return different values at different times? ie would just pushing new schemas from `additionalContributions` not be enough?
950
updateSchema(additionalContributions: readonly IJSONSchema[]) {
951
this.commandsSchemas.length = 0;
952
this.commandsEnum.length = 0;
953
this.removalCommandsEnum.length = 0;
954
this.commandsEnumDescriptions.length = 0;
955
956
const knownCommands = new Set<string>();
957
const addKnownCommand = (commandId: string, description?: string | ILocalizedString | undefined) => {
958
if (!/^_/.test(commandId)) {
959
if (!knownCommands.has(commandId)) {
960
knownCommands.add(commandId);
961
962
this.commandsEnum.push(commandId);
963
this.commandsEnumDescriptions.push(isLocalizedString(description) ? description.value : description);
964
965
// Also add the negative form for keybinding removal
966
this.removalCommandsEnum.push(`-${commandId}`);
967
}
968
}
969
};
970
971
const allCommands = CommandsRegistry.getCommands();
972
for (const [commandId, command] of allCommands) {
973
const commandMetadata = command.metadata;
974
975
addKnownCommand(commandId, commandMetadata?.description ?? MenuRegistry.getCommand(commandId)?.title);
976
977
if (!commandMetadata || !commandMetadata.args || commandMetadata.args.length !== 1 || !commandMetadata.args[0].schema) {
978
continue;
979
}
980
981
const argsSchema = commandMetadata.args[0].schema;
982
const argsRequired = (
983
(typeof commandMetadata.args[0].isOptional !== 'undefined')
984
? (!commandMetadata.args[0].isOptional)
985
: (Array.isArray(argsSchema.required) && argsSchema.required.length > 0)
986
);
987
const addition = {
988
'if': {
989
'required': ['command'],
990
'properties': {
991
'command': { 'const': commandId }
992
}
993
},
994
'then': {
995
'required': (<string[]>[]).concat(argsRequired ? ['args'] : []),
996
'properties': {
997
'args': argsSchema
998
}
999
}
1000
};
1001
1002
this.commandsSchemas.push(addition);
1003
}
1004
1005
const menuCommands = MenuRegistry.getCommands();
1006
for (const commandId of menuCommands.keys()) {
1007
addKnownCommand(commandId);
1008
}
1009
1010
this.commandsSchemas.push(...additionalContributions);
1011
this.schemaRegistry.notifySchemaChanged(KeybindingsJsonSchema.schemaId);
1012
}
1013
}
1014
1015
registerSingleton(IKeybindingService, WorkbenchKeybindingService, InstantiationType.Eager);
1016
1017