modesContentHover.ts 25.9 KB
Newer Older
E
Erich Gamma 已提交
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.
 *--------------------------------------------------------------------------------------------*/

A
Alex Dima 已提交
6
import * as nls from 'vs/nls';
M
Michel Kaporin 已提交
7
import * as dom from 'vs/base/browser/dom';
A
Alex Dima 已提交
8 9
import { CancellationToken } from 'vs/base/common/cancellation';
import { Color, RGBA } from 'vs/base/common/color';
10
import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent';
11
import { IDisposable, toDisposable, DisposableStore, combinedDisposable, MutableDisposable, Disposable } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
12 13 14
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
A
Alex Dima 已提交
15
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
16
import { DocumentColorProvider, Hover as MarkdownHover, HoverProviderRegistry, IColor, TokenizationRegistry, CodeActionTriggerType } from 'vs/editor/common/modes';
A
Alex Dima 已提交
17 18
import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color';
import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector';
19 20
import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel';
import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget';
A
Alex Dima 已提交
21 22 23
import { getHover } from 'vs/editor/contrib/hover/getHover';
import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation';
import { ContentHoverWidget } from 'vs/editor/contrib/hover/hoverWidgets';
24
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
25
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
M
Matt Bierner 已提交
26
import { coalesce, isNonEmptyArray, asArray } from 'vs/base/common/arrays';
S
Sandeep Somavarapu 已提交
27
import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers';
B
Benjamin Pasero 已提交
28
import { basename } from 'vs/base/common/resources';
29
import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService';
S
Sandeep Somavarapu 已提交
30 31
import { onUnexpectedError } from 'vs/base/common/errors';
import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener';
S
Sandeep Somavarapu 已提交
32 33
import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
34
import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async';
35 36
import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction';
import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands';
37
import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types';
38
import { IModeService } from 'vs/editor/common/services/modeService';
R
rebornix 已提交
39
import { IIdentifiedSingleEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model';
A
Alex Dima 已提交
40
import { EditorOption } from 'vs/editor/common/config/editorOptions';
41
import { Constants } from 'vs/base/common/uint';
42
import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
43
import { Progress } from 'vs/platform/progress/common/progress';
R
Rob Lourens 已提交
44
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
45

M
Michel Kaporin 已提交
46
const $ = dom.$;
E
Erich Gamma 已提交
47

48 49 50 51
class ColorHover {

	constructor(
		public readonly range: IRange,
J
Joao Moreno 已提交
52
		public readonly color: IColor,
53
		public readonly provider: DocumentColorProvider
54 55 56
	) { }
}

57 58 59 60 61 62 63 64 65
class MarkerHover {

	constructor(
		public readonly range: IRange,
		public readonly marker: IMarker,
	) { }
}

type HoverPart = MarkdownHover | ColorHover | MarkerHover;
E
Erich Gamma 已提交
66

67
class ModesContentComputer implements IHoverComputer<HoverPart[]> {
E
Erich Gamma 已提交
68

69
	private readonly _editor: ICodeEditor;
70
	private _result: HoverPart[];
M
Matt Bierner 已提交
71
	private _range?: Range;
E
Erich Gamma 已提交
72

73 74
	constructor(
		editor: ICodeEditor,
75
		private readonly _markerDecorationsService: IMarkerDecorationsService
76
	) {
E
Erich Gamma 已提交
77
		this._editor = editor;
A
Alex Dima 已提交
78
		this._result = [];
E
Erich Gamma 已提交
79 80
	}

J
Joao Moreno 已提交
81
	setRange(range: Range): void {
E
Erich Gamma 已提交
82 83 84 85
		this._range = range;
		this._result = [];
	}

J
Joao Moreno 已提交
86
	clearResult(): void {
E
Erich Gamma 已提交
87 88 89
		this._result = [];
	}

90
	computeAsync(token: CancellationToken): Promise<HoverPart[]> {
A
Alex Dima 已提交
91 92 93 94
		if (!this._editor.hasModel() || !this._range) {
			return Promise.resolve([]);
		}

J
Joao Moreno 已提交
95
		const model = this._editor.getModel();
E
Erich Gamma 已提交
96

A
Alex Dima 已提交
97
		if (!HoverProviderRegistry.has(model)) {
A
Alex Dima 已提交
98
			return Promise.resolve([]);
E
Erich Gamma 已提交
99 100
		}

101
		return getHover(model, new Position(
102 103
			this._range.startLineNumber,
			this._range.startColumn
104
		), token);
E
Erich Gamma 已提交
105 106
	}

107
	computeSync(): HoverPart[] {
A
Alex Dima 已提交
108 109 110 111
		if (!this._editor.hasModel() || !this._range) {
			return [];
		}

112
		const model = this._editor.getModel();
J
Joao Moreno 已提交
113
		const lineNumber = this._range.startLineNumber;
E
Erich Gamma 已提交
114 115 116

		if (lineNumber > this._editor.getModel().getLineCount()) {
			// Illegal line number => no results
J
Joao Moreno 已提交
117
			return [];
E
Erich Gamma 已提交
118 119
		}

120
		const colorDetector = ColorDetector.get(this._editor);
121
		const maxColumn = model.getLineMaxColumn(lineNumber);
J
Joao Moreno 已提交
122
		const lineDecorations = this._editor.getLineDecorations(lineNumber);
123
		let didFindColor = false;
J
Joao Moreno 已提交
124

125 126
		const hoverRange = this._range;
		const result = lineDecorations.map((d): HoverPart | null => {
J
Joao Moreno 已提交
127 128 129
			const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1;
			const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn;

130
			if (startColumn > hoverRange.startColumn || hoverRange.endColumn > endColumn) {
J
Joao Moreno 已提交
131 132 133
				return null;
			}

134
			const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn);
135 136 137 138 139
			const marker = this._markerDecorationsService.getMarker(model, d);
			if (marker) {
				return new MarkerHover(range, marker);
			}

140
			const colorData = colorDetector.getColorData(d.range.getStartPosition());
J
Joao Moreno 已提交
141

142
			if (!didFindColor && colorData) {
143
				didFindColor = true;
144

145 146
				const { color, range } = colorData.colorInfo;
				return new ColorHover(range, color, colorData.provider);
147
			} else {
148
				if (isEmptyMarkdownString(d.options.hoverMessage)) {
149
					return null;
E
Erich Gamma 已提交
150
				}
J
Joao Moreno 已提交
151

M
Matt Bierner 已提交
152
				const contents: IMarkdownString[] = d.options.hoverMessage ? asArray(d.options.hoverMessage) : [];
153 154
				return { contents, range };
			}
E
Erich Gamma 已提交
155
		});
J
Joao Moreno 已提交
156

157
		return coalesce(result);
E
Erich Gamma 已提交
158 159
	}

160
	onResult(result: HoverPart[], isFromSynchronousComputation: boolean): void {
E
Erich Gamma 已提交
161 162
		// Always put synchronous messages before asynchronous ones
		if (isFromSynchronousComputation) {
163
			this._result = result.concat(this._result.sort((a, b) => {
164
				if (a instanceof ColorHover) { // sort picker messages at to the top
165
					return -1;
166
				} else if (b instanceof ColorHover) {
167 168 169 170
					return 1;
				}
				return 0;
			}));
E
Erich Gamma 已提交
171 172 173 174 175
		} else {
			this._result = this._result.concat(result);
		}
	}

176
	getResult(): HoverPart[] {
E
Erich Gamma 已提交
177 178 179
		return this._result.slice(0);
	}

180
	getResultWithLoadingMessage(): HoverPart[] {
E
Erich Gamma 已提交
181 182 183
		return this._result.slice(0).concat([this._getLoadingMessage()]);
	}

184
	private _getLoadingMessage(): HoverPart {
E
Erich Gamma 已提交
185
		return {
M
Matt Bierner 已提交
186
			range: this._range,
187
			contents: [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))]
E
Erich Gamma 已提交
188 189 190 191
		};
	}
}

