codelensWidget.ts 10.4 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

J
Johannes Rieken 已提交
6
import 'vs/css!./codelensWidget';
7
import * as dom from 'vs/base/browser/dom';
A
Alex Dima 已提交
8
import { coalesce, isFalsyOrEmpty } from 'vs/base/common/arrays';
9
import { escape } from 'vs/base/common/strings';
10
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
A
Alex Dima 已提交
11 12
import { Range } from 'vs/editor/common/core/range';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
A
Alex Dima 已提交
13
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
14
import { Command, CodeLens } from 'vs/editor/common/modes';
15
import { editorCodeLensForeground } from 'vs/editor/common/view/editorColorRegistry';
16
import { CodeLensItem } from 'vs/editor/contrib/codelens/codelens';
A
Alex Dima 已提交
17 18
import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
19 20 21 22 23 24 25 26 27

class CodeLensViewZone implements editorBrowser.IViewZone {

	readonly heightInLines: number;
	readonly suppressMouseDown: boolean;
	readonly domNode: HTMLElement;

	afterLineNumber: number;

28
	private _lastHeight?: number;
29
	private readonly _onHeight: Function;
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

	constructor(afterLineNumber: number, onHeight: Function) {
		this.afterLineNumber = afterLineNumber;
		this._onHeight = onHeight;

		this.heightInLines = 1;
		this.suppressMouseDown = true;
		this.domNode = document.createElement('div');
	}

	onComputedHeight(height: number): void {
		if (this._lastHeight === undefined) {
			this._lastHeight = height;
		} else if (this._lastHeight !== height) {
			this._lastHeight = height;
			this._onHeight();
		}
	}
}

class CodeLensContentWidget implements editorBrowser.IContentWidget {

	private static _idPool: number = 0;

	// Editor.IContentWidget.allowEditorOverflow
	readonly allowEditorOverflow: boolean = false;
	readonly suppressMouseDown: boolean = true;

	private readonly _id: string;
	private readonly _domNode: HTMLElement;
	private readonly _editor: editorBrowser.ICodeEditor;
J
Johannes Rieken 已提交
61
	private readonly _commands = new Map<string, Command>();
62 63 64 65 66

	private _widgetPosition: editorBrowser.IContentWidgetPosition;

	constructor(
		editor: editorBrowser.ICodeEditor,
J
Johannes Rieken 已提交
67
		symbolRange: Range,
68
		data: CodeLensItem[]
69 70 71 72 73 74 75 76 77
	) {
		this._id = 'codeLensWidget' + (++CodeLensContentWidget._idPool);
		this._editor = editor;

		this.setSymbolRange(symbolRange);

		this._domNode = document.createElement('span');
		this._domNode.innerHTML = '&nbsp;';
		dom.addClass(this._domNode, 'codelens-decoration');
78
		this.updateHeight();
J
Johannes Rieken 已提交
79
		this.withCommands(data.map(data => data.symbol), false);
80 81
	}

82
	updateHeight(): void {
83 84 85
		const { fontInfo, lineHeight } = this._editor.getConfiguration();
		this._domNode.style.height = `${Math.round(lineHeight * 1.1)}px`;
		this._domNode.style.lineHeight = `${lineHeight}px`;
86 87
		this._domNode.style.fontSize = `${Math.round(fontInfo.fontSize * 0.9)}px`;
		this._domNode.style.paddingRight = `${Math.round(fontInfo.fontSize * 0.45)}px`;
88 89 90
		this._domNode.innerHTML = '&nbsp;';
	}

91
	withCommands(inSymbols: Array<CodeLens | undefined | null>, animate: boolean): void {
J
Johannes Rieken 已提交
92
		this._commands.clear();
93

M
Matt Bierner 已提交
94
		const symbols = coalesce(inSymbols);
J
Johannes Rieken 已提交
95
		if (isFalsyOrEmpty(symbols)) {
96
			this._domNode.innerHTML = '<span>no commands</span>';
97 98 99 100 101
			return;
		}

		let html: string[] = [];
		for (let i = 0; i < symbols.length; i++) {
M
Matt Bierner 已提交
102 103 104 105 106 107
			const command = symbols[i].command;
			if (command) {
				const title = escape(command.title);
				let part: string;
				if (command.id) {
					part = `<a id=${i}>${title}</a>`;
J
Johannes Rieken 已提交
108
					this._commands.set(String(i), command);
M
Matt Bierner 已提交
109 110 111 112
				} else {
					part = `<span>${title}</span>`;
				}
				html.push(part);
113 114 115
			}
		}

J
Johannes Rieken 已提交
116
		const wasEmpty = this._domNode.innerHTML === '' || this._domNode.innerHTML === '&nbsp;';
117 118
		this._domNode.innerHTML = html.join('<span>&nbsp;|&nbsp;</span>');
		this._editor.layoutContentWidget(this);
J
Johannes Rieken 已提交
119 120 121
		if (wasEmpty && animate) {
			dom.addClass(this._domNode, 'fadein');
		}
122 123
	}

124 125
	getCommand(link: HTMLLinkElement): Command | undefined {
		return link.parentElement === this._domNode
J
Johannes Rieken 已提交
126
			? this._commands.get(link.id)
127 128 129
			: undefined;
	}

130 131 132 133 134 135 136 137 138
	getId(): string {
		return this._id;
	}

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

