Path: blob/main/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.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 { CharCode } from '../../../base/common/charCode.js';6import * as strings from '../../../base/common/strings.js';7import { WrappingIndent, IComputedEditorOptions, EditorOption } from '../config/editorOptions.js';8import { CharacterClassifier } from '../core/characterClassifier.js';9import { FontInfo } from '../config/fontInfo.js';10import { LineInjectedText } from '../textModelEvents.js';11import { InjectedTextOptions } from '../model.js';12import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData } from '../modelLineProjectionData.js';1314export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory {15public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory {16return new MonospaceLineBreaksComputerFactory(17options.get(EditorOption.wordWrapBreakBeforeCharacters),18options.get(EditorOption.wordWrapBreakAfterCharacters)19);20}2122private readonly classifier: WrappingCharacterClassifier;2324constructor(breakBeforeChars: string, breakAfterChars: string) {25this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars);26}2728public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer {29const requests: string[] = [];30const injectedTexts: (LineInjectedText[] | null)[] = [];31const previousBreakingData: (ModelLineProjectionData | null)[] = [];32return {33addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => {34requests.push(lineText);35injectedTexts.push(injectedText);36previousBreakingData.push(previousLineBreakData);37},38finalize: () => {39const columnsForFullWidthChar = fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth;40const result: (ModelLineProjectionData | null)[] = [];41for (let i = 0, len = requests.length; i < len; i++) {42const injectedText = injectedTexts[i];43const previousLineBreakData = previousBreakingData[i];44if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText) {45result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak);46} else {47result[i] = createLineBreaks(this.classifier, requests[i], injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds);48}49}50arrPool1.length = 0;51arrPool2.length = 0;52return result;53}54};55}56}5758const enum CharacterClass {59NONE = 0,60BREAK_BEFORE = 1,61BREAK_AFTER = 2,62BREAK_IDEOGRAPHIC = 3 // for Han and Kana.63}6465class WrappingCharacterClassifier extends CharacterClassifier<CharacterClass> {6667constructor(BREAK_BEFORE: string, BREAK_AFTER: string) {68super(CharacterClass.NONE);6970for (let i = 0; i < BREAK_BEFORE.length; i++) {71this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE);72}7374for (let i = 0; i < BREAK_AFTER.length; i++) {75this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER);76}77}7879public override get(charCode: number): CharacterClass {80if (charCode >= 0 && charCode < 256) {81return <CharacterClass>this._asciiMap[charCode];82} else {83// Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges:84// 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF)85// 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF)86// 3. Hiragana and Katakana (0x3040 -- 0x30FF)87if (88(charCode >= 0x3040 && charCode <= 0x30FF)89|| (charCode >= 0x3400 && charCode <= 0x4DBF)90|| (charCode >= 0x4E00 && charCode <= 0x9FFF)91) {92return CharacterClass.BREAK_IDEOGRAPHIC;93}9495return <CharacterClass>(this._map.get(charCode) || this._defaultValue);96}97}98}99100let arrPool1: number[] = [];101let arrPool2: number[] = [];102103function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterClassifier, previousBreakingData: ModelLineProjectionData, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ModelLineProjectionData | null {104if (firstLineBreakColumn === -1) {105return null;106}107108const len = lineText.length;109if (len <= 1) {110return null;111}112113const isKeepAll = (wordBreak === 'keepAll');114115const prevBreakingOffsets = previousBreakingData.breakOffsets;116const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakOffsetsVisibleColumn;117118const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);119const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;120121const breakingOffsets: number[] = arrPool1;122const breakingOffsetsVisibleColumn: number[] = arrPool2;123let breakingOffsetsCount = 0;124let lastBreakingOffset = 0;125let lastBreakingOffsetVisibleColumn = 0;126127let breakingColumn = firstLineBreakColumn;128const prevLen = prevBreakingOffsets.length;129let prevIndex = 0;130131if (prevIndex >= 0) {132let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn);133while (prevIndex + 1 < prevLen) {134const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn);135if (distance >= bestDistance) {136break;137}138bestDistance = distance;139prevIndex++;140}141}142143while (prevIndex < prevLen) {144// Allow for prevIndex to be -1 (for the case where we hit a tab when walking backwards from the first break)145let prevBreakOffset = prevIndex < 0 ? 0 : prevBreakingOffsets[prevIndex];146let prevBreakOffsetVisibleColumn = prevIndex < 0 ? 0 : prevBreakingOffsetsVisibleColumn[prevIndex];147if (lastBreakingOffset > prevBreakOffset) {148prevBreakOffset = lastBreakingOffset;149prevBreakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn;150}151152let breakOffset = 0;153let breakOffsetVisibleColumn = 0;154155let forcedBreakOffset = 0;156let forcedBreakOffsetVisibleColumn = 0;157158// initially, we search as much as possible to the right (if it fits)159if (prevBreakOffsetVisibleColumn <= breakingColumn) {160let visibleColumn = prevBreakOffsetVisibleColumn;161let prevCharCode = prevBreakOffset === 0 ? CharCode.Null : lineText.charCodeAt(prevBreakOffset - 1);162let prevCharCodeClass = prevBreakOffset === 0 ? CharacterClass.NONE : classifier.get(prevCharCode);163let entireLineFits = true;164for (let i = prevBreakOffset; i < len; i++) {165const charStartOffset = i;166const charCode = lineText.charCodeAt(i);167let charCodeClass: number;168let charWidth: number;169170if (strings.isHighSurrogate(charCode)) {171// A surrogate pair must always be considered as a single unit, so it is never to be broken172i++;173charCodeClass = CharacterClass.NONE;174charWidth = 2;175} else {176charCodeClass = classifier.get(charCode);177charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);178}179180if (charStartOffset > lastBreakingOffset && canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {181breakOffset = charStartOffset;182breakOffsetVisibleColumn = visibleColumn;183}184185visibleColumn += charWidth;186187// check if adding character at `i` will go over the breaking column188if (visibleColumn > breakingColumn) {189// We need to break at least before character at `i`:190if (charStartOffset > lastBreakingOffset) {191forcedBreakOffset = charStartOffset;192forcedBreakOffsetVisibleColumn = visibleColumn - charWidth;193} else {194// we need to advance at least by one character195forcedBreakOffset = i + 1;196forcedBreakOffsetVisibleColumn = visibleColumn;197}198199if (visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) {200// Cannot break at `breakOffset` => reset it if it was set201breakOffset = 0;202}203204entireLineFits = false;205break;206}207208prevCharCode = charCode;209prevCharCodeClass = charCodeClass;210}211212if (entireLineFits) {213// there is no more need to break => stop the outer loop!214if (breakingOffsetsCount > 0) {215// Add last segment, no need to assign to `lastBreakingOffset` and `lastBreakingOffsetVisibleColumn`216breakingOffsets[breakingOffsetsCount] = prevBreakingOffsets[prevBreakingOffsets.length - 1];217breakingOffsetsVisibleColumn[breakingOffsetsCount] = prevBreakingOffsetsVisibleColumn[prevBreakingOffsets.length - 1];218breakingOffsetsCount++;219}220break;221}222}223224if (breakOffset === 0) {225// must search left226let visibleColumn = prevBreakOffsetVisibleColumn;227let charCode = lineText.charCodeAt(prevBreakOffset);228let charCodeClass = classifier.get(charCode);229let hitATabCharacter = false;230for (let i = prevBreakOffset - 1; i >= lastBreakingOffset; i--) {231const charStartOffset = i + 1;232const prevCharCode = lineText.charCodeAt(i);233234if (prevCharCode === CharCode.Tab) {235// cannot determine the width of a tab when going backwards, so we must go forwards236hitATabCharacter = true;237break;238}239240let prevCharCodeClass: number;241let prevCharWidth: number;242243if (strings.isLowSurrogate(prevCharCode)) {244// A surrogate pair must always be considered as a single unit, so it is never to be broken245i--;246prevCharCodeClass = CharacterClass.NONE;247prevCharWidth = 2;248} else {249prevCharCodeClass = classifier.get(prevCharCode);250prevCharWidth = (strings.isFullWidthCharacter(prevCharCode) ? columnsForFullWidthChar : 1);251}252253if (visibleColumn <= breakingColumn) {254if (forcedBreakOffset === 0) {255forcedBreakOffset = charStartOffset;256forcedBreakOffsetVisibleColumn = visibleColumn;257}258259if (visibleColumn <= breakingColumn - wrappedLineBreakColumn) {260// went too far!261break;262}263264if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {265breakOffset = charStartOffset;266breakOffsetVisibleColumn = visibleColumn;267break;268}269}270271visibleColumn -= prevCharWidth;272charCode = prevCharCode;273charCodeClass = prevCharCodeClass;274}275276if (breakOffset !== 0) {277const remainingWidthOfNextLine = wrappedLineBreakColumn - (forcedBreakOffsetVisibleColumn - breakOffsetVisibleColumn);278if (remainingWidthOfNextLine <= tabSize) {279const charCodeAtForcedBreakOffset = lineText.charCodeAt(forcedBreakOffset);280let charWidth: number;281if (strings.isHighSurrogate(charCodeAtForcedBreakOffset)) {282// A surrogate pair must always be considered as a single unit, so it is never to be broken283charWidth = 2;284} else {285charWidth = computeCharWidth(charCodeAtForcedBreakOffset, forcedBreakOffsetVisibleColumn, tabSize, columnsForFullWidthChar);286}287if (remainingWidthOfNextLine - charWidth < 0) {288// it is not worth it to break at breakOffset, it just introduces an extra needless line!289breakOffset = 0;290}291}292}293294if (hitATabCharacter) {295// cannot determine the width of a tab when going backwards, so we must go forwards from the previous break296prevIndex--;297continue;298}299}300301if (breakOffset === 0) {302// Could not find a good breaking point303breakOffset = forcedBreakOffset;304breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn;305}306307if (breakOffset <= lastBreakingOffset) {308// Make sure that we are advancing (at least one character)309const charCode = lineText.charCodeAt(lastBreakingOffset);310if (strings.isHighSurrogate(charCode)) {311// A surrogate pair must always be considered as a single unit, so it is never to be broken312breakOffset = lastBreakingOffset + 2;313breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + 2;314} else {315breakOffset = lastBreakingOffset + 1;316breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + computeCharWidth(charCode, lastBreakingOffsetVisibleColumn, tabSize, columnsForFullWidthChar);317}318}319320lastBreakingOffset = breakOffset;321breakingOffsets[breakingOffsetsCount] = breakOffset;322lastBreakingOffsetVisibleColumn = breakOffsetVisibleColumn;323breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn;324breakingOffsetsCount++;325breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn;326327while (prevIndex < 0 || (prevIndex < prevLen && prevBreakingOffsetsVisibleColumn[prevIndex] < breakOffsetVisibleColumn)) {328prevIndex++;329}330331let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn);332while (prevIndex + 1 < prevLen) {333const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn);334if (distance >= bestDistance) {335break;336}337bestDistance = distance;338prevIndex++;339}340}341342if (breakingOffsetsCount === 0) {343return null;344}345346// Doing here some object reuse which ends up helping a huge deal with GC pauses!347breakingOffsets.length = breakingOffsetsCount;348breakingOffsetsVisibleColumn.length = breakingOffsetsCount;349arrPool1 = previousBreakingData.breakOffsets;350arrPool2 = previousBreakingData.breakOffsetsVisibleColumn;351previousBreakingData.breakOffsets = breakingOffsets;352previousBreakingData.breakOffsetsVisibleColumn = breakingOffsetsVisibleColumn;353previousBreakingData.wrappedTextIndentLength = wrappedTextIndentLength;354return previousBreakingData;355}356357function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null {358const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts);359360let injectionOptions: InjectedTextOptions[] | null;361let injectionOffsets: number[] | null;362if (injectedTexts && injectedTexts.length > 0) {363injectionOptions = injectedTexts.map(t => t.options);364injectionOffsets = injectedTexts.map(text => text.column - 1);365} else {366injectionOptions = null;367injectionOffsets = null;368}369370if (firstLineBreakColumn === -1) {371if (!injectionOptions) {372return null;373}374// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK375// because `breakOffsetsVisibleColumn` will never be used because it contains injected text376return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);377}378379const len = lineText.length;380if (len <= 1) {381if (!injectionOptions) {382return null;383}384// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK385// because `breakOffsetsVisibleColumn` will never be used because it contains injected text386return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);387}388389const isKeepAll = (wordBreak === 'keepAll');390const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);391const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;392393const breakingOffsets: number[] = [];394const breakingOffsetsVisibleColumn: number[] = [];395let breakingOffsetsCount: number = 0;396let breakOffset = 0;397let breakOffsetVisibleColumn = 0;398399let breakingColumn = firstLineBreakColumn;400let prevCharCode = lineText.charCodeAt(0);401let prevCharCodeClass = classifier.get(prevCharCode);402let visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar);403404let startOffset = 1;405if (strings.isHighSurrogate(prevCharCode)) {406// A surrogate pair must always be considered as a single unit, so it is never to be broken407visibleColumn += 1;408prevCharCode = lineText.charCodeAt(1);409prevCharCodeClass = classifier.get(prevCharCode);410startOffset++;411}412413for (let i = startOffset; i < len; i++) {414const charStartOffset = i;415const charCode = lineText.charCodeAt(i);416let charCodeClass: CharacterClass;417let charWidth: number;418419if (strings.isHighSurrogate(charCode)) {420// A surrogate pair must always be considered as a single unit, so it is never to be broken421i++;422charCodeClass = CharacterClass.NONE;423charWidth = 2;424} else {425charCodeClass = classifier.get(charCode);426charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);427}428429if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {430breakOffset = charStartOffset;431breakOffsetVisibleColumn = visibleColumn;432}433434visibleColumn += charWidth;435436// literal \n shall trigger a softwrap437if (438wrapOnEscapedLineFeeds439&& i >= 2440&& (i < 3 || lineText.charAt(i - 3) !== '\\')441&& lineText.charAt(i - 2) === '\\'442&& lineText.charAt(i - 1) === 'n'443&& lineText.includes('"')444) {445visibleColumn += breakingColumn;446}447448// check if adding character at `i` will go over the breaking column449if (visibleColumn > breakingColumn) {450// We need to break at least before character at `i`:451452if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) {453// Cannot break at `breakOffset`, must break at `i`454breakOffset = charStartOffset;455breakOffsetVisibleColumn = visibleColumn - charWidth;456}457458breakingOffsets[breakingOffsetsCount] = breakOffset;459breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn;460breakingOffsetsCount++;461breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn;462breakOffset = 0;463}464465prevCharCode = charCode;466prevCharCodeClass = charCodeClass;467}468469if (breakingOffsetsCount === 0 && (!injectedTexts || injectedTexts.length === 0)) {470return null;471}472473// Add last segment474breakingOffsets[breakingOffsetsCount] = len;475breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn;476477return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength);478}479480function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number {481if (charCode === CharCode.Tab) {482return (tabSize - (visibleColumn % tabSize));483}484if (strings.isFullWidthCharacter(charCode)) {485return columnsForFullWidthChar;486}487if (charCode < 32) {488// when using `editor.renderControlCharacters`, the substitutions are often wide489return columnsForFullWidthChar;490}491return 1;492}493494function tabCharacterWidth(visibleColumn: number, tabSize: number): number {495return (tabSize - (visibleColumn % tabSize));496}497498/**499* Kinsoku Shori : Don't break after a leading character, like an open bracket500* Kinsoku Shori : Don't break before a trailing character, like a period501*/502function canBreak(prevCharCode: number, prevCharCodeClass: CharacterClass, charCode: number, charCodeClass: CharacterClass, isKeepAll: boolean): boolean {503return (504charCode !== CharCode.Space505&& (506(prevCharCodeClass === CharacterClass.BREAK_AFTER && charCodeClass !== CharacterClass.BREAK_AFTER) // break at the end of multiple BREAK_AFTER507|| (prevCharCodeClass !== CharacterClass.BREAK_BEFORE && charCodeClass === CharacterClass.BREAK_BEFORE) // break at the start of multiple BREAK_BEFORE508|| (!isKeepAll && prevCharCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && charCodeClass !== CharacterClass.BREAK_AFTER)509|| (!isKeepAll && charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && prevCharCodeClass !== CharacterClass.BREAK_BEFORE)510)511);512}513514function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): number {515let wrappedTextIndentLength = 0;516if (wrappingIndent !== WrappingIndent.None) {517const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText);518if (firstNonWhitespaceIndex !== -1) {519// Track existing indent520521for (let i = 0; i < firstNonWhitespaceIndex; i++) {522const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1);523wrappedTextIndentLength += charWidth;524}525526// Increase indent of continuation lines, if desired527const numberOfAdditionalTabs = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0);528for (let i = 0; i < numberOfAdditionalTabs; i++) {529const charWidth = tabCharacterWidth(wrappedTextIndentLength, tabSize);530wrappedTextIndentLength += charWidth;531}532533// Force sticking to beginning of line if no character would fit except for the indentation534if (wrappedTextIndentLength + columnsForFullWidthChar > firstLineBreakColumn) {535wrappedTextIndentLength = 0;536}537}538}539return wrappedTextIndentLength;540}541542543