Path: blob/main/src/vs/editor/browser/view/domLineBreaksComputer.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 { createTrustedTypesPolicy } from '../../../base/browser/trustedTypes.js';6import { CharCode } from '../../../base/common/charCode.js';7import * as strings from '../../../base/common/strings.js';8import { assertReturnsDefined } from '../../../base/common/types.js';9import { applyFontInfo } from '../config/domFontInfo.js';10import { WrappingIndent } from '../../common/config/editorOptions.js';11import { FontInfo } from '../../common/config/fontInfo.js';12import { StringBuilder } from '../../common/core/stringBuilder.js';13import { InjectedTextOptions } from '../../common/model.js';14import { ILineBreaksComputer, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js';15import { LineInjectedText } from '../../common/textModelEvents.js';1617const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value });1819export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory {2021public static create(targetWindow: Window): DOMLineBreaksComputerFactory {22return new DOMLineBreaksComputerFactory(new WeakRef(targetWindow));23}2425constructor(private targetWindow: WeakRef<Window>) {26}2728public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer {29const requests: string[] = [];30const injectedTexts: (LineInjectedText[] | null)[] = [];31return {32addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => {33requests.push(lineText);34injectedTexts.push(injectedText);35},36finalize: () => {37return createLineBreaks(assertReturnsDefined(this.targetWindow.deref()), requests, fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, injectedTexts);38}39};40}41}4243function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: FontInfo, tabSize: number, firstLineBreakColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', injectedTextsPerLine: (LineInjectedText[] | null)[]): (ModelLineProjectionData | null)[] {44function createEmptyLineBreakWithPossiblyInjectedText(requestIdx: number): ModelLineProjectionData | null {45const injectedTexts = injectedTextsPerLine[requestIdx];46if (injectedTexts) {47const lineText = LineInjectedText.applyInjectedText(requests[requestIdx], injectedTexts);4849const injectionOptions = injectedTexts.map(t => t.options);50const injectionOffsets = injectedTexts.map(text => text.column - 1);5152// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK53// because `breakOffsetsVisibleColumn` will never be used because it contains injected text54return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);55} else {56return null;57}58}5960if (firstLineBreakColumn === -1) {61const result: (ModelLineProjectionData | null)[] = [];62for (let i = 0, len = requests.length; i < len; i++) {63result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);64}65return result;66}6768const overallWidth = Math.round(firstLineBreakColumn * fontInfo.typicalHalfwidthCharacterWidth);69const additionalIndent = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0);70const additionalIndentSize = Math.round(tabSize * additionalIndent);71const additionalIndentLength = Math.ceil(fontInfo.spaceWidth * additionalIndentSize);7273const containerDomNode = document.createElement('div');74applyFontInfo(containerDomNode, fontInfo);7576const sb = new StringBuilder(10000);77const firstNonWhitespaceIndices: number[] = [];78const wrappedTextIndentLengths: number[] = [];79const renderLineContents: string[] = [];80const allCharOffsets: number[][] = [];81const allVisibleColumns: number[][] = [];82for (let i = 0; i < requests.length; i++) {83const lineContent = LineInjectedText.applyInjectedText(requests[i], injectedTextsPerLine[i]);8485let firstNonWhitespaceIndex = 0;86let wrappedTextIndentLength = 0;87let width = overallWidth;8889if (wrappingIndent !== WrappingIndent.None) {90firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);91if (firstNonWhitespaceIndex === -1) {92// all whitespace line93firstNonWhitespaceIndex = 0;9495} else {96// Track existing indent9798for (let i = 0; i < firstNonWhitespaceIndex; i++) {99const charWidth = (100lineContent.charCodeAt(i) === CharCode.Tab101? (tabSize - (wrappedTextIndentLength % tabSize))102: 1103);104wrappedTextIndentLength += charWidth;105}106107const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength);108109// Force sticking to beginning of line if no character would fit except for the indentation110if (indentWidth + fontInfo.typicalFullwidthCharacterWidth > overallWidth) {111firstNonWhitespaceIndex = 0;112wrappedTextIndentLength = 0;113} else {114width = overallWidth - indentWidth;115}116}117}118119const renderLineContent = lineContent.substr(firstNonWhitespaceIndex);120const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength);121firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex;122wrappedTextIndentLengths[i] = wrappedTextIndentLength;123renderLineContents[i] = renderLineContent;124allCharOffsets[i] = tmp[0];125allVisibleColumns[i] = tmp[1];126}127const html = sb.build();128const trustedhtml = ttPolicy?.createHTML(html) ?? html;129containerDomNode.innerHTML = trustedhtml as string;130131containerDomNode.style.position = 'absolute';132containerDomNode.style.top = '10000';133if (wordBreak === 'keepAll') {134// word-break: keep-all; overflow-wrap: anywhere135containerDomNode.style.wordBreak = 'keep-all';136containerDomNode.style.overflowWrap = 'anywhere';137} else {138// overflow-wrap: break-word139containerDomNode.style.wordBreak = 'inherit';140containerDomNode.style.overflowWrap = 'break-word';141}142targetWindow.document.body.appendChild(containerDomNode);143144const range = document.createRange();145const lineDomNodes = Array.prototype.slice.call(containerDomNode.children, 0);146147const result: (ModelLineProjectionData | null)[] = [];148for (let i = 0; i < requests.length; i++) {149const lineDomNode = lineDomNodes[i];150const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]);151if (breakOffsets === null) {152result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);153continue;154}155156const firstNonWhitespaceIndex = firstNonWhitespaceIndices[i];157const wrappedTextIndentLength = wrappedTextIndentLengths[i] + additionalIndentSize;158const visibleColumns = allVisibleColumns[i];159160const breakOffsetsVisibleColumn: number[] = [];161for (let j = 0, len = breakOffsets.length; j < len; j++) {162breakOffsetsVisibleColumn[j] = visibleColumns[breakOffsets[j]];163}164165if (firstNonWhitespaceIndex !== 0) {166// All break offsets are relative to the renderLineContent, make them absolute again167for (let j = 0, len = breakOffsets.length; j < len; j++) {168breakOffsets[j] += firstNonWhitespaceIndex;169}170}171172let injectionOptions: InjectedTextOptions[] | null;173let injectionOffsets: number[] | null;174const curInjectedTexts = injectedTextsPerLine[i];175if (curInjectedTexts) {176injectionOptions = curInjectedTexts.map(t => t.options);177injectionOffsets = curInjectedTexts.map(text => text.column - 1);178} else {179injectionOptions = null;180injectionOffsets = null;181}182183result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength);184}185186containerDomNode.remove();187return result;188}189190const enum Constants {191SPAN_MODULO_LIMIT = 16384192}193194function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number): [number[], number[]] {195196if (wrappingIndentLength !== 0) {197const hangingOffset = String(wrappingIndentLength);198sb.appendString('<div style="text-indent: -');199sb.appendString(hangingOffset);200sb.appendString('px; padding-left: ');201sb.appendString(hangingOffset);202sb.appendString('px; box-sizing: border-box; width:');203} else {204sb.appendString('<div style="width:');205}206sb.appendString(String(width));207sb.appendString('px;">');208// if (containsRTL) {209// sb.appendASCIIString('" dir="ltr');210// }211212const len = lineContent.length;213let visibleColumn = initialVisibleColumn;214let charOffset = 0;215const charOffsets: number[] = [];216const visibleColumns: number[] = [];217let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null);218219sb.appendString('<span>');220for (let charIndex = 0; charIndex < len; charIndex++) {221if (charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) {222sb.appendString('</span><span>');223}224charOffsets[charIndex] = charOffset;225visibleColumns[charIndex] = visibleColumn;226const charCode = nextCharCode;227nextCharCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null);228let producedCharacters = 1;229let charWidth = 1;230switch (charCode) {231case CharCode.Tab:232producedCharacters = (tabSize - (visibleColumn % tabSize));233charWidth = producedCharacters;234for (let space = 1; space <= producedCharacters; space++) {235if (space < producedCharacters) {236sb.appendCharCode(0xA0); // 237} else {238sb.appendASCIICharCode(CharCode.Space);239}240}241break;242243case CharCode.Space:244if (nextCharCode === CharCode.Space) {245sb.appendCharCode(0xA0); // 246} else {247sb.appendASCIICharCode(CharCode.Space);248}249break;250251case CharCode.LessThan:252sb.appendString('<');253break;254255case CharCode.GreaterThan:256sb.appendString('>');257break;258259case CharCode.Ampersand:260sb.appendString('&');261break;262263case CharCode.Null:264sb.appendString('�');265break;266267case CharCode.UTF8_BOM:268case CharCode.LINE_SEPARATOR:269case CharCode.PARAGRAPH_SEPARATOR:270case CharCode.NEXT_LINE:271sb.appendCharCode(0xFFFD);272break;273274default:275if (strings.isFullWidthCharacter(charCode)) {276charWidth++;277}278if (charCode < 32) {279sb.appendCharCode(9216 + charCode);280} else {281sb.appendCharCode(charCode);282}283}284285charOffset += producedCharacters;286visibleColumn += charWidth;287}288sb.appendString('</span>');289290charOffsets[lineContent.length] = charOffset;291visibleColumns[lineContent.length] = visibleColumn;292293sb.appendString('</div>');294295return [charOffsets, visibleColumns];296}297298function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[]): number[] | null {299if (lineContent.length <= 1) {300return null;301}302const spans = <HTMLSpanElement[]>Array.prototype.slice.call(lineDomNode.children, 0);303304const breakOffsets: number[] = [];305try {306discoverBreaks(range, spans, charOffsets, 0, null, lineContent.length - 1, null, breakOffsets);307} catch (err) {308console.log(err);309return null;310}311312if (breakOffsets.length === 0) {313return null;314}315316breakOffsets.push(lineContent.length);317return breakOffsets;318}319320function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void {321if (low === high) {322return;323}324325lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1]);326highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1]);327328if (Math.abs(lowRects[0].top - highRects[0].top) <= 0.1) {329// same line330return;331}332333// there is at least one line break between these two offsets334if (low + 1 === high) {335// the two characters are adjacent, so the line break must be exactly between them336result.push(high);337return;338}339340const mid = low + ((high - low) / 2) | 0;341const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1]);342discoverBreaks(range, spans, charOffsets, low, lowRects, mid, midRects, result);343discoverBreaks(range, spans, charOffsets, mid, midRects, high, highRects, result);344}345346function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number): DOMRectList {347range.setStart(spans[(startOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, startOffset % Constants.SPAN_MODULO_LIMIT);348range.setEnd(spans[(endOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, endOffset % Constants.SPAN_MODULO_LIMIT);349return range.getClientRects();350}351352353