keybindingsEditorContribution.ts 13.7 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

A
Alex Dima 已提交
8
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
9
import { RunOnceScheduler } from 'vs/base/common/async';
10
import { MarkdownString } from 'vs/base/common/htmlContent';
11
import { KeyCode, KeyMod, KeyChord, SimpleKeybinding } from 'vs/base/common/keyCodes';
S
Sandeep Somavarapu 已提交
12
import { Disposable } from 'vs/base/common/lifecycle';
13
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
14
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Johannes Rieken 已提交
15 16
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Range } from 'vs/editor/common/core/range';
A
Alex Dima 已提交
17
import * as editorCommon from 'vs/editor/common/editorCommon';
18
import { registerEditorContribution, ServicesAccessor, registerEditorCommand, EditorCommand } from 'vs/editor/browser/editorExtensions';
S
Sandeep Somavarapu 已提交
19
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
20
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
21 22
import { SmartSnippetInserter } from 'vs/workbench/parts/preferences/common/smartSnippetInserter';
import { DefineKeybindingOverlayWidget } from 'vs/workbench/parts/preferences/browser/keybindingWidgets';
S
Sandeep Somavarapu 已提交
23
import { FloatingClickWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets';
24
import { parseTree, Node } from 'vs/base/common/json';
25 26
import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO';
import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode';
27
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
B
Benjamin Pasero 已提交
28
import { WindowsNativeResolvedKeybinding } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
29 30
import { themeColorFromId, ThemeColor } from 'vs/platform/theme/common/themeService';
import { overviewRulerInfo, overviewRulerError } from 'vs/editor/common/view/editorColorRegistry';
A
Alex Dima 已提交
31
import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness, OverviewRulerLane } from 'vs/editor/common/model';
A
Alex Dima 已提交
32

A
Alex Dima 已提交
33
const NLS_LAUNCH_MESSAGE = nls.localize('defineKeybinding.start', "Define Keybinding");
34
const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutErrorMessage', "You won't be able to produce this key combination under your current keyboard layout.");
35 36

const INTERESTING_FILE = /keybindings\.json$/;
A
Alex Dima 已提交
37

S
Sandeep Somavarapu 已提交
38
export class DefineKeybindingController extends Disposable implements editorCommon.IEditorContribution {
39

40
	private static readonly ID = 'editor.contrib.defineKeybinding';
41

42
	public static get(editor: ICodeEditor): DefineKeybindingController {
A
Alex Dima 已提交
43
		return editor.getContribution<DefineKeybindingController>(DefineKeybindingController.ID);
44 45
	}

A
Alex Dima 已提交
46 47
	private _keybindingWidgetRenderer: KeybindingWidgetRenderer;
	private _keybindingDecorationRenderer: KeybindingEditorDecorationsRenderer;
48 49

	constructor(
S
Sandeep Somavarapu 已提交
50
		private _editor: ICodeEditor,
A
Alex Dima 已提交
51
		@IInstantiationService private _instantiationService: IInstantiationService
52
	) {
S
Sandeep Somavarapu 已提交
53
		super();
54

A
Alex Dima 已提交
55 56 57 58 59
		this._keybindingWidgetRenderer = null;
		this._keybindingDecorationRenderer = null;

		this._register(this._editor.onDidChangeModel(e => this._update()));
		this._update();
60 61 62 63 64 65
	}

	public getId(): string {
		return DefineKeybindingController.ID;
	}

A
Alex Dima 已提交
66 67
	public get keybindingWidgetRenderer(): KeybindingWidgetRenderer {
		return this._keybindingWidgetRenderer;
S
Sandeep Somavarapu 已提交
68 69
	}

70
	public dispose(): void {
A
Alex Dima 已提交
71 72
		this._disposeKeybindingWidgetRenderer();
		this._disposeKeybindingDecorationRenderer();
S
Sandeep Somavarapu 已提交
73
		super.dispose();
74 75
	}

A
Alex Dima 已提交
76 77 78 79 80 81 82 83 84 85 86 87 88
	private _update(): void {
		if (!isInterestingEditorModel(this._editor)) {
			this._disposeKeybindingWidgetRenderer();
			this._disposeKeybindingDecorationRenderer();
			return;
		}

		// Decorations are shown for the default keybindings.json **and** for the user keybindings.json
		this._createKeybindingDecorationRenderer();

		// The button to define keybindings is shown only for the user keybindings.json
		if (!this._editor.getConfiguration().readOnly) {
			this._createKeybindingWidgetRenderer();
S
Sandeep Somavarapu 已提交
89
		} else {
A
Alex Dima 已提交
90 91 92 93 94 95 96
			this._disposeKeybindingWidgetRenderer();
		}
	}

