monaco-editor-core
Version:
A browser based code editor
471 lines • 24.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 '../textModelEvents.js';
import { ModelLineProjectionData } from '../modelLineProjectionData.js';
export class MonospaceLineBreaksComputerFactory {
static create(options) {
return new MonospaceLineBreaksComputerFactory(options.get(151 /* EditorOption.wordWrapBreakBeforeCharacters */), options.get(150 /* EditorOption.wordWrapBreakAfterCharacters */));
}
constructor(breakBeforeChars, breakAfterChars) {
this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars);
}
createLineBreaksComputer(fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds) {
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;
const result = [];
for (let i = 0, len = requests.length; i < len; i++) {
const injectedText = injectedTexts[i];
const previousLineBreakData = previousBreakingData[i];
const lineText = requests[i];
const isLineFeedWrappingEnabled = wrapOnEscapedLineFeeds && lineText.includes('"') && lineText.includes('\\n');
if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText && !isLineFeedWrappingEnabled) {
result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, lineText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak);
}
else {
result[i] = createLineBreaks(this.classifier, lineText, injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak, isLineFeedWrappingEnabled);
}
}
arrPool1.length = 0;
arrPool2.length = 0;
return result;
}
};
}
}
class WrappingCharacterClassifier extends CharacterClassifier {
constructor(BREAK_BEFORE, BREAK_AFTER) {
super(0 /* CharacterClass.NONE */);
for (let i = 0; i < BREAK_BEFORE.length; i++) {
this.set(BREAK_BEFORE.charCodeAt(i), 1 /* CharacterClass.BREAK_BEFORE */);
}
for (let i = 0; i < BREAK_AFTER.length; i++) {
this.set(BREAK_AFTER.charCodeAt(i), 2 /* CharacterClass.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 /* CharacterClass.BREAK_IDEOGRAPHIC */;
}
return (this._map.get(charCode) || this._defaultValue);
}
}
}
let arrPool1 = [];
let arrPool2 = [];
function createLineBreaksFromPreviousLineBreaks(classifier, previousBreakingData, lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent, wordBreak) {
if (firstLineBreakColumn === -1) {
return null;
}
const len = lineText.length;
if (len <= 1) {
return null;
}
const isKeepAll = (wordBreak === 'keepAll');
const prevBreakingOffsets = previousBreakingData.breakOffsets;
const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakOffsetsVisibleColumn;
const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);
const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;
const breakingOffsets = arrPool1;
const 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 /* CharCode.Null */ : lineText.charCodeAt(prevBreakOffset - 1);
let prevCharCodeClass = prevBreakOffset === 0 ? 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;
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 /* CharacterClass.NONE */;
charWidth = 2;
}
else {
charCodeClass = classifier.get(charCode);
charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);
}
if (charStartOffset > lastBreakingOffset && canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {
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 /* CharCode.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 /* 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, isKeepAll)) {
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, wordBreak, wrapOnEscapedLineFeeds) {
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 isKeepAll = (wordBreak === 'keepAll');
const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);
const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;
const breakingOffsets = [];
const 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;
let wrapEscapedLineFeed = false;
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 /* CharacterClass.NONE */;
charWidth = 2;
}
else {
charCodeClass = classifier.get(charCode);
charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);
}
// literal \n shall trigger a softwrap
if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) {
breakOffset = charStartOffset;
breakOffsetVisibleColumn = visibleColumn;
wrapEscapedLineFeed = true;
}
else if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {
breakOffset = charStartOffset;
breakOffsetVisibleColumn = visibleColumn;
}
visibleColumn += charWidth;
// check if adding character at `i` will go over the breaking column
if (visibleColumn > breakingColumn || wrapEscapedLineFeed) {
// 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 /* 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, tabSize) {
return (tabSize - (visibleColumn % tabSize));
}
/**
* Checks if the current position in the text should trigger a soft wrap due to escaped line feeds.
* This handles the wrapOnEscapedLineFeeds feature which allows \n sequences in strings to trigger wrapping.
*/
function isEscapedLineBreakAtPosition(lineText, i) {
if (i >= 2 && lineText.charAt(i - 1) === 'n') {
// Check if there's an odd number of backslashes
let escapeCount = 0;
for (let j = i - 2; j >= 0; j--) {
if (lineText.charAt(j) === '\\') {
escapeCount++;
}
else {
return escapeCount % 2 === 1;
}
}
}
return false;
}
/**
* 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, isKeepAll) {
return (charCode !== 32 /* CharCode.Space */
&& ((prevCharCodeClass === 2 /* CharacterClass.BREAK_AFTER */ && charCodeClass !== 2 /* CharacterClass.BREAK_AFTER */) // break at the end of multiple BREAK_AFTER
|| (prevCharCodeClass !== 1 /* CharacterClass.BREAK_BEFORE */ && charCodeClass === 1 /* CharacterClass.BREAK_BEFORE */) // break at the start of multiple BREAK_BEFORE
|| (!isKeepAll && prevCharCodeClass === 3 /* CharacterClass.BREAK_IDEOGRAPHIC */ && charCodeClass !== 2 /* CharacterClass.BREAK_AFTER */)
|| (!isKeepAll && charCodeClass === 3 /* CharacterClass.BREAK_IDEOGRAPHIC */ && prevCharCodeClass !== 1 /* CharacterClass.BREAK_BEFORE */)));
}
function computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent) {
let wrappedTextIndentLength = 0;
if (wrappingIndent !== 0 /* 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) === 9 /* CharCode.Tab */ ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1);
wrappedTextIndentLength += charWidth;
}
// Increase indent of continuation lines, if desired
const numberOfAdditionalTabs = (wrappingIndent === 3 /* WrappingIndent.DeepIndent */ ? 2 : wrappingIndent === 2 /* 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;
}
//# sourceMappingURL=monospaceLineBreaksComputer.js.map