keybindingsEditorContribution.ts 11.3 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';
9
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
10 11
import { RunOnceScheduler } from 'vs/base/common/async';
import { MarkedString } from 'vs/base/common/htmlContent';
12
import { createKeybinding, KeyCode, KeyMod, KeyChord, createRuntimeKeybinding } from 'vs/base/common/keyCodes';
J
Johannes Rieken 已提交
13
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
14
import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO';
15
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
16
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Johannes Rieken 已提交
17 18
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Range } from 'vs/editor/common/core/range';
A
Alex Dima 已提交
19
import * as editorCommon from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
20 21 22 23 24
import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
import { CodeSnippet } from 'vs/editor/contrib/snippet/common/snippet';
import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController';
25 26
import { SmartSnippetInserter } from 'vs/workbench/parts/preferences/common/smartSnippetInserter';
import { DefineKeybindingOverlayWidget } from 'vs/workbench/parts/preferences/browser/keybindingWidgets';
27
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
A
Alex Dima 已提交
28
import { OS } from 'vs/base/common/platform';
29

A
Alex Dima 已提交
30
import EditorContextKeys = editorCommon.EditorContextKeys;
A
Alex Dima 已提交
31

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

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

38
@editorContribution
A
Alex Dima 已提交
39
export class DefineKeybindingController implements editorCommon.IEditorContribution {
40

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

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

A
Alex Dima 已提交
47
	private _editor: ICodeEditor;
J
Johannes Rieken 已提交
48
	private _keybindingService: IKeybindingService;
49
	private _launchWidget: DefineKeybindingLauncherWidget;
50
	private _defineWidget: DefineKeybindingOverlayWidget;
51
	private _toDispose: IDisposable[];
52 53
	private _modelToDispose: IDisposable[];
	private _updateDecorations: RunOnceScheduler;
54 55

	constructor(
J
Johannes Rieken 已提交
56
		editor: ICodeEditor,
57 58
		@IKeybindingService keybindingService: IKeybindingService,
		@IInstantiationService private instantiationService: IInstantiationService
59 60
	) {
		this._editor = editor;
61
		this._keybindingService = keybindingService;
62
		this._toDispose = [];
63
		this._launchWidget = new DefineKeybindingLauncherWidget(this._editor, keybindingService, () => this.launch());
64
		this._defineWidget = instantiationService.createInstance(DefineKeybindingOverlayWidget, this._editor);
65

A
Alex Dima 已提交
66
		this._toDispose.push(this._editor.onDidChangeConfiguration((e) => {
67 68 69 70 71 72
			if (isInterestingEditorModel(this._editor)) {
				this._launchWidget.show();
			} else {
				this._launchWidget.hide();
			}
		}));
A
Alex Dima 已提交
73
		this._toDispose.push(this._editor.onDidChangeModel((e) => {
74 75 76 77 78
			if (isInterestingEditorModel(this._editor)) {
				this._launchWidget.show();
			} else {
				this._launchWidget.hide();
			}
79
			this._onModel();
80
		}));
81 82 83 84 85 86

		this._updateDecorations = new RunOnceScheduler(() => this._updateDecorationsNow(), 500);
		this._toDispose.push(this._updateDecorations);

		this._modelToDispose = [];
		this._onModel();
87 88 89 90 91 92 93
	}

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

	public dispose(): void {
J
Joao Moreno 已提交
94 95
		this._modelToDispose = dispose(this._modelToDispose);
		this._toDispose = dispose(this._toDispose);
96 97 98 99 100 101 102 103
		this._launchWidget.dispose();
		this._launchWidget = null;
		this._defineWidget.dispose();
		this._defineWidget = null;
	}

	public launch(): void {
		if (isInterestingEditorModel(this._editor)) {
104
			this._defineWidget.start().then(keybinding => this._onAccepted(keybinding));
105 106 107
		}
	}

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

			SnippetController.get(this._editor).run(CodeSnippet.fromTextmate(snippetText), 0, 0);
R
rpjproost 已提交
129
		}
130
	}