	private _createKeybindingWidgetRenderer(): void {
		if (!this._keybindingWidgetRenderer) {
			this._keybindingWidgetRenderer = this._instantiationService.createInstance(KeybindingWidgetRenderer, this._editor);
97 98 99
		}
	}

A
Alex Dima 已提交
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
	private _disposeKeybindingWidgetRenderer(): void {
		if (this._keybindingWidgetRenderer) {
			this._keybindingWidgetRenderer.dispose();
			this._keybindingWidgetRenderer = null;
		}
	}

	private _createKeybindingDecorationRenderer(): void {
		if (!this._keybindingDecorationRenderer) {
			this._keybindingDecorationRenderer = this._instantiationService.createInstance(KeybindingEditorDecorationsRenderer, this._editor);
		}
	}

	private _disposeKeybindingDecorationRenderer(): void {
		if (this._keybindingDecorationRenderer) {
			this._keybindingDecorationRenderer.dispose();
			this._keybindingDecorationRenderer = null;
S
Sandeep Somavarapu 已提交
117 118 119 120
		}
	}
}

A
Alex Dima 已提交
121
export class KeybindingWidgetRenderer extends Disposable {
S
Sandeep Somavarapu 已提交
122 123 124 125 126 127 128 129 130

	private _launchWidget: FloatingClickWidget;
	private _defineWidget: DefineKeybindingOverlayWidget;

	constructor(
		private _editor: ICodeEditor,
		@IInstantiationService private _instantiationService: IInstantiationService
	) {
		super();
S
Sandeep Somavarapu 已提交
131
		this._launchWidget = this._register(this._instantiationService.createInstance(FloatingClickWidget, this._editor, NLS_LAUNCH_MESSAGE, DefineKeybindingCommand.ID));
S
Sandeep Somavarapu 已提交
132 133 134 135 136 137 138 139 140 141
		this._register(this._launchWidget.onClick(() => this.showDefineKeybindingWidget()));
		this._defineWidget = this._register(this._instantiationService.createInstance(DefineKeybindingOverlayWidget, this._editor));

		this._launchWidget.render();
	}

	public showDefineKeybindingWidget(): void {
		this._defineWidget.start().then(keybinding => this._onAccepted(keybinding));
	}

J
Johannes Rieken 已提交
142
	private _onAccepted(keybinding: string): void {
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
		this._editor.focus();
		if (keybinding) {
			let regexp = new RegExp(/\\/g);
			let backslash = regexp.test(keybinding);
			if (backslash) {
				keybinding = keybinding.slice(0, -1) + '\\\\';
			}
			let snippetText = [
				'{',
				'\t"key": ' + JSON.stringify(keybinding) + ',',
				'\t"command": "${1:commandId}",',
				'\t"when": "${2:editorTextFocus}"',
				'}$0'
			].join('\n');

			let smartInsertInfo = SmartSnippetInserter.insertSnippet(this._editor.getModel(), this._editor.getPosition());
			snippetText = smartInsertInfo.prepend + snippetText + smartInsertInfo.append;
			this._editor.setPosition(smartInsertInfo.position);

162
			SnippetController2.get(this._editor).insert(snippetText, 0, 0);
R
rpjproost 已提交
163
		}
164
	}
S
Sandeep Somavarapu 已提交
165
}
166