192 193 194 195 196
const markerCodeActionTrigger: CodeActionTrigger = {
	type: CodeActionTriggerType.Manual,
	filter: { include: CodeActionKind.QuickFix }
};

A
Alex Dima 已提交
197
export class ModesContentHoverWidget extends ContentHoverWidget {
E
Erich Gamma 已提交
198

199
	static readonly ID = 'editor.contrib.modesContentHoverWidget';
J
Joao Moreno 已提交
200

201
	private _messages: HoverPart[];
A
Alex Dima 已提交
202
	private _lastRange: Range | null;
203 204
	private readonly _computer: ModesContentComputer;
	private readonly _hoverOperation: HoverOperation<HoverPart[]>;
205
	private _highlightDecorations: string[];
E
Erich Gamma 已提交
206
	private _isChangingDecorations: boolean;
I
isidor 已提交
207
	private _shouldFocus: boolean;
A
Alex Dima 已提交
208
	private _colorPicker: ColorPickerWidget | null;
209

210 211
	private _codeLink?: HTMLElement;

212
	private readonly renderDisposable = this._register(new MutableDisposable<IDisposable>());
E
Erich Gamma 已提交
213

A
Alex Dima 已提交
214 215
	constructor(
		editor: ICodeEditor,
R
Rob Lourens 已提交
216
		_hoverVisibleKey: IContextKey<boolean>,
217
		markerDecorationsService: IMarkerDecorationsService,
218
		keybindingService: IKeybindingService,
S
Sandeep Somavarapu 已提交
219
		private readonly _themeService: IThemeService,
220
		private readonly _modeService: IModeService,
221
		private readonly _openerService: IOpenerService = NullOpenerService,
A
Alex Dima 已提交
222
	) {
R
Rob Lourens 已提交
223
		super(ModesContentHoverWidget.ID, editor, _hoverVisibleKey, keybindingService);
E
Erich Gamma 已提交
224

A
Alex Dima 已提交
225 226
		this._messages = [];
		this._lastRange = null;
227
		this._computer = new ModesContentComputer(this._editor, markerDecorationsService);
E
Erich Gamma 已提交
228 229
		this._highlightDecorations = [];
		this._isChangingDecorations = false;
P
Peng Lyu 已提交
230 231
		this._shouldFocus = false;
		this._colorPicker = null;
J
Joao Moreno 已提交
232

A
Alex Dima 已提交
233
		this._hoverOperation = new HoverOperation(
E
Erich Gamma 已提交
234
			this._computer,
235
			result => this._withResult(result, true),
E
Erich Gamma 已提交
236
			null,
A
Alex Dima 已提交
237
			result => this._withResult(result, false),
A
Alex Dima 已提交
238
			this._editor.getOption(EditorOption.hover).delay
E
Erich Gamma 已提交
239
		);
M
Michel Kaporin 已提交
240

B
Benjamin Pasero 已提交
241
		this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.FOCUS, () => {
M
Michel Kaporin 已提交
242
			if (this._colorPicker) {
243
				this.getDomNode().classList.add('colorpicker-hover');
M
Michel Kaporin 已提交
244 245
			}
		}));
