wordHighlighter.ts 13.1 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, overviewRulerSelectionHighlightForeground } from 'vs/platform/theme/common/colorRegistry';
19
import { registerThemingParticipant, themeColorFromId } 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

26 27
export const overviewRulerWordHighlightForeground = registerColor('editorOverviewRuler.wordHighlightForeground', { dark: '#A0A0A0', light: '#A0A0A0', hc: '#A0A0A0' }, nls.localize('overviewRulerWordHighlightForeground', 'Overview ruler marker color for symbol highlights.'));

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

30
	const orderedByScore = DocumentHighlightProviderRegistry.ordered(model);
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 => {
37
		return (): TPromise<DocumentHighlight[]> => {
38
			if (!foundResult) {
39 40 41
				return asWinJsPromise((token) => {
					return provider.provideDocumentHighlights(model, position, token);
				}).then(data => {
42 43 44 45
					if (Array.isArray(data) && data.length > 0) {
						foundResult = true;
						return data;
					}
M
Matt Bierner 已提交
46
					return undefined;
47
				}, err => {
48
					onUnexpectedExternalError(err);
R
Ron Buckton 已提交
49
					return undefined;
50 51
				});
			}
M
Matt Bierner 已提交
52
			return undefined;
A
tslint  
Alex Dima 已提交
53
		};
54
	})).then(values => {
A
tslint  
Alex Dima 已提交
55
		return values[0];
56 57
	});
}
E
Erich Gamma 已提交
58

59 60
CommonEditorRegistry.registerDefaultLanguageCommand('_executeDocumentHighlights', getOccurrencesAtPosition);

E
Erich Gamma 已提交
61 62
class WordHighlighter {

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

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

J
Johannes Rieken 已提交
75 76
	private lastCursorPositionChangeTime: number = 0;
	private renderDecorationsTimer: number = -1;
E
Erich Gamma 已提交
77

J
Johannes Rieken 已提交
78
	constructor(editor: editorCommon.ICommonCodeEditor) {
E
Erich Gamma 已提交
79
		this.editor = editor;
80
		this.occurrencesHighlight = this.editor.getConfiguration().contribInfo.occurrencesHighlight;
E
Erich Gamma 已提交
81 82
		this.model = this.editor.getModel();
		this.toUnhook = [];
83
		this.toUnhook.push(editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
84 85 86 87 88 89 90

			if (!this.occurrencesHighlight) {
				// Early exit if nothing needs to be done!
				// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)
				return;
			}

E
Erich Gamma 已提交
91 92
			this._onPositionChanged(e);
		}));
A
Alex Dima 已提交
93
		this.toUnhook.push(editor.onDidChangeModel((e) => {
E
Erich Gamma 已提交
94 95 96
			this._stopAll();
			this.model = this.editor.getModel();
		}));
A
Alex Dima 已提交
97
		this.toUnhook.push(editor.onDidChangeModelContent((e) => {
E
Erich Gamma 已提交
98 99
			this._stopAll();
		}));
100 101 102 103 104 105 106
		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 已提交
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132

		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) {
133
			clearTimeout(this.renderDecorationsTimer);
E
Erich Gamma 已提交
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
			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;
		}
	}