S
Sandeep Somavarapu 已提交
167
export class KeybindingEditorDecorationsRenderer extends Disposable {
168

S
Sandeep Somavarapu 已提交
169 170
	private _updateDecorations: RunOnceScheduler;
	private _dec: string[] = [];
171

S
Sandeep Somavarapu 已提交
172 173 174 175 176 177 178
	constructor(
		private _editor: ICodeEditor,
		@IKeybindingService private _keybindingService: IKeybindingService,
	) {
		super();

		this._updateDecorations = this._register(new RunOnceScheduler(() => this._updateDecorationsNow(), 500));
179

S
Sandeep Somavarapu 已提交
180
		let model = this._editor.getModel();
A
Alex Dima 已提交
181
		this._register(model.onDidChangeContent(() => this._updateDecorations.schedule()));
S
Sandeep Somavarapu 已提交
182 183
		this._register(this._keybindingService.onDidUpdateKeybindings((e) => this._updateDecorations.schedule()));
		this._register({
184 185 186 187 188 189 190 191 192
			dispose: () => {
				this._dec = this._editor.deltaDecorations(this._dec, []);
				this._updateDecorations.cancel();
			}
		});
		this._updateDecorations.schedule();
	}

	private _updateDecorationsNow(): void {
193
		const model = this._editor.getModel();
194

195
		let newDecorations: IModelDeltaDecoration[] = [];
196

197 198 199 200 201 202 203 204
		const root = parseTree(model.getValue());
		if (root && Array.isArray(root.children)) {
			for (let i = 0, len = root.children.length; i < len; i++) {
				const entry = root.children[i];
				const dec = this._getDecorationForEntry(model, entry);
				if (dec !== null) {
					newDecorations.push(dec);
				}
205
			}
206
		}
207

208 209
		this._dec = this._editor.deltaDecorations(this._dec, newDecorations);
	}
210

A
Alex Dima 已提交
211
	private _getDecorationForEntry(model: ITextModel, entry: Node): IModelDeltaDecoration {
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
		if (!Array.isArray(entry.children)) {
			return null;
		}
		for (let i = 0, len = entry.children.length; i < len; i++) {
			const prop = entry.children[i];
			if (prop.type !== 'property') {
				continue;
			}
			if (!Array.isArray(prop.children) || prop.children.length !== 2) {
				continue;
			}
			const key = prop.children[0];
			if (key.value !== 'key') {
				continue;
			}
			const value = prop.children[1];
			if (value.type !== 'string') {
				continue;
			}
231

232 233
			const resolvedKeybindings = this._keybindingService.resolveUserBinding(value.value);
			if (resolvedKeybindings.length === 0) {
234
				return this._createDecoration(true, null, null, model, value);
235 236
			}
			const resolvedKeybinding = resolvedKeybindings[0];
237 238 239 240
			let usLabel: string = null;
			if (resolvedKeybinding instanceof WindowsNativeResolvedKeybinding) {
				usLabel = resolvedKeybinding.getUSLabel();
			}
241
			if (!resolvedKeybinding.isWYSIWYG()) {
242
				return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);
243
			}
244
			if (/abnt_|oem_/.test(value.value)) {
245
				return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);
246
			}
247
			const expectedUserSettingsLabel = resolvedKeybinding.getUserSettingsLabel();
S
Sandeep Somavarapu 已提交
248
			if (!KeybindingEditorDecorationsRenderer._userSettingsFuzzyEquals(value.value, expectedUserSettingsLabel)) {
249
				return this._createDecoration(false, resolvedKeybinding.getLabel(), usLabel, model, value);
250
			}
251 252 253 254
			return null;
		}
		return null;
	}
255

256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
	static _userSettingsFuzzyEquals(a: string, b: string): boolean {
		a = a.trim().toLowerCase();
		b = b.trim().toLowerCase();

		if (a === b) {
			return true;
		}

		const [parsedA1, parsedA2] = KeybindingIO._readUserBinding(a);
		const [parsedB1, parsedB2] = KeybindingIO._readUserBinding(b);

		return (
			this._userBindingEquals(parsedA1, parsedB1)
			&& this._userBindingEquals(parsedA2, parsedB2)
		);
	}

	private static _userBindingEquals(a: SimpleKeybinding | ScanCodeBinding, b: SimpleKeybinding | ScanCodeBinding): boolean {
		if (a === null && b === null) {
			return true;
		}
		if (!a || !b) {
			return false;
		}

		if (a instanceof SimpleKeybinding && b instanceof SimpleKeybinding) {
			return a.equals(b);
		}

		if (a instanceof ScanCodeBinding && b instanceof ScanCodeBinding) {
			return a.equals(b);
		}

		return false;
	}

