Path: blob/main/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';6import { ArrayQueue } from '../../../../base/common/arrays.js';7import './glyphMargin.css';8import { IGlyphMarginWidget, IGlyphMarginWidgetPosition } from '../../editorBrowser.js';9import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';10import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';11import { ViewPart } from '../../view/viewPart.js';12import { EditorOption } from '../../../common/config/editorOptions.js';13import { Position } from '../../../common/core/position.js';14import { Range } from '../../../common/core/range.js';15import { GlyphMarginLane } from '../../../common/model.js';16import * as viewEvents from '../../../common/viewEvents.js';17import { ViewContext } from '../../../common/viewModel/viewContext.js';1819/**20* Represents a decoration that should be shown along the lines from `startLineNumber` to `endLineNumber`.21* This can end up producing multiple `LineDecorationToRender`.22*/23export class DecorationToRender {24public readonly _decorationToRenderBrand: void = undefined;2526public readonly zIndex: number;2728constructor(29public readonly startLineNumber: number,30public readonly endLineNumber: number,31public readonly className: string,32public readonly tooltip: string | null,33zIndex: number | undefined,34) {35this.zIndex = zIndex ?? 0;36}37}3839/**40* A decoration that should be shown along a line.41*/42export class LineDecorationToRender {43constructor(44public readonly className: string,45public readonly zIndex: number,46public readonly tooltip: string | null,47) { }48}4950/**51* Decorations to render on a visible line.52*/53export class VisibleLineDecorationsToRender {5455private readonly decorations: LineDecorationToRender[] = [];5657public add(decoration: LineDecorationToRender) {58this.decorations.push(decoration);59}6061public getDecorations(): LineDecorationToRender[] {62return this.decorations;63}64}6566export abstract class DedupOverlay extends DynamicViewOverlay {6768/**69* Returns an array with an element for each visible line number.70*/71protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[]): VisibleLineDecorationsToRender[] {7273const output: VisibleLineDecorationsToRender[] = [];74for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {75const lineIndex = lineNumber - visibleStartLineNumber;76output[lineIndex] = new VisibleLineDecorationsToRender();77}7879if (decorations.length === 0) {80return output;81}8283// Sort decorations by className, then by startLineNumber and then by endLineNumber84decorations.sort((a, b) => {85if (a.className === b.className) {86if (a.startLineNumber === b.startLineNumber) {87return a.endLineNumber - b.endLineNumber;88}89return a.startLineNumber - b.startLineNumber;90}91return (a.className < b.className ? -1 : 1);92});9394let prevClassName: string | null = null;95let prevEndLineIndex = 0;96for (let i = 0, len = decorations.length; i < len; i++) {97const d = decorations[i];98const className = d.className;99const zIndex = d.zIndex;100let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber;101const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber;102103if (prevClassName === className) {104// Here we avoid rendering the same className multiple times on the same line105startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex);106prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex);107} else {108prevClassName = className;109prevEndLineIndex = endLineIndex;110}111112for (let i = startLineIndex; i <= prevEndLineIndex; i++) {113output[i].add(new LineDecorationToRender(className, zIndex, d.tooltip));114}115}116117return output;118}119}120121export class GlyphMarginWidgets extends ViewPart {122123public domNode: FastDomNode<HTMLElement>;124125private _lineHeight: number;126private _glyphMargin: boolean;127private _glyphMarginLeft: number;128private _glyphMarginWidth: number;129private _glyphMarginDecorationLaneCount: number;130131private _managedDomNodes: FastDomNode<HTMLElement>[];132private _decorationGlyphsToRender: DecorationBasedGlyph[];133134private _widgets: { [key: string]: IWidgetData } = {};135136constructor(context: ViewContext) {137super(context);138this._context = context;139140const options = this._context.configuration.options;141const layoutInfo = options.get(EditorOption.layoutInfo);142143this.domNode = createFastDomNode(document.createElement('div'));144this.domNode.setClassName('glyph-margin-widgets');145this.domNode.setPosition('absolute');146this.domNode.setTop(0);147148this._lineHeight = options.get(EditorOption.lineHeight);149this._glyphMargin = options.get(EditorOption.glyphMargin);150this._glyphMarginLeft = layoutInfo.glyphMarginLeft;151this._glyphMarginWidth = layoutInfo.glyphMarginWidth;152this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;153this._managedDomNodes = [];154this._decorationGlyphsToRender = [];155}156157public override dispose(): void {158this._managedDomNodes = [];159this._decorationGlyphsToRender = [];160this._widgets = {};161super.dispose();162}163164public getWidgets(): IWidgetData[] {165return Object.values(this._widgets);166}167168// --- begin event handlers169public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {170const options = this._context.configuration.options;171const layoutInfo = options.get(EditorOption.layoutInfo);172173this._lineHeight = options.get(EditorOption.lineHeight);174this._glyphMargin = options.get(EditorOption.glyphMargin);175this._glyphMarginLeft = layoutInfo.glyphMarginLeft;176this._glyphMarginWidth = layoutInfo.glyphMarginWidth;177this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;178return true;179}180public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {181return true;182}183public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {184return true;185}186public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {187return true;188}189public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {190return true;191}192public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {193return true;194}195public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {196return e.scrollTopChanged;197}198public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {199return true;200}201202// --- end event handlers203204// --- begin widget management205206public addWidget(widget: IGlyphMarginWidget): void {207const domNode = createFastDomNode(widget.getDomNode());208209this._widgets[widget.getId()] = {210widget: widget,211preference: widget.getPosition(),212domNode: domNode,213renderInfo: null214};215216domNode.setPosition('absolute');217domNode.setDisplay('none');218domNode.setAttribute('widgetId', widget.getId());219this.domNode.appendChild(domNode);220221this.setShouldRender();222}223224public setWidgetPosition(widget: IGlyphMarginWidget, preference: IGlyphMarginWidgetPosition): boolean {225const myWidget = this._widgets[widget.getId()];226if (myWidget.preference.lane === preference.lane227&& myWidget.preference.zIndex === preference.zIndex228&& Range.equalsRange(myWidget.preference.range, preference.range)) {229return false;230}231232myWidget.preference = preference;233this.setShouldRender();234235return true;236}237238public removeWidget(widget: IGlyphMarginWidget): void {239const widgetId = widget.getId();240if (this._widgets[widgetId]) {241const widgetData = this._widgets[widgetId];242const domNode = widgetData.domNode.domNode;243delete this._widgets[widgetId];244245domNode.remove();246this.setShouldRender();247}248}249250// --- end widget management251252private _collectDecorationBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void {253const visibleStartLineNumber = ctx.visibleRange.startLineNumber;254const visibleEndLineNumber = ctx.visibleRange.endLineNumber;255const decorations = ctx.getDecorationsInViewport();256257for (const d of decorations) {258const glyphMarginClassName = d.options.glyphMarginClassName;259if (!glyphMarginClassName) {260continue;261}262263const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber);264const endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber);265const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center;266const zIndex = d.options.zIndex ?? 0;267268for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {269const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 0));270const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(lane);271requests.push(new DecorationBasedGlyphRenderRequest(lineNumber, laneIndex, zIndex, glyphMarginClassName));272}273}274}275276private _collectWidgetBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void {277const visibleStartLineNumber = ctx.visibleRange.startLineNumber;278const visibleEndLineNumber = ctx.visibleRange.endLineNumber;279280for (const widget of Object.values(this._widgets)) {281const range = widget.preference.range;282const { startLineNumber, endLineNumber } = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(Range.lift(range));283if (!startLineNumber || !endLineNumber || endLineNumber < visibleStartLineNumber || startLineNumber > visibleEndLineNumber) {284// The widget is not in the viewport285continue;286}287288// The widget is in the viewport, find a good line for it289const widgetLineNumber = Math.max(startLineNumber, visibleStartLineNumber);290const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(widgetLineNumber, 0));291const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(widget.preference.lane);292requests.push(new WidgetBasedGlyphRenderRequest(widgetLineNumber, laneIndex, widget.preference.zIndex, widget));293}294}295296private _collectSortedGlyphRenderRequests(ctx: RenderingContext): GlyphRenderRequest[] {297298const requests: GlyphRenderRequest[] = [];299300this._collectDecorationBasedGlyphRenderRequest(ctx, requests);301this._collectWidgetBasedGlyphRenderRequest(ctx, requests);302303// sort requests by lineNumber ASC, lane ASC, zIndex DESC, type DESC (widgets first), className ASC304// don't change this sort unless you understand `prepareRender` below.305requests.sort((a, b) => {306if (a.lineNumber === b.lineNumber) {307if (a.laneIndex === b.laneIndex) {308if (a.zIndex === b.zIndex) {309if (b.type === a.type) {310if (a.type === GlyphRenderRequestType.Decoration && b.type === GlyphRenderRequestType.Decoration) {311return (a.className < b.className ? -1 : 1);312}313return 0;314}315return b.type - a.type;316}317return b.zIndex - a.zIndex;318}319return a.laneIndex - b.laneIndex;320}321return a.lineNumber - b.lineNumber;322});323324return requests;325}326327/**328* Will store render information in each widget's renderInfo and in `_decorationGlyphsToRender`.329*/330public prepareRender(ctx: RenderingContext): void {331if (!this._glyphMargin) {332this._decorationGlyphsToRender = [];333return;334}335336for (const widget of Object.values(this._widgets)) {337widget.renderInfo = null;338}339340const requests = new ArrayQueue<GlyphRenderRequest>(this._collectSortedGlyphRenderRequests(ctx));341const decorationGlyphsToRender: DecorationBasedGlyph[] = [];342while (requests.length > 0) {343const first = requests.peek();344if (!first) {345// not possible346break;347}348349// Requests are sorted by lineNumber and lane, so we read all requests for this particular location350const requestsAtLocation = requests.takeWhile((el) => el.lineNumber === first.lineNumber && el.laneIndex === first.laneIndex);351if (!requestsAtLocation || requestsAtLocation.length === 0) {352// not possible353break;354}355356const winner = requestsAtLocation[0];357if (winner.type === GlyphRenderRequestType.Decoration) {358// combine all decorations with the same z-index359360const classNames: string[] = [];361// requests are sorted by zIndex, type, and className so we can dedup className by looking at the previous one362for (const request of requestsAtLocation) {363if (request.zIndex !== winner.zIndex || request.type !== winner.type) {364break;365}366if (classNames.length === 0 || classNames[classNames.length - 1] !== request.className) {367classNames.push(request.className);368}369}370371decorationGlyphsToRender.push(winner.accept(classNames.join(' '))); // TODO@joyceerhl Implement overflow for remaining decorations372} else {373// widgets cannot be combined374winner.widget.renderInfo = {375lineNumber: winner.lineNumber,376laneIndex: winner.laneIndex,377};378}379}380this._decorationGlyphsToRender = decorationGlyphsToRender;381}382383public render(ctx: RestrictedRenderingContext): void {384if (!this._glyphMargin) {385for (const widget of Object.values(this._widgets)) {386widget.domNode.setDisplay('none');387}388while (this._managedDomNodes.length > 0) {389const domNode = this._managedDomNodes.pop();390domNode?.domNode.remove();391}392return;393}394395const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount));396397// Render widgets398for (const widget of Object.values(this._widgets)) {399if (!widget.renderInfo) {400// this widget is not visible401widget.domNode.setDisplay('none');402} else {403const top = ctx.viewportData.relativeVerticalOffset[widget.renderInfo.lineNumber - ctx.viewportData.startLineNumber];404const left = this._glyphMarginLeft + widget.renderInfo.laneIndex * this._lineHeight;405406widget.domNode.setDisplay('block');407widget.domNode.setTop(top);408widget.domNode.setLeft(left);409widget.domNode.setWidth(width);410widget.domNode.setHeight(this._lineHeight);411}412}413414// Render decorations, reusing previous dom nodes as possible415for (let i = 0; i < this._decorationGlyphsToRender.length; i++) {416const dec = this._decorationGlyphsToRender[i];417const decLineNumber = dec.lineNumber;418const top = ctx.viewportData.relativeVerticalOffset[decLineNumber - ctx.viewportData.startLineNumber];419const left = this._glyphMarginLeft + dec.laneIndex * this._lineHeight;420421let domNode: FastDomNode<HTMLElement>;422if (i < this._managedDomNodes.length) {423domNode = this._managedDomNodes[i];424} else {425domNode = createFastDomNode(document.createElement('div'));426this._managedDomNodes.push(domNode);427this.domNode.appendChild(domNode);428}429const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(decLineNumber);430431domNode.setClassName(`cgmr codicon ` + dec.combinedClassName);432domNode.setPosition(`absolute`);433domNode.setTop(top);434domNode.setLeft(left);435domNode.setWidth(width);436domNode.setHeight(lineHeight);437}438439// remove extra dom nodes440while (this._managedDomNodes.length > this._decorationGlyphsToRender.length) {441const domNode = this._managedDomNodes.pop();442domNode?.domNode.remove();443}444}445}446447export interface IWidgetData {448widget: IGlyphMarginWidget;449preference: IGlyphMarginWidgetPosition;450domNode: FastDomNode<HTMLElement>;451/**452* it will contain the location where to render the widget453* or null if the widget is not visible454*/455renderInfo: IRenderInfo | null;456}457458export interface IRenderInfo {459lineNumber: number;460laneIndex: number;461}462463const enum GlyphRenderRequestType {464Decoration = 0,465Widget = 1466}467468/**469* A request to render a decoration in the glyph margin at a certain location.470*/471class DecorationBasedGlyphRenderRequest {472public readonly type = GlyphRenderRequestType.Decoration;473474constructor(475public readonly lineNumber: number,476public readonly laneIndex: number,477public readonly zIndex: number,478public readonly className: string,479) { }480481accept(combinedClassName: string): DecorationBasedGlyph {482return new DecorationBasedGlyph(this.lineNumber, this.laneIndex, combinedClassName);483}484}485486/**487* A request to render a widget in the glyph margin at a certain location.488*/489class WidgetBasedGlyphRenderRequest {490public readonly type = GlyphRenderRequestType.Widget;491492constructor(493public readonly lineNumber: number,494public readonly laneIndex: number,495public readonly zIndex: number,496public readonly widget: IWidgetData,497) { }498}499500type GlyphRenderRequest = DecorationBasedGlyphRenderRequest | WidgetBasedGlyphRenderRequest;501502class DecorationBasedGlyph {503constructor(504public readonly lineNumber: number,505public readonly laneIndex: number,506public readonly combinedClassName: string507) { }508}509510511