	setSymbolRange(range: Range): void {
M
Matt Bierner 已提交
139 140 141
		if (!this._editor.hasModel()) {
			return;
		}
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
		const lineNumber = range.startLineNumber;
		const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(lineNumber);
		this._widgetPosition = {
			position: { lineNumber: lineNumber, column: column },
			preference: [editorBrowser.ContentWidgetPositionPreference.ABOVE]
		};
	}

	getPosition(): editorBrowser.IContentWidgetPosition {
		return this._widgetPosition;
	}

	isVisible(): boolean {
		return this._domNode.hasAttribute('monaco-visible-content-widget');
	}
}

export interface IDecorationIdCallback {
	(decorationId: string): void;
}

export class CodeLensHelper {

165 166 167
	private readonly _removeDecorations: string[];
	private readonly _addDecorations: IModelDeltaDecoration[];
	private readonly _addDecorationsCallbacks: IDecorationIdCallback[];
168 169 170 171 172 173 174

	constructor() {
		this._removeDecorations = [];
		this._addDecorations = [];
		this._addDecorationsCallbacks = [];
	}

175
	addDecoration(decoration: IModelDeltaDecoration, callback: IDecorationIdCallback): void {
176 177 178 179 180 181 182 183
		this._addDecorations.push(decoration);
		this._addDecorationsCallbacks.push(callback);
	}

	removeDecoration(decorationId: string): void {
		this._removeDecorations.push(decorationId);
	}

184
	commit(changeAccessor: IModelDecorationsChangeAccessor): void {
A
Alex Dima 已提交
185
		let resultingDecorations = changeAccessor.deltaDecorations(this._removeDecorations, this._addDecorations);
186 187 188 189 190 191
		for (let i = 0, len = resultingDecorations.length; i < len; i++) {
			this._addDecorationsCallbacks[i](resultingDecorations[i]);
		}
	}
}

192
export class CodeLensWidget {
193 194 195 196 197 198

	private readonly _editor: editorBrowser.ICodeEditor;
	private readonly _viewZone: CodeLensViewZone;
	private readonly _viewZoneId: number;
	private readonly _contentWidget: CodeLensContentWidget;
	private _decorationIds: string[];
199
	private _data: CodeLensItem[];
200 201

	constructor(
202
		data: CodeLensItem[],
203 204 205
		editor: editorBrowser.ICodeEditor,
		helper: CodeLensHelper,
		viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor,
206
		updateCallback: Function
207 208 209 210 211
	) {
		this._editor = editor;
		this._data = data;
		this._decorationIds = new Array<string>(this._data.length);

M
Matt Bierner 已提交
212
		let range: Range | undefined;
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
		this._data.forEach((codeLensData, i) => {

			helper.addDecoration({
				range: codeLensData.symbol.range,
				options: ModelDecorationOptions.EMPTY
			}, id => this._decorationIds[i] = id);

			// the range contains all lenses on this line
			if (!range) {
				range = Range.lift(codeLensData.symbol.range);
			} else {
				range = Range.plusRange(range, codeLensData.symbol.range);
			}
		});

M
Matt Bierner 已提交
228
		if (range) {
J
Johannes Rieken 已提交
229
			this._contentWidget = new CodeLensContentWidget(editor, range, this._data);
M
Matt Bierner 已提交
230
			this._viewZone = new CodeLensViewZone(range.startLineNumber - 1, updateCallback);
231

M
Matt Bierner 已提交
232 233 234
			this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone);
			this._editor.addContentWidget(this._contentWidget);
		}
235 236
	}

