src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts
555 LOC · 527 covered · 28 uncovered · 82 ranges · 38 concepts · 28 introducers · 18 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
monospaceLineBreaksComputer.ts ×24
* 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 '../../../base/common/charCode.js';
import * as strings from '../../../base/common/strings.js';
import { WrappingIndent, IComputedEditorOptions, EditorOption } from '../config/editorOptions.js';
import { CharacterClassifier } from '../core/characterClassifier.js';
import { FontInfo } from '../config/fontInfo.js';
import { LineInjectedText } from '../textModelEvents.js';
import { InjectedTextOptions } from '../model.js';
import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js';
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(context: ILineBreaksComputerContext, fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer {
const lineNumbers: number[] = [];
const previousBreakingData: (ModelLineProjectionData | null)[] = [];
return {
addRequest: (lineNumber: number, previousLineBreakData: ModelLineProjectionData | null) => {
lineNumbers.push(lineNumber);
previousBreakingData.push(previousLineBreakData);
},
finalize: () => {
const columnsForFullWidthChar = fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth;
const result: (ModelLineProjectionData | null)[] = [];
for (let i = 0, len = lineNumbers.length; i < len; i++) {
const lineNumber = lineNumbers[i];
const injectedText = context.getLineInjectedText(lineNumber);
const lineText = context.getLineContent(lineNumber);
const previousLineBreakData = previousBreakingData[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);
monospaceLineBreaksComputer.ts ×15
result[i] = createLineBreaks(this.classifier, lineText, injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak, isLineFeedWrappingEnabled);
}
}
arrPool1.length = 0;
arrPool2.length = 0;
return result;
}
};
}
}
const enum CharacterClass {
NONE = 0,
BREAK_BEFORE = 1,
BREAK_AFTER = 2,
BREAK_IDEOGRAPHIC = 3 // for Han and Kana.
}
class WrappingCharacterClassifier extends CharacterClassifier<CharacterClass> {
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);
monospaceLineBreaksComputer.ts ×1
}
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) {
// Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges:
monospaceLineBreaksComputer.ts ×2
// 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>(this._map.get(charCode) || this._defaultValue);
}
let arrPool1: number[] = [];
let arrPool2: number[] = [];
function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterClassifier, previousBreakingData: ModelLineProjectionData, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ModelLineProjectionData | null {
monospaceLineBreaksComputer.ts ×15
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: number[] = arrPool1;
const breakingOffsetsVisibleColumn: number[] = 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) {
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
monospaceLineBreaksComputer.ts ×4
i++;
charCodeClass = CharacterClass.NONE;
charWidth = 2;
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 {
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
monospaceLineBreaksComputer.ts ×2
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
monospaceLineBreaksComputer.ts ×4
i--;
prevCharCodeClass = CharacterClass.NONE;
prevCharWidth = 2;
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: number;
if (strings.isHighSurrogate(charCodeAtForcedBreakOffset)) {
// A surrogate pair must always be considered as a single unit, so it is never to be broken
monospaceLineBreaksComputer.ts ×4
charWidth = 2;
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!
monospaceLineBreaksComputer.ts ×4
breakOffset = 0;
}
}
if (hitATabCharacter) {
// cannot determine the width of a tab when going backwards, so we must go forwards from the previous break
monospaceLineBreaksComputer.ts ×2
prevIndex--;
continue;
}
if (breakOffset === 0) {
// Could not find a good breaking point
breakOffset = forcedBreakOffset;
breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn;
}
if (breakOffset <= lastBreakingOffset) {
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;
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, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null {
const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts);
let injectionOptions: InjectedTextOptions[] | null;
let injectionOffsets: number[] | null;
if (injectedTexts && injectedTexts.length > 0) {
injectionOptions = injectedTexts.map(t => t.options);
injectionOffsets = injectedTexts.map(text => text.column - 1);
injectionOptions = null;
injectionOffsets = null;
}
if (firstLineBreakColumn === -1) {
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) {
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: number[] = [];
const 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
monospaceLineBreaksComputer.ts ×1
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: CharacterClass;
let charWidth: number;
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
monospaceLineBreaksComputer.ts ×1
i++;
charCodeClass = CharacterClass.NONE;
charWidth = 2;
charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);
}
// literal \n shall trigger a softwrap
if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) {
breakOffsetVisibleColumn = visibleColumn;
wrapEscapedLineFeed = true;
} else if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) {
monospaceLineBreaksComputer.ts ×24
breakOffsetVisibleColumn = visibleColumn;
}
visibleColumn += charWidth;
// check if adding character at `i` will go over the breaking column
if (visibleColumn > breakingColumn || wrapEscapedLineFeed) {
if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) {
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)) {
}
// Add last segment
breakingOffsets[breakingOffsetsCount] = len;
breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn;
return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength);
}
function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number {
if (charCode === CharCode.Tab) {
}
}
// when using `editor.renderControlCharacters`, the substitutions are often wide
monospaceLineBreaksComputer.ts ×1
return columnsForFullWidthChar;
}
}
function tabCharacterWidth(visibleColumn: number, tabSize: number): number {
monospaceLineBreaksComputer.ts ×1
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: string, i: number): boolean {
monospaceLineBreaksComputer.ts ×3
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;
}
}
}
}
/**
* 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, isKeepAll: boolean): boolean {
return (
charCode !== CharCode.Space
&& (
(prevCharCodeClass === CharacterClass.BREAK_AFTER && charCodeClass !== CharacterClass.BREAK_AFTER) // break at the end of multiple BREAK_AFTER
|| (prevCharCodeClass !== CharacterClass.BREAK_BEFORE && charCodeClass === CharacterClass.BREAK_BEFORE) // break at the start of multiple BREAK_BEFORE
monospaceLineBreaksComputer.ts ×1
|| (!isKeepAll && prevCharCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && charCodeClass !== CharacterClass.BREAK_AFTER)
|| (!isKeepAll && 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);
monospaceLineBreaksComputer.ts ×4
if (firstNonWhitespaceIndex !== -1) {
// Track existing indent
for (let i = 0; i < firstNonWhitespaceIndex; i++) {
const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1);
monospaceLineBreaksComputer.ts ×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);
monospaceLineBreaksComputer.ts ×1
wrappedTextIndentLength += charWidth;
}
// Force sticking to beginning of line if no character would fit except for the indentation
if (wrappedTextIndentLength + columnsForFullWidthChar > firstLineBreakColumn) {
}
}
}