B
Benjamin Pasero 已提交
246
		this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => {
247
			this.getDomNode().classList.remove('colorpicker-hover');
M
Michel Kaporin 已提交
248
		}));
B
Benjamin Pasero 已提交
249
		this._register(editor.onDidChangeConfiguration((e) => {
A
Alex Dima 已提交
250
			this._hoverOperation.setHoverTime(this._editor.getOption(EditorOption.hover).delay);
251
		}));
A
Alexandru Dima 已提交
252 253
		this._register(TokenizationRegistry.onDidChange((e) => {
			if (this.isVisible && this._lastRange && this._messages.length > 0) {
S
SteVen Batten 已提交
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
				this._messages = this._messages.map(msg => {
					// If a color hover is visible, we need to update the message that
					// created it so that the color matches the last chosen color
					if (msg instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.range) && this._colorPicker?.model.color) {
						const color = this._colorPicker.model.color;
						const newColor = {
							red: color.rgba.r / 255,
							green: color.rgba.g / 255,
							blue: color.rgba.b / 255,
							alpha: color.rgba.a
						};
						return new ColorHover(msg.range, newColor, msg.provider);
					} else {
						return msg;
					}
				});

271
				this._hover.contentsDomNode.textContent = '';
A
Alexandru Dima 已提交
272 273 274
				this._renderMessages(this._lastRange, this._messages);
			}
		}));
