/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IMarkerService, IMarker, MarkerSeverity, MarkerTag } from 'vs/platform/markers/common/markers'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IModelDeltaDecoration, ITextModel, IModelDecorationOptions, TrackedRangeStickiness, OverviewRulerLane, IModelDecoration } from 'vs/editor/common/model'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { themeColorFromId, ThemeColor } from 'vs/platform/theme/common/themeService'; import { overviewRulerWarning, overviewRulerInfo, overviewRulerError } from 'vs/editor/common/view/editorColorRegistry'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; import { keys } from 'vs/base/common/map'; import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; function MODEL_ID(resource: URI): string { return resource.toString(); } class MarkerDecorations extends Disposable { private readonly _markersData: Map = new Map(); constructor( readonly model: ITextModel ) { super(); this._register(toDisposable(() => { this.model.deltaDecorations(keys(this._markersData), []); this._markersData.clear(); })); } public update(markers: IMarker[], newDecorations: IModelDeltaDecoration[]): void { const ids = this.model.deltaDecorations(keys(this._markersData), newDecorations); for (let index = 0; index < ids.length; index++) { this._markersData.set(ids[index], markers[index]); } } getMarker(decoration: IModelDecoration): IMarker | null { return this._markersData.get(decoration.id); } } export class MarkerDecorationsService extends Disposable implements IMarkerDecorationsService { _serviceBrand: any; private readonly _markerDecorations: Map = new Map(); constructor( @IModelService modelService: IModelService, @IMarkerService private readonly _markerService: IMarkerService ) { super(); this._register(modelService.onModelAdded(this._onModelAdded, this)); this._register(modelService.onModelRemoved(this._onModelRemoved, this)); this._register(this._markerService.onMarkerChanged(this._handleMarkerChange, this)); } getMarker(model: ITextModel, decoration: IModelDecoration): IMarker | null { const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); return markerDecorations ? markerDecorations.getMarker(decoration) : null; } private _handleMarkerChange(changedResources: URI[]): void { changedResources.forEach((resource) => { const markerDecorations = this._markerDecorations.get(MODEL_ID(resource)); if (markerDecorations) { this.updateDecorations(markerDecorations); } }); } private _onModelAdded(model: ITextModel): void { const markerDecorations = new MarkerDecorations(model); this._markerDecorations.set(MODEL_ID(model.uri), markerDecorations); this.updateDecorations(markerDecorations); } private _onModelRemoved(model: ITextModel): void { const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); if (markerDecorations) { markerDecorations.dispose(); this._markerDecorations.delete(MODEL_ID(model.uri)); } } private updateDecorations(markerDecorations: MarkerDecorations): void { // Limit to the first 500 errors/warnings const markers = this._markerService.read({ resource: markerDecorations.model.uri, take: 500 }); let newModelDecorations: IModelDeltaDecoration[] = markers.map((marker) => { return { range: this._createDecorationRange(markerDecorations.model, marker), options: this._createDecorationOption(marker) }; }); markerDecorations.update(markers, newModelDecorations); } private _createDecorationRange(model: ITextModel, rawMarker: IMarker): Range { let ret = Range.lift(rawMarker); if (rawMarker.severity === MarkerSeverity.Hint) { if (!rawMarker.tags || rawMarker.tags.indexOf(MarkerTag.Unnecessary) === -1) { // * never render hints on multiple lines // * make enough space for three dots ret = ret.setEndPosition(ret.startLineNumber, ret.startColumn + 2); } } ret = model.validateRange(ret); if (ret.isEmpty()) { let word = model.getWordAtPosition(ret.getStartPosition()); if (word) { ret = new Range(ret.startLineNumber, word.startColumn, ret.endLineNumber, word.endColumn); } else { let maxColumn = model.getLineLastNonWhitespaceColumn(ret.startLineNumber) || model.getLineMaxColumn(ret.startLineNumber); if (maxColumn === 1) { // empty line // console.warn('marker on empty line:', marker); } else if (ret.endColumn >= maxColumn) { // behind eol ret = new Range(ret.startLineNumber, maxColumn - 1, ret.endLineNumber, maxColumn); } else { // extend marker to width = 1 ret = new Range(ret.startLineNumber, ret.startColumn, ret.endLineNumber, ret.endColumn + 1); } } } else if (rawMarker.endColumn === Number.MAX_VALUE && rawMarker.startColumn === 1 && ret.startLineNumber === ret.endLineNumber) { let minColumn = model.getLineFirstNonWhitespaceColumn(rawMarker.startLineNumber); if (minColumn < ret.endColumn) { ret = new Range(ret.startLineNumber, minColumn, ret.endLineNumber, ret.endColumn); rawMarker.startColumn = minColumn; } } return ret; } private _createDecorationOption(marker: IMarker): IModelDecorationOptions { let className: string; let color: ThemeColor | undefined = undefined; let zIndex: number; let inlineClassName: string | undefined = undefined; switch (marker.severity) { case MarkerSeverity.Hint: if (marker.tags && marker.tags.indexOf(MarkerTag.Unnecessary) >= 0) { className = ClassName.EditorUnnecessaryDecoration; } else { className = ClassName.EditorHintDecoration; } zIndex = 0; break; case MarkerSeverity.Warning: className = ClassName.EditorWarningDecoration; color = themeColorFromId(overviewRulerWarning); zIndex = 20; break; case MarkerSeverity.Info: className = ClassName.EditorInfoDecoration; color = themeColorFromId(overviewRulerInfo); zIndex = 10; break; case MarkerSeverity.Error: default: className = ClassName.EditorErrorDecoration; color = themeColorFromId(overviewRulerError); zIndex = 30; break; } if (marker.tags) { if (marker.tags.indexOf(MarkerTag.Unnecessary) !== -1) { inlineClassName = ClassName.EditorUnnecessaryInlineDecoration; } } return { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className, showIfCollapsed: true, overviewRuler: { color, position: OverviewRulerLane.Right }, zIndex, inlineClassName, }; } }