gotoErrorWidget.ts 14.9 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.
 *--------------------------------------------------------------------------------------------*/

6
import 'vs/css!./media/gotoErrorWidget';
7 8
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
9
import { dispose, DisposableStore } from 'vs/base/common/lifecycle';
10
import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers';
11 12 13
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
14
import { registerColor, oneOf, textLinkForeground, editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder, editorInfoForeground, editorInfoBorder } from 'vs/platform/theme/common/colorRegistry';
15
import { IThemeService, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
16 17 18 19
import { Color } from 'vs/base/common/color';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { ScrollType } from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
20
import { getBaseLabel, getPathLabel } from 'vs/base/common/labels';
21
import { isNonEmptyArray } from 'vs/base/common/arrays';
22
import { Event, Emitter } from 'vs/base/common/event';
23
import { PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/peekView';
24 25 26
import { basename } from 'vs/base/common/resources';
import { IAction } from 'vs/base/common/actions';
import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
27
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
A
Alex Dima 已提交
28
import { EditorOption } from 'vs/editor/common/config/editorOptions';
29
import { IOpenerService } from 'vs/platform/opener/common/opener';
30

31
class MessageWidget {
32

J
Johannes Rieken 已提交
33 34
	private _lines: number = 0;
	private _longestLineLength: number = 0;
35 36

	private readonly _editor: ICodeEditor;
J
Johannes Rieken 已提交
37 38
	private readonly _messageBlock: HTMLDivElement;
	private readonly _relatedBlock: HTMLDivElement;
39
	private readonly _scrollable: ScrollableElement;
40
	private readonly _relatedDiagnostics = new WeakMap<HTMLElement, IRelatedInformation>();
41
	private readonly _disposables: DisposableStore = new DisposableStore();
42

43 44 45 46 47 48 49 50
	private _codeLink?: HTMLElement;

	constructor(
		parent: HTMLElement,
		editor: ICodeEditor,
		onRelatedInformation: (related: IRelatedInformation) => void,
		private readonly _openerService: IOpenerService,
	) {
51 52
		this._editor = editor;

53 54 55
		const domNode = document.createElement('div');
		domNode.className = 'descriptioncontainer';

J
Johannes Rieken 已提交
56
		this._messageBlock = document.createElement('div');
57
		dom.addClass(this._messageBlock, 'message');
S
Sandeep Somavarapu 已提交
58 59
		this._messageBlock.setAttribute('aria-live', 'assertive');
		this._messageBlock.setAttribute('role', 'alert');
60 61 62 63
		domNode.appendChild(this._messageBlock);

		this._relatedBlock = document.createElement('div');
		domNode.appendChild(this._relatedBlock);
64
		this._disposables.add(dom.addStandardDisposableListener(this._relatedBlock, 'click', event => {
65 66 67
			event.preventDefault();
			const related = this._relatedDiagnostics.get(event.target);
			if (related) {
68
				onRelatedInformation(related);
69 70
			}
		}));
71

72
		this._scrollable = new ScrollableElement(domNode, {
73
			horizontal: ScrollbarVisibility.Auto,
J
Johannes Rieken 已提交
74
			vertical: ScrollbarVisibility.Auto,
75
			useShadows: false,
J
Johannes Rieken 已提交
76 77
			horizontalScrollbarSize: 3,
			verticalScrollbarSize: 3
78 79
		});
		parent.appendChild(this._scrollable.getDomNode());
80
		this._disposables.add(this._scrollable.onScroll(e => {
J
Johannes Rieken 已提交
81 82 83
			domNode.style.left = `-${e.scrollLeft}px`;
			domNode.style.top = `-${e.scrollTop}px`;
		}));
84
		this._disposables.add(this._scrollable);
85 86 87 88
	}

	dispose(): void {
		dispose(this._disposables);
89 90
	}

S
Sandeep Somavarapu 已提交
91 92
	update(marker: IMarker): void {
		const { source, message, relatedInformation, code } = marker;
93 94 95 96 97 98 99 100
		let sourceAndCodeLength = (source?.length || 0) + '()'.length;
		if (code) {
			if (typeof code === 'string') {
				sourceAndCodeLength += code.length;
			} else {
				sourceAndCodeLength += code.value.length;
			}
		}
101

102 103 104 105
		const lines = message.split(/\r\n|\r|\n/g);
		this._lines = lines.length;
		this._longestLineLength = 0;
		for (const line of lines) {
106
			this._longestLineLength = Math.max(line.length + sourceAndCodeLength, this._longestLineLength);
107 108 109
		}

		dom.clearNode(this._messageBlock);
S
Sandeep Somavarapu 已提交
110
		this._messageBlock.setAttribute('aria-label', this.getAriaLabel(marker));
S
Sandeep Somavarapu 已提交
111
		this._editor.applyFontInfo(this._messageBlock);
112 113 114 115
		let lastLineElement = this._messageBlock;
		for (const line of lines) {
			lastLineElement = document.createElement('div');
			lastLineElement.innerText = line;
S
Sandeep Somavarapu 已提交
116
			if (line === '') {
S
Sandeep Somavarapu 已提交
117
				lastLineElement.style.height = this._messageBlock.style.lineHeight;
S
Sandeep Somavarapu 已提交
118
			}
119 120 121 122 123 124 125 126 127 128 129 130 131
			this._messageBlock.appendChild(lastLineElement);
		}
		if (source || code) {
			const detailsElement = document.createElement('span');
			dom.addClass(detailsElement, 'details');
			lastLineElement.appendChild(detailsElement);
			if (source) {
				const sourceElement = document.createElement('span');
				sourceElement.innerText = source;
				dom.addClass(sourceElement, 'source');
				detailsElement.appendChild(sourceElement);
			}
			if (code) {
132 133 134 135 136 137 138
				if (typeof code === 'string') {
					const codeElement = document.createElement('span');
					codeElement.innerText = `(${code})`;
					dom.addClass(codeElement, 'code');
					detailsElement.appendChild(codeElement);
				} else {
					this._codeLink = dom.$('a.code-link');
P
Pine Wu 已提交
139
					this._codeLink.setAttribute('href', `${code.target.toString()}`);
140 141

					this._codeLink.onclick = (e) => {
P
Pine Wu 已提交
142
						this._openerService.open(code.target);
143
						e.preventDefault();
144
						e.stopPropagation();
145 146 147 148 149 150
					};

					const codeElement = dom.append(this._codeLink, dom.$('span'));
					codeElement.innerText = code.value;
					detailsElement.appendChild(this._codeLink);
				}
151 152 153
			}
		}

154
		dom.clearNode(this._relatedBlock);
S
Sandeep Somavarapu 已提交
155
		this._editor.applyFontInfo(this._relatedBlock);
156
		if (isNonEmptyArray(relatedInformation)) {
S
Sandeep Somavarapu 已提交
157
			const relatedInformationNode = this._relatedBlock.appendChild(document.createElement('div'));
A
Alex Dima 已提交
158
			relatedInformationNode.style.paddingTop = `${Math.floor(this._editor.getOption(EditorOption.lineHeight) * 0.66)}px`;
J
Johannes Rieken 已提交
159
			this._lines += 1;
160

161
			for (const related of relatedInformation) {
162 163 164

				let container = document.createElement('div');

165
				let relatedResource = document.createElement('a');
166
				dom.addClass(relatedResource, 'filename');
167
				relatedResource.innerHTML = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `;
168
				relatedResource.title = getPathLabel(related.resource, undefined);
169 170 171 172 173 174 175 176
				this._relatedDiagnostics.set(relatedResource, related);

				let relatedMessage = document.createElement('span');
				relatedMessage.innerText = related.message;

				container.appendChild(relatedResource);
				container.appendChild(relatedMessage);

J
Johannes Rieken 已提交
177
				this._lines += 1;
S
Sandeep Somavarapu 已提交
178
				relatedInformationNode.appendChild(container);
179 180 181
			}
		}

182
		const fontInfo = this._editor.getOption(EditorOption.fontInfo);
J
Johannes Rieken 已提交
183 184 185
		const scrollWidth = Math.ceil(fontInfo.typicalFullwidthCharacterWidth * this._longestLineLength * 0.75);
		const scrollHeight = fontInfo.lineHeight * this._lines;
		this._scrollable.setScrollDimensions({ scrollWidth, scrollHeight });
186 187 188
	}

	layout(height: number, width: number): void {
J
Johannes Rieken 已提交
189
		this._scrollable.getDomNode().style.height = `${height}px`;
S
Sandeep Somavarapu 已提交
190
		this._scrollable.getDomNode().style.width = `${width}px`;
J
Johannes Rieken 已提交
191 192 193 194 195
		this._scrollable.setScrollDimensions({ width, height });
	}

	getHeightInLines(): number {
		return Math.min(17, this._lines);
196
	}
S
Sandeep Somavarapu 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

	private getAriaLabel(marker: IMarker): string {
		let severityLabel = '';
		switch (marker.severity) {
			case MarkerSeverity.Error:
				severityLabel = nls.localize('Error', "Error");
				break;
			case MarkerSeverity.Warning:
				severityLabel = nls.localize('Warning', "Warning");
				break;
			case MarkerSeverity.Info:
				severityLabel = nls.localize('Info', "Info");
				break;
			case MarkerSeverity.Hint:
				severityLabel = nls.localize('Hint', "Hint");
				break;
		}

		let ariaLabel = nls.localize('marker aria', "{0} at {1}. ", severityLabel, marker.startLineNumber + ':' + marker.startColumn);
		const model = this._editor.getModel();
		if (model && (marker.startLineNumber <= model.getLineCount()) && (marker.startLineNumber >= 1)) {
			const lineContent = model.getLineContent(marker.startLineNumber);
			ariaLabel = `${lineContent}, ${ariaLabel}`;
		}
		return ariaLabel;
	}
223 224
}

225
export class MarkerNavigationWidget extends PeekViewWidget {
226

J
Johannes Rieken 已提交
227 228 229 230
	private _parentContainer!: HTMLElement;
	private _container!: HTMLElement;
	private _icon!: HTMLElement;
	private _message!: MessageWidget;
231
	private readonly _callOnDispose = new DisposableStore();
J
Johannes Rieken 已提交
232
	private _severity: MarkerSeverity;
233
	private _backgroundColor?: Color;
234
	private readonly _onDidSelectRelatedInformation = new Emitter<IRelatedInformation>();
J
Johannes Rieken 已提交
235
	private _heightInPixel!: number;
236 237

	readonly onDidSelectRelatedInformation: Event<IRelatedInformation> = this._onDidSelectRelatedInformation.event;
238 239 240

	constructor(
		editor: ICodeEditor,
241
		private readonly actions: ReadonlyArray<IAction>,
242
		private readonly _themeService: IThemeService,
243
		private readonly _openerService: IOpenerService
244 245
	) {
		super(editor, { showArrow: true, showFrame: true, isAccessible: true });
J
Johannes Rieken 已提交
246
		this._severity = MarkerSeverity.Warning;
247 248 249
		this._backgroundColor = Color.white;

		this._applyTheme(_themeService.getTheme());
250
		this._callOnDispose.add(_themeService.onThemeChange(this._applyTheme.bind(this)));
251 252 253 254 255

		this.create();
	}

	private _applyTheme(theme: ITheme) {
256
		this._backgroundColor = theme.getColor(editorMarkerNavigationBackground);
257
		let colorId = editorMarkerNavigationError;
J
Johannes Rieken 已提交
258
		if (this._severity === MarkerSeverity.Warning) {
259
			colorId = editorMarkerNavigationWarning;
J
Johannes Rieken 已提交
260
		} else if (this._severity === MarkerSeverity.Info) {
261 262
			colorId = editorMarkerNavigationInfo;
		}
263
		const frameColor = theme.getColor(colorId);
264
		this.style({
265 266
			arrowColor: frameColor,
			frameColor: frameColor,
267 268 269
			headerBackgroundColor: this._backgroundColor,
			primaryHeadingColor: theme.getColor(peekViewTitleForeground),
			secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground)
270 271 272 273 274
		}); // style() will trigger _applyStyles
	}

	protected _applyStyles(): void {
		if (this._parentContainer) {
M
Matt Bierner 已提交
275
			this._parentContainer.style.backgroundColor = this._backgroundColor ? this._backgroundColor.toString() : '';
276 277 278 279 280
		}
		super._applyStyles();
	}

	dispose(): void {
281
		this._callOnDispose.dispose();
282 283 284 285 286 287 288
		super.dispose();
	}

	focus(): void {
		this._parentContainer.focus();
	}

289 290
	protected _fillHead(container: HTMLElement): void {
		super._fillHead(container);
291
		this._actionbarWidget!.push(this.actions, { label: false, icon: true, index: 0 });
292 293
	}

294
	protected _fillTitleIcon(container: HTMLElement): void {
295
		this._icon = dom.append(container, dom.$(''));
296 297
	}

298 299
	protected _getActionBarOptions(): IActionBarOptions {
		return {
300
			orientation: ActionsOrientation.HORIZONTAL
301 302 303 304
		};
	}

	protected _fillBody(container: HTMLElement): void {
305 306 307 308 309 310 311 312
		this._parentContainer = container;
		dom.addClass(container, 'marker-widget');
		this._parentContainer.tabIndex = 0;
		this._parentContainer.setAttribute('role', 'tooltip');

		this._container = document.createElement('div');
		container.appendChild(this._container);

313
		this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService);
M
Matt Bierner 已提交
314
		this._disposables.add(this._message);
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
	}

	show(where: Position, heightInLines: number): void {
		throw new Error('call showAtMarker');
	}

	showAtMarker(marker: IMarker, markerIdx: number, markerCount: number): void {
		// update:
		// * title
		// * message
		this._container.classList.remove('stale');
		this._message.update(marker);

		// update frame color (only applied on 'show')
		this._severity = marker.severity;
		this._applyTheme(this._themeService.getTheme());

		// show
		let range = Range.lift(marker);
M
Matt Bierner 已提交
334 335
		const editorPosition = this.editor.getPosition();
		let position = editorPosition && range.containsPosition(editorPosition) ? editorPosition : range.getStartPosition();
336 337
		super.show(position, this.computeRequiredHeight());

338 339
		const model = this.editor.getModel();
		if (model) {
340 341 342
			const detail = markerCount > 1
				? nls.localize('problems', "{0} of {1} problems", markerIdx, markerCount)
				: nls.localize('change', "{0} of {1} problem", markerIdx, markerCount);
343 344
			this.setTitle(basename(model.uri), detail);
		}
M
Miguel Solorio 已提交
345
		this._icon.className = `codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(this._severity))}`;
346

347
		this.editor.revealPositionNearTop(position, ScrollType.Smooth);
S
Sandeep Somavarapu 已提交
348
		this.editor.focus();
349 350 351 352 353 354 355 356 357 358 359 360
	}

	updateMarker(marker: IMarker): void {
		this._container.classList.remove('stale');
		this._message.update(marker);
	}

	showStale() {
		this._container.classList.add('stale');
		this._relayout();
	}

361 362
	protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void {
		super._doLayoutBody(heightInPixel, widthInPixel);
S
Sandeep Somavarapu 已提交
363
		this._heightInPixel = heightInPixel;
364
		this._message.layout(heightInPixel, widthInPixel);
365
		this._container.style.height = `${heightInPixel}px`;
366 367
	}

S
Sandeep Somavarapu 已提交
368 369 370 371
	public _onWidth(widthInPixel: number): void {
		this._message.layout(this._heightInPixel, widthInPixel);
	}

372 373 374 375 376
	protected _relayout(): void {
		super._relayout(this.computeRequiredHeight());
	}

	private computeRequiredHeight() {
377
		return 3 + this._message.getHeightInLines();
378 379 380 381 382
	}
}

// theming

383 384 385
let errorDefault = oneOf(editorErrorForeground, editorErrorBorder);
let warningDefault = oneOf(editorWarningForeground, editorWarningBorder);
let infoDefault = oneOf(editorInfoForeground, editorInfoBorder);
386 387 388 389 390

export const editorMarkerNavigationError = registerColor('editorMarkerNavigationError.background', { dark: errorDefault, light: errorDefault, hc: errorDefault }, nls.localize('editorMarkerNavigationError', 'Editor marker navigation widget error color.'));
export const editorMarkerNavigationWarning = registerColor('editorMarkerNavigationWarning.background', { dark: warningDefault, light: warningDefault, hc: warningDefault }, nls.localize('editorMarkerNavigationWarning', 'Editor marker navigation widget warning color.'));
export const editorMarkerNavigationInfo = registerColor('editorMarkerNavigationInfo.background', { dark: infoDefault, light: infoDefault, hc: infoDefault }, nls.localize('editorMarkerNavigationInfo', 'Editor marker navigation widget info color.'));
export const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: '#2D2D30', light: Color.white, hc: '#0C141F' }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.'));
391 392

registerThemingParticipant((theme, collector) => {
393 394 395 396
	const linkFg = theme.getColor(textLinkForeground);
	if (linkFg) {
		collector.addRule(`.monaco-editor .marker-widget a { color: ${linkFg}; }`);
		collector.addRule(`.monaco-editor .marker-widget a.code-link span:hover { color: ${linkFg}; }`);
397 398
	}
});