A
Alex Dima 已提交
292
	private _createDecoration(isError: boolean, uiLabel: string, usLabel: string, model: ITextModel, keyNode: Node): IModelDeltaDecoration {
293
		let msg: MarkdownString;
294 295
		let className: string;
		let beforeContentClassName: string;
296
		let overviewRulerColor: ThemeColor;
297 298 299

		if (isError) {
			// this is the error case
300
			msg = new MarkdownString().appendText(NLS_KB_LAYOUT_ERROR_MESSAGE);
301 302
			className = 'keybindingError';
			beforeContentClassName = 'inlineKeybindingError';
303
			overviewRulerColor = themeColorFromId(overviewRulerError);
304 305
		} else {
			// this is the info case
306
			if (usLabel && uiLabel !== usLabel) {
307
				msg = new MarkdownString(
308 309 310 311 312 313 314
					nls.localize({
						key: 'defineKeybinding.kbLayoutLocalAndUSMessage',
						comment: [
							'Please translate maintaining the stars (*) around the placeholders such that they will be rendered in bold.',
							'The placeholders will contain a keyboard combination e.g. Ctrl+Shift+/'
						]
					}, "**{0}** for your current keyboard layout (**{1}** for US standard).", uiLabel, usLabel)
315
				);
316
			} else {
317
				msg = new MarkdownString(
318 319 320 321 322 323 324
					nls.localize({
						key: 'defineKeybinding.kbLayoutLocalMessage',
						comment: [
							'Please translate maintaining the stars (*) around the placeholder such that it will be rendered in bold.',
							'The placeholder will contain a keyboard combination e.g. Ctrl+Shift+/'
						]
					}, "**{0}** for your current keyboard layout.", uiLabel)
325
				);
326
			}
327 328
			className = 'keybindingInfo';
			beforeContentClassName = 'inlineKeybindingInfo';
329
			overviewRulerColor = themeColorFromId(overviewRulerInfo);
330
		}
331

332 333 334 335 336 337 338 339 340 341 342
		const startPosition = model.getPositionAt(keyNode.offset);
		const endPosition = model.getPositionAt(keyNode.offset + keyNode.length);
		const range = new Range(
			startPosition.lineNumber, startPosition.column,
			endPosition.lineNumber, endPosition.column
		);

		// icon + highlight + message decoration
		return {
			range: range,
			options: {
343
				stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
344 345 346 347 348 349
				className: className,
				beforeContentClassName: beforeContentClassName,
				hoverMessage: msg,
				overviewRuler: {
					color: overviewRulerColor,
					darkColor: overviewRulerColor,
350
					position: OverviewRulerLane.Right
351 352 353
				}
			}
		};
354
	}
355 356 357

}

S
Sandeep Somavarapu 已提交
358
class DefineKeybindingCommand extends EditorCommand {
359

360
	static readonly ID = 'editor.action.defineKeybinding';
361

A
Alex Dima 已提交
362
	constructor() {
363
		super({
S
Sandeep Somavarapu 已提交
364
			id: DefineKeybindingCommand.ID,
365
			precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.languageId.isEqualTo('jsonc')),
366
			kbOpts: {
367
				kbExpr: EditorContextKeys.textFocus,
A
Alex Dima 已提交
368
				primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K)
369 370
			}
		});
371 372
	}

373
	public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void {
A
Alex Dima 已提交
374
		if (!isInterestingEditorModel(editor) || editor.getConfiguration().readOnly) {
375 376
			return;
		}
377
		let controller = DefineKeybindingController.get(editor);
A
Alex Dima 已提交
378 379
		if (controller && controller.keybindingWidgetRenderer) {
			controller.keybindingWidgetRenderer.showDefineKeybindingWidget();
380
		}
381 382 383
	}
}

384
function isInterestingEditorModel(editor: ICodeEditor): boolean {
385 386 387 388
	let model = editor.getModel();
	if (!model) {
		return false;
	}
389
	let url = model.uri.toString();
390 391
	return INTERESTING_FILE.test(url);
}
S
Sandeep Somavarapu 已提交
392

393
registerEditorContribution(DefineKeybindingController);
394
registerEditorCommand(new DefineKeybindingCommand());