E
Erich Gamma 已提交
275 276
	}

J
Joao Moreno 已提交
277
	dispose(): void {
278 279 280 281
		this._hoverOperation.cancel();
		super.dispose();
	}

J
Joao Moreno 已提交
282
	onModelDecorationsChanged(): void {
E
Erich Gamma 已提交
283 284 285
		if (this._isChangingDecorations) {
			return;
		}
J
Joao Moreno 已提交
286
		if (this.isVisible) {
E
Erich Gamma 已提交
287 288 289 290
			// The decorations have changed and the hover is visible,
			// we need to recompute the displayed text
			this._hoverOperation.cancel();
			this._computer.clearResult();
291 292

			if (!this._colorPicker) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place
293
				this._hoverOperation.start(HoverStartMode.Delayed);
294
			}
E
Erich Gamma 已提交
295 296 297
		}
	}

298
	startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void {
J
Joao Moreno 已提交
299 300 301
		if (this._lastRange && this._lastRange.equalsRange(range)) {
			// We have to show the widget at the exact same range as before, so no work is needed
			return;
E
Erich Gamma 已提交
302 303 304 305
		}

		this._hoverOperation.cancel();

J
Joao Moreno 已提交
306
		if (this.isVisible) {
E
Erich Gamma 已提交
307 308 309
			// The range might have changed, but the hover is visible
			// Instead of hiding it completely, filter out messages that are still in the new range and
			// kick off a new computation
310
			if (!this._showAtPosition || this._showAtPosition.lineNumber !== range.startLineNumber) {
E
Erich Gamma 已提交
311 312
				this.hide();
			} else {
A
Alex Dima 已提交
313 314 315 316
				let filteredMessages: HoverPart[] = [];
				for (let i = 0, len = this._messages.length; i < len; i++) {
					const msg = this._messages[i];
					const rng = msg.range;
317
					if (rng && rng.startColumn <= range.startColumn && rng.endColumn >= range.endColumn) {
E
Erich Gamma 已提交
318 319 320 321
						filteredMessages.push(msg);
					}
				}
				if (filteredMessages.length > 0) {
322 323 324
					if (hoverContentsEquals(filteredMessages, this._messages)) {
						return;
					}
E
Erich Gamma 已提交
325 326 327 328 329 330 331 332 333
					this._renderMessages(range, filteredMessages);
				} else {
					this.hide();
				}
			}
		}

		this._lastRange = range;
		this._computer.setRange(range);
I
isidor 已提交
334
		this._shouldFocus = focus;
335
		this._hoverOperation.start(mode);
E
Erich Gamma 已提交
336 337
	}

J
Joao Moreno 已提交
338
	hide(): void {
E
Erich Gamma 已提交
339 340 341 342 343 344
		this._lastRange = null;
		this._hoverOperation.cancel();
		super.hide();
		this._isChangingDecorations = true;
		this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []);
		this._isChangingDecorations = false;
345
		this.renderDisposable.clear();
346
		this._colorPicker = null;
E
Erich Gamma 已提交
347 348
	}

349 350 351 352 353 354 355
	isColorPickerVisible(): boolean {
		if (this._colorPicker) {
			return true;
		}
		return false;
	}

356
	private _withResult(result: HoverPart[], complete: boolean): void {
E
Erich Gamma 已提交
357 358
		this._messages = result;

A
Alex Dima 已提交
359
		if (this._lastRange && this._messages.length > 0) {
E
Erich Gamma 已提交
360
			this._renderMessages(this._lastRange, this._messages);
361
		} else if (complete) {
E
Erich Gamma 已提交
362 363 364 365
			this.hide();
		}
	}

