wordHighlighter.ts 12.2 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7 8
import nls = require('vs/nls');

J
Johannes Rieken 已提交
9
import { sequence, asWinJsPromise } from 'vs/base/common/async';
10
import { onUnexpectedExternalError } from 'vs/base/common/errors';
J
Johannes Rieken 已提交
11 12
import { TPromise } from 'vs/base/common/winjs.base';
import { Range } from 'vs/editor/common/core/range';
A
Alex Dima 已提交
13
import * as editorCommon from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
14 15 16 17
import { CommonEditorRegistry, commonEditorContribution } from 'vs/editor/common/editorCommonExtensions';
import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProviderRegistry } from 'vs/editor/common/modes';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Position } from 'vs/editor/common/core/position';
18
import { registerColor, editorSelectionHighlight, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
19
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
20
import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
21
import { ModelDecorationOptions } from "vs/editor/common/model/textModelWithDecorations";
22

23 24
export const editorWordHighlight = registerColor('editor.wordHighlightBackground', { dark: '#575757B8', light: '#57575740', hc: null }, nls.localize('wordHighlight', 'Background color of a symbol during read-access, like reading a variable.'));
export const editorWordHighlightStrong = registerColor('editor.wordHighlightStrongBackground', { dark: '#004972B8', light: '#0e639c40', hc: null }, nls.localize('wordHighlightStrong', 'Background color of a symbol during write-access, like writing to a variable.'));
25

J
Johannes Rieken 已提交
26
export function getOccurrencesAtPosition(model: editorCommon.IReadOnlyModel, position: Position): TPromise<DocumentHighlight[]> {
27

28
	const orderedByScore = DocumentHighlightProviderRegistry.ordered(model);
29 30 31 32 33 34 35 36
	let foundResult = false;

	// in order of score ask the occurrences provider
	// until someone response with a good result
	// (good = none empty array)
	return sequence(orderedByScore.map(provider => {
		return () => {
			if (!foundResult) {
37 38 39
				return asWinJsPromise((token) => {
					return provider.provideDocumentHighlights(model, position, token);
				}).then(data => {
40 41 42 43
					if (Array.isArray(data) && data.length > 0) {
						foundResult = true;
						return data;
					}
M
Matt Bierner 已提交
44
					return undefined;
45
				}, err => {
46
					onUnexpectedExternalError(err);
47 48
				});
			}
M
Matt Bierner 已提交
49
			return undefined;
A
tslint  
Alex Dima 已提交
50
		};
51
	})).then(values => {
A
tslint  
Alex Dima 已提交
52
		return values[0];
53 54
	});
}
E
Erich Gamma 已提交
55

56 57
CommonEditorRegistry.registerDefaultLanguageCommand('_executeDocumentHighlights', getOccurrencesAtPosition);

E
Erich Gamma 已提交
58 59
class WordHighlighter {

A
Alex Dima 已提交
60
	private editor: editorCommon.ICommonCodeEditor;
61
	private occurrencesHighlight: boolean;
A
Alex Dima 已提交
62
	private model: editorCommon.IModel;
63
	private _lastWordRange: Range;
E
Erich Gamma 已提交
64
	private _decorationIds: string[];
A
Alex Dima 已提交
65
	private toUnhook: IDisposable[];
E
Erich Gamma 已提交
66

J
Johannes Rieken 已提交
67 68 69 70
	private workerRequestTokenId: number = 0;
	private workerRequest: TPromise<DocumentHighlight[]> = null;
	private workerRequestCompleted: boolean = false;
	private workerRequestValue: DocumentHighlight[] = [];
E
Erich Gamma 已提交
71

J
Johannes Rieken 已提交
72 73
	private lastCursorPositionChangeTime: number = 0;
	private renderDecorationsTimer: number = -1;
E
Erich Gamma 已提交
74

J
Johannes Rieken 已提交
75
	constructor(editor: editorCommon.ICommonCodeEditor) {
E
Erich Gamma 已提交
76
		this.editor = editor;
77
		this.occurrencesHighlight = this.editor.getConfiguration().contribInfo.occurrencesHighlight;
E
Erich Gamma 已提交
78 79
		this.model = this.editor.getModel();
		this.toUnhook = [];
80
		this.toUnhook.push(editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
E
Erich Gamma 已提交
81 82
			this._onPositionChanged(e);
		}));
A
Alex Dima 已提交
83
		this.toUnhook.push(editor.onDidChangeModel((e) => {
E
Erich Gamma 已提交
84 85 86
			this._stopAll();
			this.model = this.editor.getModel();
		}));
A
Alex Dima 已提交
87
		this.toUnhook.push(editor.onDidChangeModelContent((e) => {
E
Erich Gamma 已提交
88 89
			this._stopAll();
		}));
90 91 92 93 94 95 96
		this.toUnhook.push(editor.onDidChangeConfiguration((e) => {
			let newValue = this.editor.getConfiguration().contribInfo.occurrencesHighlight;
			if (this.occurrencesHighlight !== newValue) {
				this.occurrencesHighlight = newValue;
				this._stopAll();
			}
		}));
E
Erich Gamma 已提交
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122

		this._lastWordRange = null;
		this._decorationIds = [];
		this.workerRequestTokenId = 0;
		this.workerRequest = null;
		this.workerRequestCompleted = false;

		this.lastCursorPositionChangeTime = 0;
		this.renderDecorationsTimer = -1;
	}

	private _removeDecorations(): void {
		if (this._decorationIds.length > 0) {
			// remove decorations
			this._decorationIds = this.editor.deltaDecorations(this._decorationIds, []);
		}
	}

	private _stopAll(): void {
		this._lastWordRange = null;

		// Remove any existing decorations
		this._removeDecorations();

		// Cancel any renderDecorationsTimer
		if (this.renderDecorationsTimer !== -1) {
123
			clearTimeout(this.renderDecorationsTimer);
E
Erich Gamma 已提交
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
			this.renderDecorationsTimer = -1;
		}

		// Cancel any worker request
		if (this.workerRequest !== null) {
			this.workerRequest.cancel();
			this.workerRequest = null;
		}

		// Invalidate any worker request callback
		if (!this.workerRequestCompleted) {
			this.workerRequestTokenId++;
			this.workerRequestCompleted = true;
		}
	}