150
	private _onPositionChanged(e: ICursorPositionChangedEvent): void {
E
Erich Gamma 已提交
151

152 153 154 155 156 157
		// disabled
		if (!this.occurrencesHighlight) {
			this._stopAll();
			return;
		}

E
Erich Gamma 已提交
158
		// ignore typing & other
159
		if (e.reason !== CursorChangeReason.Explicit) {
E
Erich Gamma 已提交
160 161 162 163 164
			this._stopAll();
			return;
		}

		// no providers for this model
J
Johannes Rieken 已提交
165
		if (!DocumentHighlightProviderRegistry.has(this.model)) {
E
Erich Gamma 已提交
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:
194 195
		// - when cursor is moved to a word, trigger immediately a findOccurrences request
		// - 250ms later after the last cursor move event, render the occurrences
E
Erich Gamma 已提交
196 197 198 199 200 201 202 203
		// - 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 已提交
204
		for (var i = 0, len = this._decorationIds.length; !workerRequestIsValid && i < len; i++) {
E
Erich Gamma 已提交
205
			var range = this.model.getDecorationRange(this._decorationIds[i]);
J
Johannes Rieken 已提交
206 207
			if (range && range.startLineNumber === lineNumber) {
				if (range.startColumn <= startColumn && range.endColumn >= endColumn) {
E
Erich Gamma 已提交
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
					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
229
				clearTimeout(this.renderDecorationsTimer);
E
Erich Gamma 已提交
230 231 232 233 234 235 236 237 238 239 240
				this.renderDecorationsTimer = -1;
				this._beginRenderDecorations();
			}
		} else {
			// case d)
			// Stop all previous actions and start fresh
			this._stopAll();

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

241
			this.workerRequest = getOccurrencesAtPosition(this.model, this.editor.getPosition());
E
Erich Gamma 已提交
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264

			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
265
			this.renderDecorationsTimer = setTimeout(() => {
E
Erich Gamma 已提交
266 267 268 269 270 271 272
				this.renderDecorations();
			}, (minimumRenderTime - currentTime));
		}
	}

	private renderDecorations(): void {
		this.renderDecorationsTimer = -1;
J
Johannes Rieken 已提交
273 274
		var decorations: editorCommon.IModelDeltaDecoration[] = [];
		for (var i = 0, len = this.workerRequestValue.length; i < len; i++) {
E
Erich Gamma 已提交
275 276 277
			var info = this.workerRequestValue[i];
			decorations.push({
				range: info.range,
278
				options: WordHighlighter._getDecorationOptions(info.kind)
E
Erich Gamma 已提交
279 280 281 282 283 284
			});
		}

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

285 286 287 288 289 290 291 292 293 294 295 296 297 298
	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: {
299 300
			color: themeColorFromId(overviewRulerWordHighlightForeground),
			darkColor: themeColorFromId(overviewRulerWordHighlightForeground),
301 302 303 304 305 306 307 308
			position: editorCommon.OverviewRulerLane.Center
		}
	});

	private static _TEXT_OPTIONS = ModelDecorationOptions.register({
		stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'selectionHighlight',
		overviewRuler: {
309 310
			color: themeColorFromId(overviewRulerSelectionHighlightForeground),
			darkColor: themeColorFromId(overviewRulerSelectionHighlightForeground),
311 312 313 314 315 316 317 318
			position: editorCommon.OverviewRulerLane.Center
		}
	});

	private static _REGULAR_OPTIONS = ModelDecorationOptions.register({
		stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'wordHighlight',
		overviewRuler: {
319 320
			color: themeColorFromId(overviewRulerWordHighlightForeground),
			darkColor: themeColorFromId(overviewRulerWordHighlightForeground),
321 322 323 324
			position: editorCommon.OverviewRulerLane.Center
		}
	});

A
Alex Dima 已提交
325
	public dispose(): void {
E
Erich Gamma 已提交
326
		this._stopAll();
A
Alex Dima 已提交
327
		this.toUnhook = dispose(this.toUnhook);
E
Erich Gamma 已提交
328 329 330
	}
}

331
@commonEditorContribution
A
Alex Dima 已提交
332
class WordHighlighterContribution implements editorCommon.IEditorContribution {
E
Erich Gamma 已提交
333

334
	private static ID = 'editor.contrib.wordHighlighter';
E
Erich Gamma 已提交
335 336 337

	private wordHighligher: WordHighlighter;

J
Johannes Rieken 已提交
338
	constructor(editor: editorCommon.ICommonCodeEditor) {
E
Erich Gamma 已提交
339 340 341 342 343 344 345 346
		this.wordHighligher = new WordHighlighter(editor);
	}

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

	public dispose(): void {
A
Alex Dima 已提交
347
		this.wordHighligher.dispose();
E
Erich Gamma 已提交
348 349
	}
}
350

351
registerThemingParticipant((theme, collector) => {
352
	let selectionHighlight = theme.getColor(editorSelectionHighlight);
353
	if (selectionHighlight) {
354 355
		collector.addRule(`.monaco-editor .focused .selectionHighlight { background-color: ${selectionHighlight}; }`);
		collector.addRule(`.monaco-editor .selectionHighlight { background-color: ${selectionHighlight.transparent(0.5)}; }`);
356
	}
357 358
	let wordHighlight = theme.getColor(editorWordHighlight);
	if (wordHighlight) {
359
		collector.addRule(`.monaco-editor .wordHighlight { background-color: ${wordHighlight}; }`);
360
	}
361 362
	let wordHighlightStrong = theme.getColor(editorWordHighlightStrong);
	if (wordHighlightStrong) {
363
		collector.addRule(`.monaco-editor .wordHighlightStrong { background-color: ${wordHighlightStrong}; }`);
364
	}
365
	let hcOutline = theme.getColor(activeContrastBorder);
366
	if (hcOutline) {
367 368 369
		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; }`);
370 371 372
	}

});