366
	private _renderMessages(renderRange: Range, messages: HoverPart[]): void {
367 368
		this.renderDisposable.dispose();
		this._colorPicker = null;
E
Erich Gamma 已提交
369 370

		// update column from which to show
371
		let renderColumn = Constants.MAX_SAFE_SMALL_INTEGER;
A
Alex Dima 已提交
372
		let highlightRange: Range | null = messages[0].range ? Range.lift(messages[0].range) : null;
A
Alex Dima 已提交
373 374
		let fragment = document.createDocumentFragment();
		let isEmptyHoverContent = true;
E
Erich Gamma 已提交
375

376
		let containColorPicker = false;
377
		const markdownDisposeables = new DisposableStore();
378
		const markerMessages: MarkerHover[] = [];
E
Erich Gamma 已提交
379 380 381 382 383 384
		messages.forEach((msg) => {
			if (!msg.range) {
				return;
			}

			renderColumn = Math.min(renderColumn, msg.range.startColumn);
A
Alex Dima 已提交
385
			highlightRange = highlightRange ? Range.plusRange(highlightRange, msg.range) : Range.lift(msg.range);
E
Erich Gamma 已提交
386

387
			if (msg instanceof ColorHover) {
388 389
				containColorPicker = true;

J
Joao Moreno 已提交
390
				const { red, green, blue, alpha } = msg.color;
N
NeeEoo 已提交
391
				const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha);
J
Joao Moreno 已提交
392
				const color = new Color(rgba);
393

394 395 396 397
				if (!this._editor.hasModel()) {
					return;
				}

398 399
				const editorModel = this._editor.getModel();
				let range = new Range(msg.range.startLineNumber, msg.range.startColumn, msg.range.endLineNumber, msg.range.endColumn);
400
				let colorInfo = { range: msg.range, color: msg.color };
401

402 403
				// create blank olor picker model and widget first to ensure it's positioned correctly.
				const model = new ColorPickerModel(color, [], 0);
A
Alex Dima 已提交
404
				const widget = new ColorPickerWidget(fragment, model, this._editor.getOption(EditorOption.pixelRatio), this._themeService);
405

406
				getColorPresentations(editorModel, colorInfo, msg.provider, CancellationToken.None).then(colorPresentations => {
A
Alex Dima 已提交
407 408 409 410 411
					model.colorPresentations = colorPresentations || [];
					if (!this._editor.hasModel()) {
						// gone...
						return;
					}
412 413 414 415
					const originalText = this._editor.getModel().getValueInRange(msg.range);
					model.guessColorPresentation(color, originalText);

					const updateEditorModel = () => {
M
Matt Bierner 已提交
416 417
						let textEdits: IIdentifiedSingleEditOperation[];
						let newRange: Range;
418
						if (model.presentation.textEdit) {
M
Matt Bierner 已提交
419
							textEdits = [model.presentation.textEdit as IIdentifiedSingleEditOperation];
420 421 422 423 424 425
							newRange = new Range(
								model.presentation.textEdit.range.startLineNumber,
								model.presentation.textEdit.range.startColumn,
								model.presentation.textEdit.range.endLineNumber,
								model.presentation.textEdit.range.endColumn
							);
R
rebornix 已提交
426 427 428 429
							const trackedRange = this._editor.getModel()!._setTrackedRange(null, newRange, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter);
							this._editor.pushUndoStop();
							this._editor.executeEdits('colorpicker', textEdits);
							newRange = this._editor.getModel()!._getTrackedRange(trackedRange) || newRange;
430 431 432
						} else {
							textEdits = [{ identifier: null, range, text: model.presentation.label, forceMoveMarkers: false }];
							newRange = range.setEndPosition(range.endLineNumber, range.startColumn + model.presentation.label.length);
R
rebornix 已提交
433 434
							this._editor.pushUndoStop();
							this._editor.executeEdits('colorpicker', textEdits);
435 436 437
						}

						if (model.presentation.additionalTextEdits) {
M
Matt Bierner 已提交
438
							textEdits = [...model.presentation.additionalTextEdits as IIdentifiedSingleEditOperation[]];
A
Alex Dima 已提交
439
							this._editor.executeEdits('colorpicker', textEdits);
440
							this.hide();
E
Erich Gamma 已提交
441
						}
442
						this._editor.pushUndoStop();
443 444 445 446
						range = newRange;
					};

					const updateColorPresentations = (color: Color) => {
447
						return getColorPresentations(editorModel, {
448 449 450 451 452 453 454
							range: range,
							color: {
								red: color.rgba.r / 255,
								green: color.rgba.g / 255,
								blue: color.rgba.b / 255,
								alpha: color.rgba.a
							}
455
						}, msg.provider, CancellationToken.None).then((colorPresentations) => {
A
Alex Dima 已提交
456
							model.colorPresentations = colorPresentations || [];
457 458 459 460 461
						});
					};

					const colorListener = model.onColorFlushed((color: Color) => {
						updateColorPresentations(color).then(updateEditorModel);
J
Joao Moreno 已提交
462
					});
463
					const colorChangeListener = model.onDidChangeColor(updateColorPresentations);
464

465
					this._colorPicker = widget;
A
Alex Dima 已提交
466
					this.showAt(range.getStartPosition(), range, this._shouldFocus);
467 468
					this.updateContents(fragment);
					this._colorPicker.layout();
469

470
					this.renderDisposable.value = combinedDisposable(colorListener, colorChangeListener, widget, markdownDisposeables);
J
Joao Moreno 已提交
471
				});
472 473
			} else {
				if (msg instanceof MarkerHover) {
474
					markerMessages.push(msg);
475 476 477 478 479
					isEmptyHoverContent = false;
				} else {
					msg.contents
						.filter(contents => !isEmptyMarkdownString(contents))
						.forEach(contents => {
480 481
							const markdownHoverElement = $('div.hover-row.markdown-hover');
							const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents'));
482
							const renderer = markdownDisposeables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService));
483
							markdownDisposeables.add(renderer.onDidRenderAsync(() => {
S
Sandeep Somavarapu 已提交
484
								hoverContentsElement.className = 'hover-contents code-hover-contents';
485
								this._hover.onContentsChanged();
S
Sandeep Somavarapu 已提交
486
							}));
487
							const renderedContents = markdownDisposeables.add(renderer.render(contents));
488 489
							hoverContentsElement.appendChild(renderedContents.element);
							fragment.appendChild(markdownHoverElement);
490 491 492
							isEmptyHoverContent = false;
						});
				}
J
Joao Moreno 已提交
493
			}