140
	private _onPositionChanged(e: ICursorPositionChangedEvent): void {
E
Erich Gamma 已提交
141

142 143 144 145 146 147
		// disabled
		if (!this.occurrencesHighlight) {
			this._stopAll();
			return;
		}

E
Erich Gamma 已提交
148
		// ignore typing & other
149
		if (e.reason !== CursorChangeReason.Explicit) {
E
Erich Gamma 已提交
150 151 152 153 154
			this._stopAll();
			return;
		}

		// no providers for this model
J
Johannes Rieken 已提交
155
		if (!DocumentHighlightProviderRegistry.has(this.model)) {
E
Erich Gamma 已提交
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
			this._stopAll();
			return;
		}

		var editorSelection = this.editor.getSelection();

		// ignore multiline selection
		if (editorSelection.startLineNumber !== editorSelection.endLineNumber) {
			this._stopAll();
			return;
		}

		var lineNumber = editorSelection.startLineNumber;
		var startColumn = editorSelection.startColumn;
		var endColumn = editorSelection.endColumn;

		var word = this.model.getWordAtPosition({
			lineNumber: lineNumber,
			column: startColumn
		});

		// The selection must be inside a word or surround one word at most
		if (!word || word.startColumn > startColumn || word.endColumn < endColumn) {
			this._stopAll();
			return;
		}

		// All the effort below is trying to achieve this:
		// - when cursor is moved to a word, trigger immediately a findOccurences request
		// - 250ms later after the last cursor move event, render the occurences
		// - no flickering!

		var currentWordRange = new Range(lineNumber, word.startColumn, lineNumber, word.endColumn);

		var workerRequestIsValid = this._lastWordRange && this._lastWordRange.equalsRange(currentWordRange);

		// Even if we are on a different word, if that word is in the decorations ranges, the request is still valid
		// (Same symbol)
J
Johannes Rieken 已提交
194
		for (var i = 0, len = this._decorationIds.length; !workerRequestIsValid && i < len; i++) {
E
Erich Gamma 已提交
195
			var range = this.model.getDecorationRange(this._decorationIds[i]);
J
Johannes Rieken 已提交
196 197
			if (range && range.startLineNumber === lineNumber) {
				if (range.startColumn <= startColumn && range.endColumn >= endColumn) {
E
Erich Gamma 已提交
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
					workerRequestIsValid = true;
				}
			}
		}


		// There are 4 cases:
		// a) old workerRequest is valid & completed, renderDecorationsTimer fired
		// b) old workerRequest is valid & completed, renderDecorationsTimer not fired
		// c) old workerRequest is valid, but not completed
		// d) old workerRequest is not valid

		// For a) no action is needed
		// For c), member 'lastCursorPositionChangeTime' will be used when installing the timer so no action is needed

		this.lastCursorPositionChangeTime = (new Date()).getTime();

		if (workerRequestIsValid) {
			if (this.workerRequestCompleted && this.renderDecorationsTimer !== -1) {
				// case b)
				// Delay the firing of renderDecorationsTimer by an extra 250 ms
219
				clearTimeout(this.renderDecorationsTimer);
E
Erich Gamma 已提交
220 221 222 223 224 225 226 227 228 229 230
				this.renderDecorationsTimer = -1;
				this._beginRenderDecorations();
			}
		} else {
			// case d)
			// Stop all previous actions and start fresh
			this._stopAll();

			var myRequestId = ++this.workerRequestTokenId;
			this.workerRequestCompleted = false;

231
			this.workerRequest = getOccurrencesAtPosition(this.model, this.editor.getPosition());
E
Erich Gamma 已提交
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254

			this.workerRequest.then(data => {
				if (myRequestId === this.workerRequestTokenId) {
					this.workerRequestCompleted = true;
					this.workerRequestValue = data || [];
					this._beginRenderDecorations();
				}
			}).done();
		}

		this._lastWordRange = currentWordRange;
	}

	private _beginRenderDecorations(): void {
		var currentTime = (new Date()).getTime();
		var minimumRenderTime = this.lastCursorPositionChangeTime + 250;

		if (currentTime >= minimumRenderTime) {
			// Synchronous
			this.renderDecorationsTimer = -1;
			this.renderDecorations();
		} else {
			// Asyncrhonous
255
			this.renderDecorationsTimer = setTimeout(() => {
E
Erich Gamma 已提交
256 257 258 259 260 261 262
				this.renderDecorations();
			}, (minimumRenderTime - currentTime));
		}
	}

	private renderDecorations(): void {
		this.renderDecorationsTimer = -1;
J
Johannes Rieken 已提交
263 264
		var decorations: editorCommon.IModelDeltaDecoration[] = [];
		for (var i = 0, len = this.workerRequestValue.length; i < len; i++) {
E
Erich Gamma 已提交
265 266 267
			var info = this.workerRequestValue[i];
			decorations.push({
				range: info.range,
268
				options: WordHighlighter._getDecorationOptions(info.kind)
E
Erich Gamma 已提交
269 270 271 272 273 274
			});
		}

		this._decorationIds = this.editor.deltaDecorations(this._decorationIds, decorations);
	}

