/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CharCode } from '../../../../vs/base/common/charCode'; import * as strings from '../../../../vs/base/common/strings'; import { WrappingIndent, IComputedEditorOptions, EditorOption, } from '../../../../vs/editor/common/config/editorOptions'; import { CharacterClassifier } from '../../../../vs/editor/common/core/characterClassifier'; import { ILineBreaksComputerFactory } from '../../../../vs/editor/common/viewModel/splitLinesCollection'; import { FontInfo } from '../../../../vs/editor/common/config/fontInfo'; import { ILineBreaksComputer, LineBreakData, } from '../../../../vs/editor/common/viewModel/viewModel'; const enum CharacterClass { NONE = 0, BREAK_BEFORE = 1, BREAK_AFTER = 2, BREAK_IDEOGRAPHIC = 3, // for Han and Kana. } class WrappingCharacterClassifier extends CharacterClassifier { constructor(BREAK_BEFORE: string, BREAK_AFTER: string) { super(CharacterClass.NONE); for (let i = 0; i < BREAK_BEFORE.length; i++) { this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE); } for (let i = 0; i < BREAK_AFTER.length; i++) { this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER); } } public override get(charCode: number): CharacterClass { if (charCode >= 0 && charCode < 256) { return this._asciiMap[charCode]; } else { // Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges: // 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF) // 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF) // 3. Hiragana and Katakana (0x3040 -- 0x30FF) if ( (charCode >= 0x3040 && charCode <= 0x30ff) || (charCode >= 0x3400 && charCode <= 0x4dbf) || (charCode >= 0x4e00 && charCode <= 0x9fff) ) { return CharacterClass.BREAK_IDEOGRAPHIC; } return (this._map.get(charCode) || this._defaultValue); } } } let arrPool1: number[] = []; let arrPool2: number[] = []; export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory { public static create( options: IComputedEditorOptions ): MonospaceLineBreaksComputerFactory { return new MonospaceLineBreaksComputerFactory( options.get(EditorOption.wordWrapBreakBeforeCharacters), options.get(EditorOption.wordWrapBreakAfterCharacters) ); } private readonly classifier: WrappingCharacterClassifier; constructor(breakBeforeChars: string, breakAfterChars: string) { this.classifier = new WrappingCharacterClassifier( breakBeforeChars, breakAfterChars ); } public createLineBreaksComputer( fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent ): ILineBreaksComputer { tabSize = tabSize | 0; //@perf wrappingColumn = +wrappingColumn; //@perf let requests: string[] = []; let previousBreakingData: (LineBreakData | null)[] = []; return { addRequest: ( lineText: string, previousLineBreakData: LineBreakData | null ) => { requests.push(lineText); previousBreakingData.push(previousLineBreakData); }, finalize: () => { const columnsForFullWidthChar = fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth; //@perf let result: (LineBreakData | null)[] = []; for (let i = 0, len = requests.length; i < len; i++) { const previousLineBreakData = previousBreakingData[i]; if (previousLineBreakData) { result[i] = createLineBreaksFromPreviousLineBreaks( this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent ); } else { result[i] = createLineBreaks( this.classifier, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent ); } } arrPool1.length = 0; arrPool2.length = 0; return result; }, }; } } function createLineBreaksFromPreviousLineBreaks( classifier: WrappingCharacterClassifier, previousBreakingData: LineBreakData, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent ): LineBreakData | null { if (firstLineBreakColumn === -1) { return null; } const len = lineText.length; if (len <= 1) { return null; } const prevBreakingOffsets = previousBreakingData.breakOffsets; const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakOffsetsVisibleColumn; const wrappedTextIndentLength = computeWrappedTextIndentLength( lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent ); const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; let breakingOffsets: number[] = arrPool1; let breakingOffsetsVisibleColumn: number[] = arrPool2; let breakingOffsetsCount: number = 0; let lastBreakingOffset = 0; let lastBreakingOffsetVisibleColumn = 0; let breakingColumn = firstLineBreakColumn; const prevLen = prevBreakingOffsets.length; let prevIndex = 0; if (prevIndex >= 0) { let bestDistance = Math.abs( prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn ); while (prevIndex + 1 < prevLen) { const distance = Math.abs( prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn ); if (distance >= bestDistance) { break; } bestDistance = distance; prevIndex++; } } while (prevIndex < prevLen) { // Allow for prevIndex to be -1 (for the case where we hit a tab when walking backwards from the first break) let prevBreakOffset = prevIndex < 0 ? 0 : prevBreakingOffsets[prevIndex]; let prevBreakOffsetVisibleColumn = prevIndex < 0 ? 0 : prevBreakingOffsetsVisibleColumn[prevIndex]; if (lastBreakingOffset > prevBreakOffset) { prevBreakOffset = lastBreakingOffset; prevBreakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn; } let breakOffset = 0; let breakOffsetVisibleColumn = 0; let forcedBreakOffset = 0; let forcedBreakOffsetVisibleColumn = 0; // initially, we search as much as possible to the right (if it fits) if (prevBreakOffsetVisibleColumn <= breakingColumn) { let visibleColumn = prevBreakOffsetVisibleColumn; let prevCharCode = prevBreakOffset === 0 ? CharCode.Null : lineText.charCodeAt(prevBreakOffset - 1); let prevCharCodeClass = prevBreakOffset === 0 ? CharacterClass.NONE : classifier.get(prevCharCode); let entireLineFits = true; for (let i = prevBreakOffset; i < len; i++) { const charStartOffset = i; const charCode = lineText.charCodeAt(i); let charCodeClass: number; let charWidth: number; if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i++; charCodeClass = CharacterClass.NONE; charWidth = 2; } else { charCodeClass = classifier.get(charCode); charWidth = computeCharWidth( charCode, visibleColumn, tabSize, columnsForFullWidthChar ); } if ( charStartOffset > lastBreakingOffset && canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass) ) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; } visibleColumn += charWidth; // check if adding character at `i` will go over the breaking column if (visibleColumn > breakingColumn) { // We need to break at least before character at `i`: if (charStartOffset > lastBreakingOffset) { forcedBreakOffset = charStartOffset; forcedBreakOffsetVisibleColumn = visibleColumn - charWidth; } else { // we need to advance at least by one character forcedBreakOffset = i + 1; forcedBreakOffsetVisibleColumn = visibleColumn; } if ( visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn ) { // Cannot break at `breakOffset` => reset it if it was set breakOffset = 0; } entireLineFits = false; break; } prevCharCode = charCode; prevCharCodeClass = charCodeClass; } if (entireLineFits) { // there is no more need to break => stop the outer loop! if (breakingOffsetsCount > 0) { // Add last segment, no need to assign to `lastBreakingOffset` and `lastBreakingOffsetVisibleColumn` breakingOffsets[breakingOffsetsCount] = prevBreakingOffsets[prevBreakingOffsets.length - 1]; breakingOffsetsVisibleColumn[breakingOffsetsCount] = prevBreakingOffsetsVisibleColumn[prevBreakingOffsets.length - 1]; breakingOffsetsCount++; } break; } } if (breakOffset === 0) { // must search left let visibleColumn = prevBreakOffsetVisibleColumn; let charCode = lineText.charCodeAt(prevBreakOffset); let charCodeClass = classifier.get(charCode); let hitATabCharacter = false; for (let i = prevBreakOffset - 1; i >= lastBreakingOffset; i--) { const charStartOffset = i + 1; const prevCharCode = lineText.charCodeAt(i); if (prevCharCode === CharCode.Tab) { // cannot determine the width of a tab when going backwards, so we must go forwards hitATabCharacter = true; break; } let prevCharCodeClass: number; let prevCharWidth: number; if (strings.isLowSurrogate(prevCharCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i--; prevCharCodeClass = CharacterClass.NONE; prevCharWidth = 2; } else { prevCharCodeClass = classifier.get(prevCharCode); prevCharWidth = strings.isFullWidthCharacter(prevCharCode) ? columnsForFullWidthChar : 1; } if (visibleColumn <= breakingColumn) { if (forcedBreakOffset === 0) { forcedBreakOffset = charStartOffset; forcedBreakOffsetVisibleColumn = visibleColumn; } if (visibleColumn <= breakingColumn - wrappedLineBreakColumn) { // went too far! break; } if ( canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass) ) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; break; } } visibleColumn -= prevCharWidth; charCode = prevCharCode; charCodeClass = prevCharCodeClass; } if (breakOffset !== 0) { const remainingWidthOfNextLine = wrappedLineBreakColumn - (forcedBreakOffsetVisibleColumn - breakOffsetVisibleColumn); if (remainingWidthOfNextLine <= tabSize) { const charCodeAtForcedBreakOffset = lineText.charCodeAt(forcedBreakOffset); let charWidth: number; if (strings.isHighSurrogate(charCodeAtForcedBreakOffset)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken charWidth = 2; } else { charWidth = computeCharWidth( charCodeAtForcedBreakOffset, forcedBreakOffsetVisibleColumn, tabSize, columnsForFullWidthChar ); } if (remainingWidthOfNextLine - charWidth < 0) { // it is not worth it to break at breakOffset, it just introduces an extra needless line! breakOffset = 0; } } } if (hitATabCharacter) { // cannot determine the width of a tab when going backwards, so we must go forwards from the previous break prevIndex--; continue; } } if (breakOffset === 0) { // Could not find a good breaking point breakOffset = forcedBreakOffset; breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn; } if (breakOffset <= lastBreakingOffset) { // Make sure that we are advancing (at least one character) const charCode = lineText.charCodeAt(lastBreakingOffset); if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken breakOffset = lastBreakingOffset + 2; breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + 2; } else { breakOffset = lastBreakingOffset + 1; breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + computeCharWidth( charCode, lastBreakingOffsetVisibleColumn, tabSize, columnsForFullWidthChar ); } } lastBreakingOffset = breakOffset; breakingOffsets[breakingOffsetsCount] = breakOffset; lastBreakingOffsetVisibleColumn = breakOffsetVisibleColumn; breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; breakingOffsetsCount++; breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; while ( prevIndex < 0 || (prevIndex < prevLen && prevBreakingOffsetsVisibleColumn[prevIndex] < breakOffsetVisibleColumn) ) { prevIndex++; } let bestDistance = Math.abs( prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn ); while (prevIndex + 1 < prevLen) { const distance = Math.abs( prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn ); if (distance >= bestDistance) { break; } bestDistance = distance; prevIndex++; } } if (breakingOffsetsCount === 0) { return null; } // Doing here some object reuse which ends up helping a huge deal with GC pauses! breakingOffsets.length = breakingOffsetsCount; breakingOffsetsVisibleColumn.length = breakingOffsetsCount; arrPool1 = previousBreakingData.breakOffsets; arrPool2 = previousBreakingData.breakOffsetsVisibleColumn; previousBreakingData.breakOffsets = breakingOffsets; previousBreakingData.breakOffsetsVisibleColumn = breakingOffsetsVisibleColumn; previousBreakingData.wrappedTextIndentLength = wrappedTextIndentLength; return previousBreakingData; } function createLineBreaks( classifier: WrappingCharacterClassifier, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent ): LineBreakData | null { if (firstLineBreakColumn === -1) { return null; } const len = lineText.length; if (len <= 1) { return null; } const wrappedTextIndentLength = computeWrappedTextIndentLength( lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent ); const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; let breakingOffsets: number[] = []; let breakingOffsetsVisibleColumn: number[] = []; let breakingOffsetsCount: number = 0; let breakOffset = 0; let breakOffsetVisibleColumn = 0; let breakingColumn = firstLineBreakColumn; let prevCharCode = lineText.charCodeAt(0); let prevCharCodeClass = classifier.get(prevCharCode); let visibleColumn = computeCharWidth( prevCharCode, 0, tabSize, columnsForFullWidthChar ); let startOffset = 1; if (strings.isHighSurrogate(prevCharCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken visibleColumn += 1; prevCharCode = lineText.charCodeAt(1); prevCharCodeClass = classifier.get(prevCharCode); startOffset++; } for (let i = startOffset; i < len; i++) { const charStartOffset = i; const charCode = lineText.charCodeAt(i); let charCodeClass: number; let charWidth: number; if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i++; charCodeClass = CharacterClass.NONE; charWidth = 2; } else { charCodeClass = classifier.get(charCode); charWidth = computeCharWidth( charCode, visibleColumn, tabSize, columnsForFullWidthChar ); } if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; } visibleColumn += charWidth; // check if adding character at `i` will go over the breaking column if (visibleColumn > breakingColumn) { // We need to break at least before character at `i`: if ( breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn ) { // Cannot break at `breakOffset`, must break at `i` breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn - charWidth; } breakingOffsets[breakingOffsetsCount] = breakOffset; breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; breakingOffsetsCount++; breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; breakOffset = 0; } prevCharCode = charCode; prevCharCodeClass = charCodeClass; } if (breakingOffsetsCount === 0) { return null; } // Add last segment breakingOffsets[breakingOffsetsCount] = len; breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn; return new LineBreakData( breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength ); } function computeCharWidth( charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number ): number { if (charCode === CharCode.Tab) { return tabSize - (visibleColumn % tabSize); } if (strings.isFullWidthCharacter(charCode)) { return columnsForFullWidthChar; } if (charCode < 32) { // when using `editor.renderControlCharacters`, the substitutions are often wide return columnsForFullWidthChar; } return 1; } function tabCharacterWidth(visibleColumn: number, tabSize: number): number { return tabSize - (visibleColumn % tabSize); } /** * Kinsoku Shori : Don't break after a leading character, like an open bracket * Kinsoku Shori : Don't break before a trailing character, like a period */ function canBreak( prevCharCode: number, prevCharCodeClass: CharacterClass, charCode: number, charCodeClass: CharacterClass ): boolean { return ( charCode !== CharCode.Space && (prevCharCodeClass === CharacterClass.BREAK_AFTER || (prevCharCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && charCodeClass !== CharacterClass.BREAK_AFTER) || charCodeClass === CharacterClass.BREAK_BEFORE || (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && prevCharCodeClass !== CharacterClass.BREAK_BEFORE)) ); } function computeWrappedTextIndentLength( lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent ): number { let wrappedTextIndentLength = 0; if (wrappingIndent !== WrappingIndent.None) { const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); if (firstNonWhitespaceIndex !== -1) { // Track existing indent for (let i = 0; i < firstNonWhitespaceIndex; i++) { const charWidth = lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1; wrappedTextIndentLength += charWidth; } // Increase indent of continuation lines, if desired const numberOfAdditionalTabs = wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0; for (let i = 0; i < numberOfAdditionalTabs; i++) { const charWidth = tabCharacterWidth(wrappedTextIndentLength, tabSize); wrappedTextIndentLength += charWidth; } // Force sticking to beginning of line if no character would fit except for the indentation if ( wrappedTextIndentLength + columnsForFullWidthChar > firstLineBreakColumn ) { wrappedTextIndentLength = 0; } } } return wrappedTextIndentLength; }