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

'use strict';

import 'vs/css!./gotoError';
A
Alex Dima 已提交
9
import * as nls from 'vs/nls';
A
Alex Dima 已提交
10 11 12
import {onUnexpectedError} from 'vs/base/common/errors';
import {Emitter} from 'vs/base/common/event';
import {CommonKeybindings, KeyCode, KeyMod} from 'vs/base/common/keyCodes';
A
Alex Dima 已提交
13
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
14
import Severity from 'vs/base/common/severity';
A
Alex Dima 已提交
15
import URI from 'vs/base/common/uri';
E
Erich Gamma 已提交
16
import {TPromise} from 'vs/base/common/winjs.base';
A
Alex Dima 已提交
17 18
import * as dom from 'vs/base/browser/dom';
import {renderHtml} from 'vs/base/browser/htmlContentRenderer';
19
import {ICommandService} from 'vs/platform/commands/common/commands';
A
Alex Dima 已提交
20
import {RawContextKey, IContextKey, IContextKeyService} from 'vs/platform/contextkey/common/contextkey';
A
Alex Dima 已提交
21 22 23
import {IMarker, IMarkerService} from 'vs/platform/markers/common/markers';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {Position} from 'vs/editor/common/core/position';
24
import {Range} from 'vs/editor/common/core/range';
A
Alex Dima 已提交
25
import * as editorCommon from 'vs/editor/common/editorCommon';
A
Alex Dima 已提交
26
import {editorAction, ServicesAccessor, IActionOptions, EditorAction, EditorCommand, CommonEditorRegistry} from 'vs/editor/common/editorCommonExtensions';
A
Alex Dima 已提交
27
import {ICodeEditor} from 'vs/editor/browser/editorBrowser';
28
import {editorContribution} from 'vs/editor/browser/editorBrowserExtensions';
29
import {ZoneWidget} from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
30
import {getCodeActions, IQuickFix2} from 'vs/editor/contrib/quickFix/common/quickFix';
E
Erich Gamma 已提交
31

A
Alex Dima 已提交
32
import EditorContextKeys = editorCommon.EditorContextKeys;
A
Alex Dima 已提交
33

E
Erich Gamma 已提交
34 35
class MarkerModel {

A
Alex Dima 已提交
36
	private _editor: ICodeEditor;
E
Erich Gamma 已提交
37 38
	private _markers: IMarker[];
	private _nextIdx: number;
A
Alex Dima 已提交
39
	private _toUnbind: IDisposable[];
E
Erich Gamma 已提交
40 41 42 43
	private _ignoreSelectionChange: boolean;
	private _onCurrentMarkerChanged: Emitter<IMarker>;
	private _onMarkerSetChanged: Emitter<MarkerModel>;

A
Alex Dima 已提交
44
	constructor(editor: ICodeEditor, markers: IMarker[]) {
E
Erich Gamma 已提交
45 46 47 48 49 50 51 52 53 54
		this._editor = editor;
		this._markers = null;
		this._nextIdx = -1;
		this._toUnbind = [];
		this._ignoreSelectionChange = false;
		this._onCurrentMarkerChanged = new Emitter<IMarker>();
		this._onMarkerSetChanged = new Emitter<MarkerModel>();
		this.setMarkers(markers);

		// listen on editor
A
Alex Dima 已提交
55
		this._toUnbind.push(this._editor.onDidDispose(() => this.dispose()));
A
Alex Dima 已提交
56
		this._toUnbind.push(this._editor.onDidChangeCursorPosition(() => {
E
Erich Gamma 已提交
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
			if (!this._ignoreSelectionChange) {
				this._nextIdx = -1;
			}
		}));
	}

	public get onCurrentMarkerChanged() {
		return this._onCurrentMarkerChanged.event;
	}

	public get onMarkerSetChanged() {
		return this._onMarkerSetChanged.event;
	}

