mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
import * as nls from 'vs/nls';
|
|
import {IHTMLContentElement} from 'vs/base/common/htmlContent';
|
|
import {IJSONSchema} from 'vs/base/common/jsonSchema';
|
|
import {Keybinding} from 'vs/base/common/keyCodes';
|
|
import * as platform from 'vs/base/common/platform';
|
|
import {toDisposable} from 'vs/base/common/lifecycle';
|
|
import {IEventService} from 'vs/platform/event/common/event';
|
|
import {IExtensionMessageCollector, ExtensionsRegistry} from 'vs/platform/extensions/common/extensionsRegistry';
|
|
import {Extensions, IJSONContributionRegistry} from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
|
import {KeybindingService} from 'vs/platform/keybinding/browser/keybindingServiceImpl';
|
|
import {IStatusbarService} from 'vs/platform/statusbar/common/statusbar';
|
|
import {IOSupport} from 'vs/platform/keybinding/common/keybindingResolver';
|
|
import {ICommandService} from 'vs/platform/commands/common/commands';
|
|
import {IKeybindingItem, IUserFriendlyKeybinding} from 'vs/platform/keybinding/common/keybinding';
|
|
import {IContextKeyService} from 'vs/platform/contextkey/common/contextkey';
|
|
import {IKeybindingRule, KeybindingsRegistry} from 'vs/platform/keybinding/common/keybindingsRegistry';
|
|
import {Registry} from 'vs/platform/platform';
|
|
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
|
|
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
|
|
import {getNativeLabelProvider, getNativeAriaLabelProvider} from 'vs/workbench/services/keybinding/electron-browser/nativeKeymap';
|
|
import {IMessageService} from 'vs/platform/message/common/message';
|
|
import {ConfigWatcher} from 'vs/base/node/config';
|
|
import {IEnvironmentService} from 'vs/platform/environment/common/environment';
|
|
|
|
interface ContributedKeyBinding {
|
|
command: string;
|
|
key: string;
|
|
when?: string;
|
|
mac?: string;
|
|
linux?: string;
|
|
win?: string;
|
|
}
|
|
|
|
function isContributedKeyBindingsArray(thing: ContributedKeyBinding | ContributedKeyBinding[]): thing is ContributedKeyBinding[] {
|
|
return Array.isArray(thing);
|
|
}
|
|
|
|
function isValidContributedKeyBinding(keyBinding: ContributedKeyBinding, rejects: string[]): boolean {
|
|
if (!keyBinding) {
|
|
rejects.push(nls.localize('nonempty', "expected non-empty value."));
|
|
return false;
|
|
}
|
|
if (typeof keyBinding.command !== 'string') {
|
|
rejects.push(nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
|
|
return false;
|
|
}
|
|
if (typeof keyBinding.key !== 'string') {
|
|
rejects.push(nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'key'));
|
|
return false;
|
|
}
|
|
if (keyBinding.when && typeof keyBinding.when !== 'string') {
|
|
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
|
|
return false;
|
|
}
|
|
if (keyBinding.mac && typeof keyBinding.mac !== 'string') {
|
|
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'mac'));
|
|
return false;
|
|
}
|
|
if (keyBinding.linux && typeof keyBinding.linux !== 'string') {
|
|
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'linux'));
|
|
return false;
|
|
}
|
|
if (keyBinding.win && typeof keyBinding.win !== 'string') {
|
|
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'win'));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
let keybindingType: IJSONSchema = {
|
|
type: 'object',
|
|
default: { command: '', key: '' },
|
|
properties: {
|
|
command: {
|
|
description: nls.localize('vscode.extension.contributes.keybindings.command', 'Identifier of the command to run when keybinding is triggered.'),
|
|
type: 'string'
|
|
},
|
|
key: {
|
|
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'),
|
|
type: 'string'
|
|
},
|
|
mac: {
|
|
description: nls.localize('vscode.extension.contributes.keybindings.mac', 'Mac specific key or key sequence.'),
|
|
type: 'string'
|
|
},
|
|
linux: {
|
|
description: nls.localize('vscode.extension.contributes.keybindings.linux', 'Linux specific key or key sequence.'),
|
|
type: 'string'
|
|
},
|
|
win: {
|
|
description: nls.localize('vscode.extension.contributes.keybindings.win', 'Windows specific key or key sequence.'),
|
|
type: 'string'
|
|
},
|
|
when: {
|
|
description: nls.localize('vscode.extension.contributes.keybindings.when', 'Condition when the key is active.'),
|
|
type: 'string'
|
|
}
|
|
}
|
|
};
|
|
|
|
let keybindingsExtPoint = ExtensionsRegistry.registerExtensionPoint<ContributedKeyBinding | ContributedKeyBinding[]>('keybindings', {
|
|
description: nls.localize('vscode.extension.contributes.keybindings', "Contributes keybindings."),
|
|
oneOf: [
|
|
keybindingType,
|
|
{
|
|
type: 'array',
|
|
items: keybindingType
|
|
}
|
|
]
|
|
});
|
|
|
|
export class WorkbenchKeybindingService extends KeybindingService {
|
|
private userKeybindings: ConfigWatcher<IUserFriendlyKeybinding[]>;
|
|
|
|
constructor(
|
|
domNode: HTMLElement,
|
|
@IContextKeyService contextKeyService: IContextKeyService,
|
|
@ICommandService commandService: ICommandService,
|
|
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
|
@IEventService private eventService: IEventService,
|
|
@ITelemetryService private telemetryService: ITelemetryService,
|
|
@IMessageService messageService: IMessageService,
|
|
@IEnvironmentService private environmentService: IEnvironmentService,
|
|
@IStatusbarService statusBarService: IStatusbarService
|
|
) {
|
|
super(contextKeyService, commandService, messageService, statusBarService);
|
|
|
|
this.userKeybindings = new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [] });
|
|
this.toDispose.push(toDisposable(() => this.userKeybindings.dispose()));
|
|
|
|
keybindingsExtPoint.setHandler((extensions) => {
|
|
let commandAdded = false;
|
|
|
|
for (let extension of extensions) {
|
|
commandAdded = this._handleKeybindingsExtensionPointUser(extension.description.isBuiltin, extension.value, extension.collector) || commandAdded;
|
|
}
|
|
|
|
if (commandAdded) {
|
|
this.updateResolver();
|
|
}
|
|
});
|
|
|
|
this.toDispose.push(this.userKeybindings.onDidUpdateConfiguration(() => this.updateResolver()));
|
|
|
|
this._beginListening(domNode);
|
|
}
|
|
|
|
public customKeybindingsCount(): number {
|
|
let userKeybindings = this.userKeybindings.getConfig();
|
|
|
|
return userKeybindings.length;
|
|
}
|
|
|
|
protected _getExtraKeybindings(isFirstTime: boolean): IKeybindingItem[] {
|
|
let extraUserKeybindings: IUserFriendlyKeybinding[] = this.userKeybindings.getConfig();
|
|
if (!isFirstTime) {
|
|
let cnt = extraUserKeybindings.length;
|
|
|
|
this.telemetryService.publicLog('customKeybindingsChanged', {
|
|
keyCount: cnt
|
|
});
|
|
}
|
|
|
|
return extraUserKeybindings.map((k, i) => IOSupport.readKeybindingItem(k, i));
|
|
}
|
|
|
|
public getLabelFor(keybinding: Keybinding): string {
|
|
return keybinding.toCustomLabel(getNativeLabelProvider());
|
|
}
|
|
|
|
public getHTMLLabelFor(keybinding: Keybinding): IHTMLContentElement[] {
|
|
return keybinding.toCustomHTMLLabel(getNativeLabelProvider());
|
|
}
|
|
|
|
public getAriaLabelFor(keybinding: Keybinding): string {
|
|
return keybinding.toCustomLabel(getNativeAriaLabelProvider());
|
|
}
|
|
|
|
public getElectronAcceleratorFor(keybinding: Keybinding): string {
|
|
if (platform.isWindows) {
|
|
// electron menus always do the correct rendering on Windows
|
|
return super.getElectronAcceleratorFor(keybinding);
|
|
}
|
|
|
|
let usLabel = keybinding._toUSLabel();
|
|
let label = this.getLabelFor(keybinding);
|
|
if (usLabel !== label) {
|
|
// electron menus are incorrect in rendering (linux) and in rendering and interpreting (mac)
|
|
// for non US standard keyboard layouts
|
|
return null;
|
|
}
|
|
|
|
return super.getElectronAcceleratorFor(keybinding);
|
|
}
|
|
|
|
private _handleKeybindingsExtensionPointUser(isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: IExtensionMessageCollector): boolean {
|
|
if (isContributedKeyBindingsArray(keybindings)) {
|
|
let commandAdded = false;
|
|
for (let i = 0, len = keybindings.length; i < len; i++) {
|
|
commandAdded = this._handleKeybinding(isBuiltin, i + 1, keybindings[i], collector) || commandAdded;
|
|
}
|
|
return commandAdded;
|
|
} else {
|
|
return this._handleKeybinding(isBuiltin, 1, keybindings, collector);
|
|
}
|
|
}
|
|
|
|
private _handleKeybinding(isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: IExtensionMessageCollector): boolean {
|
|
|
|
let rejects: string[] = [];
|
|
let commandAdded = false;
|
|
|
|
if (isValidContributedKeyBinding(keybindings, rejects)) {
|
|
let rule = this._asCommandRule(isBuiltin, idx++, keybindings);
|
|
if (rule) {
|
|
KeybindingsRegistry.registerKeybindingRule(rule);
|
|
commandAdded = true;
|
|
}
|
|
}
|
|
|
|
if (rejects.length > 0) {
|
|
collector.error(nls.localize(
|
|
'invalid.keybindings',
|
|
"Invalid `contributes.{0}`: {1}",
|
|
keybindingsExtPoint.name,
|
|
rejects.join('\n')
|
|
));
|
|
}
|
|
|
|
return commandAdded;
|
|
}
|
|
|
|
private _asCommandRule(isBuiltin: boolean, idx: number, binding: ContributedKeyBinding): IKeybindingRule {
|
|
|
|
let {command, when, key, mac, linux, win} = binding;
|
|
|
|
let weight: number;
|
|
if (isBuiltin) {
|
|
weight = KeybindingsRegistry.WEIGHT.builtinExtension(idx);
|
|
} else {
|
|
weight = KeybindingsRegistry.WEIGHT.externalExtension(idx);
|
|
}
|
|
|
|
let desc = {
|
|
id: command,
|
|
when: IOSupport.readKeybindingWhen(when),
|
|
weight: weight,
|
|
primary: IOSupport.readKeybinding(key),
|
|
mac: mac && { primary: IOSupport.readKeybinding(mac) },
|
|
linux: linux && { primary: IOSupport.readKeybinding(linux) },
|
|
win: win && { primary: IOSupport.readKeybinding(win) }
|
|
};
|
|
|
|
if (!desc.primary && !desc.mac && !desc.linux && !desc.win) {
|
|
return;
|
|
}
|
|
|
|
return desc;
|
|
}
|
|
}
|
|
|
|
let schemaId = 'vscode://schemas/keybindings';
|
|
let schema: IJSONSchema = {
|
|
'id': schemaId,
|
|
'type': 'array',
|
|
'title': nls.localize('keybindings.json.title', "Keybindings configuration"),
|
|
'items': {
|
|
'required': ['key'],
|
|
'type': 'object',
|
|
'defaultSnippets': [{ 'body': { 'key': '{{_}}', 'command': '{{_}}', 'when': '{{_}}' } }],
|
|
'properties': {
|
|
'key': {
|
|
'type': 'string',
|
|
'description': nls.localize('keybindings.json.key', 'Key or key sequence (separated by space)'),
|
|
},
|
|
'command': {
|
|
'description': nls.localize('keybindings.json.command', 'Name of the command to execute'),
|
|
},
|
|
'when': {
|
|
'type': 'string',
|
|
'description': nls.localize('keybindings.json.when', 'Condition when the key is active.')
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let schemaRegistry = <IJSONContributionRegistry>Registry.as(Extensions.JSONContribution);
|
|
schemaRegistry.registerSchema(schemaId, schema);
|