keybindingsEditorContribution.ts 13.9 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';
J
Joao Moreno 已提交
25
import { ScanCodeBinding } from 'vs/base/common/scanCode';
26
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
B
Benjamin Pasero 已提交
27
import { WindowsNativeResolvedKeybinding } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
28 29
import { themeColorFromId, ThemeColor } from 'vs/platform/theme/common/themeService';
import { overviewRulerInfo, overviewRulerError } from 'vs/editor/common/view/editorColorRegistry';
A
Alex Dima 已提交
30
import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness, OverviewRulerLane } from 'vs/editor/common/model';
31
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
J
Joao Moreno 已提交
32
import { KeybindingParser } from 'vs/base/common/keybindingParser';
A
Alex Dima 已提交
33

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	private _launchWidget: FloatingClickWidget;
	private _defineWidget: DefineKeybindingOverlayWidget;

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

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

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

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

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

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

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

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

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

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

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

A
Alex Dima 已提交
212
	private _getDecorationForEntry(model: ITextModel, entry: Node): IModelDeltaDecoration {
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
		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;
			}
232

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

262 263 264 265 266 267 268 269
	static _userSettingsFuzzyEquals(a: string, b: string): boolean {
		a = a.trim().toLowerCase();
		b = b.trim().toLowerCase();

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

J
Joao Moreno 已提交
270 271
		const [parsedA1, parsedA2] = KeybindingParser.parseUserBinding(a);
		const [parsedB1, parsedB2] = KeybindingParser.parseUserBinding(b);
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297

		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 已提交
298
	private _createDecoration(isError: boolean, uiLabel: string, usLabel: string, model: ITextModel, keyNode: Node): IModelDeltaDecoration {
299
		let msg: MarkdownString;
300 301
		let className: string;
		let beforeContentClassName: string;
302
		let overviewRulerColor: ThemeColor;
303 304 305

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

338 339 340 341 342 343 344 345 346 347 348
		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: {
349
				stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
350 351 352 353 354
				className: className,
				beforeContentClassName: beforeContentClassName,
				hoverMessage: msg,
				overviewRuler: {
					color: overviewRulerColor,
355
					position: OverviewRulerLane.Right
356 357 358
				}
			}
		};
359
	}
360 361 362

}

S
Sandeep Somavarapu 已提交
363
class DefineKeybindingCommand extends EditorCommand {
364

365
	static readonly ID = 'editor.action.defineKeybinding';
366

A
Alex Dima 已提交
367
	constructor() {
368
		super({
S
Sandeep Somavarapu 已提交
369
			id: DefineKeybindingCommand.ID,
370
			precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.languageId.isEqualTo('jsonc')),
371
			kbOpts: {
372
				kbExpr: EditorContextKeys.editorTextFocus,
A
Alex Dima 已提交
373
				primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K),
374
				weight: KeybindingWeight.EditorContrib
375 376
			}
		});
377 378
	}

379
	public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void {
A
Alex Dima 已提交
380
		if (!isInterestingEditorModel(editor) || editor.getConfiguration().readOnly) {
381 382
			return;
		}
383
		let controller = DefineKeybindingController.get(editor);
A
Alex Dima 已提交
384 385
		if (controller && controller.keybindingWidgetRenderer) {
			controller.keybindingWidgetRenderer.showDefineKeybindingWidget();
386
		}
387 388 389
	}
}

390
function isInterestingEditorModel(editor: ICodeEditor): boolean {
391 392 393 394
	let model = editor.getModel();
	if (!model) {
		return false;
	}
395
	let url = model.uri.toString();
396 397
	return INTERESTING_FILE.test(url);
}
S
Sandeep Somavarapu 已提交
398

399
registerEditorContribution(DefineKeybindingController);
400
registerEditorCommand(new DefineKeybindingCommand());