M
Matt Bierner 已提交
237
	dispose(helper: CodeLensHelper, viewZoneChangeAccessor?: editorBrowser.IViewZoneChangeAccessor): void {
238
		while (this._decorationIds.length) {
M
Matt Bierner 已提交
239
			helper.removeDecoration(this._decorationIds.pop()!);
240 241 242 243 244 245 246 247
		}
		if (viewZoneChangeAccessor) {
			viewZoneChangeAccessor.removeZone(this._viewZoneId);
		}
		this._editor.removeContentWidget(this._contentWidget);
	}

	isValid(): boolean {
M
Matt Bierner 已提交
248 249 250 251
		if (!this._editor.hasModel()) {
			return false;
		}
		const model = this._editor.getModel();
252
		return this._decorationIds.some((id, i) => {
M
Matt Bierner 已提交
253
			const range = model.getDecorationRange(id);
254
			const symbol = this._data[i].symbol;
M
Matt Bierner 已提交
255
			return !!(range && Range.isEmpty(symbol.range) === range.isEmpty());
256 257 258
		});
	}

259
	updateCodeLensSymbols(data: CodeLensItem[], helper: CodeLensHelper): void {
260
		while (this._decorationIds.length) {
M
Matt Bierner 已提交
261
			helper.removeDecoration(this._decorationIds.pop()!);
262 263 264 265 266 267 268 269 270 271 272
		}
		this._data = data;
		this._decorationIds = new Array<string>(this._data.length);
		this._data.forEach((codeLensData, i) => {
			helper.addDecoration({
				range: codeLensData.symbol.range,
				options: ModelDecorationOptions.EMPTY
			}, id => this._decorationIds[i] = id);
		});
	}

273
	computeIfNecessary(model: ITextModel): CodeLensItem[] | null {
274 275 276 277 278 279
		if (!this._contentWidget.isVisible()) {
			return null;
		}

		// Read editor current state
		for (let i = 0; i < this._decorationIds.length; i++) {
M
Matt Bierner 已提交
280 281 282 283
			const range = model.getDecorationRange(this._decorationIds[i]);
			if (range) {
				this._data[i].symbol.range = range;
			}
284 285 286 287
		}
		return this._data;
	}

288
	updateCommands(symbols: Array<CodeLens | undefined | null>): void {
J
Johannes Rieken 已提交
289
		this._contentWidget.withCommands(symbols, true);
290 291 292 293 294 295 296
		for (let i = 0; i < this._data.length; i++) {
			const resolved = symbols[i];
			if (resolved) {
				const { symbol } = this._data[i];
				symbol.command = resolved.command || symbol.command;
			}
		}
297 298
	}

299 300 301 302
	updateHeight(): void {
		this._contentWidget.updateHeight();
	}

303 304 305 306
	getCommand(link: HTMLLinkElement): Command | undefined {
		return this._contentWidget.getCommand(link);
	}

307
	getLineNumber(): number {
M
Matt Bierner 已提交
308 309 310 311 312
		if (this._editor.hasModel()) {
			const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
			if (range) {
				return range.startLineNumber;
			}
313 314 315 316 317
		}
		return -1;
	}

	update(viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): void {
M
Matt Bierner 已提交
318
		if (this.isValid() && this._editor.hasModel()) {
319
			const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
M
Matt Bierner 已提交
320 321 322
			if (range) {
				this._viewZone.afterLineNumber = range.startLineNumber - 1;
				viewZoneChangeAccessor.layoutZone(this._viewZoneId);
323

M
Matt Bierner 已提交
324 325 326
				this._contentWidget.setSymbolRange(range);
				this._editor.layoutContentWidget(this._contentWidget);
			}
327 328 329 330 331
		}
	}
}

registerThemingParticipant((theme, collector) => {
M
Matt Bierner 已提交
332
	const codeLensForeground = theme.getColor(editorCodeLensForeground);
333 334 335
	if (codeLensForeground) {
		collector.addRule(`.monaco-editor .codelens-decoration { color: ${codeLensForeground}; }`);
	}
M
Matt Bierner 已提交
336
	const activeLinkForeground = theme.getColor(editorActiveLinkForeground);
337 338 339 340
	if (activeLinkForeground) {
		collector.addRule(`.monaco-editor .codelens-decoration > a:hover { color: ${activeLinkForeground} !important; }`);
	}
});