Path: blob/main/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts
5237 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];44const lineText = requests[i];45const isLineFeedWrappingEnabled = wrapOnEscapedLineFeeds && lineText.includes('"') && lineText.includes('\\n');46if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText && !isLineFeedWrappingEnabled) {47result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, lineText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak);48} else {49result[i] = createLineBreaks(this.classifier, lineText, injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak, isLineFeedWrappingEnabled);50}51}52arrPool1.length = 0;53arrPool2.length = 0;54return result;55}56};57}58}5960const enum CharacterClass {61NONE = 0,62BREAK_BEFORE = 1,63BREAK_AFTER = 2,64BREAK_IDEOGRAPHIC = 3 // for Han and Kana.65}6667class WrappingCharacterClassifier extends CharacterClassifier<CharacterClass> {6869constructor(BREAK_BEFORE: string, BREAK_AFTER: string) {70super(CharacterClass.NONE);7172for (let i = 0; i < BREAK_BEFORE.length; i++) {73this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE);74}7576for (let i = 0; i < BREAK_AFTER.length; i++) {77this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER);78}79}8081public override get(charCode: number): CharacterClass {82if (charCode >= 0 && charCode < 256) {83return <CharacterClass>this._asciiMap[charCode];84} else {85// Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges:86// 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF)87// 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF)88// 3. Hiragana and Katakana (0x3040 -- 0x30FF)89if (90(charCode >= 0x3040 && charCode <= 0x30FF)91|| (charCode >= 0x3400 && charCode <= 0x4DBF)92|| (charCode >= 0x4E00 && charCode <= 0x9FFF)93) {94return CharacterClass.BREAK_IDEOGRAPHIC;95}9697return <CharacterClass>(this._map.get(charCode) || this._defaultValue);98}99}100}101102let arrPool1: number[] = [];103let arrPool2: number[] = [];104105function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterClassifier, previousBreakingData: ModelLineProjectionData, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ModelLineProjectionData | null {106if (firstLineBreakColumn === -1) {107return null;108}109110const len = lineText.length;111if (len <= 1) {112return null;113}114115const isKeepAll = (wordBreak === 'keepAll');116117const prevBreakingOffsets = previousBreakingData.breakOffsets;118const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakOffsetsVisibleColumn;119120const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);121const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;122123const breakingOffsets: number[] = arrPool1;124const breakingOffsetsVisibleColumn: number[] = arrPool2;125let breakingOffsetsCount = 0;126let lastBreakingOffset = 0;127let lastBreakingOffsetVisibleColumn = 0;128129let breakingColumn = firstLineBreakColumn;130const prevLen = prevBreakingOffsets.length;131let prevIndex = 0;132133if (prevIndex >= 0) {134let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn);135while (prevIndex + 1 < prevLen) {136const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn);137if (distance >= bestDistance) {138break;139}140bestDistance = distance;141prevIndex++;142}143}144145while (prevIndex < prevLen) {146// Allow for prevIndex to be -1 (for the case where we hit a tab when walking backwards from the first break)147let prevBreakOffset = prevIndex < 0 ? 0 : prevBreakingOffsets[prevIndex];148let prevBreakOffsetVisibleColumn = prevIndex < 0 ? 0 : prevBreakingOffsetsVisibleColumn[prevIndex];149if (lastBreakingOffset > prevBreakOffset) {150prevBreakOffset = lastBreakingOffset;151prevBreakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn;152}153154let breakOffset = 0;155let breakOffsetVisibleColumn = 0;156157let forcedBreakOffset = 0;158let forcedBreakOffsetVisibleColumn = 0;159160// initially, we search as much as possible to the right (if it fits)161if (prevBreakOffsetVisibleColumn <= breakingColumn) {162let visibleColumn = prevBreakOffsetVisibleColumn;163let prevCharCode = prevBreakOffset === 0 ? CharCode.Null : lineText.charCodeAt(prevBreakOffset - 1);164let prevCharCodeClass = prevBreakOffset === 0 ? CharacterClass.NONE : classifier.get(prevCharCode);165let entireLineFits = true;166for (let i = prevBreakOffset; i < len; i++) {167const charStartOffset = i;168const charCode = lineText.charCodeAt(i);169let charCodeClass: number;170let charWidth: number;171172if (strings.isHighSurrogate(charCode)) {173// A surrogate pair must always be considered as a single unit, so it is never to be broken174i++;175charCodeClass = CharacterClass.NONE;176charWidth = 2;177} else {178charCodeClass = classifier.get(charCode);179charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);180}181182if (charStartOffset > lastBreakingOffset && canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {183breakOffset = charStartOffset;184breakOffsetVisibleColumn = visibleColumn;185}186187visibleColumn += charWidth;188189// check if adding character at `i` will go over the breaking column190if (visibleColumn > breakingColumn) {191// We need to break at least before character at `i`:192if (charStartOffset > lastBreakingOffset) {193forcedBreakOffset = charStartOffset;194forcedBreakOffsetVisibleColumn = visibleColumn - charWidth;195} else {196// we need to advance at least by one character197forcedBreakOffset = i + 1;198forcedBreakOffsetVisibleColumn = visibleColumn;199}200201if (visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) {202// Cannot break at `breakOffset` => reset it if it was set203breakOffset = 0;204}205206entireLineFits = false;207break;208}209210prevCharCode = charCode;211prevCharCodeClass = charCodeClass;212}213214if (entireLineFits) {215// there is no more need to break => stop the outer loop!216if (breakingOffsetsCount > 0) {217// Add last segment, no need to assign to `lastBreakingOffset` and `lastBreakingOffsetVisibleColumn`218breakingOffsets[breakingOffsetsCount] = prevBreakingOffsets[prevBreakingOffsets.length - 1];219breakingOffsetsVisibleColumn[breakingOffsetsCount] = prevBreakingOffsetsVisibleColumn[prevBreakingOffsets.length - 1];220breakingOffsetsCount++;221}222break;223}224}225226if (breakOffset === 0) {227// must search left228let visibleColumn = prevBreakOffsetVisibleColumn;229let charCode = lineText.charCodeAt(prevBreakOffset);230let charCodeClass = classifier.get(charCode);231let hitATabCharacter = false;232for (let i = prevBreakOffset - 1; i >= lastBreakingOffset; i--) {233const charStartOffset = i + 1;234const prevCharCode = lineText.charCodeAt(i);235236if (prevCharCode === CharCode.Tab) {237// cannot determine the width of a tab when going backwards, so we must go forwards238hitATabCharacter = true;239break;240}241242let prevCharCodeClass: number;243let prevCharWidth: number;244245if (strings.isLowSurrogate(prevCharCode)) {246// A surrogate pair must always be considered as a single unit, so it is never to be broken247i--;248prevCharCodeClass = CharacterClass.NONE;249prevCharWidth = 2;250} else {251prevCharCodeClass = classifier.get(prevCharCode);252prevCharWidth = (strings.isFullWidthCharacter(prevCharCode) ? columnsForFullWidthChar : 1);253}254255if (visibleColumn <= breakingColumn) {256if (forcedBreakOffset === 0) {257forcedBreakOffset = charStartOffset;258forcedBreakOffsetVisibleColumn = visibleColumn;259}260261if (visibleColumn <= breakingColumn - wrappedLineBreakColumn) {262// went too far!263break;264}265266if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {267breakOffset = charStartOffset;268breakOffsetVisibleColumn = visibleColumn;269break;270}271}272273visibleColumn -= prevCharWidth;274charCode = prevCharCode;275charCodeClass = prevCharCodeClass;276}277278if (breakOffset !== 0) {279const remainingWidthOfNextLine = wrappedLineBreakColumn - (forcedBreakOffsetVisibleColumn - breakOffsetVisibleColumn);280if (remainingWidthOfNextLine <= tabSize) {281const charCodeAtForcedBreakOffset = lineText.charCodeAt(forcedBreakOffset);282let charWidth: number;283if (strings.isHighSurrogate(charCodeAtForcedBreakOffset)) {284// A surrogate pair must always be considered as a single unit, so it is never to be broken285charWidth = 2;286} else {287charWidth = computeCharWidth(charCodeAtForcedBreakOffset, forcedBreakOffsetVisibleColumn, tabSize, columnsForFullWidthChar);288}289if (remainingWidthOfNextLine - charWidth < 0) {290// it is not worth it to break at breakOffset, it just introduces an extra needless line!291breakOffset = 0;292}293}294}295296if (hitATabCharacter) {297// cannot determine the width of a tab when going backwards, so we must go forwards from the previous break298prevIndex--;299continue;300}301}302303if (breakOffset === 0) {304// Could not find a good breaking point305breakOffset = forcedBreakOffset;306breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn;307}308309if (breakOffset <= lastBreakingOffset) {310// Make sure that we are advancing (at least one character)311const charCode = lineText.charCodeAt(lastBreakingOffset);312if (strings.isHighSurrogate(charCode)) {313// A surrogate pair must always be considered as a single unit, so it is never to be broken314breakOffset = lastBreakingOffset + 2;315breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + 2;316} else {317breakOffset = lastBreakingOffset + 1;318breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + computeCharWidth(charCode, lastBreakingOffsetVisibleColumn, tabSize, columnsForFullWidthChar);319}320}321322lastBreakingOffset = breakOffset;323breakingOffsets[breakingOffsetsCount] = breakOffset;324lastBreakingOffsetVisibleColumn = breakOffsetVisibleColumn;325breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn;326breakingOffsetsCount++;327breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn;328329while (prevIndex < 0 || (prevIndex < prevLen && prevBreakingOffsetsVisibleColumn[prevIndex] < breakOffsetVisibleColumn)) {330prevIndex++;331}332333let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn);334while (prevIndex + 1 < prevLen) {335const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn);336if (distance >= bestDistance) {337break;338}339bestDistance = distance;340prevIndex++;341}342}343344if (breakingOffsetsCount === 0) {345return null;346}347348// Doing here some object reuse which ends up helping a huge deal with GC pauses!349breakingOffsets.length = breakingOffsetsCount;350breakingOffsetsVisibleColumn.length = breakingOffsetsCount;351arrPool1 = previousBreakingData.breakOffsets;352arrPool2 = previousBreakingData.breakOffsetsVisibleColumn;353previousBreakingData.breakOffsets = breakingOffsets;354previousBreakingData.breakOffsetsVisibleColumn = breakingOffsetsVisibleColumn;355previousBreakingData.wrappedTextIndentLength = wrappedTextIndentLength;356return previousBreakingData;357}358359function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null {360const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts);361362let injectionOptions: InjectedTextOptions[] | null;363let injectionOffsets: number[] | null;364if (injectedTexts && injectedTexts.length > 0) {365injectionOptions = injectedTexts.map(t => t.options);366injectionOffsets = injectedTexts.map(text => text.column - 1);367} else {368injectionOptions = null;369injectionOffsets = null;370}371372if (firstLineBreakColumn === -1) {373if (!injectionOptions) {374return null;375}376// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK377// because `breakOffsetsVisibleColumn` will never be used because it contains injected text378return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);379}380381const len = lineText.length;382if (len <= 1) {383if (!injectionOptions) {384return null;385}386// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK387// because `breakOffsetsVisibleColumn` will never be used because it contains injected text388return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);389}390391const isKeepAll = (wordBreak === 'keepAll');392const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);393const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;394395const breakingOffsets: number[] = [];396const breakingOffsetsVisibleColumn: number[] = [];397let breakingOffsetsCount: number = 0;398let breakOffset = 0;399let breakOffsetVisibleColumn = 0;400401let breakingColumn = firstLineBreakColumn;402let prevCharCode = lineText.charCodeAt(0);403let prevCharCodeClass = classifier.get(prevCharCode);404let visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar);405406let startOffset = 1;407if (strings.isHighSurrogate(prevCharCode)) {408// A surrogate pair must always be considered as a single unit, so it is never to be broken409visibleColumn += 1;410prevCharCode = lineText.charCodeAt(1);411prevCharCodeClass = classifier.get(prevCharCode);412startOffset++;413}414415for (let i = startOffset; i < len; i++) {416const charStartOffset = i;417const charCode = lineText.charCodeAt(i);418let charCodeClass: CharacterClass;419let charWidth: number;420let wrapEscapedLineFeed = false;421422if (strings.isHighSurrogate(charCode)) {423// A surrogate pair must always be considered as a single unit, so it is never to be broken424i++;425charCodeClass = CharacterClass.NONE;426charWidth = 2;427} else {428charCodeClass = classifier.get(charCode);429charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);430}431432// literal \n shall trigger a softwrap433if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) {434breakOffset = charStartOffset;435breakOffsetVisibleColumn = visibleColumn;436wrapEscapedLineFeed = true;437} else if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {438breakOffset = charStartOffset;439breakOffsetVisibleColumn = visibleColumn;440}441442visibleColumn += charWidth;443444// check if adding character at `i` will go over the breaking column445if (visibleColumn > breakingColumn || wrapEscapedLineFeed) {446// We need to break at least before character at `i`:447448if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) {449// Cannot break at `breakOffset`, must break at `i`450breakOffset = charStartOffset;451breakOffsetVisibleColumn = visibleColumn - charWidth;452}453454breakingOffsets[breakingOffsetsCount] = breakOffset;455breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn;456breakingOffsetsCount++;457breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn;458breakOffset = 0;459}460461prevCharCode = charCode;462prevCharCodeClass = charCodeClass;463}464465if (breakingOffsetsCount === 0 && (!injectedTexts || injectedTexts.length === 0)) {466return null;467}468469// Add last segment470breakingOffsets[breakingOffsetsCount] = len;471breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn;472473return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength);474}475476function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number {477if (charCode === CharCode.Tab) {478return (tabSize - (visibleColumn % tabSize));479}480if (strings.isFullWidthCharacter(charCode)) {481return columnsForFullWidthChar;482}483if (charCode < 32) {484// when using `editor.renderControlCharacters`, the substitutions are often wide485return columnsForFullWidthChar;486}487return 1;488}489490function tabCharacterWidth(visibleColumn: number, tabSize: number): number {491return (tabSize - (visibleColumn % tabSize));492}493494/**495* Checks if the current position in the text should trigger a soft wrap due to escaped line feeds.496* This handles the wrapOnEscapedLineFeeds feature which allows \n sequences in strings to trigger wrapping.497*/498function isEscapedLineBreakAtPosition(lineText: string, i: number): boolean {499if (i >= 2 && lineText.charAt(i - 1) === 'n') {500// Check if there's an odd number of backslashes501let escapeCount = 0;502for (let j = i - 2; j >= 0; j--) {503if (lineText.charAt(j) === '\\') {504escapeCount++;505} else {506return escapeCount % 2 === 1;507}508}509}510return false;511}512513/**514* Kinsoku Shori : Don't break after a leading character, like an open bracket515* Kinsoku Shori : Don't break before a trailing character, like a period516*/517function canBreak(prevCharCode: number, prevCharCodeClass: CharacterClass, charCode: number, charCodeClass: CharacterClass, isKeepAll: boolean): boolean {518return (519charCode !== CharCode.Space520&& (521(prevCharCodeClass === CharacterClass.BREAK_AFTER && charCodeClass !== CharacterClass.BREAK_AFTER) // break at the end of multiple BREAK_AFTER522|| (prevCharCodeClass !== CharacterClass.BREAK_BEFORE && charCodeClass === CharacterClass.BREAK_BEFORE) // break at the start of multiple BREAK_BEFORE523|| (!isKeepAll && prevCharCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && charCodeClass !== CharacterClass.BREAK_AFTER)524|| (!isKeepAll && charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && prevCharCodeClass !== CharacterClass.BREAK_BEFORE)525)526);527}528529function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): number {530let wrappedTextIndentLength = 0;531if (wrappingIndent !== WrappingIndent.None) {532const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText);533if (firstNonWhitespaceIndex !== -1) {534// Track existing indent535536for (let i = 0; i < firstNonWhitespaceIndex; i++) {537const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1);538wrappedTextIndentLength += charWidth;539}540541// Increase indent of continuation lines, if desired542const numberOfAdditionalTabs = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0);543for (let i = 0; i < numberOfAdditionalTabs; i++) {544const charWidth = tabCharacterWidth(wrappedTextIndentLength, tabSize);545wrappedTextIndentLength += charWidth;546}547548// Force sticking to beginning of line if no character would fit except for the indentation549if (wrappedTextIndentLength + columnsForFullWidthChar > firstLineBreakColumn) {550wrappedTextIndentLength = 0;551}552}553}554return wrappedTextIndentLength;555}556557558