keybindingService.ts 10.6 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
8 9 10
import { IHTMLContentElement } from 'vs/base/common/htmlContent';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Keybinding } from 'vs/base/common/keybinding';
11
import * as platform from 'vs/base/common/platform';
J
Johannes Rieken 已提交
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
import { toDisposable } from 'vs/base/common/lifecycle';
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 { 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';
E
Erich Gamma 已提交
28 29 30 31 32 33 34 35 36 37

interface ContributedKeyBinding {
	command: string;
	key: string;
	when?: string;
	mac?: string;
	linux?: string;
	win?: string;
}

38
function isContributedKeyBindingsArray(thing: ContributedKeyBinding | ContributedKeyBinding[]): thing is ContributedKeyBinding[] {
E
Erich Gamma 已提交
39 40 41 42 43 44 45 46 47
	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') {
B
Benjamin Pasero 已提交
48
		rejects.push(nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
E
Erich Gamma 已提交
49 50 51
		return false;
	}
	if (typeof keyBinding.key !== 'string') {
B
Benjamin Pasero 已提交
52
		rejects.push(nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'key'));
E
Erich Gamma 已提交
53 54 55
		return false;
	}
	if (keyBinding.when && typeof keyBinding.when !== 'string') {
B
Benjamin Pasero 已提交
56
		rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
E
Erich Gamma 已提交
57 58 59
		return false;
	}
	if (keyBinding.mac && typeof keyBinding.mac !== 'string') {
B
Benjamin Pasero 已提交
60
		rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'mac'));
E
Erich Gamma 已提交
61 62 63
		return false;
	}
	if (keyBinding.linux && typeof keyBinding.linux !== 'string') {
B
Benjamin Pasero 已提交
64
		rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'linux'));
E
Erich Gamma 已提交
65 66 67
		return false;
	}
	if (keyBinding.win && typeof keyBinding.win !== 'string') {
B
Benjamin Pasero 已提交
68
		rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'win'));
E
Erich Gamma 已提交
69 70 71 72 73
		return false;
	}
	return true;
}

74
let keybindingType: IJSONSchema = {
E
Erich Gamma 已提交
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
	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'
		}
	}
};

A
Alex Dima 已提交
105
let keybindingsExtPoint = ExtensionsRegistry.registerExtensionPoint<ContributedKeyBinding | ContributedKeyBinding[]>('keybindings', [], {
E
Erich Gamma 已提交
106 107 108 109 110 111 112 113 114 115
	description: nls.localize('vscode.extension.contributes.keybindings', "Contributes keybindings."),
	oneOf: [
		keybindingType,
		{
			type: 'array',
			items: keybindingType
		}
	]
});

A
Alex Dima 已提交
116
export class WorkbenchKeybindingService extends KeybindingService {
117
	private userKeybindings: ConfigWatcher<IUserFriendlyKeybinding[]>;
E
Erich Gamma 已提交
118

119 120
	constructor(
		domNode: HTMLElement,
121
		@IContextKeyService contextKeyService: IContextKeyService,
122
		@ICommandService commandService: ICommandService,
123
		@ITelemetryService private telemetryService: ITelemetryService,
124
		@IMessageService messageService: IMessageService,
125
		@IEnvironmentService environmentService: IEnvironmentService,
126
		@IStatusbarService statusBarService: IStatusbarService
127
	) {
128
		super(contextKeyService, commandService, messageService, statusBarService);
129 130

		this.userKeybindings = new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [] });
B
Benjamin Pasero 已提交
131
		this.toDispose.push(toDisposable(() => this.userKeybindings.dispose()));
132

E
Erich Gamma 已提交
133 134 135 136 137 138 139 140 141 142 143
		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();
			}
		});
144

145 146
		this.toDispose.push(this.userKeybindings.onDidUpdateConfiguration(() => this.updateResolver()));

147
		this._beginListening(domNode);
E
Erich Gamma 已提交
148 149
	}

150 151 152 153 154 155 156 157
	private _safeGetConfig(): IUserFriendlyKeybinding[] {
		let rawConfig = this.userKeybindings.getConfig();
		if (Array.isArray(rawConfig)) {
			return rawConfig;
		}
		return [];
	}