	public setMarkers(markers: IMarker[]): void {
		// assign
		this._markers = markers || [];

		// sort markers
76
		this._markers.sort((left, right) => Severity.compare(left.severity, right.severity) || Range.compareRangesUsingStarts(left, right));
E
Erich Gamma 已提交
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145

		this._nextIdx = -1;
		this._onMarkerSetChanged.fire(this);
	}

	public withoutWatchingEditorPosition(callback: () => void): void {
		this._ignoreSelectionChange = true;
		try {
			callback();
		} finally {
			this._ignoreSelectionChange = false;
		}
	}

	private initIdx(fwd: boolean): void {
		var found = false;
		var position = this._editor.getPosition();
		for (var i = 0, len = this._markers.length; i < len && !found; i++) {
			var pos = { lineNumber: this._markers[i].startLineNumber, column: this._markers[i].startColumn };
			if (position.isBeforeOrEqual(pos)) {
				this._nextIdx = i + (fwd ? 0 : -1);
				found = true;
			}
		}
		if (!found) {
			// after the last change
			this._nextIdx = fwd ? 0 : this._markers.length - 1;
		}
		if (this._nextIdx < 0) {
			this._nextIdx = this._markers.length - 1;
		}
	}

	private move(fwd: boolean): void {
		if (!this.canNavigate()) {
			this._onCurrentMarkerChanged.fire(undefined);
			return;
		}

		if (this._nextIdx === -1) {
			this.initIdx(fwd);

		} else if (fwd) {
			this._nextIdx += 1;
			if (this._nextIdx >= this._markers.length) {
				this._nextIdx = 0;
			}
		} else {
			this._nextIdx -= 1;
			if (this._nextIdx < 0) {
				this._nextIdx = this._markers.length - 1;
			}
		}
		var marker = this._markers[this._nextIdx];
		this._onCurrentMarkerChanged.fire(marker);
	}

	public canNavigate(): boolean {
		return this._markers.length > 0;
	}

	public next(): void {
		this.move(true);
	}

	public previous(): void {
		this.move(false);
	}

146 147 148 149
	public findMarkerAtPosition(pos: editorCommon.IPosition): IMarker {
		for (const marker of this._markers) {
			if (Range.containsPosition(marker, pos)) {
				return marker;
E
Erich Gamma 已提交
150 151 152 153
			}
		}
	}

154 155 156
	public get stats(): { errors: number; others: number; } {
		let errors = 0;
		let others = 0;
E
Erich Gamma 已提交
157

158 159 160 161 162 163 164 165
		for (let marker of this._markers) {
			if (marker.severity === Severity.Error) {
				errors += 1;
			} else {
				others += 1;
			}
		}
		return { errors, others };
E
Erich Gamma 已提交
166 167
	}

J
Johannes Rieken 已提交
168 169 170 171 172 173 174 175
	public get total() {
		return this._markers.length;
	}

	public indexOf(marker: IMarker): number {
		return 1 + this._markers.indexOf(marker);
	}

E
Erich Gamma 已提交
176 177 178 179 180 181 182 183 184 185 186 187 188 189
	public reveal(): void {

		if (this._nextIdx === -1) {
			return;
		}

		this.withoutWatchingEditorPosition(() => {
			var pos = new Position(this._markers[this._nextIdx].startLineNumber, this._markers[this._nextIdx].startColumn);
			this._editor.setPosition(pos);
			this._editor.revealPositionInCenter(pos);
		});
	}

	public dispose(): void {
A
Alex Dima 已提交
190
		this._toUnbind = dispose(this._toUnbind);
E
Erich Gamma 已提交
191 192 193
	}
}

194 195 196 197 198 199 200 201 202
class FixesWidget {

	domNode: HTMLDivElement;

	private _disposeOnUpdate: IDisposable[] = [];
	private _listener: IDisposable;

