/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as strings from '../../../base/common/strings.js'; import { CharacterClassifier } from '../core/characterClassifier.js'; import { LineInjectedText } from '../model/textModelEvents.js'; import { ModelLineProjectionData } from './modelLineProjectionData.js'; export class MonospaceLineBreaksComputerFactory { constructor(breakBeforeChars, breakAfterChars) { this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars); } static create(options) { return new MonospaceLineBreaksComputerFactory(options.get(119 /* wordWrapBreakBeforeCharacters */), options.get(118 /* wordWrapBreakAfterCharacters */)); } createLineBreaksComputer(fontInfo, tabSize, wrappingColumn, wrappingIndent) { const requests = []; const injectedTexts = []; const previousBreakingData = []; return { addRequest: (lineText, injectedText, previousLineBreakData) => { requests.push(lineText); injectedTexts.push(injectedText); previousBreakingData.push(previousLineBreakData); }, finalize: () => { const columnsForFullWidthChar = fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth; let result = []; for (let i = 0, len = requests.length; i < len; i++) { const injectedText = injectedTexts[i]; const previousLineBreakData = previousBreakingData[i]; if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText) { result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); } else { result[i] = createLineBreaks(this.classifier, requests[i], injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); } } arrPool1.length = 0; arrPool2.length = 0; return result; } }; } } class WrappingCharacterClassifier extends CharacterClassifier { constructor(BREAK_BEFORE, BREAK_AFTER) { super(0 /* NONE */); for (let i = 0; i < BREAK_BEFORE.length; i++) { this.set(BREAK_BEFORE.charCodeAt(i), 1 /* BREAK_BEFORE */); } for (let i = 0; i < BREAK_AFTER.length; i++) { this.set(BREAK_AFTER.charCodeAt(i), 2 /* BREAK_AFTER */); } } get(charCode) { 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 3 /* BREAK_IDEOGRAPHIC */; } return (this._map.get(charCode) || this._defaultValue); } } } let arrPool1 = []; let arrPool2 = []; function createLineBreaksFromPreviousLineBreaks(classifier, previousBreakingData, lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent) { 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 = arrPool1; let breakingOffsetsVisibleColumn = arrPool2; let breakingOffsetsCount = 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 ? 0 /* Null */ : lineText.charCodeAt(prevBreakOffset - 1); let prevCharCodeClass = prevBreakOffset === 0 ? 0 /* NONE */ : classifier.get(prevCharCode); let entireLineFits = true; for (let i = prevBreakOffset; i < len; i++) { const charStartOffset = i; const charCode = lineText.charCodeAt(i); let charCodeClass; let charWidth; if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i++; charCodeClass = 0 /* 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 === 9 /* Tab */) { // cannot determine the width of a tab when going backwards, so we must go forwards hitATabCharacter = true; break; } let prevCharCodeClass; let prevCharWidth; if (strings.isLowSurrogate(prevCharCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i--; prevCharCodeClass = 0 /* 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; 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, _lineText, injectedTexts, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent) { const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts); let injectionOptions; let injectionOffsets; if (injectedTexts && injectedTexts.length > 0) { injectionOptions = injectedTexts.map(t => t.options); injectionOffsets = injectedTexts.map(text => text.column - 1); } else { injectionOptions = null; injectionOffsets = null; } if (firstLineBreakColumn === -1) { if (!injectionOptions) { return null; } // creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK // because `breakOffsetsVisibleColumn` will never be used because it contains injected text return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0); } const len = lineText.length; if (len <= 1) { if (!injectionOptions) { return null; } // creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK // because `breakOffsetsVisibleColumn` will never be used because it contains injected text return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0); } const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; let breakingOffsets = []; let breakingOffsetsVisibleColumn = []; let breakingOffsetsCount = 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; let charWidth; if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i++; charCodeClass = 0 /* 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 && (!injectedTexts || injectedTexts.length === 0)) { return null; } // Add last segment breakingOffsets[breakingOffsetsCount] = len; breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn; return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); } function computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar) { if (charCode === 9 /* 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, tabSize) { 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, prevCharCodeClass, charCode, charCodeClass) { return (charCode !== 32 /* Space */ && ((prevCharCodeClass === 2 /* BREAK_AFTER */) || (prevCharCodeClass === 3 /* BREAK_IDEOGRAPHIC */ && charCodeClass !== 2 /* BREAK_AFTER */) || (charCodeClass === 1 /* BREAK_BEFORE */) || (charCodeClass === 3 /* BREAK_IDEOGRAPHIC */ && prevCharCodeClass !== 1 /* BREAK_BEFORE */))); } function computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent) { let wrappedTextIndentLength = 0; if (wrappingIndent !== 0 /* None */) { const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); if (firstNonWhitespaceIndex !== -1) { // Track existing indent for (let i = 0; i < firstNonWhitespaceIndex; i++) { const charWidth = (lineText.charCodeAt(i) === 9 /* Tab */ ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1); wrappedTextIndentLength += charWidth; } // Increase indent of continuation lines, if desired const numberOfAdditionalTabs = (wrappingIndent === 3 /* DeepIndent */ ? 2 : wrappingIndent === 2 /* 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; }