Path: blob/main/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts
5240 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*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';6import { EditorOptions, WrappingIndent } from '../../../common/config/editorOptions.js';7import { FontInfo } from '../../../common/config/fontInfo.js';8import { ILineBreaksComputerFactory, ModelLineProjectionData } from '../../../common/modelLineProjectionData.js';9import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js';1011function parseAnnotatedText(annotatedText: string): { text: string; indices: number[] } {12let text = '';13let currentLineIndex = 0;14const indices: number[] = [];15for (let i = 0, len = annotatedText.length; i < len; i++) {16if (annotatedText.charAt(i) === '|') {17currentLineIndex++;18} else {19text += annotatedText.charAt(i);20indices[text.length - 1] = currentLineIndex;21}22}23return { text: text, indices: indices };24}2526function toAnnotatedText(text: string, lineBreakData: ModelLineProjectionData | null): string {27// Insert line break markers again, according to algorithm28let actualAnnotatedText = '';29if (lineBreakData) {30let previousLineIndex = 0;31for (let i = 0, len = text.length; i < len; i++) {32const r = lineBreakData.translateToOutputPosition(i);33if (previousLineIndex !== r.outputLineIndex) {34previousLineIndex = r.outputLineIndex;35actualAnnotatedText += '|';36}37actualAnnotatedText += text.charAt(i);38}39} else {40// No wrapping41actualAnnotatedText = text;42}43return actualAnnotatedText;44}4546function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean, text: string, previousLineBreakData: ModelLineProjectionData | null): ModelLineProjectionData | null {47const fontInfo = new FontInfo({48pixelRatio: 1,49fontFamily: 'testFontFamily',50fontWeight: 'normal',51fontSize: 14,52fontFeatureSettings: '',53fontVariationSettings: '',54lineHeight: 19,55letterSpacing: 0,56isMonospace: true,57typicalHalfwidthCharacterWidth: 7,58typicalFullwidthCharacterWidth: 7 * columnsForFullWidthChar,59canUseHalfwidthRightwardsArrow: true,60spaceWidth: 7,61middotWidth: 7,62wsmiddotWidth: 7,63maxDigitWidth: 764}, false);65const lineBreaksComputer = factory.createLineBreaksComputer(fontInfo, tabSize, breakAfter, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds);66const previousLineBreakDataClone = previousLineBreakData ? new ModelLineProjectionData(null, null, previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null;67lineBreaksComputer.addRequest(text, null, previousLineBreakDataClone);68return lineBreaksComputer.finalize()[0];69}7071function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None, wordBreak: 'normal' | 'keepAll' = 'normal'): ModelLineProjectionData | null {72// Create version of `annotatedText` with line break markers removed73const text = parseAnnotatedText(annotatedText).text;74const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, wordBreak, false, text, null);75const actualAnnotatedText = toAnnotatedText(text, lineBreakData);7677assert.strictEqual(actualAnnotatedText, annotatedText);7879return lineBreakData;80}8182suite('Editor ViewModel - MonospaceLineBreaksComputer', () => {8384ensureNoDisposablesAreLeakedInTestSuite();8586test('MonospaceLineBreaksComputer', () => {8788const factory = new MonospaceLineBreaksComputerFactory('(', '\t).');8990// Empty string91assertLineBreaks(factory, 4, 5, '');9293// No wrapping if not necessary94assertLineBreaks(factory, 4, 5, 'aaa');95assertLineBreaks(factory, 4, 5, 'aaaaa');96assertLineBreaks(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');9798// Acts like hard wrapping if no char found99assertLineBreaks(factory, 4, 5, 'aaaaa|a');100101// Honors wrapping character102assertLineBreaks(factory, 4, 5, 'aaaaa|.');103assertLineBreaks(factory, 4, 5, 'aaaaa|a.|aaa.|aa');104assertLineBreaks(factory, 4, 5, 'aaaaa|a..|aaa.|aa');105assertLineBreaks(factory, 4, 5, 'aaaaa|a...|aaa.|aa');106assertLineBreaks(factory, 4, 5, 'aaaaa|a....|aaa.|aa');107108// Honors tabs when computing wrapping position109assertLineBreaks(factory, 4, 5, '\t');110assertLineBreaks(factory, 4, 5, '\t|aaa');111assertLineBreaks(factory, 4, 5, '\t|a\t|aa');112assertLineBreaks(factory, 4, 5, 'aa\ta');113assertLineBreaks(factory, 4, 5, 'aa\t|aa');114115// Honors wrapping before characters (& gives it priority)116assertLineBreaks(factory, 4, 5, 'aaa.|aa');117assertLineBreaks(factory, 4, 5, 'aaa(.|aa');118119// Honors wrapping after characters (& gives it priority)120assertLineBreaks(factory, 4, 5, 'aaa))|).aaa');121assertLineBreaks(factory, 4, 5, 'aaa))|).|aaaa');122assertLineBreaks(factory, 4, 5, 'aaa)|().|aaa');123assertLineBreaks(factory, 4, 5, 'aaa|(().|aaa');124assertLineBreaks(factory, 4, 5, 'aa.|(().|aaa');125assertLineBreaks(factory, 4, 5, 'aa.|(.).|aaa');126});127128function assertLineBreakDataEqual(a: ModelLineProjectionData | null, b: ModelLineProjectionData | null): void {129if (!a || !b) {130assert.deepStrictEqual(a, b);131return;132}133assert.deepStrictEqual(a.breakOffsets, b.breakOffsets);134assert.deepStrictEqual(a.wrappedTextIndentLength, b.wrappedTextIndentLength);135for (let i = 0; i < a.breakOffsetsVisibleColumn.length; i++) {136const diff = a.breakOffsetsVisibleColumn[i] - b.breakOffsetsVisibleColumn[i];137assert.ok(diff < 0.001);138}139}140141function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None, columnsForFullWidthChar: number = 2): void {142// sanity check the test143assert.strictEqual(text, parseAnnotatedText(annotatedText1).text);144assert.strictEqual(text, parseAnnotatedText(annotatedText2).text);145146// check that the direct mapping is ok for 1147const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, null);148assert.strictEqual(toAnnotatedText(text, directLineBreakData1), annotatedText1);149150// check that the direct mapping is ok for 2151const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, null);152assert.strictEqual(toAnnotatedText(text, directLineBreakData2), annotatedText2);153154// check that going from 1 to 2 is ok155const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, directLineBreakData1);156assert.strictEqual(toAnnotatedText(text, lineBreakData2from1), annotatedText2);157assertLineBreakDataEqual(lineBreakData2from1, directLineBreakData2);158159// check that going from 2 to 1 is ok160const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, directLineBreakData2);161assert.strictEqual(toAnnotatedText(text, lineBreakData1from2), annotatedText1);162assertLineBreakDataEqual(lineBreakData1from2, directLineBreakData1);163}164165test('MonospaceLineBreaksComputer incremental 1', () => {166167const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);168169assertIncrementalLineBreaks(170factory, 'just some text and more', 4,17110, 'just some |text and |more',17215, 'just some text |and more'173);174175assertIncrementalLineBreaks(176factory, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.', 4,17747, 'Cu scripserit suscipiantur eos, in affert |pericula contentiones sed, cetero sanctus et |pro. Ius vidit magna regione te, sit ei |elaboraret liberavisse. Mundi verear eu mea, |eam vero scriptorem in, vix in menandri |assueverit. Natum definiebas cu vim. Vim |doming vocibus efficiantur id. In indoctum |deseruisse voluptatum vim, ad debitis verterem |sed.',178142, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret |liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur |id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.',179);180181assertIncrementalLineBreaks(182factory, 'An his legere persecuti, oblique delicata efficiantur ex vix, vel at graecis officiis maluisset. Et per impedit voluptua, usu discere maiorum at. Ut assum ornatus temporibus vis, an sea melius pericula. Ea dicunt oblique phaedrum nam, eu duo movet nobis. His melius facilis eu, vim malorum temporibus ne. Nec no sale regione, meliore civibus placerat id eam. Mea alii fabulas definitionem te, agam volutpat ad vis, et per bonorum nonumes repudiandae.', 4,18357, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt |oblique phaedrum nam, eu duo movet nobis. His melius |facilis eu, vim malorum temporibus ne. Nec no sale |regione, meliore civibus placerat id eam. Mea alii |fabulas definitionem te, agam volutpat ad vis, et per |bonorum nonumes repudiandae.',18458, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt oblique |phaedrum nam, eu duo movet nobis. His melius facilis eu, |vim malorum temporibus ne. Nec no sale regione, meliore |civibus placerat id eam. Mea alii fabulas definitionem |te, agam volutpat ad vis, et per bonorum nonumes |repudiandae.'185);186187assertIncrementalLineBreaks(188factory, '\t\t"owner": "vscode",', 4,18914, '\t\t"owner|": |"vscod|e",',19016, '\t\t"owner":| |"vscode"|,',191WrappingIndent.Same192);193194assertIncrementalLineBreaks(195factory, 'ππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌπ&π¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬', 4,19651, 'ππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌπ&|π¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬',19750, 'ππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌπ|&|π¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬',198WrappingIndent.Same199);200201assertIncrementalLineBreaks(202factory, 'ππ¬&ππ', 4,2035, 'ππ¬&|ππ',2044, 'ππ¬|&|ππ',205WrappingIndent.Same206);207208assertIncrementalLineBreaks(209factory, '\t\tfunc(\'πππΌπππΌπ&π¬πππ¬ππππΌππ¬\', WrappingIndent.Same);', 4,21026, '\t\tfunc|(\'πππΌπππΌπ&|π¬πππ¬ππππΌπ|π¬\', |WrappingIndent.|Same);',21127, '\t\tfunc|(\'πππΌπππΌπ&|π¬πππ¬ππππΌπ|π¬\', |WrappingIndent.|Same);',212WrappingIndent.Same213);214215assertIncrementalLineBreaks(216factory, 'factory, "xtxtfunc(x"πππΌπππΌπ&π¬πππ¬ππππΌππ¬x"', 4,21716, 'factory, |"xtxtfunc|(x"πππΌπππΌ|π&|π¬πππ¬ππππΌ|ππ¬x"',21817, 'factory, |"xtxtfunc|(x"πππΌπππΌπ|&π¬πππ¬ππππΌ|ππ¬x"',219WrappingIndent.Same220);221});222223test('issue #95686: CRITICAL: loop forever on the monospaceLineBreaksComputer', () => {224const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);225assertIncrementalLineBreaks(226factory,227' <tr dmx-class:table-danger="(alt <= 50)" dmx-class:table-warning="(alt <= 200)" dmx-class:table-primary="(alt <= 400)" dmx-class:table-info="(alt <= 800)" dmx-class:table-success="(alt >= 400)">',2284,229179, ' <tr dmx-class:table-danger="(alt <= 50)" dmx-class:table-warning="(alt <= 200)" dmx-class:table-primary="(alt <= 400)" dmx-class:table-info="(alt <= 800)" |dmx-class:table-success="(alt >= 400)">',2301, ' | | | | | |<|t|r| |d|m|x|-|c|l|a|s|s|:|t|a|b|l|e|-|d|a|n|g|e|r|=|"|(|a|l|t| |<|=| |5|0|)|"| |d|m|x|-|c|l|a|s|s|:|t|a|b|l|e|-|w|a|r|n|i|n|g|=|"|(|a|l|t| |<|=| |2|0|0|)|"| |d|m|x|-|c|l|a|s|s|:|t|a|b|l|e|-|p|r|i|m|a|r|y|=|"|(|a|l|t| |<|=| |4|0|0|)|"| |d|m|x|-|c|l|a|s|s|:|t|a|b|l|e|-|i|n|f|o|=|"|(|a|l|t| |<|=| |8|0|0|)|"| |d|m|x|-|c|l|a|s|s|:|t|a|b|l|e|-|s|u|c|c|e|s|s|=|"|(|a|l|t| |>|=| |4|0|0|)|"|>',231WrappingIndent.Same232);233});234235test('issue #110392: Occasional crash when resize with panel on the right', () => {236const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);237assertIncrementalLineBreaks(238factory,239'δ½ ε₯½ **hello** **hello** **hello-world** hey there!',2404,24115, 'δ½ ε₯½ **hello** |**hello** |**hello-world**| hey there!',2421, 'δ½ |ε₯½| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|-|w|o|r|l|d|*|*| |h|e|y| |t|h|e|r|e|!',243WrappingIndent.Same,2441.6605405405405405245);246});247248test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => {249const factory = new MonospaceLineBreaksComputerFactory('(', '\t)');250assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89');251assertLineBreaks(factory, 4, 5, '\u3042 \u5b89|\u5b89');252assertLineBreaks(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89');253assertLineBreaks(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89');254assertLineBreaks(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89');255assertLineBreaks(factory, 4, 5, 'aa |(\u5b89aa|\u5b89');256});257258test('MonospaceLineBreaksComputer - WrappingIndent.Same', () => {259const factory = new MonospaceLineBreaksComputerFactory('', '\t ');260assertLineBreaks(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same);261});262263test('issue #16332: Scroll bar overlaying on top of text', () => {264const factory = new MonospaceLineBreaksComputerFactory('', '\t ');265assertLineBreaks(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent);266});267268test('issue #35162: wrappingIndent not consistently working', () => {269const factory = new MonospaceLineBreaksComputerFactory('', '\t ');270const mapper = assertLineBreaks(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent);271assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length);272});273274test('issue #75494: surrogate pairs', () => {275const factory = new MonospaceLineBreaksComputerFactory('\t', ' ');276assertLineBreaks(factory, 4, 49, 'ππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌ|ππ¬ππππΌππ¬ππππΌππ¬ππππΌππ¬ππππΌ|ππ¬', WrappingIndent.Same);277});278279test('issue #75494: surrogate pairs overrun 1', () => {280const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);281assertLineBreaks(factory, 4, 4, 'ππ¬|&|ππ', WrappingIndent.Same);282});283284test('issue #75494: surrogate pairs overrun 2', () => {285const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);286assertLineBreaks(factory, 4, 17, 'factory, |"xtxtfunc|(x"πππΌπππΌπ|&π¬πππ¬ππππΌ|ππ¬x"', WrappingIndent.Same);287});288289test('MonospaceLineBreaksComputer - WrappingIndent.DeepIndent', () => {290const factory = new MonospaceLineBreaksComputerFactory('', '\t ');291const mapper = assertLineBreaks(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent);292assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length);293});294295test('issue #33366: Word wrap algorithm behaves differently around punctuation', () => {296const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);297assertLineBreaks(factory, 4, 23, 'this is a line of |text, text that sits |on a line', WrappingIndent.Same);298});299300test('issue #152773: Word wrap algorithm behaves differently with bracket followed by comma', () => {301const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);302assertLineBreaks(factory, 4, 24, 'this is a line of |(text), text that sits |on a line', WrappingIndent.Same);303});304305test('issue #112382: Word wrap doesn\'t work well with control characters', () => {306const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);307assertLineBreaks(factory, 4, 6, '\x06\x06\x06|\x06\x06\x06', WrappingIndent.Same);308});309310test('Word break work well with Chinese/Japanese/Korean (CJK) text when setting normal', () => {311const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);312assertLineBreaks(factory, 4, 5, 'δ½ ε₯½|1111', WrappingIndent.Same, 'normal');313});314315test('Word break work well with Chinese/Japanese/Korean (CJK) text when setting keepAll', () => {316const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);317assertLineBreaks(factory, 4, 8, 'δ½ ε₯½1111', WrappingIndent.Same, 'keepAll');318});319320test('issue #258022: wrapOnEscapedLineFeeds: should work correctly after editor resize', () => {321const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);322323// Test text with escaped line feeds - simulates a JSON string with \n324// The \n should trigger a soft wrap when wrapOnEscapedLineFeeds is enabled325const text = '"Short text with\\nescaped newline and an escaped\\\\nbackslash"';326327// First, compute line breaks with wrapOnEscapedLineFeeds enabled at initial width328const initialBreakData = getLineBreakData(factory, 4, 30, 2, WrappingIndent.None, 'normal', true, text, null);329const initialAnnotatedText = toAnnotatedText(text, initialBreakData);330331// Verify the escaped \n triggers a wrap in the initial case332assert.ok(initialAnnotatedText.includes('with\\n'), 'Initial case should wrap at escaped line feeds');333334// Now simulate editor resize by computing line breaks with different width using previous data335// This triggers createLineBreaksFromPreviousLineBreaks which has the bug336const resizedBreakData = getLineBreakData(factory, 4, 35, 2, WrappingIndent.None, 'normal', true, text, initialBreakData);337const resizedAnnotatedText = toAnnotatedText(text, resizedBreakData);338339// Compute fresh line breaks at the new width (without using previous data)340// This uses createLineBreaks which correctly handles wrapOnEscapedLineFeeds341const freshBreakData = getLineBreakData(factory, 4, 35, 2, WrappingIndent.None, 'normal', true, text, null);342const freshAnnotatedText = toAnnotatedText(text, freshBreakData);343344// Fresh computation should still wrap at escaped line feeds345assert.ok(freshAnnotatedText.includes('with\\n'), 'Fresh computation should wrap at escaped line feeds');346347// BUG DEMONSTRATION: Incremental computation after resize doesn't handle escaped line feeds348// The two results should be identical, but they're not due to the bug349assert.strictEqual(350resizedAnnotatedText,351freshAnnotatedText,352`Bug: Incremental and fresh computations differ for escaped line feeds.\n` +353`Incremental (resize): ${resizedAnnotatedText}\n` +354`Fresh computation: ${freshAnnotatedText}\n` +355`The incremental path (createLineBreaksFromPreviousLineBreaks) doesn't handle wrapOnEscapedLineFeeds`356);357});358});359360361