158
	public customKeybindingsCount(): number {
159
		let userKeybindings = this._safeGetConfig();
160 161

		return userKeybindings.length;
162 163 164
	}

	protected _getExtraKeybindings(isFirstTime: boolean): IKeybindingItem[] {
165
		let extraUserKeybindings: IUserFriendlyKeybinding[] = this._safeGetConfig();
166 167
		if (!isFirstTime) {
			let cnt = extraUserKeybindings.length;
168

169 170 171
			this.telemetryService.publicLog('customKeybindingsChanged', {
				keyCount: cnt
			});
172
		}
173 174

		return extraUserKeybindings.map((k, i) => IOSupport.readKeybindingItem(k, i));
175 176
	}

177
	public getLabelFor(keybinding: Keybinding): string {
178
		return keybinding.toCustomLabel(getNativeLabelProvider());
179 180
	}

181
	public getHTMLLabelFor(keybinding: Keybinding): IHTMLContentElement[] {
182
		return keybinding.toCustomHTMLLabel(getNativeLabelProvider());
183 184
	}

185 186 187 188
	public getAriaLabelFor(keybinding: Keybinding): string {
		return keybinding.toCustomLabel(getNativeAriaLabelProvider());
	}

189
	public getElectronAcceleratorFor(keybinding: Keybinding): string {
190
		if (platform.isWindows) {
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
			// 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);
	}

206
	private _handleKeybindingsExtensionPointUser(isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: IExtensionMessageCollector): boolean {
E
Erich Gamma 已提交
207 208 209 210 211 212 213 214 215 216 217
		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);
		}
	}

218
	private _handleKeybinding(isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: IExtensionMessageCollector): boolean {
E
Erich Gamma 已提交
219 220 221 222 223 224 225

		let rejects: string[] = [];
		let commandAdded = false;

		if (isValidContributedKeyBinding(keybindings, rejects)) {
			let rule = this._asCommandRule(isBuiltin, idx++, keybindings);
			if (rule) {
A
Alex Dima 已提交
226
				KeybindingsRegistry.registerKeybindingRule(rule);
E
Erich Gamma 已提交
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
				commandAdded = true;
			}
		}

		if (rejects.length > 0) {
			collector.error(nls.localize(
				'invalid.keybindings',
				"Invalid `contributes.{0}`: {1}",
				keybindingsExtPoint.name,
				rejects.join('\n')
			));
		}

		return commandAdded;
	}

A
Alex Dima 已提交
243
	private _asCommandRule(isBuiltin: boolean, idx: number, binding: ContributedKeyBinding): IKeybindingRule {
E
Erich Gamma 已提交
244 245 246 247 248 249 250 251 252 253 254 255

		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,
256
			when: IOSupport.readKeybindingWhen(when),
E
Erich Gamma 已提交
257 258 259 260 261
			weight: weight,
			primary: IOSupport.readKeybinding(key),
			mac: mac && { primary: IOSupport.readKeybinding(mac) },
			linux: linux && { primary: IOSupport.readKeybinding(linux) },
			win: win && { primary: IOSupport.readKeybinding(win) }
B
Benjamin Pasero 已提交
262
		};
E
Erich Gamma 已提交
263 264 265 266 267 268 269

		if (!desc.primary && !desc.mac && !desc.linux && !desc.win) {
			return;
		}

		return desc;
	}
270
}
271

272
let schemaId = 'vscode://schemas/keybindings';
273
let schema: IJSONSchema = {
274 275 276 277 278 279
	'id': schemaId,
	'type': 'array',
	'title': nls.localize('keybindings.json.title', "Keybindings configuration"),
	'items': {
		'required': ['key'],
		'type': 'object',
280
		'defaultSnippets': [{ 'body': { 'key': '{{_}}', 'command': '{{_}}', 'when': '{{_}}' } }],
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
		'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.')
			}
		}
	}
};

297
let schemaRegistry = <IJSONContributionRegistry>Registry.as(Extensions.JSONContribution);
298
schemaRegistry.registerSchema(schemaId, schema);