Path: blob/main/src/vs/editor/browser/view/viewLayer.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { FastDomNode, createFastDomNode } from '../../../base/browser/fastDomNode.js';6import { createTrustedTypesPolicy } from '../../../base/browser/trustedTypes.js';7import { BugIndicatingError } from '../../../base/common/errors.js';8import { EditorOption } from '../../common/config/editorOptions.js';9import { StringBuilder } from '../../common/core/stringBuilder.js';10import * as viewEvents from '../../common/viewEvents.js';11import { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';12import { ViewContext } from '../../common/viewModel/viewContext.js';1314/**15* Represents a visible line16*/17export interface IVisibleLine extends ILine {18getDomNode(): HTMLElement | null;19setDomNode(domNode: HTMLElement): void;2021/**22* Return null if the HTML should not be touched.23* Return the new HTML otherwise.24*/25renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean;2627/**28* Layout the line.29*/30layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void;31}3233export interface ILine {34onContentChanged(): void;35onTokensChanged(): void;36}3738export interface ILineFactory<T extends ILine> {39createLine(): T;40}4142export class RenderedLinesCollection<T extends ILine> {43private _lines!: T[];44private _rendLineNumberStart!: number;4546constructor(47private readonly _lineFactory: ILineFactory<T>,48) {49this._set(1, []);50}5152public flush(): void {53this._set(1, []);54}5556_set(rendLineNumberStart: number, lines: T[]): void {57this._lines = lines;58this._rendLineNumberStart = rendLineNumberStart;59}6061_get(): { rendLineNumberStart: number; lines: T[] } {62return {63rendLineNumberStart: this._rendLineNumberStart,64lines: this._lines65};66}6768/**69* @returns Inclusive line number that is inside this collection70*/71public getStartLineNumber(): number {72return this._rendLineNumberStart;73}7475/**76* @returns Inclusive line number that is inside this collection77*/78public getEndLineNumber(): number {79return this._rendLineNumberStart + this._lines.length - 1;80}8182public getCount(): number {83return this._lines.length;84}8586public getLine(lineNumber: number): T {87const lineIndex = lineNumber - this._rendLineNumberStart;88if (lineIndex < 0 || lineIndex >= this._lines.length) {89throw new BugIndicatingError('Illegal value for lineNumber');90}91return this._lines[lineIndex];92}9394/**95* @returns Lines that were removed from this collection96*/97public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): T[] | null {98if (this.getCount() === 0) {99// no lines100return null;101}102103const startLineNumber = this.getStartLineNumber();104const endLineNumber = this.getEndLineNumber();105106if (deleteToLineNumber < startLineNumber) {107// deleting above the viewport108const deleteCnt = deleteToLineNumber - deleteFromLineNumber + 1;109this._rendLineNumberStart -= deleteCnt;110return null;111}112113if (deleteFromLineNumber > endLineNumber) {114// deleted below the viewport115return null;116}117118// Record what needs to be deleted119let deleteStartIndex = 0;120let deleteCount = 0;121for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {122const lineIndex = lineNumber - this._rendLineNumberStart;123124if (deleteFromLineNumber <= lineNumber && lineNumber <= deleteToLineNumber) {125// this is a line to be deleted126if (deleteCount === 0) {127// this is the first line to be deleted128deleteStartIndex = lineIndex;129deleteCount = 1;130} else {131deleteCount++;132}133}134}135136// Adjust this._rendLineNumberStart for lines deleted above137if (deleteFromLineNumber < startLineNumber) {138// Something was deleted above139let deleteAboveCount = 0;140141if (deleteToLineNumber < startLineNumber) {142// the entire deleted lines are above143deleteAboveCount = deleteToLineNumber - deleteFromLineNumber + 1;144} else {145deleteAboveCount = startLineNumber - deleteFromLineNumber;146}147148this._rendLineNumberStart -= deleteAboveCount;149}150151const deleted = this._lines.splice(deleteStartIndex, deleteCount);152return deleted;153}154155public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {156const changeToLineNumber = changeFromLineNumber + changeCount - 1;157if (this.getCount() === 0) {158// no lines159return false;160}161162const startLineNumber = this.getStartLineNumber();163const endLineNumber = this.getEndLineNumber();164165let someoneNotified = false;166167for (let changedLineNumber = changeFromLineNumber; changedLineNumber <= changeToLineNumber; changedLineNumber++) {168if (changedLineNumber >= startLineNumber && changedLineNumber <= endLineNumber) {169// Notify the line170this._lines[changedLineNumber - this._rendLineNumberStart].onContentChanged();171someoneNotified = true;172}173}174175return someoneNotified;176}177178public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): T[] | null {179if (this.getCount() === 0) {180// no lines181return null;182}183184const insertCnt = insertToLineNumber - insertFromLineNumber + 1;185const startLineNumber = this.getStartLineNumber();186const endLineNumber = this.getEndLineNumber();187188if (insertFromLineNumber <= startLineNumber) {189// inserting above the viewport190this._rendLineNumberStart += insertCnt;191return null;192}193194if (insertFromLineNumber > endLineNumber) {195// inserting below the viewport196return null;197}198199if (insertCnt + insertFromLineNumber > endLineNumber) {200// insert inside the viewport in such a way that all remaining lines are pushed outside201const deleted = this._lines.splice(insertFromLineNumber - this._rendLineNumberStart, endLineNumber - insertFromLineNumber + 1);202return deleted;203}204205// insert inside the viewport, push out some lines, but not all remaining lines206const newLines: T[] = [];207for (let i = 0; i < insertCnt; i++) {208newLines[i] = this._lineFactory.createLine();209}210const insertIndex = insertFromLineNumber - this._rendLineNumberStart;211const beforeLines = this._lines.slice(0, insertIndex);212const afterLines = this._lines.slice(insertIndex, this._lines.length - insertCnt);213const deletedLines = this._lines.slice(this._lines.length - insertCnt, this._lines.length);214215this._lines = beforeLines.concat(newLines).concat(afterLines);216217return deletedLines;218}219220public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {221if (this.getCount() === 0) {222// no lines223return false;224}225226const startLineNumber = this.getStartLineNumber();227const endLineNumber = this.getEndLineNumber();228229let notifiedSomeone = false;230for (let i = 0, len = ranges.length; i < len; i++) {231const rng = ranges[i];232233if (rng.toLineNumber < startLineNumber || rng.fromLineNumber > endLineNumber) {234// range outside viewport235continue;236}237238const from = Math.max(startLineNumber, rng.fromLineNumber);239const to = Math.min(endLineNumber, rng.toLineNumber);240241for (let lineNumber = from; lineNumber <= to; lineNumber++) {242const lineIndex = lineNumber - this._rendLineNumberStart;243this._lines[lineIndex].onTokensChanged();244notifiedSomeone = true;245}246}247248return notifiedSomeone;249}250}251252export class VisibleLinesCollection<T extends IVisibleLine> {253254public readonly domNode: FastDomNode<HTMLElement>;255private readonly _linesCollection: RenderedLinesCollection<T>;256257constructor(258private readonly _viewContext: ViewContext,259private readonly _lineFactory: ILineFactory<T>,260) {261this.domNode = this._createDomNode();262this._linesCollection = new RenderedLinesCollection<T>(this._lineFactory);263}264265private _createDomNode(): FastDomNode<HTMLElement> {266const domNode = createFastDomNode(document.createElement('div'));267domNode.setClassName('view-layer');268domNode.setPosition('absolute');269domNode.domNode.setAttribute('role', 'presentation');270domNode.domNode.setAttribute('aria-hidden', 'true');271return domNode;272}273274// ---- begin view event handlers275276public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {277if (e.hasChanged(EditorOption.layoutInfo)) {278return true;279}280return false;281}282283public onFlushed(e: viewEvents.ViewFlushedEvent, flushDom?: boolean): boolean {284// No need to clear the dom node because a full .innerHTML will occur in285// ViewLayerRenderer._render, however the fallback mechanism in the286// GPU renderer may cause this to be necessary as the .innerHTML call287// may not happen depending on the new state, leaving stale DOM nodes288// around.289if (flushDom) {290const start = this._linesCollection.getStartLineNumber();291const end = this._linesCollection.getEndLineNumber();292for (let i = start; i <= end; i++) {293this._linesCollection.getLine(i).getDomNode()?.remove();294}295}296this._linesCollection.flush();297return true;298}299300public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {301return this._linesCollection.onLinesChanged(e.fromLineNumber, e.count);302}303304public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {305const deleted = this._linesCollection.onLinesDeleted(e.fromLineNumber, e.toLineNumber);306if (deleted) {307// Remove from DOM308for (let i = 0, len = deleted.length; i < len; i++) {309const lineDomNode = deleted[i].getDomNode();310lineDomNode?.remove();311}312}313314return true;315}316317public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {318const deleted = this._linesCollection.onLinesInserted(e.fromLineNumber, e.toLineNumber);319if (deleted) {320// Remove from DOM321for (let i = 0, len = deleted.length; i < len; i++) {322const lineDomNode = deleted[i].getDomNode();323lineDomNode?.remove();324}325}326327return true;328}329330public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {331return e.scrollTopChanged;332}333334public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {335return this._linesCollection.onTokensChanged(e.ranges);336}337338public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {339return true;340}341342// ---- end view event handlers343344public getStartLineNumber(): number {345return this._linesCollection.getStartLineNumber();346}347348public getEndLineNumber(): number {349return this._linesCollection.getEndLineNumber();350}351352public getVisibleLine(lineNumber: number): T {353return this._linesCollection.getLine(lineNumber);354}355356public renderLines(viewportData: ViewportData): void {357358const inp = this._linesCollection._get();359360const renderer = new ViewLayerRenderer<T>(this.domNode.domNode, this._lineFactory, viewportData, this._viewContext);361362const ctx: IRendererContext<T> = {363rendLineNumberStart: inp.rendLineNumberStart,364lines: inp.lines,365linesLength: inp.lines.length366};367368// Decide if this render will do a single update (single large .innerHTML) or many updates (inserting/removing dom nodes)369const resCtx = renderer.render(ctx, viewportData.startLineNumber, viewportData.endLineNumber, viewportData.relativeVerticalOffset);370371this._linesCollection._set(resCtx.rendLineNumberStart, resCtx.lines);372}373}374375interface IRendererContext<T extends IVisibleLine> {376rendLineNumberStart: number;377lines: T[];378linesLength: number;379}380381class ViewLayerRenderer<T extends IVisibleLine> {382383private static _ttPolicy = createTrustedTypesPolicy('editorViewLayer', { createHTML: value => value });384385constructor(386private readonly _domNode: HTMLElement,387private readonly _lineFactory: ILineFactory<T>,388private readonly _viewportData: ViewportData,389private readonly _viewContext: ViewContext390) {391}392393public render(inContext: IRendererContext<T>, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext<T> {394395const ctx: IRendererContext<T> = {396rendLineNumberStart: inContext.rendLineNumberStart,397lines: inContext.lines.slice(0),398linesLength: inContext.linesLength399};400401if ((ctx.rendLineNumberStart + ctx.linesLength - 1 < startLineNumber) || (stopLineNumber < ctx.rendLineNumberStart)) {402// There is no overlap whatsoever403ctx.rendLineNumberStart = startLineNumber;404ctx.linesLength = stopLineNumber - startLineNumber + 1;405ctx.lines = [];406for (let x = startLineNumber; x <= stopLineNumber; x++) {407ctx.lines[x - startLineNumber] = this._lineFactory.createLine();408}409this._finishRendering(ctx, true, deltaTop);410return ctx;411}412413// Update lines which will remain untouched414this._renderUntouchedLines(415ctx,416Math.max(startLineNumber - ctx.rendLineNumberStart, 0),417Math.min(stopLineNumber - ctx.rendLineNumberStart, ctx.linesLength - 1),418deltaTop,419startLineNumber420);421422if (ctx.rendLineNumberStart > startLineNumber) {423// Insert lines before424const fromLineNumber = startLineNumber;425const toLineNumber = Math.min(stopLineNumber, ctx.rendLineNumberStart - 1);426if (fromLineNumber <= toLineNumber) {427this._insertLinesBefore(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);428ctx.linesLength += toLineNumber - fromLineNumber + 1;429}430} else if (ctx.rendLineNumberStart < startLineNumber) {431// Remove lines before432const removeCnt = Math.min(ctx.linesLength, startLineNumber - ctx.rendLineNumberStart);433if (removeCnt > 0) {434this._removeLinesBefore(ctx, removeCnt);435ctx.linesLength -= removeCnt;436}437}438439ctx.rendLineNumberStart = startLineNumber;440441if (ctx.rendLineNumberStart + ctx.linesLength - 1 < stopLineNumber) {442// Insert lines after443const fromLineNumber = ctx.rendLineNumberStart + ctx.linesLength;444const toLineNumber = stopLineNumber;445446if (fromLineNumber <= toLineNumber) {447this._insertLinesAfter(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);448ctx.linesLength += toLineNumber - fromLineNumber + 1;449}450451} else if (ctx.rendLineNumberStart + ctx.linesLength - 1 > stopLineNumber) {452// Remove lines after453const fromLineNumber = Math.max(0, stopLineNumber - ctx.rendLineNumberStart + 1);454const toLineNumber = ctx.linesLength - 1;455const removeCnt = toLineNumber - fromLineNumber + 1;456457if (removeCnt > 0) {458this._removeLinesAfter(ctx, removeCnt);459ctx.linesLength -= removeCnt;460}461}462463this._finishRendering(ctx, false, deltaTop);464465return ctx;466}467468private _renderUntouchedLines(ctx: IRendererContext<T>, startIndex: number, endIndex: number, deltaTop: number[], deltaLN: number): void {469const rendLineNumberStart = ctx.rendLineNumberStart;470const lines = ctx.lines;471472for (let i = startIndex; i <= endIndex; i++) {473const lineNumber = rendLineNumberStart + i;474lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this._lineHeightForLineNumber(lineNumber));475}476}477478private _insertLinesBefore(ctx: IRendererContext<T>, fromLineNumber: number, toLineNumber: number, deltaTop: number[], deltaLN: number): void {479const newLines: T[] = [];480let newLinesLen = 0;481for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {482newLines[newLinesLen++] = this._lineFactory.createLine();483}484ctx.lines = newLines.concat(ctx.lines);485}486487private _removeLinesBefore(ctx: IRendererContext<T>, removeCount: number): void {488for (let i = 0; i < removeCount; i++) {489const lineDomNode = ctx.lines[i].getDomNode();490lineDomNode?.remove();491}492ctx.lines.splice(0, removeCount);493}494495private _insertLinesAfter(ctx: IRendererContext<T>, fromLineNumber: number, toLineNumber: number, deltaTop: number[], deltaLN: number): void {496const newLines: T[] = [];497let newLinesLen = 0;498for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {499newLines[newLinesLen++] = this._lineFactory.createLine();500}501ctx.lines = ctx.lines.concat(newLines);502}503504private _removeLinesAfter(ctx: IRendererContext<T>, removeCount: number): void {505const removeIndex = ctx.linesLength - removeCount;506507for (let i = 0; i < removeCount; i++) {508const lineDomNode = ctx.lines[removeIndex + i].getDomNode();509lineDomNode?.remove();510}511ctx.lines.splice(removeIndex, removeCount);512}513514private _finishRenderingNewLines(ctx: IRendererContext<T>, domNodeIsEmpty: boolean, newLinesHTML: string | TrustedHTML, wasNew: boolean[]): void {515if (ViewLayerRenderer._ttPolicy) {516newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML as string);517}518const lastChild = <HTMLElement>this._domNode.lastChild;519if (domNodeIsEmpty || !lastChild) {520this._domNode.innerHTML = newLinesHTML as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393;521} else {522lastChild.insertAdjacentHTML('afterend', newLinesHTML as string);523}524525let currChild = <HTMLElement>this._domNode.lastChild;526for (let i = ctx.linesLength - 1; i >= 0; i--) {527const line = ctx.lines[i];528if (wasNew[i]) {529line.setDomNode(currChild);530currChild = <HTMLElement>currChild.previousSibling;531}532}533}534535private _finishRenderingInvalidLines(ctx: IRendererContext<T>, invalidLinesHTML: string | TrustedHTML, wasInvalid: boolean[]): void {536const hugeDomNode = document.createElement('div');537538if (ViewLayerRenderer._ttPolicy) {539invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML as string);540}541hugeDomNode.innerHTML = invalidLinesHTML as string;542543for (let i = 0; i < ctx.linesLength; i++) {544const line = ctx.lines[i];545if (wasInvalid[i]) {546const source = <HTMLElement>hugeDomNode.firstChild;547const lineDomNode = line.getDomNode()!;548lineDomNode.parentNode!.replaceChild(source, lineDomNode);549line.setDomNode(source);550}551}552}553554private static readonly _sb = new StringBuilder(100000);555556private _finishRendering(ctx: IRendererContext<T>, domNodeIsEmpty: boolean, deltaTop: number[]): void {557558const sb = ViewLayerRenderer._sb;559const linesLength = ctx.linesLength;560const lines = ctx.lines;561const rendLineNumberStart = ctx.rendLineNumberStart;562563const wasNew: boolean[] = [];564{565sb.reset();566let hadNewLine = false;567568for (let i = 0; i < linesLength; i++) {569const line = lines[i];570wasNew[i] = false;571572const lineDomNode = line.getDomNode();573if (lineDomNode) {574// line is not new575continue;576}577578const renderedLineNumber = i + rendLineNumberStart;579const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb);580if (!renderResult) {581// line does not need rendering582continue;583}584585wasNew[i] = true;586hadNewLine = true;587}588589if (hadNewLine) {590this._finishRenderingNewLines(ctx, domNodeIsEmpty, sb.build(), wasNew);591}592}593594{595sb.reset();596597let hadInvalidLine = false;598const wasInvalid: boolean[] = [];599600for (let i = 0; i < linesLength; i++) {601const line = lines[i];602wasInvalid[i] = false;603604if (wasNew[i]) {605// line was new606continue;607}608609const renderedLineNumber = i + rendLineNumberStart;610const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb);611if (!renderResult) {612// line does not need rendering613continue;614}615616wasInvalid[i] = true;617hadInvalidLine = true;618}619620if (hadInvalidLine) {621this._finishRenderingInvalidLines(ctx, sb.build(), wasInvalid);622}623}624}625626private _lineHeightForLineNumber(lineNumber: number): number {627return this._viewContext.viewLayout.getLineHeightForLineNumber(lineNumber);628}629}630631632