keybindingsEditorContribution.ts 13.6 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

A
Alex Dima 已提交
32
const NLS_LAUNCH_MESSAGE = nls.localize('defineKeybinding.start', "Define Keybinding");
33
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.");
34 35

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

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

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

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

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

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

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

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

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

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

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

A
Alex Dima 已提交
75 76 77 78 79 80 81 82 83 84 85 86 87
	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 已提交
88
		} else {
A
Alex Dima 已提交
89 90 91 92 93 94 95
			this._disposeKeybindingWidgetRenderer();
		}
	}

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

A
Alex Dima 已提交
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
	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 已提交
116 117 118 119
		}
	}
}

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

	private _launchWidget: FloatingClickWidget;
	private _defineWidget: DefineKeybindingOverlayWidget;

	constructor(
		private _editor: ICodeEditor,
		@IInstantiationService private _instantiationService: IInstantiationService
	) {
		super();
S
Sandeep Somavarapu 已提交
130
		this._launchWidget = this._register(this._instantiationService.createInstance(FloatingClickWidget, this._editor, NLS_LAUNCH_MESSAGE, DefineKeybindingCommand.ID));
S
Sandeep Somavarapu 已提交
131 132 133 134 135 136 137 138 139 140
		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 已提交
141
	private _onAccepted(keybinding: string): void {
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
		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);

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

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

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

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

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

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

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

194
		let newDecorations: editorCommon.IModelDeltaDecoration[] = [];
195

196 197 198 199 200 201 202 203
		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);
				}
204
			}
205
		}
206

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

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
	private _getDecorationForEntry(model: editorCommon.IModel, entry: Node): editorCommon.IModelDeltaDecoration {
		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;
			}
230

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

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
	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;
	}

291
	private _createDecoration(isError: boolean, uiLabel: string, usLabel: string, model: editorCommon.IModel, keyNode: Node): editorCommon.IModelDeltaDecoration {
292
		let msg: MarkdownString;
293 294
		let className: string;
		let beforeContentClassName: string;
295
		let overviewRulerColor: ThemeColor;
296 297 298

		if (isError) {
			// this is the error case
299
			msg = new MarkdownString().appendText(NLS_KB_LAYOUT_ERROR_MESSAGE);
300 301
			className = 'keybindingError';
			beforeContentClassName = 'inlineKeybindingError';
302
			overviewRulerColor = themeColorFromId(overviewRulerError);
303 304
		} else {
			// this is the info case
305
			if (usLabel && uiLabel !== usLabel) {
306
				msg = new MarkdownString(
307 308 309 310 311 312 313
					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)
314
				);
315
			} else {
316
				msg = new MarkdownString(
317 318 319 320 321 322 323
					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)
324
				);
325
			}
326 327
			className = 'keybindingInfo';
			beforeContentClassName = 'inlineKeybindingInfo';
328
			overviewRulerColor = themeColorFromId(overviewRulerInfo);
329
		}
330

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
		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: {
				stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
				className: className,
				beforeContentClassName: beforeContentClassName,
				hoverMessage: msg,
				overviewRuler: {
					color: overviewRulerColor,
					darkColor: overviewRulerColor,
					position: editorCommon.OverviewRulerLane.Right
				}
			}
		};
353
	}
354 355 356

}

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

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

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

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

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

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