131 132

	private _onModel(): void {
J
Joao Moreno 已提交
133
		this._modelToDispose = dispose(this._modelToDispose);
134 135 136 137 138 139

		let model = this._editor.getModel();
		if (!model) {
			return;
		}

140
		let url = model.uri.toString();
141 142 143 144
		if (!INTERESTING_FILE.test(url)) {
			return;
		}

A
Alex Dima 已提交
145
		this._modelToDispose.push(model.onDidChangeContent((e) => this._updateDecorations.schedule()));
146 147 148 149 150 151 152 153 154
		this._modelToDispose.push({
			dispose: () => {
				this._dec = this._editor.deltaDecorations(this._dec, []);
				this._updateDecorations.cancel();
			}
		});
		this._updateDecorations.schedule();
	}

J
Johannes Rieken 已提交
155
	private _dec: string[] = [];
156 157
	private _updateDecorationsNow(): void {
		let model = this._editor.getModel();
A
Alex Dima 已提交
158
		let regex = KeybindingIO.getUserSettingsKeybindingRegex();
159

160
		var m = model.findMatches(regex, false, true, false, false, false).map(m => m.range);
161 162 163 164 165 166 167

		let data = m.map((range) => {
			let text = model.getValueInRange(range);

			let strKeybinding = text.substring(1, text.length - 1);
			strKeybinding = strKeybinding.replace(/\\\\/g, '\\');

A
Alex Dima 已提交
168
			let numKeybinding = KeybindingIO.readKeybinding(strKeybinding, OS);
169

A
Alex Dima 已提交
170
			let keybinding = createKeybinding(numKeybinding);
171
			let resolvedKeybinding = this._keybindingService.resolveKeybinding(keybinding);
172

173
			const usResolvedKeybinding = new USLayoutResolvedKeybinding(createRuntimeKeybinding(keybinding, OS), OS);
174 175 176
			return {
				strKeybinding: strKeybinding,
				keybinding: keybinding,
A
Alex Dima 已提交
177
				usLabel: usResolvedKeybinding.getLabel(),
178
				label: resolvedKeybinding.getLabel(),
179 180 181 182 183 184 185 186
				range: range
			};
		});

		data = data.filter((entry) => {
			return (entry.usLabel !== entry.label);
		});

A
Alex Dima 已提交
187
		let newDecorations: editorCommon.IModelDeltaDecoration[] = [];
188
		data.forEach((item) => {
J
Johannes Rieken 已提交
189
			let msg: MarkedString[];
190
			let className: string;
191
			let beforeContentClassName: string;
192
			let overviewRulerColor: string;
193

194 195
			if (!item.label) {
				// this is the error case
196
				msg = [NLS_KB_LAYOUT_ERROR_MESSAGE];
197
				className = 'keybindingError';
198
				beforeContentClassName = 'inlineKeybindingError';
199
				overviewRulerColor = 'rgba(250, 100, 100, 0.6)';
200
			} else {
201
				// this is the info case
202
				msg = [NLS_KB_LAYOUT_INFO_MESSAGE];
203
				msg = msg.concat(item.label);
204
				className = 'keybindingInfo';
205
				beforeContentClassName = 'inlineKeybindingInfo';
206
				overviewRulerColor = 'rgba(100, 100, 250, 0.6)';
207 208
			}

209 210 211 212
			// icon decoration
			newDecorations.push({
				range: new Range(item.range.startLineNumber, item.range.startColumn, item.range.startLineNumber, item.range.startColumn + 1),
				options: {
A
Alex Dima 已提交
213
					stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
214
					beforeContentClassName: beforeContentClassName
215
				}
216 217 218 219 220 221
			});

			// highlight + message decoration
			newDecorations.push({
				range: item.range,
				options: {
A
Alex Dima 已提交
222
					stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
223
					className: className,
224
					hoverMessage: msg,
225 226 227
					overviewRuler: {
						color: overviewRulerColor,
						darkColor: overviewRulerColor,
A
Alex Dima 已提交
228
						position: editorCommon.OverviewRulerLane.Right
229 230 231 232 233 234
					}
				}
			});
		});

		this._dec = this._editor.deltaDecorations(this._dec, newDecorations);
235
	}
236 237
}

