Support dedicated lane for breakpoint decorations (#180013)

* Add fast path for margin decoration access

* Support glyph margin decoration lanes

* Do not center extension-contributed decorations

* Show breakpoints in dedicated right lane

* Only recompute lane count if glyph margin was affected

* Add explicit type for className / zIndex pair and fix problems in other consumers of `DedupOverlay`

* Avoid arrays of arrays

* Figure out if two lanes are needed in O(N)

---------

Co-authored-by: Alex Dima <alexdima@microsoft.com>
This commit is contained in:
Joyce Er
2023-04-19 16:26:20 -07:00
committed by GitHub
parent 0724039270
commit b5e90fd0b1
19 changed files with 264 additions and 45 deletions

View File

@@ -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 {

View File

@@ -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}\';',

View File

@@ -16,5 +16,4 @@
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -18,23 +18,60 @@ 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 class RenderedDecoration {
constructor(
public readonly className: string,
public readonly zIndex: number,
) { }
}
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[]): [string, number][][] {
protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[], decorationLaneCount: number): LineRenderedDecorations[] {
const output: [string, number][][] = [];
const output: LineRenderedDecorations[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
output[lineIndex] = [];
output[lineIndex] = new LineRenderedDecorations();
}
if (decorations.length === 0) {
@@ -59,6 +96,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 +107,7 @@ export abstract class DedupOverlay extends DynamicViewOverlay {
}
for (let i = startLineIndex; i <= prevEndLineIndex; i++) {
output[i].push([className, zIndex]);
output[i].add(lane, new RenderedDecoration(className, zIndex));
}
}
@@ -84,6 +122,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 +136,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 +157,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 +192,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,31 +209,40 @@ 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;"></div>';
const common = '" style="width:' + width + 'px' + ';height:' + lineHeight + 'px;';
const output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
const renderInfo = toRender[lineIndex];
if (renderInfo.length === 0) {
if (renderInfo.isEmpty()) {
output[lineIndex] = '';
} else {
// Sort decorations to render in descending order by zIndex
renderInfo.sort(([_, aIndex], [__, bIndex]) => {
return bIndex - aIndex;
});
output[lineIndex] = (
'<div class="cgmr codicon '
+ renderInfo[0][0]
+ common
);
let css = '';
for (let lane = 1; lane <= this._glyphMarginDecorationLaneCount; lane += 1) {
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 += (
'<div class="cgmr codicon '
+ winningDecoration.className // TODO@joyceerhl Implement overflow for remaining decorations
+ common
+ 'left:' + left + 'px;"></div>'
);
}
output[lineIndex] = css;
}
}

View File

@@ -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();
@@ -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 += '<div class="cldr ' + classNames[i][0] + common;
for (const decoration of decorations) {
lineOutput += '<div class="cldr ' + decoration.className + common;
}
output[lineIndex] = lineOutput;
}

View File

@@ -73,15 +73,15 @@ 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++) {
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 += '<div class="cmdr ' + classNames[i][0] + '" style=""></div>';
for (const decoration of decorations) {
lineOutput += '<div class="cmdr ' + decoration.className + '" style=""></div>';
}
output[lineIndex] = lineOutput;
}