From f81e7e70c32d21673600a77261c793333ed6bc63 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 5 Jun 2023 17:02:59 -0700 Subject: [PATCH 1/8] Implement GlyphMarginWidgets --- src/vs/editor/browser/editorBrowser.ts | 58 ++++- .../services/abstractCodeEditorService.ts | 2 +- src/vs/editor/browser/view.ts | 35 ++- .../viewParts/glyphMargin/glyphMargin.ts | 240 +++++++++++++++--- .../editor/browser/widget/codeEditorWidget.ts | 49 +++- src/vs/monaco.d.ts | 55 ++++ 6 files changed, 403 insertions(+), 36 deletions(-) diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index eb084bf8ac3e4..35877146ede7f 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -12,7 +12,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, PositionAffinity } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, PositionAffinity, GlyphMarginLane } from 'vs/editor/common/model'; import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager'; @@ -251,6 +251,48 @@ export interface IOverlayWidget { getPosition(): IOverlayWidgetPosition | null; } +/** + * A glyph margin widget renders in the editor glyph margin. + */ +export interface IGlyphMarginWidget { + /** + * The class name to apply to the glyph widget. + * todo@joyceerhl is this really necessary? + */ + className: string; + /** + * Get a unique identifier of the glyph widget. + */ + getId(): string; + /** + * Get the dom node of the glyph widget. + */ + getDomNode(): HTMLElement; + /** + * Get the placement of the glyph widget. + */ + getPosition(): IGlyphMarginWidgetPosition; +} + +/** + * A position for rendering glyph margin widgets. + */ +export interface IGlyphMarginWidgetPosition { + /** + * The glyph margin lane where the widget should be shown. + */ + lane: GlyphMarginLane; + /** + * The priority order of the widget, used for determining which widget + * to render when there are multiple. + */ + zIndex: number; + /** + * The editor range that this widget applies to. + */ + range: IRange; +} + /** * Type of hit element with the mouse in the editor. */ @@ -993,6 +1035,20 @@ export interface ICodeEditor extends editorCommon.IEditor { */ removeOverlayWidget(widget: IOverlayWidget): void; + /** + * Add a glyph margin widget. Widgets must have unique ids, otherwise they will be overwritten. + */ + addGlyphMarginWidget(widget: IGlyphMarginWidget): void; + /** + * Layout/Reposition a glyph margin widget. This is a ping to the editor to call widget.getPosition() + * and update appropriately. + */ + layoutGlyphMarginWidget(widget: IGlyphMarginWidget): void; + /** + * Remove a glyph margin widget. + */ + removeGlyphMarginWidget(widget: IGlyphMarginWidget): void; + /** * Change the view zones. View zones are lost when a new model is attached to the editor. */ diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index 62db35d5a0ccf..cc0f8669a0343 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -786,7 +786,7 @@ class DecorationCSSRules { } /** - * Build the CSS for decorations styled via `glpyhMarginClassName`. + * Build the CSS for decorations styled via `glyphMarginClassName`. */ private getCSSTextForModelDecorationGlyphMarginClassName(opts: IThemeDecorationRenderOptions | undefined): string { if (!opts) { diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 922e0dd6e52ff..30b12efbbb17e 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -11,7 +11,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler'; import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { IVisibleRangeProvider, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; -import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions } from 'vs/editor/browser/editorBrowser'; +import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions, IGlyphMarginWidget, IGlyphMarginWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; @@ -20,7 +20,6 @@ import { ViewContentWidgets } from 'vs/editor/browser/viewParts/contentWidgets/c import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from 'vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight'; import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations'; import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar'; -import { GlyphMarginOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides'; import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers'; import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines'; @@ -52,6 +51,7 @@ import { BlockDecorations } from 'vs/editor/browser/viewParts/blockDecorations/b import { inputLatency } from 'vs/base/browser/performance'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { WhitespaceOverlay } from 'vs/editor/browser/viewParts/whitespace/whitespace'; +import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; export interface IContentWidgetData { @@ -64,6 +64,11 @@ export interface IOverlayWidgetData { position: IOverlayWidgetPosition | null; } +export interface IGlyphMarginWidgetData { + widget: IGlyphMarginWidget; + position: IGlyphMarginWidgetPosition; +} + export class View extends ViewEventHandler { private readonly _scrollbar: EditorScrollbar; @@ -77,6 +82,7 @@ export class View extends ViewEventHandler { private readonly _viewZones: ViewZones; private readonly _contentWidgets: ViewContentWidgets; private readonly _overlayWidgets: ViewOverlayWidgets; + private readonly _glyphMarginWidgets: GlyphMarginWidgets; private readonly _viewCursors: ViewCursors; private readonly _viewParts: ViewPart[]; @@ -160,7 +166,6 @@ export class View extends ViewEventHandler { const marginViewOverlays = new MarginViewOverlays(this._context); this._viewParts.push(marginViewOverlays); marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context)); - marginViewOverlays.addDynamicOverlay(new GlyphMarginOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context)); @@ -170,6 +175,11 @@ export class View extends ViewEventHandler { margin.getDomNode().appendChild(marginViewOverlays.getDomNode()); this._viewParts.push(margin); + // Glyph margin widgets + this._glyphMarginWidgets = new GlyphMarginWidgets(this._context); + this._viewParts.push(this._glyphMarginWidgets); + margin.getDomNode().appendChild(this._glyphMarginWidgets.domNode); + // Content widgets this._contentWidgets = new ViewContentWidgets(this._context, this.domNode); this._viewParts.push(this._contentWidgets); @@ -202,6 +212,7 @@ export class View extends ViewEventHandler { this._linesContent.appendChild(this._viewZones.domNode); this._linesContent.appendChild(this._viewLines.getDomNode()); this._linesContent.appendChild(this._contentWidgets.domNode); + // this._linesContent.appendChild(this._glyphMarginWidgets.domNode); this._linesContent.appendChild(this._viewCursors.getDomNode()); this._overflowGuardContainer.appendChild(margin.getDomNode()); this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); @@ -548,6 +559,24 @@ export class View extends ViewEventHandler { this._scheduleRender(); } + public addGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void { + this._glyphMarginWidgets.addWidget(widgetData.widget); + this._scheduleRender(); + } + + public layoutGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void { + const newPreference = widgetData.position; + const shouldRender = this._glyphMarginWidgets.setWidgetPosition(widgetData.widget, newPreference); + if (shouldRender) { + this._scheduleRender(); + } + } + + public removeGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void { + this._glyphMarginWidgets.removeWidget(widgetData.widget); + this._scheduleRender(); + } + // --- END CodeEditor helpers } diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 0b35f4b06edc5..054c015663595 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -5,11 +5,49 @@ import 'vs/css!./glyphMargin'; import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; -import { RenderingContext } from 'vs/editor/browser/view/renderingContext'; +import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; +import { IGlyphMarginWidget, IGlyphMarginWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; -import * as viewEvents from 'vs/editor/common/viewEvents'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import * as viewEvents from 'vs/editor/common/viewEvents'; +import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; +export class LineRenderedWidgets { + + private readonly lines: GlyphMarginWidget[][] = []; + + public add(line: number, decoration: GlyphMarginWidget) { + while (line >= this.lines.length) { + this.lines.push([]); + } + this.lines[line].push(decoration); + } + + public getLineDecorations(lineIndex: number): GlyphMarginWidget[] { + if (lineIndex < this.lines.length) { + return this.lines[lineIndex]; + } + return []; + } +} + +export class GlyphMarginWidget { + + constructor( + public domNode: FastDomNode, + public className: string, + public lineNumber: number, + public left: number, + public width: number, + public isManaged: boolean, + ) { + this.domNode.setPosition('absolute'); + this.domNode.setDisplay('none'); + this.domNode.setVisibility('hidden'); + this.domNode.setMaxWidth(width); + } +} export class DecorationToRender { _decorationToRenderBrand: void = undefined; @@ -19,13 +57,15 @@ export class DecorationToRender { public className: string; public readonly zIndex: number; public readonly decorationLane: number; + public readonly domNode: FastDomNode | undefined; - constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex?: number, decorationLane?: number) { + constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex?: number, decorationLane?: number, domNode?: FastDomNode) { this.startLineNumber = +startLineNumber; this.endLineNumber = +endLineNumber; this.className = String(className); this.zIndex = zIndex ?? 0; this.decorationLane = decorationLane ?? 1; + this.domNode = domNode; } } @@ -33,6 +73,7 @@ export class RenderedDecoration { constructor( public readonly className: string, public readonly zIndex: number, + public readonly domNode?: FastDomNode, ) { } } @@ -107,7 +148,7 @@ export abstract class DedupOverlay extends DynamicViewOverlay { } for (let i = startLineIndex; i <= prevEndLineIndex; i++) { - output[i].add(lane, new RenderedDecoration(className, zIndex)); + output[i].add(lane, new RenderedDecoration(className, zIndex, d.domNode)); } } @@ -115,28 +156,45 @@ export abstract class DedupOverlay extends DynamicViewOverlay { } } -export class GlyphMarginOverlay extends DedupOverlay { +export interface IWidgetData { + widget: IGlyphMarginWidget; + preference: IGlyphMarginWidgetPosition; + domNode: FastDomNode; +} + +export class GlyphMarginWidgets extends ViewPart { + + public domNode: FastDomNode; - private readonly _context: ViewContext; private _lineHeight: number; private _glyphMargin: boolean; private _glyphMarginLeft: number; private _glyphMarginWidth: number; private _glyphMarginDecorationLaneCount: number; - private _renderResult: string[] | null; + + private _previousRenderResult: LineRenderedWidgets | null; + private _renderResult: LineRenderedWidgets | null; + + private _widgets: { [key: string]: IWidgetData } = {}; constructor(context: ViewContext) { - super(); + super(context); this._context = context; const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); + this.domNode = createFastDomNode(document.createElement('div')); + this.domNode.setClassName('glyphMarginWidgets'); + this.domNode.setPosition('absolute'); + this.domNode.setTop(0); + this._lineHeight = options.get(EditorOption.lineHeight); this._glyphMargin = options.get(EditorOption.glyphMargin); this._glyphMarginLeft = layoutInfo.glyphMarginLeft; this._glyphMarginWidth = layoutInfo.glyphMarginWidth; this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount; + this._previousRenderResult = null; this._renderResult = null; this._context.addEventHandler(this); } @@ -144,11 +202,11 @@ export class GlyphMarginOverlay extends DedupOverlay { public override dispose(): void { this._context.removeEventHandler(this); this._renderResult = null; + this._widgets = {}; super.dispose(); } // --- begin event handlers - public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); @@ -183,7 +241,48 @@ export class GlyphMarginOverlay extends DedupOverlay { } // --- end event handlers + // --- begin widget management + public addWidget(widget: IGlyphMarginWidget): void { + const domNode = createFastDomNode(widget.getDomNode()); + + this._widgets[widget.getId()] = { + widget: widget, + preference: widget.getPosition(), + domNode: domNode + }; + + domNode.setPosition('absolute'); + domNode.setAttribute('widgetId', widget.getId()); + this.domNode.appendChild(domNode); + this.setShouldRender(); + } + + public setWidgetPosition(widget: IGlyphMarginWidget, preference: IGlyphMarginWidgetPosition): boolean { + const myWidget = this._widgets[widget.getId()]; + if (myWidget.preference === preference) { + return false; + } + + myWidget.preference = preference; + this.setShouldRender(); + + return true; + } + + public removeWidget(widget: IGlyphMarginWidget): void { + const widgetId = widget.getId(); + if (this._widgets[widgetId]) { + const widgetData = this._widgets[widgetId]; + const domNode = widgetData.domNode.domNode; + delete this._widgets[widgetId]; + + domNode.parentNode?.removeChild(domNode); + this.setShouldRender(); + } + } + + // --- end widget management protected _getDecorations(ctx: RenderingContext): DecorationToRender[] { const decorations = ctx.getDecorationsInViewport(); const r: DecorationToRender[] = []; @@ -197,9 +296,65 @@ export class GlyphMarginOverlay extends DedupOverlay { r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, glyphMarginClassName, zIndex, lane); } } + const widgets = Object.values(this._widgets); + for (let i = 0, len = widgets.length; i < len; i++) { + const w = widgets[i]; + const glyphMarginClassName = w.widget.className; + if (glyphMarginClassName) { + r[rLen++] = new DecorationToRender(w.preference.range.startLineNumber, w.preference.range.endLineNumber, glyphMarginClassName, w.preference.zIndex, w.preference.lane, w.domNode); + } + } return r; } + protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): LineRenderedDecorations[] { + + const output: LineRenderedDecorations[] = []; + for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { + const lineIndex = lineNumber - visibleStartLineNumber; + output[lineIndex] = new LineRenderedDecorations(); + } + + if (decorations.length === 0) { + return output; + } + + decorations.sort((a, b) => { + if (a.className === b.className) { + if (a.startLineNumber === b.startLineNumber) { + return a.endLineNumber - b.endLineNumber; + } + return a.startLineNumber - b.startLineNumber; + } + return (a.className < b.className ? -1 : 1); + }); + + let prevClassName: string | null = null; + let prevEndLineIndex = 0; + for (let i = 0, len = decorations.length; i < len; i++) { + const d = decorations[i]; + const className = d.className; + const zIndex = d.zIndex; + let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber; + const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber; + const lane = Math.min(d.decorationLane, decorationLaneCount); + + if (prevClassName === className) { + startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex); + prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex); + } else { + prevClassName = className; + prevEndLineIndex = endLineIndex; + } + + for (let i = startLineIndex; i <= prevEndLineIndex; i++) { + output[i].add(lane, new RenderedDecoration(className, zIndex, d.domNode)); + } + } + + return output; + } + public prepareRender(ctx: RenderingContext): void { if (!this._glyphMargin) { this._renderResult = null; @@ -211,19 +366,16 @@ export class GlyphMarginOverlay extends DedupOverlay { const decorationsToRender = this._getDecorations(ctx); const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, decorationsToRender, this._glyphMarginDecorationLaneCount); - const lineHeight = this._lineHeight.toString(); - const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount)).toString(); - const common = '" style="width:' + width + 'px' + ';height:' + lineHeight + 'px;'; + const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount)); - const output: string[] = []; + const output = new LineRenderedWidgets(); for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; const renderInfo = toRender[lineIndex]; if (renderInfo.isEmpty()) { - output[lineIndex] = ''; + continue; } else { - let css = ''; for (let lane = 1; lane <= this._glyphMarginDecorationLaneCount; lane += 1) { const decorations = renderInfo.getLaneDecorations(lane); if (decorations.length === 0) { @@ -240,29 +392,57 @@ export class GlyphMarginOverlay extends DedupOverlay { } winningDecorationClassNames.push(decoration.className); } - const left = (this._glyphMarginLeft + (lane - 1) * this._lineHeight).toString(); - css += ( - '
' - ); + const left = this._glyphMarginLeft + (lane - 1) * this._lineHeight; + output.add(lineNumber, new GlyphMarginWidget(winningDecoration.domNode ?? createFastDomNode(document.createElement('div')), winningDecorationClassNames.join(' '), lineNumber, left, width, winningDecoration.domNode !== undefined)); } - output[lineIndex] = css; } } - + this._previousRenderResult = this._renderResult; this._renderResult = output; } - public render(startLineNumber: number, lineNumber: number): string { + public render(ctx: RestrictedRenderingContext): void { + const { startLineNumber, endLineNumber } = ctx.viewportData; + + // Clean up any existing render results + if (this._previousRenderResult) { + for (let lineNumber = startLineNumber; lineNumber < endLineNumber; lineNumber += 1) { + const decorations = this._previousRenderResult.getLineDecorations(lineNumber); + if (lineNumber < 0 || decorations.length === 0) { + continue; + } + decorations.forEach((widget) => { + widget.domNode.setDisplay('none'); + if (!widget.isManaged) { + widget.domNode.domNode.parentNode?.removeChild(widget.domNode.domNode); + } + }); + } + } + if (!this._renderResult) { - return ''; + return; } - const lineIndex = lineNumber - startLineNumber; - if (lineIndex < 0 || lineIndex >= this._renderResult.length) { - return ''; + + // Render new render results + for (let lineNumber = startLineNumber; lineNumber < endLineNumber; lineNumber += 1) { + const decorations = this._renderResult.getLineDecorations(lineNumber); + if (lineNumber < 0 || decorations.length === 0) { + continue; + } + decorations.forEach((widget) => this._renderWidget(widget)); } - return this._renderResult[lineIndex]; + return; + } + + private _renderWidget(renderedWidget: GlyphMarginWidget): void { + renderedWidget.domNode.setClassName(`cgmr codicon ${renderedWidget.className}`); + renderedWidget.domNode.setLeft(renderedWidget.left); + renderedWidget.domNode.setWidth(renderedWidget.width); + renderedWidget.domNode.setHeight(this._lineHeight); + renderedWidget.domNode.setTop((renderedWidget.lineNumber - 1) * this._lineHeight); + renderedWidget.domNode.setVisibility('inherit'); + renderedWidget.domNode.setDisplay('block'); + this.domNode.appendChild(renderedWidget.domNode); } } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 181b2c279e4c2..fa6bb98fbfa74 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -21,7 +21,7 @@ import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; -import { IContentWidgetData, IOverlayWidgetData, View } from 'vs/editor/browser/view'; +import { IContentWidgetData, IGlyphMarginWidgetData, IOverlayWidgetData, View } from 'vs/editor/browser/view'; import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; @@ -255,6 +255,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private _contentWidgets: { [key: string]: IContentWidgetData }; private _overlayWidgets: { [key: string]: IOverlayWidgetData }; + private _glyphMarginWidgets: { [key: string]: IGlyphMarginWidgetData }; /** * map from "parent" decoration type to live decoration ids. @@ -323,6 +324,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._contentWidgets = {}; this._overlayWidgets = {}; + this._glyphMarginWidgets = {}; let contributions: IEditorContributionDescription[]; if (Array.isArray(codeEditorWidgetOptions.contributions)) { @@ -1510,6 +1512,45 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } } + public addGlyphMarginWidget(widget: editorBrowser.IGlyphMarginWidget): void { + const widgetData: IGlyphMarginWidgetData = { + widget: widget, + position: widget.getPosition() + }; + + if (this._glyphMarginWidgets.hasOwnProperty(widget.getId())) { + console.warn('Overwriting an overlay widget with the same id.'); + } + + this._glyphMarginWidgets[widget.getId()] = widgetData; + + if (this._modelData && this._modelData.hasRealView) { + this._modelData.view.addGlyphMarginWidget(widgetData); + } + } + + public layoutGlyphMarginWidget(widget: editorBrowser.IGlyphMarginWidget): void { + const widgetId = widget.getId(); + if (this._glyphMarginWidgets.hasOwnProperty(widgetId)) { + const widgetData = this._glyphMarginWidgets[widgetId]; + widgetData.position = widget.getPosition(); + if (this._modelData && this._modelData.hasRealView) { + this._modelData.view.layoutGlyphMarginWidget(widgetData); + } + } + } + + public removeGlyphMarginWidget(widget: editorBrowser.IGlyphMarginWidget): void { + const widgetId = widget.getId(); + if (this._glyphMarginWidgets.hasOwnProperty(widgetId)) { + const widgetData = this._glyphMarginWidgets[widgetId]; + delete this._glyphMarginWidgets[widgetId]; + if (this._modelData && this._modelData.hasRealView) { + this._modelData.view.removeGlyphMarginWidget(widgetData); + } + } + } + public changeViewZones(callback: (accessor: editorBrowser.IViewZoneChangeAccessor) => void): void { if (!this._modelData || !this._modelData.hasRealView) { return; @@ -1718,6 +1759,12 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE view.addOverlayWidget(this._overlayWidgets[widgetId]); } + keys = Object.keys(this._glyphMarginWidgets); + for (let i = 0, len = keys.length; i < len; i++) { + const widgetId = keys[i]; + view.addGlyphMarginWidget(this._glyphMarginWidgets[widgetId]); + } + view.render(false, true); view.domNode.domNode.setAttribute('data-uri', model.uri.toString()); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 1aeebd607deaa..5b4451f15bd48 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5379,6 +5379,48 @@ declare namespace monaco.editor { getPosition(): IOverlayWidgetPosition | null; } + /** + * A glyph margin widget renders in the editor glyph margin. + */ + export interface IGlyphMarginWidget { + /** + * The class name to apply to the glyph widget. + * todo@joyceerhl is this really necessary? + */ + className: string; + /** + * Get a unique identifier of the glyph widget. + */ + getId(): string; + /** + * Get the dom node of the glyph widget. + */ + getDomNode(): HTMLElement; + /** + * Get the placement of the glyph widget. + */ + getPosition(): IGlyphMarginWidgetPosition; + } + + /** + * A position for rendering glyph margin widgets. + */ + export interface IGlyphMarginWidgetPosition { + /** + * The glyph margin lane where the widget should be shown. + */ + lane: GlyphMarginLane; + /** + * The priority order of the widget, used for determining which widget + * to render when there are multiple. + */ + zIndex: number; + /** + * The editor range that this widget applies to. + */ + range: IRange; + } + /** * Type of hit element with the mouse in the editor. */ @@ -5951,6 +5993,19 @@ declare namespace monaco.editor { * Remove an overlay widget. */ removeOverlayWidget(widget: IOverlayWidget): void; + /** + * Add a glyph margin widget. Widgets must have unique ids, otherwise they will be overwritten. + */ + addGlyphMarginWidget(widget: IGlyphMarginWidget): void; + /** + * Layout/Reposition a glyph margin widget. This is a ping to the editor to call widget.getPosition() + * and update appropriately. + */ + layoutGlyphMarginWidget(widget: IGlyphMarginWidget): void; + /** + * Remove a glyph margin widget. + */ + removeGlyphMarginWidget(widget: IGlyphMarginWidget): void; /** * Change the view zones. View zones are lost when a new model is attached to the editor. */ From f8def0d7a47c863e3ba7b6157d75732bd8e027c2 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 5 Jun 2023 21:09:04 -0700 Subject: [PATCH 2/8] Account for widgets when setting lane width --- src/vs/editor/browser/editorBrowser.ts | 1 - .../editor/browser/widget/codeEditorWidget.ts | 62 ++++++++++++++++++- .../editor/common/viewModel/viewModelImpl.ts | 50 +-------------- src/vs/monaco.d.ts | 1 - 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 35877146ede7f..16eb216f8049a 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -257,7 +257,6 @@ export interface IOverlayWidget { export interface IGlyphMarginWidget { /** * The class name to apply to the glyph widget. - * todo@joyceerhl is this really necessary? */ className: string; /** diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index fa6bb98fbfa74..8ea269e0eb8c9 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -32,7 +32,7 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { InternalEditorAction } from 'vs/editor/common/editorAction'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer, IAttachedView } from 'vs/editor/common/model'; +import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer, IAttachedView, GlyphMarginLane } from 'vs/editor/common/model'; import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -1519,13 +1519,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }; if (this._glyphMarginWidgets.hasOwnProperty(widget.getId())) { - console.warn('Overwriting an overlay widget with the same id.'); + console.warn('Overwriting a glyph margin widget with the same id.'); } this._glyphMarginWidgets[widget.getId()] = widgetData; if (this._modelData && this._modelData.hasRealView) { this._modelData.view.addGlyphMarginWidget(widgetData); + this._setGlyphMarginLaneWidth(); } } @@ -1536,6 +1537,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE widgetData.position = widget.getPosition(); if (this._modelData && this._modelData.hasRealView) { this._modelData.view.layoutGlyphMarginWidget(widgetData); + this._setGlyphMarginLaneWidth(); } } } @@ -1547,8 +1549,61 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE delete this._glyphMarginWidgets[widgetId]; if (this._modelData && this._modelData.hasRealView) { this._modelData.view.removeGlyphMarginWidget(widgetData); + this._setGlyphMarginLaneWidth(); + } + } + } + + private _setGlyphMarginLaneWidth() { + const decorations = this._modelData?.model.getAllMarginDecorations() ?? []; + decorations.push(...Object.values(this._glyphMarginWidgets).map((widget) => ({ + ownerId: 0, + id: widget.widget.getId(), + options: { glyphMargin: { position: widget.position.lane }, description: widget.widget.getId(), }, + range: new Range(widget.position.range.startLineNumber, widget.position.range.startColumn, widget.position.range.endLineNumber, widget.position.range.endColumn), + }))); + + let hasTwoLanes = false; + + // Decorations are already sorted by their start position, but protect against future changes + decorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + + let leftDecRange: Range | null = null; + let rightDecRange: Range | null = null; + for (const decoration of decorations) { + const position = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; + + if (position === GlyphMarginLane.Left && (!leftDecRange || Range.compareRangesUsingEnds(leftDecRange, decoration.range) < 0)) { + // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane + leftDecRange = decoration.range; + } + + if (position === GlyphMarginLane.Right && (!rightDecRange || Range.compareRangesUsingEnds(rightDecRange, decoration.range) < 0)) { + // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane + rightDecRange = decoration.range; + } + + if (leftDecRange && rightDecRange) { + + if (leftDecRange.endLineNumber < rightDecRange.startLineNumber) { + // there's no chance for `leftDecRange` to ever intersect something going further + leftDecRange = null; + continue; + } + + if (rightDecRange.endLineNumber < leftDecRange.startLineNumber) { + // there's no chance for `rightDecRange` to ever intersect something going further + rightDecRange = null; + continue; + } + + // leftDecRange and rightDecRange are intersecting or touching => we need two lanes + hasTwoLanes = true; + break; } } + + this._configuration.setGlyphMarginDecorationLaneCount(hasTwoLanes ? 2 : 1); } public changeViewZones(callback: (accessor: editorBrowser.IViewZoneChangeAccessor) => void): void { @@ -1722,6 +1777,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } case OutgoingViewModelEventKind.ModelDecorationsChanged: this._onDidChangeModelDecorations.fire(e.event); + if (e.event.affectsGlyphMargin) { + this._setGlyphMarginLaneWidth(); + } break; case OutgoingViewModelEventKind.ModelLanguageChanged: this._domElement.setAttribute('data-mode-id', model.getLanguageId()); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index cf61c0bac20b5..eb18dced3c1f6 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -19,7 +19,7 @@ import { Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorState, IViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; -import { EndOfLinePreference, GlyphMarginLane, IAttachedView, ICursorStateComputer, IIdentifiedSingleEditOperation, ITextModel, PositionAffinity, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { EndOfLinePreference, IAttachedView, ICursorStateComputer, IIdentifiedSingleEditOperation, ITextModel, PositionAffinity, TrackedRangeStickiness } from 'vs/editor/common/model'; import { IActiveIndentGuideInfo, BracketGuideOptions, IndentGuide } from 'vs/editor/common/textModelGuides'; import { ModelDecorationMinimapOptions, ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel'; import * as textModelEvents from 'vs/editor/common/textModelEvents'; @@ -459,54 +459,6 @@ export class ViewModel extends Disposable implements IViewModel { this._register(this.model.onDidChangeDecorations((e) => { this._decorations.onModelDecorationsChanged(); - - // Determine whether we need to resize the glyph margin - if (e.affectsGlyphMargin) { - const decorations = this.model.getAllMarginDecorations(); - - let hasTwoLanes = false; - - // Decorations are already sorted by their start position, but protect against future changes - decorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - - let leftDecRange: Range | null = null; - let rightDecRange: Range | null = null; - for (const decoration of decorations) { - const position = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; - - if (position === GlyphMarginLane.Left && (!leftDecRange || Range.compareRangesUsingEnds(leftDecRange, decoration.range) < 0)) { - // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane - leftDecRange = decoration.range; - } - - if (position === GlyphMarginLane.Right && (!rightDecRange || Range.compareRangesUsingEnds(rightDecRange, decoration.range) < 0)) { - // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane - rightDecRange = decoration.range; - } - - if (leftDecRange && rightDecRange) { - - if (leftDecRange.endLineNumber < rightDecRange.startLineNumber) { - // there's no chance for `leftDecRange` to ever intersect something going further - leftDecRange = null; - continue; - } - - if (rightDecRange.endLineNumber < leftDecRange.startLineNumber) { - // there's no chance for `rightDecRange` to ever intersect something going further - rightDecRange = null; - continue; - } - - // leftDecRange and rightDecRange are intersecting or touching => we need two lanes - hasTwoLanes = true; - break; - } - } - - this._configuration.setGlyphMarginDecorationLaneCount(hasTwoLanes ? 2 : 1); - } - this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); this._eventDispatcher.emitOutgoingEvent(new ModelDecorationsChangedEvent(e)); })); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5b4451f15bd48..47c4bc47ae113 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5385,7 +5385,6 @@ declare namespace monaco.editor { export interface IGlyphMarginWidget { /** * The class name to apply to the glyph widget. - * todo@joyceerhl is this really necessary? */ className: string; /** From 146aea8d9152e8c9b431fe880f81f124f953ea8a Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 6 Jun 2023 14:43:33 -0700 Subject: [PATCH 3/8] Account for viewzones when setting widget top --- src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 054c015663595..28283a9b623db 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -430,17 +430,18 @@ export class GlyphMarginWidgets extends ViewPart { if (lineNumber < 0 || decorations.length === 0) { continue; } - decorations.forEach((widget) => this._renderWidget(widget)); + decorations.forEach((widget) => this._renderWidget(ctx, widget)); } return; } - private _renderWidget(renderedWidget: GlyphMarginWidget): void { + private _renderWidget(ctx: RestrictedRenderingContext, renderedWidget: GlyphMarginWidget): void { renderedWidget.domNode.setClassName(`cgmr codicon ${renderedWidget.className}`); renderedWidget.domNode.setLeft(renderedWidget.left); renderedWidget.domNode.setWidth(renderedWidget.width); renderedWidget.domNode.setHeight(this._lineHeight); - renderedWidget.domNode.setTop((renderedWidget.lineNumber - 1) * this._lineHeight); + + renderedWidget.domNode.setTop(ctx.getVerticalOffsetForLineNumber(renderedWidget.lineNumber, true)); renderedWidget.domNode.setVisibility('inherit'); renderedWidget.domNode.setDisplay('block'); this.domNode.appendChild(renderedWidget.domNode); From 989f560d5b8966ece24e7c26864a3bcee5130d81 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 6 Jun 2023 16:32:45 -0700 Subject: [PATCH 4/8] Try to fix integration test failure --- src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 28283a9b623db..412be74578365 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -196,7 +196,6 @@ export class GlyphMarginWidgets extends ViewPart { this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount; this._previousRenderResult = null; this._renderResult = null; - this._context.addEventHandler(this); } public override dispose(): void { From a1a890fb8d7ea675c20098f11a3a56c69d239c91 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 13 Jun 2023 14:59:48 +0200 Subject: [PATCH 5/8] Compute glyph margin lane count at most once per frame --- src/vs/editor/browser/view.ts | 83 +++++++++++++++++-- .../viewParts/glyphMargin/glyphMargin.ts | 5 +- .../editor/browser/widget/codeEditorWidget.ts | 60 +------------- 3 files changed, 82 insertions(+), 66 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 30b12efbbb17e..82d1fb7a26969 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -5,6 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { Selection } from 'vs/editor/common/core/selection'; +import { Range } from 'vs/editor/common/core/range'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -52,6 +53,7 @@ import { inputLatency } from 'vs/base/browser/performance'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { WhitespaceOverlay } from 'vs/editor/browser/viewParts/whitespace/whitespace'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; +import { GlyphMarginLane } from 'vs/editor/common/model'; export interface IContentWidgetData { @@ -95,6 +97,7 @@ export class View extends ViewEventHandler { private readonly _overflowGuardContainer: FastDomNode; // Actual mutable state + private _shouldRecomputeGlyphMarginLanes: boolean = false; private _renderAnimationFrame: IDisposable | null; constructor( @@ -170,15 +173,15 @@ export class View extends ViewEventHandler { marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context)); - const margin = new Margin(this._context); - margin.getDomNode().appendChild(this._viewZones.marginDomNode); - margin.getDomNode().appendChild(marginViewOverlays.getDomNode()); - this._viewParts.push(margin); - // Glyph margin widgets this._glyphMarginWidgets = new GlyphMarginWidgets(this._context); this._viewParts.push(this._glyphMarginWidgets); + + const margin = new Margin(this._context); + margin.getDomNode().appendChild(this._viewZones.marginDomNode); + margin.getDomNode().appendChild(marginViewOverlays.getDomNode()); margin.getDomNode().appendChild(this._glyphMarginWidgets.domNode); + this._viewParts.push(margin); // Content widgets this._contentWidgets = new ViewContentWidgets(this._context, this.domNode); @@ -212,7 +215,6 @@ export class View extends ViewEventHandler { this._linesContent.appendChild(this._viewZones.domNode); this._linesContent.appendChild(this._viewLines.getDomNode()); this._linesContent.appendChild(this._contentWidgets.domNode); - // this._linesContent.appendChild(this._glyphMarginWidgets.domNode); this._linesContent.appendChild(this._viewCursors.getDomNode()); this._overflowGuardContainer.appendChild(margin.getDomNode()); this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); @@ -237,10 +239,70 @@ export class View extends ViewEventHandler { } private _flushAccumulatedAndRenderNow(): void { + if (this._shouldRecomputeGlyphMarginLanes) { + this._shouldRecomputeGlyphMarginLanes = false; + this._context.configuration.setGlyphMarginDecorationLaneCount(this._computeGlyphMarginLaneCount()); + } inputLatency.onRenderStart(); this._renderNow(); } + private _computeGlyphMarginLaneCount(): number { + const model = this._context.viewModel.model; + type Glyph = { range: Range; lane: GlyphMarginLane }; + let glyphs: Glyph[] = []; + + // Add all margin decorations + glyphs = glyphs.concat(model.getAllMarginDecorations().map((decoration) => { + const lane = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; + return { range: decoration.range, lane }; + })); + + // Add all glyph margin widgets + glyphs = glyphs.concat(this._glyphMarginWidgets.getWidgets().map((widget) => { + const range = model.validateRange(widget.preference.range); + return { range, lane: widget.preference.lane }; + })); + + // Sorted by their start position + glyphs.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + + let leftDecRange: Range | null = null; + let rightDecRange: Range | null = null; + for (const decoration of glyphs) { + + if (decoration.lane === GlyphMarginLane.Left && (!leftDecRange || Range.compareRangesUsingEnds(leftDecRange, decoration.range) < 0)) { + // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane + leftDecRange = decoration.range; + } + + if (decoration.lane === GlyphMarginLane.Right && (!rightDecRange || Range.compareRangesUsingEnds(rightDecRange, decoration.range) < 0)) { + // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane + rightDecRange = decoration.range; + } + + if (leftDecRange && rightDecRange) { + + if (leftDecRange.endLineNumber < rightDecRange.startLineNumber) { + // there's no chance for `leftDecRange` to ever intersect something going further + leftDecRange = null; + continue; + } + + if (rightDecRange.endLineNumber < leftDecRange.startLineNumber) { + // there's no chance for `rightDecRange` to ever intersect something going further + rightDecRange = null; + continue; + } + + // leftDecRange and rightDecRange are intersecting or touching => we need two lanes + return 2; + } + } + + return 1; + } + private _createPointerHandlerHelper(): IPointerHandlerHelper { return { viewDomNode: this.domNode.domNode, @@ -328,6 +390,12 @@ export class View extends ViewEventHandler { this._selections = e.selections; return false; } + public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { + if (e.affectsGlyphMargin) { + this._shouldRecomputeGlyphMarginLanes = true; + } + return false; + } public override onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { this.domNode.setClassName(this._getEditorClassName()); return false; @@ -561,6 +629,7 @@ export class View extends ViewEventHandler { public addGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void { this._glyphMarginWidgets.addWidget(widgetData.widget); + this._shouldRecomputeGlyphMarginLanes = true; this._scheduleRender(); } @@ -568,12 +637,14 @@ export class View extends ViewEventHandler { const newPreference = widgetData.position; const shouldRender = this._glyphMarginWidgets.setWidgetPosition(widgetData.widget, newPreference); if (shouldRender) { + this._shouldRecomputeGlyphMarginLanes = true; this._scheduleRender(); } } public removeGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void { this._glyphMarginWidgets.removeWidget(widgetData.widget); + this._shouldRecomputeGlyphMarginLanes = true; this._scheduleRender(); } diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 412be74578365..a0ea2e5a71ed6 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -199,12 +199,15 @@ export class GlyphMarginWidgets extends ViewPart { } public override dispose(): void { - this._context.removeEventHandler(this); this._renderResult = null; this._widgets = {}; super.dispose(); } + public getWidgets(): IWidgetData[] { + return Object.values(this._widgets); + } + // --- begin event handlers public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 8ea269e0eb8c9..14eac1ff18612 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -32,7 +32,7 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { InternalEditorAction } from 'vs/editor/common/editorAction'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer, IAttachedView, GlyphMarginLane } from 'vs/editor/common/model'; +import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer, IAttachedView } from 'vs/editor/common/model'; import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -1526,7 +1526,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (this._modelData && this._modelData.hasRealView) { this._modelData.view.addGlyphMarginWidget(widgetData); - this._setGlyphMarginLaneWidth(); } } @@ -1537,7 +1536,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE widgetData.position = widget.getPosition(); if (this._modelData && this._modelData.hasRealView) { this._modelData.view.layoutGlyphMarginWidget(widgetData); - this._setGlyphMarginLaneWidth(); } } } @@ -1549,63 +1547,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE delete this._glyphMarginWidgets[widgetId]; if (this._modelData && this._modelData.hasRealView) { this._modelData.view.removeGlyphMarginWidget(widgetData); - this._setGlyphMarginLaneWidth(); } } } - private _setGlyphMarginLaneWidth() { - const decorations = this._modelData?.model.getAllMarginDecorations() ?? []; - decorations.push(...Object.values(this._glyphMarginWidgets).map((widget) => ({ - ownerId: 0, - id: widget.widget.getId(), - options: { glyphMargin: { position: widget.position.lane }, description: widget.widget.getId(), }, - range: new Range(widget.position.range.startLineNumber, widget.position.range.startColumn, widget.position.range.endLineNumber, widget.position.range.endColumn), - }))); - - let hasTwoLanes = false; - - // Decorations are already sorted by their start position, but protect against future changes - decorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - - let leftDecRange: Range | null = null; - let rightDecRange: Range | null = null; - for (const decoration of decorations) { - const position = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; - - if (position === GlyphMarginLane.Left && (!leftDecRange || Range.compareRangesUsingEnds(leftDecRange, decoration.range) < 0)) { - // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane - leftDecRange = decoration.range; - } - - if (position === GlyphMarginLane.Right && (!rightDecRange || Range.compareRangesUsingEnds(rightDecRange, decoration.range) < 0)) { - // assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane - rightDecRange = decoration.range; - } - - if (leftDecRange && rightDecRange) { - - if (leftDecRange.endLineNumber < rightDecRange.startLineNumber) { - // there's no chance for `leftDecRange` to ever intersect something going further - leftDecRange = null; - continue; - } - - if (rightDecRange.endLineNumber < leftDecRange.startLineNumber) { - // there's no chance for `rightDecRange` to ever intersect something going further - rightDecRange = null; - continue; - } - - // leftDecRange and rightDecRange are intersecting or touching => we need two lanes - hasTwoLanes = true; - break; - } - } - - this._configuration.setGlyphMarginDecorationLaneCount(hasTwoLanes ? 2 : 1); - } - public changeViewZones(callback: (accessor: editorBrowser.IViewZoneChangeAccessor) => void): void { if (!this._modelData || !this._modelData.hasRealView) { return; @@ -1777,9 +1722,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } case OutgoingViewModelEventKind.ModelDecorationsChanged: this._onDidChangeModelDecorations.fire(e.event); - if (e.event.affectsGlyphMargin) { - this._setGlyphMarginLaneWidth(); - } break; case OutgoingViewModelEventKind.ModelLanguageChanged: this._domElement.setAttribute('data-mode-id', model.getLanguageId()); From fda41b80d37bbdae27d36bfe8c936cc369d3993a Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 13 Jun 2023 08:28:15 -0700 Subject: [PATCH 6/8] Address comments --- src/vs/editor/browser/editorBrowser.ts | 4 ---- .../browser/viewParts/glyphMargin/glyphMargin.ts | 12 +++++++++--- src/vs/monaco.d.ts | 4 ---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 16eb216f8049a..f174bbfdf666d 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -255,10 +255,6 @@ export interface IOverlayWidget { * A glyph margin widget renders in the editor glyph margin. */ export interface IGlyphMarginWidget { - /** - * The class name to apply to the glyph widget. - */ - className: string; /** * Get a unique identifier of the glyph widget. */ diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index a0ea2e5a71ed6..72dd25d2d3468 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -6,6 +6,7 @@ import 'vs/css!./glyphMargin'; import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; +import { Range } from 'vs/editor/common/core/range'; import { IGlyphMarginWidget, IGlyphMarginWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; @@ -262,7 +263,9 @@ export class GlyphMarginWidgets extends ViewPart { public setWidgetPosition(widget: IGlyphMarginWidget, preference: IGlyphMarginWidgetPosition): boolean { const myWidget = this._widgets[widget.getId()]; - if (myWidget.preference === preference) { + if (myWidget.preference.lane === preference.lane + && myWidget.preference.zIndex === preference.zIndex + && Range.equalsRange(myWidget.preference.range, preference.range)) { return false; } @@ -301,7 +304,7 @@ export class GlyphMarginWidgets extends ViewPart { const widgets = Object.values(this._widgets); for (let i = 0, len = widgets.length; i < len; i++) { const w = widgets[i]; - const glyphMarginClassName = w.widget.className; + const glyphMarginClassName = w.widget.getDomNode().className; if (glyphMarginClassName) { r[rLen++] = new DecorationToRender(w.preference.range.startLineNumber, w.preference.range.endLineNumber, glyphMarginClassName, w.preference.zIndex, w.preference.lane, w.domNode); } @@ -438,7 +441,10 @@ export class GlyphMarginWidgets extends ViewPart { } private _renderWidget(ctx: RestrictedRenderingContext, renderedWidget: GlyphMarginWidget): void { - renderedWidget.domNode.setClassName(`cgmr codicon ${renderedWidget.className}`); + const className = renderedWidget.className.includes('cgmr codicon ') + ? renderedWidget.className + : `cgmr codicon ${renderedWidget.className}`; + renderedWidget.domNode.setClassName(className); renderedWidget.domNode.setLeft(renderedWidget.left); renderedWidget.domNode.setWidth(renderedWidget.width); renderedWidget.domNode.setHeight(this._lineHeight); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 47c4bc47ae113..a3e9b38bfe495 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5383,10 +5383,6 @@ declare namespace monaco.editor { * A glyph margin widget renders in the editor glyph margin. */ export interface IGlyphMarginWidget { - /** - * The class name to apply to the glyph widget. - */ - className: string; /** * Get a unique identifier of the glyph widget. */ From c2a34688050e0746af41969ef639ad9c03156480 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 15 Jun 2023 15:36:01 -0700 Subject: [PATCH 7/8] Fix glyph widget top when there are codelenses --- src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 72dd25d2d3468..7789f93f6efa4 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -449,7 +449,8 @@ export class GlyphMarginWidgets extends ViewPart { renderedWidget.domNode.setWidth(renderedWidget.width); renderedWidget.domNode.setHeight(this._lineHeight); - renderedWidget.domNode.setTop(ctx.getVerticalOffsetForLineNumber(renderedWidget.lineNumber, true)); + const top = ctx.viewportData.relativeVerticalOffset[renderedWidget.lineNumber - ctx.viewportData.startLineNumber]; + renderedWidget.domNode.setTop(top); renderedWidget.domNode.setVisibility('inherit'); renderedWidget.domNode.setDisplay('block'); this.domNode.appendChild(renderedWidget.domNode); From 3b7ce0f8c402a03d18e605e43cfb1caed2fa4b24 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 16 Jun 2023 14:47:49 +0200 Subject: [PATCH 8/8] Simplify rendering code --- .../viewParts/glyphMargin/glyphMargin.ts | 460 ++++++++++-------- .../linesDecorations/linesDecorations.ts | 4 +- .../marginDecorations/marginDecorations.ts | 4 +- 3 files changed, 255 insertions(+), 213 deletions(-) diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 7789f93f6efa4..919d0aad1f982 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -3,53 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./glyphMargin'; -import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { Range } from 'vs/editor/common/core/range'; +import { ArrayQueue } from 'vs/base/common/arrays'; +import 'vs/css!./glyphMargin'; import { IGlyphMarginWidget, IGlyphMarginWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; +import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; -import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; import * as viewEvents from 'vs/editor/common/viewEvents'; -import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; - -export class LineRenderedWidgets { - - private readonly lines: GlyphMarginWidget[][] = []; - - public add(line: number, decoration: GlyphMarginWidget) { - while (line >= this.lines.length) { - this.lines.push([]); - } - this.lines[line].push(decoration); - } - - public getLineDecorations(lineIndex: number): GlyphMarginWidget[] { - if (lineIndex < this.lines.length) { - return this.lines[lineIndex]; - } - return []; - } -} - -export class GlyphMarginWidget { - - constructor( - public domNode: FastDomNode, - public className: string, - public lineNumber: number, - public left: number, - public width: number, - public isManaged: boolean, - ) { - this.domNode.setPosition('absolute'); - this.domNode.setDisplay('none'); - this.domNode.setVisibility('hidden'); - this.domNode.setMaxWidth(width); - } -} +import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; +/** + * Represents a decoration that should be shown along the lines from `startLineNumber` to `endLineNumber`. + * This can end up producing multiple `LineDecorationToRender`. + */ export class DecorationToRender { _decorationToRenderBrand: void = undefined; @@ -57,69 +26,59 @@ export class DecorationToRender { public endLineNumber: number; public className: string; public readonly zIndex: number; - public readonly decorationLane: number; - public readonly domNode: FastDomNode | undefined; - constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex?: number, decorationLane?: number, domNode?: FastDomNode) { + constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex: number | undefined) { this.startLineNumber = +startLineNumber; this.endLineNumber = +endLineNumber; this.className = String(className); this.zIndex = zIndex ?? 0; - this.decorationLane = decorationLane ?? 1; - this.domNode = domNode; } } -export class RenderedDecoration { +/** + * A decoration that should be shown along a line. + */ +export class LineDecorationToRender { constructor( public readonly className: string, public readonly zIndex: number, - public readonly domNode?: FastDomNode, ) { } } -export class LineRenderedDecorations { +/** + * Decorations to render on a visible line. + */ +export class VisibleLineDecorationsToRender { - private readonly lanes: RenderedDecoration[][] = []; + private readonly decorations: LineDecorationToRender[] = []; - public add(lane: number, decoration: RenderedDecoration) { - while (lane >= this.lanes.length) { - this.lanes.push([]); - } - this.lanes[lane].push(decoration); + public add(decoration: LineDecorationToRender) { + this.decorations.push(decoration); } - public getLaneDecorations(laneIndex: number): RenderedDecoration[] { - if (laneIndex < this.lanes.length) { - return this.lanes[laneIndex]; - } - return []; - } - - public isEmpty(): boolean { - for (const lane of this.lanes) { - if (lane.length > 0) { - return false; - } - } - return true; + public getDecorations(): LineDecorationToRender[] { + return this.decorations; } } export abstract class DedupOverlay extends DynamicViewOverlay { - protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): LineRenderedDecorations[] { + /** + * Returns an array with an element for each visible line number. + */ + protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[]): VisibleLineDecorationsToRender[] { - const output: LineRenderedDecorations[] = []; + const output: VisibleLineDecorationsToRender[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - output[lineIndex] = new LineRenderedDecorations(); + output[lineIndex] = new VisibleLineDecorationsToRender(); } if (decorations.length === 0) { return output; } + // Sort decorations by className, then by startLineNumber and then by endLineNumber decorations.sort((a, b) => { if (a.className === b.className) { if (a.startLineNumber === b.startLineNumber) { @@ -138,9 +97,9 @@ export abstract class DedupOverlay extends DynamicViewOverlay { const zIndex = d.zIndex; let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber; const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber; - const lane = Math.min(d.decorationLane, decorationLaneCount); if (prevClassName === className) { + // Here we avoid rendering the same className multiple times on the same line startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex); prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex); } else { @@ -149,7 +108,7 @@ export abstract class DedupOverlay extends DynamicViewOverlay { } for (let i = startLineIndex; i <= prevEndLineIndex; i++) { - output[i].add(lane, new RenderedDecoration(className, zIndex, d.domNode)); + output[i].add(new LineDecorationToRender(className, zIndex)); } } @@ -157,12 +116,6 @@ export abstract class DedupOverlay extends DynamicViewOverlay { } } -export interface IWidgetData { - widget: IGlyphMarginWidget; - preference: IGlyphMarginWidgetPosition; - domNode: FastDomNode; -} - export class GlyphMarginWidgets extends ViewPart { public domNode: FastDomNode; @@ -173,8 +126,8 @@ export class GlyphMarginWidgets extends ViewPart { private _glyphMarginWidth: number; private _glyphMarginDecorationLaneCount: number; - private _previousRenderResult: LineRenderedWidgets | null; - private _renderResult: LineRenderedWidgets | null; + private _managedDomNodes: FastDomNode[]; + private _decorationGlyphsToRender: DecorationBasedGlyph[]; private _widgets: { [key: string]: IWidgetData } = {}; @@ -195,12 +148,13 @@ export class GlyphMarginWidgets extends ViewPart { this._glyphMarginLeft = layoutInfo.glyphMarginLeft; this._glyphMarginWidth = layoutInfo.glyphMarginWidth; this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount; - this._previousRenderResult = null; - this._renderResult = null; + this._managedDomNodes = []; + this._decorationGlyphsToRender = []; } public override dispose(): void { - this._renderResult = null; + this._managedDomNodes = []; + this._decorationGlyphsToRender = []; this._widgets = {}; super.dispose(); } @@ -244,17 +198,21 @@ export class GlyphMarginWidgets extends ViewPart { } // --- end event handlers + // --- begin widget management + public addWidget(widget: IGlyphMarginWidget): void { const domNode = createFastDomNode(widget.getDomNode()); this._widgets[widget.getId()] = { widget: widget, preference: widget.getPosition(), - domNode: domNode + domNode: domNode, + renderInfo: null }; domNode.setPosition('absolute'); + domNode.setDisplay('none'); domNode.setAttribute('widgetId', widget.getId()); this.domNode.appendChild(domNode); @@ -288,171 +246,255 @@ export class GlyphMarginWidgets extends ViewPart { } // --- end widget management - protected _getDecorations(ctx: RenderingContext): DecorationToRender[] { + + private _collectDecorationBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void { + const visibleStartLineNumber = ctx.visibleRange.startLineNumber; + const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const decorations = ctx.getDecorationsInViewport(); - const r: DecorationToRender[] = []; - let rLen = 0; - for (let i = 0, len = decorations.length; i < len; i++) { - const d = decorations[i]; + + for (const d of decorations) { const glyphMarginClassName = d.options.glyphMarginClassName; - const zIndex = d.options.zIndex; - const lane = d.options.glyphMargin?.position; - if (glyphMarginClassName) { - r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, glyphMarginClassName, zIndex, lane); + if (!glyphMarginClassName) { + continue; } - } - const widgets = Object.values(this._widgets); - for (let i = 0, len = widgets.length; i < len; i++) { - const w = widgets[i]; - const glyphMarginClassName = w.widget.getDomNode().className; - if (glyphMarginClassName) { - r[rLen++] = new DecorationToRender(w.preference.range.startLineNumber, w.preference.range.endLineNumber, glyphMarginClassName, w.preference.zIndex, w.preference.lane, w.domNode); + + const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber); + const endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber); + const lane = Math.min(d.options.glyphMargin?.position ?? 1, this._glyphMarginDecorationLaneCount); + const zIndex = d.options.zIndex ?? 0; + + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + requests.push(new DecorationBasedGlyphRenderRequest(lineNumber, lane, zIndex, glyphMarginClassName)); } } - return r; } - protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): LineRenderedDecorations[] { + private _collectWidgetBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void { + const visibleStartLineNumber = ctx.visibleRange.startLineNumber; + const visibleEndLineNumber = ctx.visibleRange.endLineNumber; - const output: LineRenderedDecorations[] = []; - for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { - const lineIndex = lineNumber - visibleStartLineNumber; - output[lineIndex] = new LineRenderedDecorations(); - } + for (const widget of Object.values(this._widgets)) { + const range = widget.preference.range; + if (range.endLineNumber < visibleStartLineNumber || range.startLineNumber > visibleEndLineNumber) { + // The widget is not in the viewport + continue; + } - if (decorations.length === 0) { - return output; + // The widget is in the viewport, find a good line for it + const widgetLineNumber = Math.max(range.startLineNumber, visibleStartLineNumber); + const lane = Math.min(widget.preference.lane, this._glyphMarginDecorationLaneCount); + requests.push(new WidgetBasedGlyphRenderRequest(widgetLineNumber, lane, widget.preference.zIndex, widget)); } + } - decorations.sort((a, b) => { - if (a.className === b.className) { - if (a.startLineNumber === b.startLineNumber) { - return a.endLineNumber - b.endLineNumber; - } - return a.startLineNumber - b.startLineNumber; - } - return (a.className < b.className ? -1 : 1); - }); + private _collectSortedGlyphRenderRequests(ctx: RenderingContext): GlyphRenderRequest[] { - let prevClassName: string | null = null; - let prevEndLineIndex = 0; - for (let i = 0, len = decorations.length; i < len; i++) { - const d = decorations[i]; - const className = d.className; - const zIndex = d.zIndex; - let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber; - const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber; - const lane = Math.min(d.decorationLane, decorationLaneCount); + const requests: GlyphRenderRequest[] = []; - if (prevClassName === className) { - startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex); - prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex); - } else { - prevClassName = className; - prevEndLineIndex = endLineIndex; - } + this._collectDecorationBasedGlyphRenderRequest(ctx, requests); + this._collectWidgetBasedGlyphRenderRequest(ctx, requests); - for (let i = startLineIndex; i <= prevEndLineIndex; i++) { - output[i].add(lane, new RenderedDecoration(className, zIndex, d.domNode)); + // sort requests by lineNumber ASC, lane ASC, zIndex DESC, type DESC (widgets first), className ASC + // don't change this sort unless you understand `prepareRender` below. + requests.sort((a, b) => { + if (a.lineNumber === b.lineNumber) { + if (a.lane === b.lane) { + if (a.zIndex === b.zIndex) { + if (b.type === a.type) { + if (a.type === GlyphRenderRequestType.Decoration && b.type === GlyphRenderRequestType.Decoration) { + return (a.className < b.className ? -1 : 1); + } + return 0; + } + return b.type - a.type; + } + return b.zIndex - a.zIndex; + } + return a.lane - b.lane; } - } + return a.lineNumber - b.lineNumber; + }); - return output; + return requests; } + /** + * Will store render information in each widget's renderInfo and in `_decorationGlyphsToRender`. + */ public prepareRender(ctx: RenderingContext): void { if (!this._glyphMargin) { - this._renderResult = null; + this._decorationGlyphsToRender = []; return; } - const visibleStartLineNumber = ctx.visibleRange.startLineNumber; - const visibleEndLineNumber = ctx.visibleRange.endLineNumber; - const decorationsToRender = this._getDecorations(ctx); - const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, decorationsToRender, this._glyphMarginDecorationLaneCount); + for (const widget of Object.values(this._widgets)) { + widget.renderInfo = null; + } - const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount)); + const requests = new ArrayQueue(this._collectSortedGlyphRenderRequests(ctx)); + const decorationGlyphsToRender: DecorationBasedGlyph[] = []; + while (requests.length > 0) { + const first = requests.peek(); + if (!first) { + // not possible + break; + } - const output = new LineRenderedWidgets(); - for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { - const lineIndex = lineNumber - visibleStartLineNumber; - const renderInfo = toRender[lineIndex]; + // Requests are sorted by lineNumber and lane, so we read all requests for this particular location + const requestsAtLocation = requests.takeWhile((el) => el.lineNumber === first.lineNumber && el.lane === first.lane); + if (!requestsAtLocation || requestsAtLocation.length === 0) { + // not possible + break; + } - if (renderInfo.isEmpty()) { - continue; - } else { - for (let lane = 1; lane <= this._glyphMarginDecorationLaneCount; lane += 1) { - const decorations = renderInfo.getLaneDecorations(lane); - if (decorations.length === 0) { - continue; + const winner = requestsAtLocation[0]; + if (winner.type === GlyphRenderRequestType.Decoration) { + // combine all decorations with the same z-index + + const classNames: string[] = []; + // requests are sorted by zIndex, type, and className so we can dedup className by looking at the previous one + for (const request of requestsAtLocation) { + if (request.zIndex !== winner.zIndex || request.type !== winner.type) { + break; } - decorations.sort((a, b) => b.zIndex - a.zIndex); - // Render winning decorations with the same zIndex together - const winningDecoration: RenderedDecoration = decorations[0]; - const winningDecorationClassNames = [winningDecoration.className]; - for (let i = 1; i < decorations.length; i += 1) { - const decoration = decorations[i]; - if (decoration.zIndex !== winningDecoration.zIndex) { - break; - } - winningDecorationClassNames.push(decoration.className); + if (classNames.length === 0 || classNames[classNames.length - 1] !== request.className) { + classNames.push(request.className); } - const left = this._glyphMarginLeft + (lane - 1) * this._lineHeight; - output.add(lineNumber, new GlyphMarginWidget(winningDecoration.domNode ?? createFastDomNode(document.createElement('div')), winningDecorationClassNames.join(' '), lineNumber, left, width, winningDecoration.domNode !== undefined)); } + + decorationGlyphsToRender.push(winner.accept(classNames.join(' '))); // TODO@joyceerhl Implement overflow for remaining decorations + } else { + // widgets cannot be combined + winner.widget.renderInfo = { + lineNumber: winner.lineNumber, + lane: winner.lane, + }; } } - this._previousRenderResult = this._renderResult; - this._renderResult = output; + this._decorationGlyphsToRender = decorationGlyphsToRender; } public render(ctx: RestrictedRenderingContext): void { - const { startLineNumber, endLineNumber } = ctx.viewportData; - - // Clean up any existing render results - if (this._previousRenderResult) { - for (let lineNumber = startLineNumber; lineNumber < endLineNumber; lineNumber += 1) { - const decorations = this._previousRenderResult.getLineDecorations(lineNumber); - if (lineNumber < 0 || decorations.length === 0) { - continue; - } - decorations.forEach((widget) => { - widget.domNode.setDisplay('none'); - if (!widget.isManaged) { - widget.domNode.domNode.parentNode?.removeChild(widget.domNode.domNode); - } - }); + if (!this._glyphMargin) { + for (const widget of Object.values(this._widgets)) { + widget.domNode.setDisplay('none'); } + while (this._managedDomNodes.length > 0) { + const domNode = this._managedDomNodes.pop(); + domNode?.domNode.remove(); + } + return; } - if (!this._renderResult) { - return; + const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount)); + + // Render widgets + for (const widget of Object.values(this._widgets)) { + if (!widget.renderInfo) { + // this widget is not visible + widget.domNode.setDisplay('none'); + } else { + const top = ctx.viewportData.relativeVerticalOffset[widget.renderInfo.lineNumber - ctx.viewportData.startLineNumber]; + const left = this._glyphMarginLeft + (widget.renderInfo.lane - 1) * this._lineHeight; + + widget.domNode.setDisplay('block'); + widget.domNode.setTop(top); + widget.domNode.setLeft(left); + widget.domNode.setWidth(width); + widget.domNode.setHeight(this._lineHeight); + } } - // Render new render results - for (let lineNumber = startLineNumber; lineNumber < endLineNumber; lineNumber += 1) { - const decorations = this._renderResult.getLineDecorations(lineNumber); - if (lineNumber < 0 || decorations.length === 0) { - continue; + // Render decorations, reusing previous dom nodes as possible + for (let i = 0; i < this._decorationGlyphsToRender.length; i++) { + const dec = this._decorationGlyphsToRender[i]; + const top = ctx.viewportData.relativeVerticalOffset[dec.lineNumber - ctx.viewportData.startLineNumber]; + const left = this._glyphMarginLeft + (dec.lane - 1) * this._lineHeight; + + let domNode: FastDomNode; + if (i < this._managedDomNodes.length) { + domNode = this._managedDomNodes[i]; + } else { + domNode = createFastDomNode(document.createElement('div')); + this._managedDomNodes.push(domNode); + this.domNode.appendChild(domNode); } - decorations.forEach((widget) => this._renderWidget(ctx, widget)); + + domNode.setClassName(`cgmr codicon ` + dec.combinedClassName); + domNode.setPosition(`absolute`); + domNode.setTop(top); + domNode.setLeft(left); + domNode.setWidth(width); + domNode.setHeight(this._lineHeight); + } + + // remove extra dom nodes + while (this._managedDomNodes.length > this._decorationGlyphsToRender.length) { + const domNode = this._managedDomNodes.pop(); + domNode?.domNode.remove(); } - return; } +} - private _renderWidget(ctx: RestrictedRenderingContext, renderedWidget: GlyphMarginWidget): void { - const className = renderedWidget.className.includes('cgmr codicon ') - ? renderedWidget.className - : `cgmr codicon ${renderedWidget.className}`; - renderedWidget.domNode.setClassName(className); - renderedWidget.domNode.setLeft(renderedWidget.left); - renderedWidget.domNode.setWidth(renderedWidget.width); - renderedWidget.domNode.setHeight(this._lineHeight); - - const top = ctx.viewportData.relativeVerticalOffset[renderedWidget.lineNumber - ctx.viewportData.startLineNumber]; - renderedWidget.domNode.setTop(top); - renderedWidget.domNode.setVisibility('inherit'); - renderedWidget.domNode.setDisplay('block'); - this.domNode.appendChild(renderedWidget.domNode); +export interface IWidgetData { + widget: IGlyphMarginWidget; + preference: IGlyphMarginWidgetPosition; + domNode: FastDomNode; + /** + * it will contain the location where to render the widget + * or null if the widget is not visible + */ + renderInfo: IRenderInfo | null; +} + +export interface IRenderInfo { + lineNumber: number; + lane: number; +} + +const enum GlyphRenderRequestType { + Decoration = 0, + Widget = 1 +} + +/** + * A request to render a decoration in the glyph margin at a certain location. + */ +class DecorationBasedGlyphRenderRequest { + public readonly type = GlyphRenderRequestType.Decoration; + + constructor( + public readonly lineNumber: number, + public readonly lane: number, + public readonly zIndex: number, + public readonly className: string, + ) { } + + accept(combinedClassName: string): DecorationBasedGlyph { + return new DecorationBasedGlyph(this.lineNumber, this.lane, combinedClassName); } } + +/** + * A request to render a widget in the glyph margin at a certain location. + */ +class WidgetBasedGlyphRenderRequest { + public readonly type = GlyphRenderRequestType.Widget; + + constructor( + public readonly lineNumber: number, + public readonly lane: number, + public readonly zIndex: number, + public readonly widget: IWidgetData, + ) { } +} + +type GlyphRenderRequest = DecorationBasedGlyphRenderRequest | WidgetBasedGlyphRenderRequest; + +class DecorationBasedGlyph { + constructor( + public readonly lineNumber: number, + public readonly lane: number, + public readonly combinedClassName: string + ) { } +} diff --git a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts index 9ed72e6acf0bf..95d8caaed5123 100644 --- a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts +++ b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts @@ -91,7 +91,7 @@ export class LinesDecorationsOverlay extends DedupOverlay { public prepareRender(ctx: RenderingContext): void { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; - const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx), 1); + const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx)); const left = this._decorationsLeft.toString(); const width = this._decorationsWidth.toString(); @@ -100,7 +100,7 @@ export class LinesDecorationsOverlay extends DedupOverlay { const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - const decorations = toRender[lineIndex].getLaneDecorations(1); // there is only one lane, see _render call above + const decorations = toRender[lineIndex].getDecorations(); let lineOutput = ''; for (const decoration of decorations) { lineOutput += '
';