Path: blob/main/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts
5363 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 (const d of decorations) {97const className = d.className;98const zIndex = d.zIndex;99let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber;100const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber;101102if (prevClassName === className) {103// Here we avoid rendering the same className multiple times on the same line104startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex);105prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex);106} else {107prevClassName = className;108prevEndLineIndex = endLineIndex;109}110111for (let lineIndex = startLineIndex; lineIndex <= prevEndLineIndex; lineIndex++) {112output[lineIndex].add(new LineDecorationToRender(className, zIndex, d.tooltip));113}114}115116return output;117}118}119120export class GlyphMarginWidgets extends ViewPart {121122public domNode: FastDomNode<HTMLElement>;123124private _lineHeight: number;125private _glyphMargin: boolean;126private _glyphMarginLeft: number;127private _glyphMarginWidth: number;128private _glyphMarginDecorationLaneCount: number;129130private _managedDomNodes: FastDomNode<HTMLElement>[];131private _decorationGlyphsToRender: DecorationBasedGlyph[];132133private _widgets: { [key: string]: IWidgetData } = {};134135constructor(context: ViewContext) {136super(context);137this._context = context;138139const options = this._context.configuration.options;140const layoutInfo = options.get(EditorOption.layoutInfo);141142this.domNode = createFastDomNode(document.createElement('div'));143this.domNode.setClassName('glyph-margin-widgets');144this.domNode.setPosition('absolute');145this.domNode.setTop(0);146147this._lineHeight = options.get(EditorOption.lineHeight);148this._glyphMargin = options.get(EditorOption.glyphMargin);149this._glyphMarginLeft = layoutInfo.glyphMarginLeft;150this._glyphMarginWidth = layoutInfo.glyphMarginWidth;151this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;152this._managedDomNodes = [];153this._decorationGlyphsToRender = [];154}155156public override dispose(): void {157this._managedDomNodes = [];158this._decorationGlyphsToRender = [];159this._widgets = {};160super.dispose();161}162163public getWidgets(): IWidgetData[] {164return Object.values(this._widgets);165}166167// --- begin event handlers168public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {169const options = this._context.configuration.options;170const layoutInfo = options.get(EditorOption.layoutInfo);171172this._lineHeight = options.get(EditorOption.lineHeight);173this._glyphMargin = options.get(EditorOption.glyphMargin);174this._glyphMarginLeft = layoutInfo.glyphMarginLeft;175this._glyphMarginWidth = layoutInfo.glyphMarginWidth;176this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;177return true;178}179public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {180return true;181}182public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {183return true;184}185public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {186return true;187}188public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {189return true;190}191public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {192return true;193}194public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {195return e.scrollTopChanged;196}197public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {198return true;199}200201// --- end event handlers202203// --- begin widget management204205public addWidget(widget: IGlyphMarginWidget): void {206const domNode = createFastDomNode(widget.getDomNode());207208this._widgets[widget.getId()] = {209widget: widget,210preference: widget.getPosition(),211domNode: domNode,212renderInfo: null213};214215domNode.setPosition('absolute');216domNode.setDisplay('none');217domNode.setAttribute('widgetId', widget.getId());218this.domNode.appendChild(domNode);219220this.setShouldRender();221}222223public setWidgetPosition(widget: IGlyphMarginWidget, preference: IGlyphMarginWidgetPosition): boolean {224const myWidget = this._widgets[widget.getId()];225if (myWidget.preference.lane === preference.lane226&& myWidget.preference.zIndex === preference.zIndex227&& Range.equalsRange(myWidget.preference.range, preference.range)) {228return false;229}230231myWidget.preference = preference;232this.setShouldRender();233234return true;235}236237public removeWidget(widget: IGlyphMarginWidget): void {238const widgetId = widget.getId();239if (this._widgets[widgetId]) {240const widgetData = this._widgets[widgetId];241const domNode = widgetData.domNode.domNode;242delete this._widgets[widgetId];243244domNode.remove();245this.setShouldRender();246}247}248249// --- end widget management250251private _collectDecorationBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void {252const visibleStartLineNumber = ctx.visibleRange.startLineNumber;253const visibleEndLineNumber = ctx.visibleRange.endLineNumber;254const decorations = ctx.getDecorationsInViewport();255256for (const d of decorations) {257const glyphMarginClassName = d.options.glyphMarginClassName;258if (!glyphMarginClassName) {259continue;260}261262const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber);263const endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber);264const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center;265const zIndex = d.options.zIndex ?? 0;266267for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {268const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 0));269const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(lane);270requests.push(new DecorationBasedGlyphRenderRequest(lineNumber, laneIndex, zIndex, glyphMarginClassName));271}272}273}274275private _collectWidgetBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void {276const visibleStartLineNumber = ctx.visibleRange.startLineNumber;277const visibleEndLineNumber = ctx.visibleRange.endLineNumber;278279for (const widget of Object.values(this._widgets)) {280const range = widget.preference.range;281const { startLineNumber, endLineNumber } = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(Range.lift(range));282if (!startLineNumber || !endLineNumber || endLineNumber < visibleStartLineNumber || startLineNumber > visibleEndLineNumber) {283// The widget is not in the viewport284continue;285}286287// The widget is in the viewport, find a good line for it288const widgetLineNumber = Math.max(startLineNumber, visibleStartLineNumber);289const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(widgetLineNumber, 0));290const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(widget.preference.lane);291requests.push(new WidgetBasedGlyphRenderRequest(widgetLineNumber, laneIndex, widget.preference.zIndex, widget));292}293}294295private _collectSortedGlyphRenderRequests(ctx: RenderingContext): GlyphRenderRequest[] {296297const requests: GlyphRenderRequest[] = [];298299this._collectDecorationBasedGlyphRenderRequest(ctx, requests);300this._collectWidgetBasedGlyphRenderRequest(ctx, requests);301302// sort requests by lineNumber ASC, lane ASC, zIndex DESC, type DESC (widgets first), className ASC303// don't change this sort unless you understand `prepareRender` below.304requests.sort((a, b) => {305if (a.lineNumber === b.lineNumber) {306if (a.laneIndex === b.laneIndex) {307if (a.zIndex === b.zIndex) {308if (b.type === a.type) {309if (a.type === GlyphRenderRequestType.Decoration && b.type === GlyphRenderRequestType.Decoration) {310return (a.className < b.className ? -1 : 1);311}312return 0;313}314return b.type - a.type;315}316return b.zIndex - a.zIndex;317}318return a.laneIndex - b.laneIndex;319}320return a.lineNumber - b.lineNumber;321});322323return requests;324}325326/**327* Will store render information in each widget's renderInfo and in `_decorationGlyphsToRender`.328*/329public prepareRender(ctx: RenderingContext): void {330if (!this._glyphMargin) {331this._decorationGlyphsToRender = [];332return;333}334335for (const widget of Object.values(this._widgets)) {336widget.renderInfo = null;337}338339const requests = new ArrayQueue<GlyphRenderRequest>(this._collectSortedGlyphRenderRequests(ctx));340const decorationGlyphsToRender: DecorationBasedGlyph[] = [];341while (requests.length > 0) {342const first = requests.peek();343if (!first) {344// not possible345break;346}347348// Requests are sorted by lineNumber and lane, so we read all requests for this particular location349const requestsAtLocation = requests.takeWhile((el) => el.lineNumber === first.lineNumber && el.laneIndex === first.laneIndex);350if (!requestsAtLocation || requestsAtLocation.length === 0) {351// not possible352break;353}354355const winner = requestsAtLocation[0];356if (winner.type === GlyphRenderRequestType.Decoration) {357// combine all decorations with the same z-index358359const classNames: string[] = [];360// requests are sorted by zIndex, type, and className so we can dedup className by looking at the previous one361for (const request of requestsAtLocation) {362if (request.zIndex !== winner.zIndex || request.type !== winner.type) {363break;364}365if (classNames.length === 0 || classNames[classNames.length - 1] !== request.className) {366classNames.push(request.className);367}368}369370decorationGlyphsToRender.push(winner.accept(classNames.join(' '))); // TODO@joyceerhl Implement overflow for remaining decorations371} else {372// widgets cannot be combined373winner.widget.renderInfo = {374lineNumber: winner.lineNumber,375laneIndex: winner.laneIndex,376};377}378}379this._decorationGlyphsToRender = decorationGlyphsToRender;380}381382public render(ctx: RestrictedRenderingContext): void {383if (!this._glyphMargin) {384for (const widget of Object.values(this._widgets)) {385widget.domNode.setDisplay('none');386}387while (this._managedDomNodes.length > 0) {388const domNode = this._managedDomNodes.pop();389domNode?.domNode.remove();390}391return;392}393394const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount));395396// Render widgets397for (const widget of Object.values(this._widgets)) {398if (!widget.renderInfo) {399// this widget is not visible400widget.domNode.setDisplay('none');401} else {402const top = ctx.viewportData.relativeVerticalOffset[widget.renderInfo.lineNumber - ctx.viewportData.startLineNumber];403const left = this._glyphMarginLeft + widget.renderInfo.laneIndex * this._lineHeight;404405widget.domNode.setDisplay('block');406widget.domNode.setTop(top);407widget.domNode.setLeft(left);408widget.domNode.setWidth(width);409widget.domNode.setHeight(this._lineHeight);410}411}412413// Render decorations, reusing previous dom nodes as possible414for (let i = 0; i < this._decorationGlyphsToRender.length; i++) {415const dec = this._decorationGlyphsToRender[i];416const decLineNumber = dec.lineNumber;417const top = ctx.viewportData.relativeVerticalOffset[decLineNumber - ctx.viewportData.startLineNumber];418const left = this._glyphMarginLeft + dec.laneIndex * this._lineHeight;419420let domNode: FastDomNode<HTMLElement>;421if (i < this._managedDomNodes.length) {422domNode = this._managedDomNodes[i];423} else {424domNode = createFastDomNode(document.createElement('div'));425this._managedDomNodes.push(domNode);426this.domNode.appendChild(domNode);427}428const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(decLineNumber);429430domNode.setClassName(`cgmr codicon ` + dec.combinedClassName);431domNode.setPosition(`absolute`);432domNode.setTop(top);433domNode.setLeft(left);434domNode.setWidth(width);435domNode.setHeight(lineHeight);436}437438// remove extra dom nodes439while (this._managedDomNodes.length > this._decorationGlyphsToRender.length) {440const domNode = this._managedDomNodes.pop();441domNode?.domNode.remove();442}443}444}445446export interface IWidgetData {447widget: IGlyphMarginWidget;448preference: IGlyphMarginWidgetPosition;449domNode: FastDomNode<HTMLElement>;450/**451* it will contain the location where to render the widget452* or null if the widget is not visible453*/454renderInfo: IRenderInfo | null;455}456457export interface IRenderInfo {458lineNumber: number;459laneIndex: number;460}461462const enum GlyphRenderRequestType {463Decoration = 0,464Widget = 1465}466467/**468* A request to render a decoration in the glyph margin at a certain location.469*/470class DecorationBasedGlyphRenderRequest {471public readonly type = GlyphRenderRequestType.Decoration;472473constructor(474public readonly lineNumber: number,475public readonly laneIndex: number,476public readonly zIndex: number,477public readonly className: string,478) { }479480accept(combinedClassName: string): DecorationBasedGlyph {481return new DecorationBasedGlyph(this.lineNumber, this.laneIndex, combinedClassName);482}483}484485/**486* A request to render a widget in the glyph margin at a certain location.487*/488class WidgetBasedGlyphRenderRequest {489public readonly type = GlyphRenderRequestType.Widget;490491constructor(492public readonly lineNumber: number,493public readonly laneIndex: number,494public readonly zIndex: number,495public readonly widget: IWidgetData,496) { }497}498499type GlyphRenderRequest = DecorationBasedGlyphRenderRequest | WidgetBasedGlyphRenderRequest;500501class DecorationBasedGlyph {502constructor(503public readonly lineNumber: number,504public readonly laneIndex: number,505public readonly combinedClassName: string506) { }507}508509510