A
Alex Dima 已提交
238
class DefineKeybindingLauncherWidget implements IOverlayWidget {
239 240 241

	private static ID = 'editor.contrib.defineKeybindingLauncherWidget';

A
Alex Dima 已提交
242
	private _editor: ICodeEditor;
243 244 245

	private _domNode: HTMLElement;
	private _toDispose: IDisposable[];
246
	private _isVisible: boolean;
247

J
Johannes Rieken 已提交
248
	constructor(editor: ICodeEditor, keybindingService: IKeybindingService, onLaunch: () => void) {
249 250 251 252
		this._editor = editor;
		this._domNode = document.createElement('div');
		this._domNode.className = 'defineKeybindingLauncher';
		this._domNode.style.display = 'none';
253
		this._isVisible = false;
254
		let keybinding = keybindingService.lookupKeybinding(DefineKeybindingAction.ID);
255
		let extra = '';
B
Benjamin Pasero 已提交
256
		if (keybinding) {
257
			extra += ' (' + keybinding.getLabel() + ')';
258
		}
A
Alex Dima 已提交
259
		this._domNode.appendChild(document.createTextNode(NLS_LAUNCH_MESSAGE + extra));
260 261

		this._toDispose = [];
A
Alex Dima 已提交
262
		this._toDispose.push(dom.addDisposableListener(this._domNode, 'click', (e) => {
263
			onLaunch();
A
tslint  
Alex Dima 已提交
264
		}));
265 266 267 268 269 270

		this._editor.addOverlayWidget(this);
	}

	public dispose(): void {
		this._editor.removeOverlayWidget(this);
J
Joao Moreno 已提交
271
		this._toDispose = dispose(this._toDispose);
272 273 274
	}

	public show(): void {
275 276 277
		if (this._isVisible) {
			return;
		}
278
		this._domNode.style.display = 'block';
279 280
		this._isVisible = true;
		this._editor.layoutOverlayWidget(this);
281 282 283
	}

	public hide(): void {
284 285 286
		if (!this._isVisible) {
			return;
		}
287
		this._domNode.style.display = 'none';
288 289
		this._isVisible = false;
		this._editor.layoutOverlayWidget(this);
290 291 292 293 294 295 296 297 298 299 300 301
	}

	// ----- IOverlayWidget API

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

	public getDomNode(): HTMLElement {
		return this._domNode;
	}

A
Alex Dima 已提交
302
	public getPosition(): IOverlayWidgetPosition {
303
		return {
A
Alex Dima 已提交
304
			preference: this._isVisible ? OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER : null
305 306 307 308
		};
	}
}

A
Alex Dima 已提交
309
@editorAction
A
Alex Dima 已提交
310
export class DefineKeybindingAction extends EditorAction {
311 312 313

	static ID = 'editor.action.defineKeybinding';

A
Alex Dima 已提交
314
	constructor() {
315 316
		super({
			id: DefineKeybindingAction.ID,
J
Johannes Rieken 已提交
317
			label: nls.localize('DefineKeybindingAction', "Define Keybinding"),
318
			alias: 'Define Keybinding',
A
Alex Dima 已提交
319
			precondition: ContextKeyExpr.and(EditorContextKeys.Writable, EditorContextKeys.LanguageId.isEqualTo('json')),
320 321
			kbOpts: {
				kbExpr: EditorContextKeys.TextFocus,
A
Alex Dima 已提交
322
				primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K)
323 324
			}
		});
325 326
	}

J
Johannes Rieken 已提交
327
	public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
328 329 330
		if (!isInterestingEditorModel(editor)) {
			return;
		}
331 332 333 334
		let controller = DefineKeybindingController.get(editor);
		if (controller) {
			controller.launch();
		}
335 336 337
	}
}

J
Johannes Rieken 已提交
338
function isInterestingEditorModel(editor: editorCommon.ICommonCodeEditor): boolean {
339 340 341
	if (editor.getConfiguration().readOnly) {
		return false;
	}
342 343 344 345
	let model = editor.getModel();
	if (!model) {
		return false;
	}
346
	let url = model.uri.toString();
347 348
	return INTERESTING_FILE.test(url);
}