E
Erich Gamma 已提交
494 495
		});

S
Sandeep Somavarapu 已提交
496 497
		if (markerMessages.length) {
			markerMessages.forEach(msg => fragment.appendChild(this.renderMarkerHover(msg)));
S
Sandeep Somavarapu 已提交
498 499
			const markerHoverForStatusbar = markerMessages.length === 1 ? markerMessages[0] : markerMessages.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0];
			fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar));
S
Sandeep Somavarapu 已提交
500
		}
501

E
Erich Gamma 已提交
502 503
		// show

504
		if (!containColorPicker && !isEmptyHoverContent) {
A
Alex Dima 已提交
505
			this.showAt(new Position(renderRange.startLineNumber, renderColumn), highlightRange, this._shouldFocus);
506
			this.updateContents(fragment);
M
Michel Kaporin 已提交
507
		}
J
Joao Moreno 已提交
508

E
Erich Gamma 已提交
509
		this._isChangingDecorations = true;
A
Alex Dima 已提交
510
		this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, highlightRange ? [{
E
Erich Gamma 已提交
511
			range: highlightRange,
512
			options: ModesContentHoverWidget._DECORATION_OPTIONS
A
Alex Dima 已提交
513
		}] : []);
E
Erich Gamma 已提交
514 515
		this._isChangingDecorations = false;
	}
516