275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
	private static _getDecorationOptions(kind: DocumentHighlightKind): ModelDecorationOptions {
		if (kind === DocumentHighlightKind.Write) {
			return this._WRITE_OPTIONS;
		} else if (kind === DocumentHighlightKind.Text) {
			return this._TEXT_OPTIONS;
		} else {
			return this._REGULAR_OPTIONS;
		}
	}

	private static _WRITE_OPTIONS = ModelDecorationOptions.register({
		stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'wordHighlightStrong',
		overviewRuler: {
			color: '#A0A0A0',
			darkColor: '#A0A0A0',
			position: editorCommon.OverviewRulerLane.Center
		}
	});

	private static _TEXT_OPTIONS = ModelDecorationOptions.register({
		stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'selectionHighlight',
		overviewRuler: {
			color: '#A0A0A0',
			darkColor: '#A0A0A0',
			position: editorCommon.OverviewRulerLane.Center
		}
	});

	private static _REGULAR_OPTIONS = ModelDecorationOptions.register({
		stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'wordHighlight',
		overviewRuler: {
			color: '#A0A0A0',
			darkColor: '#A0A0A0',
			position: editorCommon.OverviewRulerLane.Center
		}
	});

A
Alex Dima 已提交
315
	public dispose(): void {
E
Erich Gamma 已提交
316
		this._stopAll();
A
Alex Dima 已提交
317
		this.toUnhook = dispose(this.toUnhook);
E
Erich Gamma 已提交
318 319 320
	}
}

321
@commonEditorContribution
A
Alex Dima 已提交
322
class WordHighlighterContribution implements editorCommon.IEditorContribution {
E
Erich Gamma 已提交
323

324
	private static ID = 'editor.contrib.wordHighlighter';
E
Erich Gamma 已提交
325 326 327

	private wordHighligher: WordHighlighter;

J
Johannes Rieken 已提交
328
	constructor(editor: editorCommon.ICommonCodeEditor) {
E
Erich Gamma 已提交
329 330 331 332 333 334 335 336
		this.wordHighligher = new WordHighlighter(editor);
	}

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

	public dispose(): void {
A
Alex Dima 已提交
337
		this.wordHighligher.dispose();
E
Erich Gamma 已提交
338 339
	}
}
340

341
registerThemingParticipant((theme, collector) => {
342
	let selectionHighlight = theme.getColor(editorSelectionHighlight);
343
	if (selectionHighlight) {
344 345
		collector.addRule(`.monaco-editor .focused .selectionHighlight { background-color: ${selectionHighlight}; }`);
		collector.addRule(`.monaco-editor .selectionHighlight { background-color: ${selectionHighlight.transparent(0.5)}; }`);
346
	}
347 348
	let wordHighlight = theme.getColor(editorWordHighlight);
	if (wordHighlight) {
349
		collector.addRule(`.monaco-editor .wordHighlight { background-color: ${wordHighlight}; }`);
350
	}
351 352
	let wordHighlightStrong = theme.getColor(editorWordHighlightStrong);
	if (wordHighlightStrong) {
353
		collector.addRule(`.monaco-editor .wordHighlightStrong { background-color: ${wordHighlightStrong}; }`);
354
	}
355
	let hcOutline = theme.getColor(activeContrastBorder);
356
	if (hcOutline) {
357 358 359
		collector.addRule(`.monaco-editor .selectionHighlight { border: 1px dotted ${hcOutline}; box-sizing: border-box; }`);
		collector.addRule(`.monaco-editor .wordHighlight { border: 1px dashed ${hcOutline}; box-sizing: border-box; }`);
		collector.addRule(`.monaco-editor .wordHighlightStrong { border: 1px dashed ${hcOutline}; box-sizing: border-box; }`);
360 361 362
	}

});