/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ILineBreaksComputerFactory } from '../../../../vs/editor/common/viewModel/splitLinesCollection'; import { WrappingIndent } from '../../../../vs/editor/common/config/editorOptions'; import { FontInfo } from '../../../../vs/editor/common/config/fontInfo'; import { createStringBuilder, IStringBuilder, } from '../../../../vs/editor/common/core/stringBuilder'; import { CharCode } from '../../../../vs/base/common/charCode'; import * as strings from '../../../../vs/base/common/strings'; import { Configuration } from '../../../../vs/editor/browser/config/configuration'; import { ILineBreaksComputer, LineBreakData, } from '../../../../vs/editor/common/viewModel/viewModel'; const ttPolicy = (window as any).trustedTypes?.createPolicy( 'domLineBreaksComputer', { createHTML: (value: any) => value, } ); export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory { public static create(): DOMLineBreaksComputerFactory { return new DOMLineBreaksComputerFactory(); } constructor() {} public createLineBreaksComputer( fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent ): ILineBreaksComputer { tabSize = tabSize | 0; //@perf wrappingColumn = +wrappingColumn; //@perf let requests: string[] = []; return { addRequest: ( lineText: string, previousLineBreakData: LineBreakData | null ) => { requests.push(lineText); }, finalize: () => { return createLineBreaks( requests, fontInfo, tabSize, wrappingColumn, wrappingIndent ); }, }; } } function createLineBreaks( requests: string[], fontInfo: FontInfo, tabSize: number, firstLineBreakColumn: number, wrappingIndent: WrappingIndent ): (LineBreakData | null)[] { if (firstLineBreakColumn === -1) { const result: null[] = []; for (let i = 0, len = requests.length; i < len; i++) { result[i] = null; } return result; } const overallWidth = Math.round( firstLineBreakColumn * fontInfo.typicalHalfwidthCharacterWidth ); // Cannot respect WrappingIndent.Indent and WrappingIndent.DeepIndent because that would require // two dom layouts, in order to first set the width of the first line, and then set the width of the wrapped lines if ( wrappingIndent === WrappingIndent.Indent || wrappingIndent === WrappingIndent.DeepIndent ) { wrappingIndent = WrappingIndent.Same; } const containerDomNode = document.createElement('div'); Configuration.applyFontInfoSlow(containerDomNode, fontInfo); const sb = createStringBuilder(10000); const firstNonWhitespaceIndices: number[] = []; const wrappedTextIndentLengths: number[] = []; const renderLineContents: string[] = []; const allCharOffsets: number[][] = []; const allVisibleColumns: number[][] = []; for (let i = 0; i < requests.length; i++) { const lineContent = requests[i]; let firstNonWhitespaceIndex = 0; let wrappedTextIndentLength = 0; let width = overallWidth; if (wrappingIndent !== WrappingIndent.None) { firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); if (firstNonWhitespaceIndex === -1) { // all whitespace line firstNonWhitespaceIndex = 0; } else { // Track existing indent for (let i = 0; i < firstNonWhitespaceIndex; i++) { const charWidth = lineContent.charCodeAt(i) === CharCode.Tab ? tabSize - (wrappedTextIndentLength % tabSize) : 1; wrappedTextIndentLength += charWidth; } const indentWidth = Math.ceil( fontInfo.spaceWidth * wrappedTextIndentLength ); // Force sticking to beginning of line if no character would fit except for the indentation if ( indentWidth + fontInfo.typicalFullwidthCharacterWidth > overallWidth ) { firstNonWhitespaceIndex = 0; wrappedTextIndentLength = 0; } else { width = overallWidth - indentWidth; } } } const renderLineContent = lineContent.substr(firstNonWhitespaceIndex); const tmp = renderLine( renderLineContent, wrappedTextIndentLength, tabSize, width, sb ); firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex; wrappedTextIndentLengths[i] = wrappedTextIndentLength; renderLineContents[i] = renderLineContent; allCharOffsets[i] = tmp[0]; allVisibleColumns[i] = tmp[1]; } const html = sb.build(); const trustedhtml = ttPolicy?.createHTML(html) ?? html; containerDomNode.innerHTML = trustedhtml as string; containerDomNode.style.position = 'absolute'; containerDomNode.style.top = '10000'; containerDomNode.style.wordWrap = 'break-word'; document.body.appendChild(containerDomNode); let range = document.createRange(); const lineDomNodes = Array.prototype.slice.call(containerDomNode.children, 0); let result: (LineBreakData | null)[] = []; for (let i = 0; i < requests.length; i++) { const lineDomNode = lineDomNodes[i]; const breakOffsets: number[] | null = readLineBreaks( range, lineDomNode, renderLineContents[i], allCharOffsets[i] ); if (breakOffsets === null) { result[i] = null; continue; } const firstNonWhitespaceIndex = firstNonWhitespaceIndices[i]; const wrappedTextIndentLength = wrappedTextIndentLengths[i]; const visibleColumns = allVisibleColumns[i]; const breakOffsetsVisibleColumn: number[] = []; for (let j = 0, len = breakOffsets.length; j < len; j++) { breakOffsetsVisibleColumn[j] = visibleColumns[breakOffsets[j]]; } if (firstNonWhitespaceIndex !== 0) { // All break offsets are relative to the renderLineContent, make them absolute again for (let j = 0, len = breakOffsets.length; j < len; j++) { breakOffsets[j] += firstNonWhitespaceIndex; } } result[i] = new LineBreakData( breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength ); } document.body.removeChild(containerDomNode); return result; } const enum Constants { SPAN_MODULO_LIMIT = 16384, } function renderLine( lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: IStringBuilder ): [number[], number[]] { sb.appendASCIIString('