517
	private renderMarkerHover(markerHover: MarkerHover): HTMLElement {
518 519
		const hoverElement = $('div.hover-row');
		const markerElement = dom.append(hoverElement, $('div.marker.hover-contents'));
520 521
		const { source, message, code, relatedInformation } = markerHover.marker;

S
Sandeep Somavarapu 已提交
522
		this._editor.applyFontInfo(markerElement);
523
		const messageElement = dom.append(markerElement, $('span'));
524
		messageElement.style.whiteSpace = 'pre-wrap';
S
Sandeep Somavarapu 已提交
525
		messageElement.innerText = message;
526 527

		if (source || code) {
S
Sandeep Somavarapu 已提交
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
			// Code has link
			if (code && typeof code !== 'string') {
				const sourceAndCodeElement = $('span');
				if (source) {
					const sourceElement = dom.append(sourceAndCodeElement, $('span'));
					sourceElement.innerText = source;
				}
				this._codeLink = dom.append(sourceAndCodeElement, $('a.code-link'));
				this._codeLink.setAttribute('href', code.target.toString());

				this._codeLink.onclick = (e) => {
					this._openerService.open(code.target);
					e.preventDefault();
					e.stopPropagation();
				};

				const codeElement = dom.append(this._codeLink, $('span'));
				codeElement.innerText = code.value;

				const detailsElement = dom.append(markerElement, sourceAndCodeElement);
				detailsElement.style.opacity = '0.6';
				detailsElement.style.paddingLeft = '6px';
			} else {
551 552 553 554 555
				const detailsElement = dom.append(markerElement, $('span'));
				detailsElement.style.opacity = '0.6';
				detailsElement.style.paddingLeft = '6px';
				detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`;
			}
556 557 558 559
		}

		if (isNonEmptyArray(relatedInformation)) {
			for (const { message, resource, startLineNumber, startColumn } of relatedInformation) {
560 561 562
				const relatedInfoContainer = dom.append(markerElement, $('div'));
				relatedInfoContainer.style.marginTop = '8px';
				const a = dom.append(relatedInfoContainer, $('a'));
S
Sandeep Somavarapu 已提交
563
				a.innerText = `${basename(resource)}(${startLineNumber}, ${startColumn}): `;
S
Sandeep Somavarapu 已提交
564 565 566 567
				a.style.cursor = 'pointer';
				a.onclick = e => {
					e.stopPropagation();
					e.preventDefault();
S
Sandeep Somavarapu 已提交
568
					if (this._openerService) {
569
						this._openerService.open(resource.with({ fragment: `${startLineNumber},${startColumn}` }), { fromUserGesture: true }).catch(onUnexpectedError);
S
Sandeep Somavarapu 已提交
570
					}
S
Sandeep Somavarapu 已提交
571
				};
572
				const messageElement = dom.append<HTMLAnchorElement>(relatedInfoContainer, $('span'));
S
Sandeep Somavarapu 已提交
573 574
				messageElement.innerText = message;
				this._editor.applyFontInfo(messageElement);
575 576
			}
		}
577

S
Sandeep Somavarapu 已提交
578 579 580
		return hoverElement;
	}

S
Sandeep Somavarapu 已提交
581
	private recentMarkerCodeActionsInfo: { marker: IMarker, hasCodeActions: boolean } | undefined = undefined;
S
Sandeep Somavarapu 已提交
582 583
	private renderMarkerStatusbar(markerHover: MarkerHover): HTMLElement {
		const hoverElement = $('div.hover-row.status-bar');
584
		const disposables = new DisposableStore();
585
		const actionsElement = dom.append(hoverElement, $('div.actions'));
S
Sandeep Somavarapu 已提交
586
		if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) {
587
			disposables.add(this._renderAction(actionsElement, {
S
Sandeep Somavarapu 已提交
588
				label: nls.localize('peek problem', "Peek Problem"),
S
Sandeep Somavarapu 已提交
589 590 591
				commandId: NextMarkerAction.ID,
				run: () => {
					this.hide();
592
					MarkerController.get(this._editor).showAtMarker(markerHover.marker);
S
Sandeep Somavarapu 已提交
593 594 595 596
					this._editor.focus();
				}
			}));
		}
597

598 599
		if (!this._editor.getOption(EditorOption.readOnly)) {
			const quickfixPlaceholderElement = dom.append(actionsElement, $('div'));
S
Sandeep Somavarapu 已提交
600 601 602 603 604 605 606 607 608
			if (this.recentMarkerCodeActionsInfo) {
				if (IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker) === IMarkerData.makeKey(markerHover.marker)) {
					if (!this.recentMarkerCodeActionsInfo.hasCodeActions) {
						quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available");
					}
				} else {
					this.recentMarkerCodeActionsInfo = undefined;
				}
			}
609
			const updatePlaceholderDisposable = this.recentMarkerCodeActionsInfo && !this.recentMarkerCodeActionsInfo.hasCodeActions ? Disposable.None : disposables.add(disposableTimeout(() => quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."), 200));
A
Andrew Jones 已提交
610
			if (!quickfixPlaceholderElement.textContent) {
611 612
				// Have some content in here to avoid flickering
				quickfixPlaceholderElement.textContent = String.fromCharCode(0xA0); // &nbsp;
A
Andrew Jones 已提交
613
			}
614 615 616
			const codeActionsPromise = this.getCodeActions(markerHover.marker);
			disposables.add(toDisposable(() => codeActionsPromise.cancel()));
			codeActionsPromise.then(actions => {
617
				updatePlaceholderDisposable.dispose();
S
Sandeep Somavarapu 已提交
618
				this.recentMarkerCodeActionsInfo = { marker: markerHover.marker, hasCodeActions: actions.validActions.length > 0 };
619

S
Sandeep Somavarapu 已提交
620
				if (!this.recentMarkerCodeActionsInfo.hasCodeActions) {
621
					actions.dispose();
622 623
					quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available");
					return;
624
				}
S
Sandeep Somavarapu 已提交
625
				quickfixPlaceholderElement.style.display = 'none';
626

627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
				let showing = false;
				disposables.add(toDisposable(() => {
					if (!showing) {
						actions.dispose();
					}
				}));

				disposables.add(this._renderAction(actionsElement, {
					label: nls.localize('quick fixes', "Quick Fix..."),
					commandId: QuickFixAction.Id,
					run: (target) => {
						showing = true;
						const controller = QuickFixController.get(this._editor);
						const elementPosition = dom.getDomNodePagePosition(target);
						// Hide the hover pre-emptively, otherwise the editor can close the code actions
						// context menu as well when using keyboard navigation
						this.hide();
						controller.showCodeActions(markerCodeActionTrigger, actions, {
							x: elementPosition.left + 6,
							y: elementPosition.top + elementPosition.height + 6
						});
					}
				}));
			});
		}
652

653
		this.renderDisposable.value = disposables;
654 655 656
		return hoverElement;
	}

657
	private getCodeActions(marker: IMarker): CancelablePromise<CodeActionSet> {
658 659
		return createCancelablePromise(cancellationToken => {
			return getCodeActions(
660 661
				this._editor.getModel()!,
				new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn),
662
				markerCodeActionTrigger,
663
				Progress.None,
664
				cancellationToken);
665 666 667
		});
	}

668
	private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({
669 670 671
		className: 'hoverHighlight'
	});
}
672

673
function hoverContentsEquals(first: HoverPart[], second: HoverPart[]): boolean {
674 675 676 677
	if ((!first && second) || (first && !second) || first.length !== second.length) {
		return false;
	}
	for (let i = 0; i < first.length; i++) {
678 679
		const firstElement = first[i];
		const secondElement = second[i];
680 681 682 683
		if (firstElement instanceof MarkerHover && secondElement instanceof MarkerHover) {
			return IMarkerData.makeKey(firstElement.marker) === IMarkerData.makeKey(secondElement.marker);
		}
		if (firstElement instanceof ColorHover || secondElement instanceof ColorHover) {
684 685
			return false;
		}
686
		if (firstElement instanceof MarkerHover || secondElement instanceof MarkerHover) {
687 688 689
			return false;
		}
		if (!markedStringsEquals(firstElement.contents, secondElement.contents)) {
690 691 692 693 694
			return false;
		}
	}
	return true;
}
695 696 697 698

registerThemingParticipant((theme, collector) => {
	const linkFg = theme.getColor(textLinkForeground);
	if (linkFg) {
699
		collector.addRule(`.monaco-hover .hover-contents a.code-link span:hover { color: ${linkFg}; }`);
700 701
	}
});