keybindingsEditorContribution.ts 11.5 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 10
import { RunOnceScheduler } from 'vs/base/common/async';
import { MarkedString } 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 { ServicesAccessor, registerEditorCommand, EditorCommand } from 'vs/editor/common/editorCommonExtensions';
S
Sandeep Somavarapu 已提交
19
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
J
Johannes Rieken 已提交
20
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
21
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
22 23
import { SmartSnippetInserter } from 'vs/workbench/parts/preferences/common/smartSnippetInserter';
import { DefineKeybindingOverlayWidget } from 'vs/workbench/parts/preferences/browser/keybindingWidgets';
S
Sandeep Somavarapu 已提交
24
import { FloatingClickWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets';
25
import { parseTree, Node } from 'vs/base/common/json';
26 27
import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO';
import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode';
28
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
A
Alex Dima 已提交
29

A
Alex Dima 已提交
30
const NLS_LAUNCH_MESSAGE = nls.localize('defineKeybinding.start', "Define Keybinding");
31 32
const NLS_KB_LAYOUT_INFO_MESSAGE = nls.localize('defineKeybinding.kbLayoutInfoMessage', "For your current keyboard layout press ");
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.");
33 34

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

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

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

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

S
Sandeep Somavarapu 已提交
45
	private _keybindingEditorRenderer: KeybindingEditorRenderer;
46 47

	constructor(
S
Sandeep Somavarapu 已提交
48
		private _editor: ICodeEditor,
49 50
		@IKeybindingService keybindingService: IKeybindingService,
		@IInstantiationService private instantiationService: IInstantiationService
51
	) {
S
Sandeep Somavarapu 已提交
52
		super();
53

S
Sandeep Somavarapu 已提交
54 55
		this._register(this._editor.onDidChangeModel(e => this._renderKeybindingEditor()));
		this._renderKeybindingEditor();
56 57 58 59 60 61
	}

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

S
Sandeep Somavarapu 已提交
62 63 64 65
	public get keybindingEditorRenderer(): KeybindingEditorRenderer {
		return this._keybindingEditorRenderer;
	}

66
	public dispose(): void {
S
Sandeep Somavarapu 已提交
67 68
		this._disposeKeybindingEditorRenderer();
		super.dispose();
69 70
	}

S
Sandeep Somavarapu 已提交
71
	private _renderKeybindingEditor(): void {
72
		if (isInterestingEditorModel(this._editor)) {
S
Sandeep Somavarapu 已提交
73 74 75 76 77
			if (!this._keybindingEditorRenderer) {
				this._keybindingEditorRenderer = this.instantiationService.createInstance(KeybindingEditorRenderer, this._editor);
			}
		} else {
			this._disposeKeybindingEditorRenderer();
78 79 80
		}
	}

S
Sandeep Somavarapu 已提交
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
	private _disposeKeybindingEditorRenderer(): void {
		if (this._keybindingEditorRenderer) {
			this._keybindingEditorRenderer.dispose();
			this._keybindingEditorRenderer = null;
		}
	}
}

export class KeybindingEditorRenderer extends Disposable {

	private _launchWidget: FloatingClickWidget;
	private _defineWidget: DefineKeybindingOverlayWidget;

	constructor(
		private _editor: ICodeEditor,
		@IInstantiationService private _instantiationService: IInstantiationService
	) {
		super();
S
Sandeep Somavarapu 已提交
99
		this._launchWidget = this._register(this._instantiationService.createInstance(FloatingClickWidget, this._editor, NLS_LAUNCH_MESSAGE, DefineKeybindingCommand.ID));
S
Sandeep Somavarapu 已提交
100 101 102 103 104 105 106 107 108 109 110
		this._register(this._launchWidget.onClick(() => this.showDefineKeybindingWidget()));
		this._defineWidget = this._register(this._instantiationService.createInstance(DefineKeybindingOverlayWidget, this._editor));

		this._register(this._instantiationService.createInstance(KeybindingEditorDecorationsRenderer, this._editor));
		this._launchWidget.render();
	}

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

J
Johannes Rieken 已提交
111
	private _onAccepted(keybinding: string): void {
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
		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);

131
			SnippetController2.get(this._editor).insert(snippetText, 0, 0);
R
rpjproost 已提交
132
		}
133
	}
S
Sandeep Somavarapu 已提交
134
}
135

