Path: blob/main/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts
3296 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});319});320321322