	constructor(
		container: HTMLElement,
203
		@ICommandService private _commandService: ICommandService
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
	) {
		this.domNode = document.createElement('div');
		container.appendChild(this.domNode);

		this._listener = dom.addStandardDisposableListener(container, 'keydown', (e) => {
			switch (e.asKeybinding()) {
				case CommonKeybindings.LEFT_ARROW:
					this._move(true);
					// this._goLeft();
					e.preventDefault();
					e.stopPropagation();
					break;
				case CommonKeybindings.RIGHT_ARROW:
					this._move(false);
					// this._goRight();
					e.preventDefault();
					e.stopPropagation();
					break;
			}
		});
	}

	dispose(): void {
		this._disposeOnUpdate = dispose(this._disposeOnUpdate);
		this._listener = dispose(this._listener);
	}

	update(fixes: TPromise<IQuickFix2[]>): TPromise<any> {
		this._disposeOnUpdate = dispose(this._disposeOnUpdate);
		this.domNode.style.display = 'none';
		return fixes.then(fixes => this._doUpdate(fixes), onUnexpectedError);
	}

	private _doUpdate(fixes: IQuickFix2[]): void {

		dom.clearNode(this.domNode);

		if (!fixes || fixes.length === 0) {
			return;
		}

		// light bulb and label
		let quickfixhead = document.createElement('span');
		quickfixhead.className = 'quickfixhead';
		quickfixhead.appendChild(document.createTextNode(fixes.length > 1
			? nls.localize('quickfix.multiple.label', 'Suggested fixes: ')
			: nls.localize('quickfix.single.label', 'Suggested fix: ')));
		this.domNode.appendChild(quickfixhead);

		// each fix as entry
		const container = document.createElement('span');
		container.className = 'quickfixcontainer';

		fixes.forEach((fix, idx, arr) => {

			if (idx > 0) {
				let separator = document.createElement('span');
				separator.appendChild(document.createTextNode(', '));
				container.appendChild(separator);
			}

			let entry = document.createElement('a');
			entry.tabIndex = 0;
			entry.className = `quickfixentry`;
			entry.dataset['idx'] = String(idx);
			entry.dataset['next'] = String(idx < arr.length - 1 ? idx + 1 : 0);
			entry.dataset['prev'] = String(idx > 0 ? idx - 1 : arr.length - 1);
			entry.appendChild(document.createTextNode(fix.command.title));
			this._disposeOnUpdate.push(dom.addDisposableListener(entry, dom.EventType.CLICK, () => {
273
				this._commandService.executeCommand(fix.command.id, ...fix.command.arguments);
274 275 276 277 278 279
				return true;
			}));
			this._disposeOnUpdate.push(dom.addStandardDisposableListener(entry, 'keydown', (e) => {
				switch (e.asKeybinding()) {
					case CommonKeybindings.ENTER:
					case CommonKeybindings.SPACE:
280
						this._commandService.executeCommand(fix.command.id, ...fix.command.arguments);
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
						e.preventDefault();
						e.stopPropagation();
				}
			}));
			container.appendChild(entry);
		});

		this.domNode.appendChild(container);
		this.domNode.style.display = '';
	}

	private _move(left: boolean): void {
		let target: HTMLElement;
		if (document.activeElement.classList.contains('quickfixentry')) {
			let current = <HTMLElement> document.activeElement;
			let idx = left ? current.dataset['prev'] : current.dataset['next'];
			target = <HTMLElement>this.domNode.querySelector(`a[data-idx='${idx}']`);
		} else {
			target = <HTMLElement> this.domNode.querySelector('.quickfixentry');
		}
		target.focus();
	}
}