S
Sandeep Somavarapu 已提交
136
export class KeybindingEditorDecorationsRenderer extends Disposable {
137

S
Sandeep Somavarapu 已提交
138 139
	private _updateDecorations: RunOnceScheduler;
	private _dec: string[] = [];
140

S
Sandeep Somavarapu 已提交
141 142 143 144 145 146 147
	constructor(
		private _editor: ICodeEditor,
		@IKeybindingService private _keybindingService: IKeybindingService,
	) {
		super();

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

S
Sandeep Somavarapu 已提交
149 150 151 152
		let model = this._editor.getModel();
		this._register(model.onDidChangeContent((e) => this._updateDecorations.schedule()));
		this._register(this._keybindingService.onDidUpdateKeybindings((e) => this._updateDecorations.schedule()));
		this._register({
153 154 155 156 157 158 159 160 161
			dispose: () => {
				this._dec = this._editor.deltaDecorations(this._dec, []);
				this._updateDecorations.cancel();
			}
		});
		this._updateDecorations.schedule();
	}

	private _updateDecorationsNow(): void {
162
		const model = this._editor.getModel();
163

164
		let newDecorations: editorCommon.IModelDeltaDecoration[] = [];
165

166 167 168 169 170 171 172 173
		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);
				}
174
			}
175
		}
176

177 178
		this._dec = this._editor.deltaDecorations(this._dec, newDecorations);
	}
179

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
	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;
			}
200

201 202 203 204 205 206 207 208 209
			const resolvedKeybindings = this._keybindingService.resolveUserBinding(value.value);
			if (resolvedKeybindings.length === 0) {
				return this._createDecoration(true, null, model, value);
			}
			const resolvedKeybinding = resolvedKeybindings[0];
			if (!resolvedKeybinding.isWYSIWYG()) {
				return this._createDecoration(false, resolvedKeybinding.getLabel(), model, value);
			}
			const expectedUserSettingsLabel = resolvedKeybinding.getUserSettingsLabel();
S
Sandeep Somavarapu 已提交
210
			if (!KeybindingEditorDecorationsRenderer._userSettingsFuzzyEquals(value.value, expectedUserSettingsLabel)) {
211
				return this._createDecoration(false, resolvedKeybinding.getLabel(), model, value);
212
			}
213 214 215 216
			return null;
		}
		return null;
	}
217

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
	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;
	}

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
	private _createDecoration(isError: boolean, message: string, model: editorCommon.IModel, keyNode: Node): editorCommon.IModelDeltaDecoration {
		let msg: MarkedString[];
		let className: string;
		let beforeContentClassName: string;
		let overviewRulerColor: string;

		if (isError) {
			// this is the error case
			msg = [NLS_KB_LAYOUT_ERROR_MESSAGE];
			className = 'keybindingError';
			beforeContentClassName = 'inlineKeybindingError';
			overviewRulerColor = 'rgba(250, 100, 100, 0.6)';
		} else {
			// this is the info case
			msg = [NLS_KB_LAYOUT_INFO_MESSAGE];
			msg = msg.concat(message);
			className = 'keybindingInfo';
			beforeContentClassName = 'inlineKeybindingInfo';
			overviewRulerColor = 'rgba(100, 100, 250, 0.6)';
		}
274

275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
		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
				}
			}
		};
297
	}
298 299 300

}

S
Sandeep Somavarapu 已提交
301
class DefineKeybindingCommand extends EditorCommand {
302 303 304

	static ID = 'editor.action.defineKeybinding';

A
Alex Dima 已提交
305
	constructor() {
306
		super({
S
Sandeep Somavarapu 已提交
307
			id: DefineKeybindingCommand.ID,
308
			precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.languageId.isEqualTo('json')),
309
			kbOpts: {
310
				kbExpr: EditorContextKeys.textFocus,
A
Alex Dima 已提交
311
				primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K)
312 313
			}
		});
314 315
	}

S
Sandeep Somavarapu 已提交
316
	public runEditorCommand(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
317 318 319
		if (!isInterestingEditorModel(editor)) {
			return;
		}
320
		let controller = DefineKeybindingController.get(editor);
S
Sandeep Somavarapu 已提交
321 322
		if (controller && controller.keybindingEditorRenderer) {
			controller.keybindingEditorRenderer.showDefineKeybindingWidget();
323
		}
324 325 326
	}
}

J
Johannes Rieken 已提交
327
function isInterestingEditorModel(editor: editorCommon.ICommonCodeEditor): boolean {
328 329 330
	if (editor.getConfiguration().readOnly) {
		return false;
	}
331 332 333 334
	let model = editor.getModel();
	if (!model) {
		return false;
	}
335
	let url = model.uri.toString();
336 337
	return INTERESTING_FILE.test(url);
}
S
Sandeep Somavarapu 已提交
338

339
registerEditorCommand(new DefineKeybindingCommand());