From adaad1a6cd5cada95bb5b4ff2eb075f8accb0e09 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 14 Apr 2023 07:56:01 -0700 Subject: [PATCH 1/8] Add fast path for margin decoration access --- .../editor/browser/view/renderingContext.ts | 4 +- .../viewParts/glyphMargin/glyphMargin.ts | 2 +- src/vs/editor/common/model.ts | 10 +++- src/vs/editor/common/model/intervalTree.ts | 35 +++++++++--- src/vs/editor/common/model/textModel.ts | 56 +++++++++++-------- .../viewLayout/viewLinesViewportData.ts | 4 +- src/vs/editor/common/viewModel.ts | 2 +- .../common/viewModel/viewModelDecorations.ts | 16 +++--- .../editor/common/viewModel/viewModelImpl.ts | 4 +- .../editor/common/viewModel/viewModelLines.ts | 10 ++-- .../test/common/model/intervalTree.test.ts | 4 +- src/vs/monaco.d.ts | 9 ++- .../viewModel/notebookViewModelImpl.ts | 8 +-- 13 files changed, 105 insertions(+), 59 deletions(-) diff --git a/src/vs/editor/browser/view/renderingContext.ts b/src/vs/editor/browser/view/renderingContext.ts index 035cf58878821..e3db86ed71cb7 100644 --- a/src/vs/editor/browser/view/renderingContext.ts +++ b/src/vs/editor/browser/view/renderingContext.ts @@ -61,8 +61,8 @@ export abstract class RestrictedRenderingContext { return this._viewLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones); } - public getDecorationsInViewport(): ViewModelDecoration[] { - return this.viewportData.getDecorationsInViewport(); + public getDecorationsInViewport(onlyMarginDecorations?: boolean): ViewModelDecoration[] { + return this.viewportData.getDecorationsInViewport(onlyMarginDecorations); } } diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index f7ef3e47d64cc..e3a57d6d63228 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -144,7 +144,7 @@ export class GlyphMarginOverlay extends DedupOverlay { // --- end event handlers protected _getDecorations(ctx: RenderingContext): DecorationToRender[] { - const decorations = ctx.getDecorationsInViewport(); + const decorations = ctx.getDecorationsInViewport(true); const r: DecorationToRender[] = []; let rLen = 0; for (let i = 0, len = decorations.length; i < len; i++) { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index c58e1f814589b..4047585aeb0e0 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -974,9 +974,11 @@ export interface ITextModel { * @param range The range to search in * @param ownerId If set, it will ignore decorations belonging to other owners. * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). + * @param onlyMinimapDecorations If set, it will return only decorations that render in the minimap. + * @param onlyMarginDecorations If set, it will return only decorations that render in the glyph margin. * @return An array with the decorations */ - getDecorationsInRange(range: IRange, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; + getDecorationsInRange(range: IRange, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean, onlyMarginDecorations?: boolean): IModelDecoration[]; /** * Gets all the decorations as an array. @@ -985,6 +987,12 @@ export interface ITextModel { */ getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + /** + * Gets all decorations that render in the glyph margin as an array. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getAllMarginDecorations(ownerId?: number): IModelDecoration[]; + /** * Gets all the decorations that should be rendered in the overview ruler as an array. * @param ownerId If set, it will ignore decorations belonging to other owners. diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index ce7ffeaa1ca49..ff5d9105dd2c7 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -47,6 +47,10 @@ const enum Constants { CollapseOnReplaceEditMaskInverse = 0b11011111, CollapseOnReplaceEditOffset = 5, + IsMarginMask = 0b01000000, + IsMarginMaskInverse = 0b10111111, + IsMarginOffset = 6, + /** * Due to how deletion works (in order to avoid always walking the right subtree of the deleted node), * the deltas for nodes can grow and shrink dramatically. It has been observed, in practice, that unless @@ -94,6 +98,14 @@ function setNodeIsForValidation(node: IntervalNode, value: boolean): void { (node.metadata & Constants.IsForValidationMaskInverse) | ((value ? 1 : 0) << Constants.IsForValidationOffset) ); } +function getNodeIsInGlyphMargin(node: IntervalNode): boolean { + return ((node.metadata & Constants.IsMarginMask) >>> Constants.IsMarginOffset) === 1; +} +function setNodeIsInGlyphMargin(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.IsMarginMaskInverse) | ((value ? 1 : 0) << Constants.IsMarginOffset) + ); +} function getNodeStickiness(node: IntervalNode): TrackedRangeStickiness { return ((node.metadata & Constants.StickinessMask) >>> Constants.StickinessOffset); } @@ -157,6 +169,7 @@ export class IntervalNode { this.ownerId = 0; this.options = null!; setNodeIsForValidation(this, false); + setNodeIsInGlyphMargin(this, false); _setNodeStickiness(this, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); setCollapseOnReplaceEdit(this, false); @@ -186,6 +199,7 @@ export class IntervalNode { || className === ClassName.EditorWarningDecoration || className === ClassName.EditorInfoDecoration )); + setNodeIsInGlyphMargin(this, this.options.glyphMarginClassName !== null); _setNodeStickiness(this, this.options.stickiness); setCollapseOnReplaceEdit(this, this.options.collapseOnReplaceEdit); } @@ -222,18 +236,18 @@ export class IntervalTree { this.requestNormalizeDelta = false; } - public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] { if (this.root === SENTINEL) { return []; } - return intervalSearch(this, start, end, filterOwnerId, filterOutValidation, cachedVersionId); + return intervalSearch(this, start, end, filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); } - public search(filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + public search(filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] { if (this.root === SENTINEL) { return []; } - return search(this, filterOwnerId, filterOutValidation, cachedVersionId); + return search(this, filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); } /** @@ -305,7 +319,7 @@ export class IntervalTree { } public getAllInOrder(): IntervalNode[] { - return search(this, 0, false, 0); + return search(this, 0, false, 0, false); } private _normalizeDeltaIfNecessary(): void { @@ -680,7 +694,7 @@ function collectNodesPostOrder(T: IntervalTree): IntervalNode[] { return result; } -function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { +function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] { let node = T.root; let delta = 0; let nodeStart = 0; @@ -718,6 +732,10 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo if (filterOutValidation && getNodeIsForValidation(node)) { include = false; } + if (onlyMarginDecorations && !getNodeIsInGlyphMargin(node)) { + include = false; + } + if (include) { result[resultLen++] = node; } @@ -737,7 +755,7 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo return result; } -function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { +function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] { // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree // Now, it is known that two intervals A and B overlap only when both // A.low <= B.high and A.high >= B.low. When searching the trees for @@ -803,6 +821,9 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num if (filterOutValidation && getNodeIsForValidation(node)) { include = false; } + if (onlyMarginDecorations && !getNodeIsInGlyphMargin(node)) { + include = false; + } if (include) { result[resultLen++] = node; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 30e4690d885be..a12ac88240b7c 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1706,28 +1706,28 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this.getLinesDecorations(lineNumber, lineNumber, ownerId, filterOutValidation); } - public getLinesDecorations(_startLineNumber: number, _endLineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] { + public getLinesDecorations(_startLineNumber: number, _endLineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false, onlyMarginDecorations: boolean = false): model.IModelDecoration[] { const lineCount = this.getLineCount(); const startLineNumber = Math.min(lineCount, Math.max(1, _startLineNumber)); const endLineNumber = Math.min(lineCount, Math.max(1, _endLineNumber)); const endColumn = this.getLineMaxColumn(endLineNumber); const range = new Range(startLineNumber, 1, endLineNumber, endColumn); - const decorations = this._getDecorationsInRange(range, ownerId, filterOutValidation); + const decorations = this._getDecorationsInRange(range, ownerId, filterOutValidation, onlyMarginDecorations); pushMany(decorations, this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation)); return decorations; } - public getDecorationsInRange(range: IRange, ownerId: number = 0, filterOutValidation: boolean = false, onlyMinimapDecorations: boolean = false): model.IModelDecoration[] { + public getDecorationsInRange(range: IRange, ownerId: number = 0, filterOutValidation: boolean = false, onlyMinimapDecorations: boolean = false, onlyMarginDecorations: boolean = false): model.IModelDecoration[] { const validatedRange = this.validateRange(range); - const decorations = this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation); + const decorations = this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation, onlyMarginDecorations); pushMany(decorations, this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, onlyMinimapDecorations)); return decorations; } public getOverviewRulerDecorations(ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] { - return this._decorationsTree.getAll(this, ownerId, filterOutValidation, true); + return this._decorationsTree.getAll(this, ownerId, filterOutValidation, true, false); } public getInjectedTextDecorations(ownerId: number = 0): model.IModelDecoration[] { @@ -1743,15 +1743,19 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] { - let result = this._decorationsTree.getAll(this, ownerId, filterOutValidation, false); + let result = this._decorationsTree.getAll(this, ownerId, filterOutValidation, false, false); result = result.concat(this._decorationProvider.getAllDecorations(ownerId, filterOutValidation)); return result; } - private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): model.IModelDecoration[] { + public getAllMarginDecorations(ownerId: number = 0): model.IModelDecoration[] { + return this._decorationsTree.getAll(this, ownerId, false, false, true); + } + + private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean, onlyMarginDecorations: boolean): model.IModelDecoration[] { const startOffset = this._buffer.getOffsetAt(filterRange.startLineNumber, filterRange.startColumn); const endOffset = this._buffer.getOffsetAt(filterRange.endLineNumber, filterRange.endColumn); - return this._decorationsTree.getAllInInterval(this, startOffset, endOffset, filterOwnerId, filterOutValidation); + return this._decorationsTree.getAllInInterval(this, startOffset, endOffset, filterOwnerId, filterOutValidation, onlyMarginDecorations); } public getRangeAt(start: number, end: number): Range { @@ -2013,7 +2017,7 @@ class DecorationsTrees { } public ensureAllNodesHaveRanges(host: IDecorationsTreesHost): void { - this.getAll(host, 0, false, false); + this.getAll(host, 0, false, false, false); } private _ensureNodesHaveRanges(host: IDecorationsTreesHost, nodes: IntervalNode[]): model.IModelDecoration[] { @@ -2025,44 +2029,48 @@ class DecorationsTrees { return nodes; } - public getAllInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number, filterOutValidation: boolean): model.IModelDecoration[] { + public getAllInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, onlyMarginDecorations: boolean): model.IModelDecoration[] { const versionId = host.getVersionId(); - const result = this._intervalSearch(start, end, filterOwnerId, filterOutValidation, versionId); + const result = this._intervalSearch(start, end, filterOwnerId, filterOutValidation, versionId, onlyMarginDecorations); return this._ensureNodesHaveRanges(host, result); } - private _intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { - const r0 = this._decorationsTree0.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); - const r1 = this._decorationsTree1.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); - const r2 = this._injectedTextDecorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); + private _intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] { + const r0 = this._decorationsTree0.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); + const r1 = this._decorationsTree1.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); + const r2 = this._injectedTextDecorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); return r0.concat(r1).concat(r2); } public getInjectedTextInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number): model.IModelDecoration[] { const versionId = host.getVersionId(); - const result = this._injectedTextDecorationsTree.intervalSearch(start, end, filterOwnerId, false, versionId); + const result = this._injectedTextDecorationsTree.intervalSearch(start, end, filterOwnerId, false, versionId, false); return this._ensureNodesHaveRanges(host, result).filter((i) => i.options.showIfCollapsed || !i.range.isEmpty()); } public getAllInjectedText(host: IDecorationsTreesHost, filterOwnerId: number): model.IModelDecoration[] { const versionId = host.getVersionId(); - const result = this._injectedTextDecorationsTree.search(filterOwnerId, false, versionId); + const result = this._injectedTextDecorationsTree.search(filterOwnerId, false, versionId, false); return this._ensureNodesHaveRanges(host, result).filter((i) => i.options.showIfCollapsed || !i.range.isEmpty()); } - public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean): model.IModelDecoration[] { + public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, onlyMarginDecorations: boolean): model.IModelDecoration[] { const versionId = host.getVersionId(); - const result = this._search(filterOwnerId, filterOutValidation, overviewRulerOnly, versionId); + const result = this._search(filterOwnerId, filterOutValidation, overviewRulerOnly, versionId, onlyMarginDecorations); return this._ensureNodesHaveRanges(host, result); } - private _search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { + private _search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] { if (overviewRulerOnly) { - return this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId); + return this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); + } else if (onlyMarginDecorations) { + const r0 = this._decorationsTree0.search(filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); + const r1 = this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); + return r0.concat(r1); } else { - const r0 = this._decorationsTree0.search(filterOwnerId, filterOutValidation, cachedVersionId); - const r1 = this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId); - const r2 = this._injectedTextDecorationsTree.search(filterOwnerId, filterOutValidation, cachedVersionId); + const r0 = this._decorationsTree0.search(filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); + const r1 = this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); + const r2 = this._injectedTextDecorationsTree.search(filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); return r0.concat(r1).concat(r2); } } diff --git a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts index 8ddcfddb99d50..4d5dd8c3b320d 100644 --- a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts +++ b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts @@ -73,7 +73,7 @@ export class ViewportData { return this._model.getViewportViewLineRenderingData(this.visibleRange, lineNumber); } - public getDecorationsInViewport(): ViewModelDecoration[] { - return this._model.getDecorationsInViewport(this.visibleRange); + public getDecorationsInViewport(onlyMarginDecorations?: boolean): ViewModelDecoration[] { + return this._model.getDecorationsInViewport(this.visibleRange, false, onlyMarginDecorations); } } diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 0161633e0638e..40aa15cecc6bf 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -40,7 +40,7 @@ export interface IViewModel extends ICursorSimpleModel { onCompositionStart(): void; onCompositionEnd(): void; - getDecorationsInViewport(visibleRange: Range, onlyMinimapDecorations?: boolean): ViewModelDecoration[]; + getDecorationsInViewport(visibleRange: Range, onlyMinimapDecorations?: boolean, onlyMarginDecorations?: boolean): ViewModelDecoration[]; getViewportViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData; getViewLineRenderingData(lineNumber: number): ViewLineRenderingData; getViewLineData(lineNumber: number): ViewLineData; diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 5bce2245abb9a..d4af6427c5085 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -37,6 +37,7 @@ export class ViewModelDecorations implements IDisposable { private _cachedModelDecorationsResolver: IDecorationsViewportData | null; private _cachedModelDecorationsResolverViewRange: Range | null; private _cachedOnlyMinimapDecorations: boolean | null = null; + private _cachedOnlyMarginDecorations: boolean | null = null; constructor(editorId: number, model: ITextModel, configuration: IEditorConfiguration, linesCollection: IViewModelLines, coordinatesConverter: ICoordinatesConverter) { this.editorId = editorId; @@ -97,25 +98,26 @@ export class ViewModelDecorations implements IDisposable { return r; } - public getDecorationsViewportData(viewRange: Range, onlyMinimapDecorations: boolean = false): IDecorationsViewportData { + public getDecorationsViewportData(viewRange: Range, onlyMinimapDecorations: boolean = false, onlyMarginDecorations: boolean = false): IDecorationsViewportData { let cacheIsValid = (this._cachedModelDecorationsResolver !== null); cacheIsValid = cacheIsValid && (viewRange.equalsRange(this._cachedModelDecorationsResolverViewRange)); - cacheIsValid = cacheIsValid && (this._cachedOnlyMinimapDecorations === onlyMinimapDecorations); + cacheIsValid = cacheIsValid && (this._cachedOnlyMinimapDecorations === onlyMinimapDecorations) && (this._cachedOnlyMarginDecorations === onlyMarginDecorations); if (!cacheIsValid) { - this._cachedModelDecorationsResolver = this._getDecorationsInRange(viewRange, onlyMinimapDecorations); + this._cachedModelDecorationsResolver = this._getDecorationsInRange(viewRange, onlyMinimapDecorations, onlyMarginDecorations); this._cachedModelDecorationsResolverViewRange = viewRange; this._cachedOnlyMinimapDecorations = onlyMinimapDecorations; + this._cachedOnlyMarginDecorations = onlyMarginDecorations; } return this._cachedModelDecorationsResolver!; } - public getInlineDecorationsOnLine(lineNumber: number, onlyMinimapDecorations: boolean = false): InlineDecoration[] { + public getInlineDecorationsOnLine(lineNumber: number, onlyMinimapDecorations: boolean = false, onlyMarginDecorations: boolean = false): InlineDecoration[] { const range = new Range(lineNumber, this._linesCollection.getViewLineMinColumn(lineNumber), lineNumber, this._linesCollection.getViewLineMaxColumn(lineNumber)); - return this._getDecorationsInRange(range, onlyMinimapDecorations).inlineDecorations[0]; + return this._getDecorationsInRange(range, onlyMinimapDecorations, onlyMarginDecorations).inlineDecorations[0]; } - private _getDecorationsInRange(viewRange: Range, onlyMinimapDecorations: boolean): IDecorationsViewportData { - const modelDecorations = this._linesCollection.getDecorationsInRange(viewRange, this.editorId, filterValidationDecorations(this.configuration.options), onlyMinimapDecorations); + private _getDecorationsInRange(viewRange: Range, onlyMinimapDecorations: boolean, onlyMarginDecorations: boolean): IDecorationsViewportData { + const modelDecorations = this._linesCollection.getDecorationsInRange(viewRange, this.editorId, filterValidationDecorations(this.configuration.options), onlyMinimapDecorations, onlyMarginDecorations); const startLineNumber = viewRange.startLineNumber; const endLineNumber = viewRange.endLineNumber; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 7c13d6f13f27c..ec681bbca042c 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -688,8 +688,8 @@ export class ViewModel extends Disposable implements IViewModel { return result + 2; } - public getDecorationsInViewport(visibleRange: Range, onlyMinimapDecorations: boolean = false): ViewModelDecoration[] { - return this._decorations.getDecorationsViewportData(visibleRange, onlyMinimapDecorations).decorations; + public getDecorationsInViewport(visibleRange: Range, onlyMinimapDecorations: boolean = false, onlyMarginDecorations: boolean = false): ViewModelDecoration[] { + return this._decorations.getDecorationsViewportData(visibleRange, onlyMinimapDecorations, onlyMarginDecorations).decorations; } public getInjectedTextAt(viewPosition: Position): InjectedText | null { diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 96746d3bb9f90..bf7fa8a996e7d 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -45,7 +45,7 @@ export interface IViewModelLines extends IDisposable { getViewLineData(viewLineNumber: number): ViewLineData; getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): Array; - getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean, onlyMinimapDecorations: boolean): IModelDecoration[]; + getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean, onlyMinimapDecorations: boolean, onlyMarginDecorations: boolean): IModelDecoration[]; getInjectedTextAt(viewPosition: Position): InjectedText | null; @@ -893,14 +893,14 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { return this.modelLineProjections[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); } - public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean, onlyMinimapDecorations: boolean): IModelDecoration[] { + public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean, onlyMinimapDecorations: boolean, onlyMarginDecorations: boolean): IModelDecoration[] { const modelStart = this.convertViewPositionToModelPosition(range.startLineNumber, range.startColumn); const modelEnd = this.convertViewPositionToModelPosition(range.endLineNumber, range.endColumn); if (modelEnd.lineNumber - modelStart.lineNumber <= range.endLineNumber - range.startLineNumber) { // most likely there are no hidden lines => fast path // fetch decorations from column 1 to cover the case of wrapped lines that have whole line decorations at column 1 - return this.model.getDecorationsInRange(new Range(modelStart.lineNumber, 1, modelEnd.lineNumber, modelEnd.column), ownerId, filterOutValidation, onlyMinimapDecorations); + return this.model.getDecorationsInRange(new Range(modelStart.lineNumber, 1, modelEnd.lineNumber, modelEnd.column), ownerId, filterOutValidation, onlyMinimapDecorations, onlyMarginDecorations); } let result: IModelDecoration[] = []; @@ -1226,8 +1226,8 @@ export class ViewModelLinesFromModelAsIs implements IViewModelLines { return result; } - public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean, onlyMinimapDecorations: boolean): IModelDecoration[] { - return this.model.getDecorationsInRange(range, ownerId, filterOutValidation, onlyMinimapDecorations); + public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean, onlyMinimapDecorations: boolean, onlyMarginDecorations: boolean): IModelDecoration[] { + return this.model.getDecorationsInRange(range, ownerId, filterOutValidation, onlyMinimapDecorations, onlyMarginDecorations); } normalizePosition(position: Position, affinity: PositionAffinity): Position { diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index aec5992f06b67..4b19bee62a020 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -108,7 +108,7 @@ suite('IntervalTree', () => { this._oracle.insert(this._oracleNodes[op.id]!); } else { - const actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0); + const actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0, false); const actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); const expected = this._oracle.search(new Interval(op.begin, op.end)); assert.deepStrictEqual(actual, expected); @@ -498,7 +498,7 @@ suite('IntervalTree', () => { const T = createCormenTree(); function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { - const actualNodes = T.intervalSearch(start, end, 0, false, 0); + const actualNodes = T.intervalSearch(start, end, 0, false, 0, false); const actual = actualNodes.map((n) => <[number, number]>[n.cachedAbsoluteStart, n.cachedAbsoluteEnd]); assert.deepStrictEqual(actual, expected); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 79b3eed6aed78..8717d9fe70e24 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2120,15 +2120,22 @@ declare namespace monaco.editor { * @param range The range to search in * @param ownerId If set, it will ignore decorations belonging to other owners. * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). + * @param onlyMinimapDecorations If set, it will return only decorations that render in the minimap. + * @param onlyMarginDecorations If set, it will return only decorations that render in the glyph margin. * @return An array with the decorations */ - getDecorationsInRange(range: IRange, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; + getDecorationsInRange(range: IRange, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean, onlyMarginDecorations?: boolean): IModelDecoration[]; /** * Gets all the decorations as an array. * @param ownerId If set, it will ignore decorations belonging to other owners. * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + /** + * Gets all decorations that render in the glyph margin as an array. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getAllMarginDecorations(ownerId?: number): IModelDecoration[]; /** * Gets all the decorations that should be rendered in the overview ruler as an array. * @param ownerId If set, it will ignore decorations belonging to other owners. diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 653d624c6a19c..2202aa37c7e94 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -44,13 +44,13 @@ class DecorationsTree { this._decorationsTree = new IntervalTree(); } - public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { - const r1 = this._decorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); + public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, onlyMarginDecorations: boolean = false): IntervalNode[] { + const r1 = this._decorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); return r1; } - public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { - return this._decorationsTree.search(filterOwnerId, filterOutValidation, cachedVersionId); + public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] { + return this._decorationsTree.search(filterOwnerId, filterOutValidation, cachedVersionId, onlyMarginDecorations); } From 4488fabad5b1bf4bb520c16e7abcd525ddf443ec Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 14 Apr 2023 14:20:44 -0700 Subject: [PATCH 2/8] Support glyph margin decoration lanes --- .../browser/config/editorConfiguration.ts | 12 ++++- .../viewParts/glyphMargin/glyphMargin.css | 1 - .../viewParts/glyphMargin/glyphMargin.ts | 54 ++++++++++++------- .../linesDecorations/linesDecorations.ts | 2 +- .../marginDecorations/marginDecorations.ts | 2 +- .../common/config/editorConfiguration.ts | 4 ++ src/vs/editor/common/config/editorOptions.ts | 13 ++++- src/vs/editor/common/model.ts | 24 ++++++++- src/vs/editor/common/model/textModel.ts | 10 ++++ .../common/standalone/standaloneEnums.ts | 8 +++ .../editor/common/viewModel/viewModelImpl.ts | 16 +++++- .../standalone/browser/standaloneEditor.ts | 1 + .../config/editorLayoutProvider.test.ts | 20 +++++++ src/vs/monaco.d.ts | 28 +++++++++- 14 files changed, 165 insertions(+), 30 deletions(-) diff --git a/src/vs/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts index 7e99e40ce4799..fc197da1d5726 100644 --- a/src/vs/editor/browser/config/editorConfiguration.ts +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -47,6 +47,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat private _viewLineCount: number = 1; private _lineNumbersDigitCount: number = 1; private _reservedHeight: number = 0; + private _glyphMarginDecorationLaneCount: number = 1; private readonly _computeOptionsMemory: ComputeOptionsMemory = new ComputeOptionsMemory(); /** @@ -117,7 +118,8 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat emptySelectionClipboard: partialEnv.emptySelectionClipboard, pixelRatio: partialEnv.pixelRatio, tabFocusMode: TabFocus.getTabFocusMode(TabFocusContext.Editor), - accessibilitySupport: partialEnv.accessibilitySupport + accessibilitySupport: partialEnv.accessibilitySupport, + glyphMarginDecorationLaneCount: this._glyphMarginDecorationLaneCount }; return EditorOptionsUtil.computeOptions(this._validatedOptions, env); } @@ -193,6 +195,14 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat this._reservedHeight = reservedHeight; this._recomputeOptions(); } + + public setGlyphMarginDecorationLaneCount(decorationLaneCount: number): void { + if (this._glyphMarginDecorationLaneCount === decorationLaneCount) { + return; + } + this._glyphMarginDecorationLaneCount = decorationLaneCount; + this._recomputeOptions(); + } } function digitCount(n: number): number { diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css index 17f7d5defd5b5..ee86a567698a1 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css @@ -16,5 +16,4 @@ position: absolute; display: flex; align-items: center; - justify-content: center; } diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index e3a57d6d63228..a45483c62eecd 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -18,23 +18,25 @@ export class DecorationToRender { public endLineNumber: number; public className: string; public readonly zIndex: number; + public readonly decorationLane: number; - constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex?: number) { + constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex?: number, decorationLane?: number) { this.startLineNumber = +startLineNumber; this.endLineNumber = +endLineNumber; this.className = String(className); this.zIndex = zIndex ?? 0; + this.decorationLane = decorationLane ?? 1; } } export abstract class DedupOverlay extends DynamicViewOverlay { - protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[]): [string, number][][] { + protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): [string, number][][][] { - const output: [string, number][][] = []; + const output: [string, number][][][] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - output[lineIndex] = []; + output[lineIndex] = [[]]; } if (decorations.length === 0) { @@ -59,6 +61,7 @@ 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) { startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex); @@ -69,7 +72,8 @@ export abstract class DedupOverlay extends DynamicViewOverlay { } for (let i = startLineIndex; i <= prevEndLineIndex; i++) { - output[i].push([className, zIndex]); + output[i][lane] = (output[i][lane] ?? []); + output[i][lane].push([className, zIndex]); } } @@ -84,6 +88,7 @@ export class GlyphMarginOverlay extends DedupOverlay { private _glyphMargin: boolean; private _glyphMarginLeft: number; private _glyphMarginWidth: number; + private _glyphMarginDecorationLaneCount: number; private _renderResult: string[] | null; constructor(context: ViewContext) { @@ -97,6 +102,7 @@ export class GlyphMarginOverlay extends DedupOverlay { this._glyphMargin = options.get(EditorOption.glyphMargin); this._glyphMarginLeft = layoutInfo.glyphMarginLeft; this._glyphMarginWidth = layoutInfo.glyphMarginWidth; + this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount; this._renderResult = null; this._context.addEventHandler(this); } @@ -117,6 +123,7 @@ export class GlyphMarginOverlay extends DedupOverlay { this._glyphMargin = options.get(EditorOption.glyphMargin); this._glyphMarginLeft = layoutInfo.glyphMarginLeft; this._glyphMarginWidth = layoutInfo.glyphMarginWidth; + this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount; return true; } public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { @@ -151,8 +158,9 @@ export class GlyphMarginOverlay extends DedupOverlay { const d = decorations[i]; 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); + r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, glyphMarginClassName, zIndex, lane); } } return r; @@ -167,12 +175,11 @@ export class GlyphMarginOverlay extends DedupOverlay { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const decorationsToRender = this._getDecorations(ctx); - const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, decorationsToRender); + const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, decorationsToRender, this._glyphMarginDecorationLaneCount); const lineHeight = this._lineHeight.toString(); - const left = this._glyphMarginLeft.toString(); const width = this._glyphMarginWidth.toString(); - const common = '" style="left:' + left + 'px;width:' + width + 'px' + ';height:' + lineHeight + 'px;">'; + const common = '" style="width:' + width + 'px' + ';height:' + lineHeight + 'px;'; const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { @@ -182,16 +189,25 @@ export class GlyphMarginOverlay extends DedupOverlay { if (renderInfo.length === 0) { output[lineIndex] = ''; } else { - // Sort decorations to render in descending order by zIndex - renderInfo.sort(([_, aIndex], [__, bIndex]) => { - return bIndex - aIndex; - }); - - output[lineIndex] = ( - '
' + ); + } + output[lineIndex] = css; } } diff --git a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts index fd468078c65b8..000959ae1b62a 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)); + const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx), 1); const left = this._decorationsLeft.toString(); const width = this._decorationsWidth.toString(); diff --git a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts index 73efd46b55476..8af56099ea6f1 100644 --- a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts +++ b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts @@ -73,7 +73,7 @@ export class MarginViewLineDecorationsOverlay 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)); + const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx), 1); const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { diff --git a/src/vs/editor/common/config/editorConfiguration.ts b/src/vs/editor/common/config/editorConfiguration.ts index 454d81cb089ec..415f8532d8526 100644 --- a/src/vs/editor/common/config/editorConfiguration.ts +++ b/src/vs/editor/common/config/editorConfiguration.ts @@ -55,4 +55,8 @@ export interface IEditorConfiguration extends IDisposable { * Set reserved height above. */ setReservedHeight(reservedHeight: number): void; + /** + * Set the number of decoration lanes to be rendered in the glyph margin. + */ + setGlyphMarginDecorationLaneCount(decorationLaneCount: number): void; } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 4539a77148154..363cf06bff9ac 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -830,6 +830,7 @@ export interface IEnvironmentalOptions { readonly pixelRatio: number; readonly tabFocusMode: boolean; readonly accessibilitySupport: AccessibilitySupport; + readonly glyphMarginDecorationLaneCount: number; } /** @@ -2063,6 +2064,11 @@ export interface EditorLayoutInfo { */ readonly glyphMarginWidth: number; + /** + * The number of decoration lanes to render in the glyph margin. + */ + readonly glyphMarginDecorationLaneCount: number; + /** * Left position for the line numbers. */ @@ -2150,6 +2156,7 @@ export interface EditorLayoutInfoComputerEnv { readonly typicalHalfwidthCharacterWidth: number; readonly maxDigitWidth: number; readonly pixelRatio: number; + readonly glyphMarginDecorationLaneCount: number; } /** @@ -2217,7 +2224,8 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption { this._decorations.onModelDecorationsChanged(); + + // Determine whether we need to resize the glyph margin + const decorations = this.model.getAllMarginDecorations(); + const decorationLanes = new Map>(); + for (const decoration of decorations) { + for (let i = decoration.range.startLineNumber; i <= decoration.range.endLineNumber; i++) { + const position = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; + decorationLanes.set(i, (decorationLanes.get(i) ?? new Set()).add(position)); + } + } + const widestDecorationLane = [...decorationLanes.values()].reduce((prev, curr) => curr.size > prev ? curr.size : prev, 0); + const maxDecorations = Math.max(widestDecorationLane, 1); + this._configuration.setGlyphMarginDecorationLaneCount(maxDecorations); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); this._eventDispatcher.emitOutgoingEvent(new ModelDecorationsChangedEvent(e)); })); diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 56bda1f9ac9f8..09a1bb51898b2 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -560,6 +560,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { MouseTargetType: standaloneEnums.MouseTargetType, OverlayWidgetPositionPreference: standaloneEnums.OverlayWidgetPositionPreference, OverviewRulerLane: standaloneEnums.OverviewRulerLane, + GlyphMarginLane: standaloneEnums.GlyphMarginLane, RenderLineNumbersType: standaloneEnums.RenderLineNumbersType, RenderMinimap: standaloneEnums.RenderMinimap, ScrollbarVisibility: standaloneEnums.ScrollbarVisibility, diff --git a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index b8c1d0d459276..87882eeb24b41 100644 --- a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts @@ -96,6 +96,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { typicalHalfwidthCharacterWidth: input.typicalHalfwidthCharacterWidth, maxDigitWidth: input.maxDigitWidth, pixelRatio: input.pixelRatio, + glyphMarginDecorationLaneCount: 1, }); assert.deepStrictEqual(actual, expected); } @@ -127,6 +128,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -195,6 +197,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -263,6 +266,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -331,6 +335,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -399,6 +404,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -467,6 +473,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 50, @@ -535,6 +542,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 50, @@ -603,6 +611,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 60, @@ -671,6 +680,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 30, @@ -739,6 +749,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 30, @@ -807,6 +818,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -875,6 +887,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -943,6 +956,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -1011,6 +1025,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 55, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 55, lineNumbersWidth: 0, @@ -1081,6 +1096,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -1151,6 +1167,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -1221,6 +1238,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -1291,6 +1309,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 0, lineNumbersWidth: 0, @@ -1359,6 +1378,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 30, + glyphMarginDecorationLaneCount: 1, lineNumbersLeft: 30, lineNumbersWidth: 36, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8717d9fe70e24..6125bd3897e1d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1529,6 +1529,14 @@ declare namespace monaco.editor { Full = 7 } + /** + * Vertical Lane in the glyph margin of the editor. + */ + export enum GlyphMarginLane { + Left = 1, + Right = 2 + } + /** * Position in the minimap to render the decoration. */ @@ -1550,6 +1558,13 @@ declare namespace monaco.editor { darkColor?: string | ThemeColor; } + export interface IModelDecorationGlyphMarginOptions { + /** + * The position in the glyph margin. + */ + position: GlyphMarginLane; + } + /** * Options for rendering a model decoration in the overview ruler. */ @@ -1561,11 +1576,11 @@ declare namespace monaco.editor { } /** - * Options for rendering a model decoration in the overview ruler. + * Options for rendering a model decoration in the minimap. */ export interface IModelDecorationMinimapOptions extends IDecorationOptions { /** - * The position in the overview ruler. + * The position in the minimap. */ position: MinimapPosition; } @@ -1625,6 +1640,11 @@ declare namespace monaco.editor { * If set, the decoration will be rendered in the glyph margin with this CSS class name. */ glyphMarginClassName?: string | null; + /** + * If set and the decoration has {@link glyphMarginClassName} set, render this decoration + * with the specified {@link IModelDecorationGlyphMarginOptions} in the glyph margin. + */ + glyphMargin?: IModelDecorationGlyphMarginOptions | null; /** * If set, the decoration will be rendered in the lines decorations with this CSS class name. */ @@ -4034,6 +4054,10 @@ declare namespace monaco.editor { * The width of the glyph margin. */ readonly glyphMarginWidth: number; + /** + * The number of decoration lanes to render in the glyph margin. + */ + readonly glyphMarginDecorationLaneCount: number; /** * Left position for the line numbers. */ From 0b92dbdb37cd5379232b23688e42e097f90ac3a9 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 14 Apr 2023 14:22:57 -0700 Subject: [PATCH 3/8] Do not center extension-contributed decorations --- .../browser/services/abstractCodeEditorService.ts | 2 +- .../browser/services/decorationRenderOptions.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index 62db35d5a0ccf..0225250768c40 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -585,7 +585,7 @@ export const _CSS_MAP: { [prop: string]: string } = { cursor: 'cursor:{0};', letterSpacing: 'letter-spacing:{0};', - gutterIconPath: 'background:{0} center center no-repeat;', + gutterIconPath: 'background:{0} no-repeat;', gutterIconSize: 'background-size:{0};', contentText: 'content:\'{0}\';', diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 68fa314066d3c..2ca90ed4e329c 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -41,7 +41,7 @@ suite('Decoration Render Options', () => { const styleSheet = s.globalStyleSheet; s.registerDecorationType('test', 'example', options); const sheet = readStyleSheet(styleSheet); - assert(sheet.indexOf(`{background:url('https://github.com/microsoft/vscode/blob/main/resources/linux/code.png') center center no-repeat;background-size:contain;}`) >= 0); + assert(sheet.indexOf(`{background:url('https://github.com/microsoft/vscode/blob/main/resources/linux/code.png') no-repeat;background-size:contain;}`) >= 0); assert(sheet.indexOf(`{background-color:red;border-color:yellow;box-sizing: border-box;}`) >= 0); }); @@ -108,14 +108,14 @@ suite('Decoration Render Options', () => { // URI, only minimal encoding s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('data:image/svg+xml;base64,PHN2ZyB4b+') }); - assert(readStyleSheet(styleSheet).indexOf(`{background:url('data:image/svg+xml;base64,PHN2ZyB4b+') center center no-repeat;}`) > 0); + assert(readStyleSheet(styleSheet).indexOf(`{background:url('data:image/svg+xml;base64,PHN2ZyB4b+') no-repeat;}`) > 0); s.removeDecorationType('example'); function assertBackground(url1: string, url2: string) { const actual = readStyleSheet(styleSheet); assert( - actual.indexOf(`{background:url('${url1}') center center no-repeat;}`) > 0 - || actual.indexOf(`{background:url('${url2}') center center no-repeat;}`) > 0 + actual.indexOf(`{background:url('${url1}') no-repeat;}`) > 0 + || actual.indexOf(`{background:url('${url2}') no-repeat;}`) > 0 ); } @@ -142,7 +142,7 @@ suite('Decoration Render Options', () => { } s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('http://test/pa\'th') }); - assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa%27th') center center no-repeat;}`) > 0); + assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa%27th') no-repeat;}`) > 0); s.removeDecorationType('example'); }); }); From a776a0521ea71a11e21cf0f4800b0b8bcd558db0 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 14 Apr 2023 14:25:51 -0700 Subject: [PATCH 4/8] Show breakpoints in dedicated right lane --- .../contrib/debug/browser/breakpointEditorContribution.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 53e9e2fff5939..33dbb1b2c4dad 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -26,7 +26,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { IModelDecorationOptions, IModelDecorationOverviewRulerOptions, IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { GlyphMarginLane, IModelDecorationOptions, IModelDecorationOverviewRulerOptions, IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -54,6 +54,7 @@ interface IBreakpointDecoration { const breakpointHelperDecoration: IModelDecorationOptions = { description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), + glyphMargin: { position: GlyphMarginLane.Right }, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; @@ -133,6 +134,7 @@ function getBreakpointDecorationOptions(accessor: ServicesAccessor, model: IText const renderInline = breakpoint.column && (hasOtherBreakpointsOnLine || breakpoint.column > model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber)); return { description: 'breakpoint-decoration', + glyphMargin: { position: GlyphMarginLane.Right }, glyphMarginClassName: ThemeIcon.asClassName(icon), glyphMarginHoverMessage, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, From 566c713d6f936e387ffdd6d8534b42cfa09b48c5 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 14 Apr 2023 21:47:02 -0700 Subject: [PATCH 5/8] Only recompute lane count if glyph margin was affected --- src/vs/editor/common/model/textModel.ts | 10 +++++++++- src/vs/editor/common/textModelEvents.ts | 1 + src/vs/editor/common/viewEvents.ts | 3 +++ .../editor/common/viewModel/viewModelImpl.ts | 20 ++++++++++--------- src/vs/monaco.d.ts | 1 + 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index fdd8aff796023..2ef0d7bea5820 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2354,6 +2354,7 @@ class DidChangeDecorationsEmitter extends Disposable { private _affectsMinimap: boolean; private _affectsOverviewRuler: boolean; private _affectedInjectedTextLines: Set | null = null; + private _affectsGlyphMargin: boolean; constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set | null) => void) { super(); @@ -2361,6 +2362,7 @@ class DidChangeDecorationsEmitter extends Disposable { this._shouldFireDeferred = false; this._affectsMinimap = false; this._affectsOverviewRuler = false; + this._affectsGlyphMargin = false; } hasListeners(): boolean { @@ -2397,12 +2399,16 @@ class DidChangeDecorationsEmitter extends Disposable { if (!this._affectsOverviewRuler) { this._affectsOverviewRuler = options.overviewRuler && options.overviewRuler.color ? true : false; } + if (!this._affectsGlyphMargin) { + this._affectsGlyphMargin = options.glyphMarginClassName ? true : false; + } this.tryFire(); } public fire(): void { this._affectsMinimap = true; this._affectsOverviewRuler = true; + this._affectsGlyphMargin = true; this.tryFire(); } @@ -2419,11 +2425,13 @@ class DidChangeDecorationsEmitter extends Disposable { const event: IModelDecorationsChangedEvent = { affectsMinimap: this._affectsMinimap, - affectsOverviewRuler: this._affectsOverviewRuler + affectsOverviewRuler: this._affectsOverviewRuler, + affectsGlyphMargin: this._affectsGlyphMargin }; this._shouldFireDeferred = false; this._affectsMinimap = false; this._affectsOverviewRuler = false; + this._affectsGlyphMargin = false; this._actual.fire(event); } } diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index c078ae0ec4d90..8c25e06776d81 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -90,6 +90,7 @@ export interface IModelContentChangedEvent { export interface IModelDecorationsChangedEvent { readonly affectsMinimap: boolean; readonly affectsOverviewRuler: boolean; + readonly affectsGlyphMargin: boolean; } /** diff --git a/src/vs/editor/common/viewEvents.ts b/src/vs/editor/common/viewEvents.ts index 2182d17b7f8cd..0e6cb0fa16670 100644 --- a/src/vs/editor/common/viewEvents.ts +++ b/src/vs/editor/common/viewEvents.ts @@ -75,14 +75,17 @@ export class ViewDecorationsChangedEvent { readonly affectsMinimap: boolean; readonly affectsOverviewRuler: boolean; + readonly affectsGlyphMargin: boolean; constructor(source: IModelDecorationsChangedEvent | null) { if (source) { this.affectsMinimap = source.affectsMinimap; this.affectsOverviewRuler = source.affectsOverviewRuler; + this.affectsGlyphMargin = source.affectsGlyphMargin; } else { this.affectsMinimap = true; this.affectsOverviewRuler = true; + this.affectsGlyphMargin = true; } } } diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index ede7eda771df0..2c0b823eb0864 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -461,17 +461,19 @@ export class ViewModel extends Disposable implements IViewModel { this._decorations.onModelDecorationsChanged(); // Determine whether we need to resize the glyph margin - const decorations = this.model.getAllMarginDecorations(); - const decorationLanes = new Map>(); - for (const decoration of decorations) { - for (let i = decoration.range.startLineNumber; i <= decoration.range.endLineNumber; i++) { - const position = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; - decorationLanes.set(i, (decorationLanes.get(i) ?? new Set()).add(position)); + if (e.affectsGlyphMargin) { + const decorations = this.model.getAllMarginDecorations(); + const decorationLanes = new Map>(); + for (const decoration of decorations) { + for (let i = decoration.range.startLineNumber; i <= decoration.range.endLineNumber; i++) { + const position = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; + decorationLanes.set(i, (decorationLanes.get(i) ?? new Set()).add(position)); + } } + const widestDecorationLane = [...decorationLanes.values()].reduce((prev, curr) => curr.size > prev ? curr.size : prev, 0); + const maxDecorations = Math.max(widestDecorationLane, 1); + this._configuration.setGlyphMarginDecorationLaneCount(maxDecorations); } - const widestDecorationLane = [...decorationLanes.values()].reduce((prev, curr) => curr.size > prev ? curr.size : prev, 0); - const maxDecorations = Math.max(widestDecorationLane, 1); - this._configuration.setGlyphMarginDecorationLaneCount(maxDecorations); 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 6125bd3897e1d..1f7e86a809969 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2971,6 +2971,7 @@ declare namespace monaco.editor { export interface IModelDecorationsChangedEvent { readonly affectsMinimap: boolean; readonly affectsOverviewRuler: boolean; + readonly affectsGlyphMargin: boolean; } export interface IModelOptionsChangedEvent { From 36664070e0e49f4e1328b08456786e92a61659f4 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 19 Apr 2023 23:05:58 +0200 Subject: [PATCH 6/8] Add explicit type for className / zIndex pair and fix problems in other consumers of `DedupOverlay` --- .../viewParts/glyphMargin/glyphMargin.ts | 19 +++++++++++++------ .../linesDecorations/linesDecorations.ts | 2 +- .../marginDecorations/marginDecorations.ts | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index dd98cf7a60745..8cefc778c1293 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -29,11 +29,18 @@ export class DecorationToRender { } } +export class RenderedDecoration { + constructor( + public readonly className: string, + public readonly zIndex: number, + ) { } +} + export abstract class DedupOverlay extends DynamicViewOverlay { - protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): [string, number][][][] { + protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): RenderedDecoration[][][] { - const output: [string, number][][][] = []; + const output: RenderedDecoration[][][] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; output[lineIndex] = [[]]; @@ -73,7 +80,7 @@ export abstract class DedupOverlay extends DynamicViewOverlay { for (let i = startLineIndex; i <= prevEndLineIndex; i++) { output[i][lane] = (output[i][lane] ?? []); - output[i][lane].push([className, zIndex]); + output[i][lane].push(new RenderedDecoration(className, zIndex)); } } @@ -195,14 +202,14 @@ export class GlyphMarginOverlay extends DedupOverlay { if (!decorations) { continue; } - decorations.sort(([_, aIndex], [__, bIndex]) => { + decorations.sort((a, b) => { // Sort decorations to render in descending order by zIndex - return bIndex - aIndex; + return b.zIndex - a.zIndex; }); const left = (this._glyphMarginLeft + (lane - 1) * this._lineHeight).toString(); css += ( '
' ); diff --git a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts index b2ec69784d259..2b127536a3da0 100644 --- a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts +++ b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts @@ -103,7 +103,7 @@ export class LinesDecorationsOverlay extends DedupOverlay { const classNames = toRender[lineIndex]; let lineOutput = ''; for (let i = 0, len = classNames.length; i < len; i++) { - lineOutput += '
'; + lineOutput += '
'; } output[lineIndex] = lineOutput; } From 8f6ce9d3627d3dbbcb99b630fe15f15719b969a2 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 19 Apr 2023 23:45:02 +0200 Subject: [PATCH 7/8] Avoid arrays of arrays --- .../viewParts/glyphMargin/glyphMargin.ts | 46 +++++++++++++++---- .../linesDecorations/linesDecorations.ts | 6 +-- .../marginDecorations/marginDecorations.ts | 6 +-- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 8cefc778c1293..0af60f7571f6b 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -36,14 +36,42 @@ export class RenderedDecoration { ) { } } +export class LineRenderedDecorations { + + private readonly lanes: RenderedDecoration[][] = []; + + public add(lane: number, decoration: RenderedDecoration) { + while (lane >= this.lanes.length) { + this.lanes.push([]); + } + this.lanes[lane].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; + } +} + export abstract class DedupOverlay extends DynamicViewOverlay { - protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): RenderedDecoration[][][] { + protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): LineRenderedDecorations[] { - const output: RenderedDecoration[][][] = []; + const output: LineRenderedDecorations[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - output[lineIndex] = [[]]; + output[lineIndex] = new LineRenderedDecorations(); } if (decorations.length === 0) { @@ -79,8 +107,7 @@ export abstract class DedupOverlay extends DynamicViewOverlay { } for (let i = startLineIndex; i <= prevEndLineIndex; i++) { - output[i][lane] = (output[i][lane] ?? []); - output[i][lane].push(new RenderedDecoration(className, zIndex)); + output[i].add(lane, new RenderedDecoration(className, zIndex)); } } @@ -193,23 +220,24 @@ export class GlyphMarginOverlay extends DedupOverlay { const lineIndex = lineNumber - visibleStartLineNumber; const renderInfo = toRender[lineIndex]; - if (renderInfo.length === 0) { + if (renderInfo.isEmpty()) { output[lineIndex] = ''; } else { let css = ''; for (let lane = 1; lane <= this._glyphMarginDecorationLaneCount; lane += 1) { - const decorations = renderInfo[lane]; - if (!decorations) { + const decorations = renderInfo.getLaneDecorations(lane); + if (decorations.length === 0) { continue; } decorations.sort((a, b) => { // Sort decorations to render in descending order by zIndex return b.zIndex - a.zIndex; }); + const winningDecoration: RenderedDecoration = decorations[0]; const left = (this._glyphMarginLeft + (lane - 1) * this._lineHeight).toString(); css += ( '
' ); diff --git a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts index 2b127536a3da0..9ed72e6acf0bf 100644 --- a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts +++ b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts @@ -100,10 +100,10 @@ export class LinesDecorationsOverlay extends DedupOverlay { const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - const classNames = toRender[lineIndex]; + const decorations = toRender[lineIndex].getLaneDecorations(1); // there is only one lane, see _render call above let lineOutput = ''; - for (let i = 0, len = classNames.length; i < len; i++) { - lineOutput += '
'; + for (const decoration of decorations) { + lineOutput += '
'; } output[lineIndex] = lineOutput; } From b9d2556d84331daa4f814f2f30c87dcaf8f33798 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 20 Apr 2023 01:00:10 +0200 Subject: [PATCH 8/8] Figure out if two lanes are needed in O(N) --- .../editor/common/viewModel/viewModelImpl.ts | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index eeeab17fd3dc2..cf61c0bac20b5 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -463,16 +463,48 @@ export class ViewModel extends Disposable implements IViewModel { // Determine whether we need to resize the glyph margin if (e.affectsGlyphMargin) { const decorations = this.model.getAllMarginDecorations(); - const decorationLanes = new Map>(); + + 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) { - for (let i = decoration.range.startLineNumber; i <= decoration.range.endLineNumber; i++) { - const position = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left; - decorationLanes.set(i, (decorationLanes.get(i) ?? new Set()).add(position)); + 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; } } - const widestDecorationLane = [...decorationLanes.values()].reduce((prev, curr) => curr.size > prev ? curr.size : prev, 0); - const maxDecorations = Math.max(widestDecorationLane, 1); - this._configuration.setGlyphMarginDecorationLaneCount(maxDecorations); + + this._configuration.setGlyphMarginDecorationLaneCount(hasTwoLanes ? 2 : 1); } this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e));