305
class MarkerNavigationWidget extends ZoneWidget {
E
Erich Gamma 已提交
306

307
	private _parentContainer: HTMLElement;
308
	private _container: HTMLElement;
309 310
	private _title: HTMLElement;
	private _messages: HTMLElement;
311
	private _fixesWidget: FixesWidget;
A
Alex Dima 已提交
312
	private _callOnDispose: IDisposable[] = [];
E
Erich Gamma 已提交
313

314
	constructor(editor: ICodeEditor, private _model: MarkerModel, private _commandService: ICommandService) {
315
		super(editor, { showArrow: true, showFrame: true, isAccessible: true });
E
Erich Gamma 已提交
316 317 318 319
		this.create();
		this._wireModelAndView();
	}

320
	protected _fillContainer(container: HTMLElement): void {
321 322 323 324
		this._parentContainer = container;
		dom.addClass(container, 'marker-widget');
		this._parentContainer.tabIndex = 0;
		this._parentContainer.setAttribute('role', 'tooltip');
325

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

329 330 331 332 333 334 335 336 337 338
		this._title = document.createElement('div');
		this._title.className = 'block title';
		this._container.appendChild(this._title);

		this._messages = document.createElement('div');
		this.editor.applyFontInfo(this._messages);
		this._messages.className = 'block descriptioncontainer';
		this._messages.setAttribute('aria-live', 'assertive');
		this._messages.setAttribute('role', 'alert');
		this._container.appendChild(this._messages);
339

340
		this._fixesWidget = new FixesWidget(this._container, this._commandService);
341
		this._fixesWidget.domNode.classList.add('fixes');
342
		this._callOnDispose.push(this._fixesWidget);
343 344
	}

345
	public show(where: editorCommon.IPosition, heightInLines: number): void {
346
		super.show(where, heightInLines);
347
		this._parentContainer.focus();
E
Erich Gamma 已提交
348 349 350
	}

	private _wireModelAndView(): void {
351
		// listen to events
E
Erich Gamma 已提交
352
		this._model.onCurrentMarkerChanged(this.showAtMarker, this, this._callOnDispose);
353
		this._model.onMarkerSetChanged(this._onMarkersChanged, this, this._callOnDispose);
E
Erich Gamma 已提交
354 355 356 357 358 359 360 361
	}

	public showAtMarker(marker: IMarker): void {

		if (!marker) {
			return;
		}

362
		// update frame color
363
		this.options.frameColor = MarkerNavigationWidget._getFrameColorFromMarker(marker);
E
Erich Gamma 已提交
364

J
Johannes Rieken 已提交
365
		// update meta title
366 367 368 369 370
		if (marker.source) {
			this._title.innerHTML = nls.localize('title.w_source', "({0}/{1}) [{2}]", this._model.indexOf(marker), this._model.total, marker.source);
		} else {
			this._title.innerHTML = nls.localize('title.wo_source', "({0}/{1})", this._model.indexOf(marker), this._model.total);
		}
J
Johannes Rieken 已提交
371

E
Erich Gamma 已提交
372
		// update label and show
373 374 375
		dom.clearNode(this._messages);

		this._messages.appendChild(renderHtml(marker.message));
376

377
		const range = Range.lift(marker);
378
		this._model.withoutWatchingEditorPosition(() => this.show(range.getStartPosition(), this.computeRequiredHeight()));
379

380 381 382
		// check for fixes and update widget
		this._fixesWidget
			.update(getCodeActions(this.editor.getModel(), range))
383 384 385
			.then(() => this.show(range.getStartPosition(), this.computeRequiredHeight()));
	}

386 387 388 389 390 391 392 393 394 395 396 397 398
	private _onMarkersChanged(): void {

		const marker = this._model.findMarkerAtPosition(this.position);
		this.options.frameColor = MarkerNavigationWidget._getFrameColorFromMarker(marker);
		const newQuickFixes = marker
			? getCodeActions(this.editor.getModel(), Range.lift(marker))
			: TPromise.as([]);

		this._fixesWidget
			.update(newQuickFixes)
			.then(() => this.show(this.position, this.computeRequiredHeight()));
	}

399 400
	private computeRequiredHeight() {
		// minimum one line content, add one line for zone widget decorations
401
		let lineHeight = this.editor.getConfiguration().lineHeight || 12;
402
		return Math.max(1, Math.ceil(this._container.clientHeight / lineHeight)) + 1;
E
Erich Gamma 已提交
403 404
	}

405 406 407 408 409 410 411 412 413 414 415 416 417
	private static _getFrameColorFromMarker(marker: IMarker): string {
		if (marker) {
			switch (marker.severity) {
				case Severity.Error:
					return '#ff5a5a';
				case Severity.Warning:
				case Severity.Info:
					return '#5aac5a';
			}
		}
		return '#ccc';
	}

E
Erich Gamma 已提交
418
	public dispose(): void {
J
Joao Moreno 已提交
419
		this._callOnDispose = dispose(this._callOnDispose);
E
Erich Gamma 已提交
420 421 422 423
		super.dispose();
	}
}

A
Alex Dima 已提交
424
class MarkerNavigationAction extends EditorAction {
E
Erich Gamma 已提交
425 426 427

	private _isNext: boolean;

428 429
	constructor(next: boolean, opts:IActionOptions) {
		super(opts);
E
Erich Gamma 已提交
430 431 432
		this._isNext = next;
	}

A
Alex Dima 已提交
433 434 435
	public run(accessor:ServicesAccessor, editor:editorCommon.ICommonCodeEditor): void {
		const telemetryService = accessor.get(ITelemetryService);

A
Alex Dima 已提交
436
		let model = MarkerController.get(editor).getOrCreateModel();
A
Alex Dima 已提交
437
		telemetryService.publicLog('zoneWidgetShown', { mode: 'go to error' });
E
Erich Gamma 已提交
438 439 440 441 442 443 444 445 446 447 448
		if (model) {
			if (this._isNext) {
				model.next();
			} else {
				model.previous();
			}
			model.reveal();
		}
	}
}

449
@editorContribution
A
Alex Dima 已提交
450
class MarkerController implements editorCommon.IEditorContribution {
E
Erich Gamma 已提交
451

452
	private static ID = 'editor.contrib.markerController';
E
Erich Gamma 已提交
453

A
Alex Dima 已提交
454 455
	public static get(editor: editorCommon.ICommonCodeEditor): MarkerController {
		return editor.getContribution<MarkerController>(MarkerController.ID);
E
Erich Gamma 已提交
456 457
	}

458
	private _editor: ICodeEditor;
E
Erich Gamma 已提交
459 460
	private _model: MarkerModel;
	private _zone: MarkerNavigationWidget;
A
Alex Dima 已提交
461
	private _callOnClose: IDisposable[] = [];
A
Alex Dima 已提交
462
	private _markersNavigationVisible: IContextKey<boolean>;
E
Erich Gamma 已提交
463

464
	constructor(
465 466
		editor: ICodeEditor,
		@IMarkerService private _markerService: IMarkerService,
467
		@IContextKeyService private _contextKeyService: IContextKeyService,
468
		@ICommandService private _commandService: ICommandService
469 470
	) {
		this._editor = editor;
471
		this._markersNavigationVisible = CONTEXT_MARKERS_NAVIGATION_VISIBLE.bindTo(this._contextKeyService);
E
Erich Gamma 已提交
472 473 474 475 476 477 478 479 480 481 482 483
	}

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

	public dispose(): void {
		this._cleanUp();
	}

	private _cleanUp(): void {
		this._markersNavigationVisible.reset();
J
Joao Moreno 已提交
484
		this._callOnClose = dispose(this._callOnClose);
485
		this._zone = null;
E
Erich Gamma 已提交
486 487 488 489 490 491 492 493 494 495
		this._model = null;
	}

	public getOrCreateModel(): MarkerModel {

		if (this._model) {
			return this._model;
		}

		var markers = this._getMarkers();
496
		this._model = new MarkerModel(this._editor, markers);
497
		this._zone = new MarkerNavigationWidget(this._editor, this._model, this._commandService);
E
Erich Gamma 已提交
498 499 500
		this._markersNavigationVisible.set(true);

		this._callOnClose.push(this._model);
501
		this._callOnClose.push(this._zone);
E
Erich Gamma 已提交
502

503
		this._callOnClose.push(this._editor.onDidChangeModel(() => this._cleanUp()));
E
Erich Gamma 已提交
504
		this._model.onCurrentMarkerChanged(marker => !marker && this._cleanUp(), undefined, this._callOnClose);
505 506 507
		this._markerService.onMarkerChanged(this._onMarkerChanged, this, this._callOnClose);
		return this._model;
	}
E
Erich Gamma 已提交
508 509

	public closeMarkersNavigation(): void {
510 511
		this._cleanUp();
		this._editor.focus();
E
Erich Gamma 已提交
512 513 514
	}

	private _onMarkerChanged(changedResources: URI[]): void {
515
		if (!changedResources.some(r => this._editor.getModel().uri.toString() === r.toString())) {
E
Erich Gamma 已提交
516 517 518 519 520 521
			return;
		}
		this._model.setMarkers(this._getMarkers());
	}

	private _getMarkers(): IMarker[] {
522
		var resource = this._editor.getModel().uri,
523
			markers = this._markerService.read({ resource: resource });
E
Erich Gamma 已提交
524 525 526 527 528

		return markers;
	}
}

A
Alex Dima 已提交
529
@editorAction
E
Erich Gamma 已提交
530
class NextMarkerAction extends MarkerNavigationAction {
A
Alex Dima 已提交
531
	constructor() {
532 533 534 535 536 537 538 539 540 541
		super(true, {
			id: 'editor.action.marker.next',
			label: nls.localize('markerAction.next.label', "Go to Next Error or Warning"),
			alias: 'Go to Next Error or Warning',
			precondition: EditorContextKeys.Writable,
			kbOpts: {
				kbExpr: EditorContextKeys.Focus,
				primary: KeyCode.F8
			}
		});
E
Erich Gamma 已提交
542 543 544
	}
}

A
Alex Dima 已提交
545
@editorAction
E
Erich Gamma 已提交
546
class PrevMarkerAction extends MarkerNavigationAction {
A
Alex Dima 已提交
547
	constructor() {
548 549 550 551 552 553 554 555 556 557
		super(false, {
			id: 'editor.action.marker.prev',
			label: nls.localize('markerAction.previous.label', "Go to Previous Error or Warning"),
			alias: 'Go to Previous Error or Warning',
			precondition: EditorContextKeys.Writable,
			kbOpts: {
				kbExpr: EditorContextKeys.Focus,
				primary: KeyMod.Shift | KeyCode.F8
			}
		});
E
Erich Gamma 已提交
558 559 560
	}
}

A
Alex Dima 已提交
561
var CONTEXT_MARKERS_NAVIGATION_VISIBLE = new RawContextKey<boolean>('markersNavigationVisible', false);
A
Alex Dima 已提交
562

A
Alex Dima 已提交
563
const MarkerCommand = EditorCommand.bindToContribution<MarkerController>(MarkerController.get);
E
Erich Gamma 已提交
564

565
CommonEditorRegistry.registerEditorCommand(new MarkerCommand({
566 567 568 569 570 571
	id: 'closeMarkersNavigation',
	precondition: CONTEXT_MARKERS_NAVIGATION_VISIBLE,
	handler: x => x.closeMarkersNavigation(),
	kbOpts: {
		weight: CommonEditorRegistry.commandWeight(50),
		kbExpr: EditorContextKeys.Focus,
A
Alex Dima 已提交
572 573 574
		primary: KeyCode.Escape,
		secondary: [KeyMod.Shift | KeyCode.Escape]
	}
575
}));