Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import assert from 'assert';
6
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
7
import { EditorOptions, WrappingIndent } from '../../../common/config/editorOptions.js';
8
import { FontInfo } from '../../../common/config/fontInfo.js';
9
import { ILineBreaksComputerFactory, ModelLineProjectionData } from '../../../common/modelLineProjectionData.js';
10
import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js';
11
12
function parseAnnotatedText(annotatedText: string): { text: string; indices: number[] } {
13
let text = '';
14
let currentLineIndex = 0;
15
const indices: number[] = [];
16
for (let i = 0, len = annotatedText.length; i < len; i++) {
17
if (annotatedText.charAt(i) === '|') {
18
currentLineIndex++;
19
} else {
20
text += annotatedText.charAt(i);
21
indices[text.length - 1] = currentLineIndex;
22
}
23
}
24
return { text: text, indices: indices };
25
}
26
27
function toAnnotatedText(text: string, lineBreakData: ModelLineProjectionData | null): string {
28
// Insert line break markers again, according to algorithm
29
let actualAnnotatedText = '';
30
if (lineBreakData) {
31
let previousLineIndex = 0;
32
for (let i = 0, len = text.length; i < len; i++) {
33
const r = lineBreakData.translateToOutputPosition(i);
34
if (previousLineIndex !== r.outputLineIndex) {
35
previousLineIndex = r.outputLineIndex;
36
actualAnnotatedText += '|';
37
}
38
actualAnnotatedText += text.charAt(i);
39
}
40
} else {
41
// No wrapping
42
actualAnnotatedText = text;
43
}
44
return actualAnnotatedText;
45
}
46
47
function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean, text: string, previousLineBreakData: ModelLineProjectionData | null): ModelLineProjectionData | null {
48
const fontInfo = new FontInfo({
49
pixelRatio: 1,
50
fontFamily: 'testFontFamily',
51
fontWeight: 'normal',
52
fontSize: 14,
53
fontFeatureSettings: '',
54
fontVariationSettings: '',
55
lineHeight: 19,
56
letterSpacing: 0,
57
isMonospace: true,
58
typicalHalfwidthCharacterWidth: 7,
59
typicalFullwidthCharacterWidth: 7 * columnsForFullWidthChar,
60
canUseHalfwidthRightwardsArrow: true,
61
spaceWidth: 7,
62
middotWidth: 7,
63
wsmiddotWidth: 7,
64
maxDigitWidth: 7
65
}, false);
66
const lineBreaksComputer = factory.createLineBreaksComputer(fontInfo, tabSize, breakAfter, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds);
67
const previousLineBreakDataClone = previousLineBreakData ? new ModelLineProjectionData(null, null, previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null;
68
lineBreaksComputer.addRequest(text, null, previousLineBreakDataClone);
69
return lineBreaksComputer.finalize()[0];
70
}
71
72
function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None, wordBreak: 'normal' | 'keepAll' = 'normal'): ModelLineProjectionData | null {
73
// Create version of `annotatedText` with line break markers removed
74
const text = parseAnnotatedText(annotatedText).text;
75
const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, wordBreak, false, text, null);
76
const actualAnnotatedText = toAnnotatedText(text, lineBreakData);
77
78
assert.strictEqual(actualAnnotatedText, annotatedText);
79
80
return lineBreakData;
81
}
82
83
suite('Editor ViewModel - MonospaceLineBreaksComputer', () => {
84
85
ensureNoDisposablesAreLeakedInTestSuite();
86
87
test('MonospaceLineBreaksComputer', () => {
88
89
const factory = new MonospaceLineBreaksComputerFactory('(', '\t).');
90
91
// Empty string
92
assertLineBreaks(factory, 4, 5, '');
93
94
// No wrapping if not necessary
95
assertLineBreaks(factory, 4, 5, 'aaa');
96
assertLineBreaks(factory, 4, 5, 'aaaaa');
97
assertLineBreaks(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
98
99
// Acts like hard wrapping if no char found
100
assertLineBreaks(factory, 4, 5, 'aaaaa|a');
101
102
// Honors wrapping character
103
assertLineBreaks(factory, 4, 5, 'aaaaa|.');
104
assertLineBreaks(factory, 4, 5, 'aaaaa|a.|aaa.|aa');
105
assertLineBreaks(factory, 4, 5, 'aaaaa|a..|aaa.|aa');
106
assertLineBreaks(factory, 4, 5, 'aaaaa|a...|aaa.|aa');
107
assertLineBreaks(factory, 4, 5, 'aaaaa|a....|aaa.|aa');
108
109
// Honors tabs when computing wrapping position
110
assertLineBreaks(factory, 4, 5, '\t');
111
assertLineBreaks(factory, 4, 5, '\t|aaa');
112
assertLineBreaks(factory, 4, 5, '\t|a\t|aa');
113
assertLineBreaks(factory, 4, 5, 'aa\ta');
114
assertLineBreaks(factory, 4, 5, 'aa\t|aa');
115
116
// Honors wrapping before characters (& gives it priority)
117
assertLineBreaks(factory, 4, 5, 'aaa.|aa');
118
assertLineBreaks(factory, 4, 5, 'aaa(.|aa');
119
120
// Honors wrapping after characters (& gives it priority)
121
assertLineBreaks(factory, 4, 5, 'aaa))|).aaa');
122
assertLineBreaks(factory, 4, 5, 'aaa))|).|aaaa');
123
assertLineBreaks(factory, 4, 5, 'aaa)|().|aaa');
124
assertLineBreaks(factory, 4, 5, 'aaa|(().|aaa');
125
assertLineBreaks(factory, 4, 5, 'aa.|(().|aaa');
126
assertLineBreaks(factory, 4, 5, 'aa.|(.).|aaa');
127
});
128
129
function assertLineBreakDataEqual(a: ModelLineProjectionData | null, b: ModelLineProjectionData | null): void {
130
if (!a || !b) {
131
assert.deepStrictEqual(a, b);
132
return;
133
}
134
assert.deepStrictEqual(a.breakOffsets, b.breakOffsets);
135
assert.deepStrictEqual(a.wrappedTextIndentLength, b.wrappedTextIndentLength);
136
for (let i = 0; i < a.breakOffsetsVisibleColumn.length; i++) {
137
const diff = a.breakOffsetsVisibleColumn[i] - b.breakOffsetsVisibleColumn[i];
138
assert.ok(diff < 0.001);
139
}
140
}
141
142
function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None, columnsForFullWidthChar: number = 2): void {
143
// sanity check the test
144
assert.strictEqual(text, parseAnnotatedText(annotatedText1).text);
145
assert.strictEqual(text, parseAnnotatedText(annotatedText2).text);
146
147
// check that the direct mapping is ok for 1
148
const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, null);
149
assert.strictEqual(toAnnotatedText(text, directLineBreakData1), annotatedText1);
150
151
// check that the direct mapping is ok for 2
152
const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, null);
153
assert.strictEqual(toAnnotatedText(text, directLineBreakData2), annotatedText2);
154
155
// check that going from 1 to 2 is ok
156
const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, directLineBreakData1);
157
assert.strictEqual(toAnnotatedText(text, lineBreakData2from1), annotatedText2);
158
assertLineBreakDataEqual(lineBreakData2from1, directLineBreakData2);
159
160
// check that going from 2 to 1 is ok
161
const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, directLineBreakData2);
162
assert.strictEqual(toAnnotatedText(text, lineBreakData1from2), annotatedText1);
163
assertLineBreakDataEqual(lineBreakData1from2, directLineBreakData1);
164
}
165
166
test('MonospaceLineBreaksComputer incremental 1', () => {
167
168
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
169
170
assertIncrementalLineBreaks(
171
factory, 'just some text and more', 4,
172
10, 'just some |text and |more',
173
15, 'just some text |and more'
174
);
175
176
assertIncrementalLineBreaks(
177
factory, '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,
178
47, '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
142, '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.',
180
);
181
182
assertIncrementalLineBreaks(
183
factory, '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,
184
57, '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
58, '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.'
186
);
187
188
assertIncrementalLineBreaks(
189
factory, '\t\t"owner": "vscode",', 4,
190
14, '\t\t"owner|": |"vscod|e",',
191
16, '\t\t"owner":| |"vscode"|,',
192
WrappingIndent.Same
193
);
194
195
assertIncrementalLineBreaks(
196
factory, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', 4,
197
51, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬',
198
50, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇|&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬',
199
WrappingIndent.Same
200
);
201
202
assertIncrementalLineBreaks(
203
factory, '🐇👬&🌞🌖', 4,
204
5, '🐇👬&|🌞🌖',
205
4, '🐇👬|&|🌞🌖',
206
WrappingIndent.Same
207
);
208
209
assertIncrementalLineBreaks(
210
factory, '\t\tfunc(\'🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬\', WrappingIndent.Same);', 4,
211
26, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);',
212
27, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);',
213
WrappingIndent.Same
214
);
215
216
assertIncrementalLineBreaks(
217
factory, 'factory, "xtxtfunc(x"🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬x"', 4,
218
16, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼|🐇&|👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"',
219
17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"',
220
WrappingIndent.Same
221
);
222
});
223
224
test('issue #95686: CRITICAL: loop forever on the monospaceLineBreaksComputer', () => {
225
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
226
assertIncrementalLineBreaks(
227
factory,
228
' <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)">',
229
4,
230
179, ' <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)">',
231
1, ' | | | | | |<|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|)|"|>',
232
WrappingIndent.Same
233
);
234
});
235
236
test('issue #110392: Occasional crash when resize with panel on the right', () => {
237
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
238
assertIncrementalLineBreaks(
239
factory,
240
'你好 **hello** **hello** **hello-world** hey there!',
241
4,
242
15, '你好 **hello** |**hello** |**hello-world**| hey there!',
243
1, '你|好| |*|*|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|!',
244
WrappingIndent.Same,
245
1.6605405405405405
246
);
247
});
248
249
test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => {
250
const factory = new MonospaceLineBreaksComputerFactory('(', '\t)');
251
assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89');
252
assertLineBreaks(factory, 4, 5, '\u3042 \u5b89|\u5b89');
253
assertLineBreaks(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89');
254
assertLineBreaks(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89');
255
assertLineBreaks(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89');
256
assertLineBreaks(factory, 4, 5, 'aa |(\u5b89aa|\u5b89');
257
});
258
259
test('MonospaceLineBreaksComputer - WrappingIndent.Same', () => {
260
const factory = new MonospaceLineBreaksComputerFactory('', '\t ');
261
assertLineBreaks(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same);
262
});
263
264
test('issue #16332: Scroll bar overlaying on top of text', () => {
265
const factory = new MonospaceLineBreaksComputerFactory('', '\t ');
266
assertLineBreaks(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent);
267
});
268
269
test('issue #35162: wrappingIndent not consistently working', () => {
270
const factory = new MonospaceLineBreaksComputerFactory('', '\t ');
271
const mapper = assertLineBreaks(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent);
272
assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length);
273
});
274
275
test('issue #75494: surrogate pairs', () => {
276
const factory = new MonospaceLineBreaksComputerFactory('\t', ' ');
277
assertLineBreaks(factory, 4, 49, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬', WrappingIndent.Same);
278
});
279
280
test('issue #75494: surrogate pairs overrun 1', () => {
281
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
282
assertLineBreaks(factory, 4, 4, '🐇👬|&|🌞🌖', WrappingIndent.Same);
283
});
284
285
test('issue #75494: surrogate pairs overrun 2', () => {
286
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
287
assertLineBreaks(factory, 4, 17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', WrappingIndent.Same);
288
});
289
290
test('MonospaceLineBreaksComputer - WrappingIndent.DeepIndent', () => {
291
const factory = new MonospaceLineBreaksComputerFactory('', '\t ');
292
const 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);
293
assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length);
294
});
295
296
test('issue #33366: Word wrap algorithm behaves differently around punctuation', () => {
297
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
298
assertLineBreaks(factory, 4, 23, 'this is a line of |text, text that sits |on a line', WrappingIndent.Same);
299
});
300
301
test('issue #152773: Word wrap algorithm behaves differently with bracket followed by comma', () => {
302
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
303
assertLineBreaks(factory, 4, 24, 'this is a line of |(text), text that sits |on a line', WrappingIndent.Same);
304
});
305
306
test('issue #112382: Word wrap doesn\'t work well with control characters', () => {
307
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
308
assertLineBreaks(factory, 4, 6, '\x06\x06\x06|\x06\x06\x06', WrappingIndent.Same);
309
});
310
311
test('Word break work well with Chinese/Japanese/Korean (CJK) text when setting normal', () => {
312
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
313
assertLineBreaks(factory, 4, 5, '你好|1111', WrappingIndent.Same, 'normal');
314
});
315
316
test('Word break work well with Chinese/Japanese/Korean (CJK) text when setting keepAll', () => {
317
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
318
assertLineBreaks(factory, 4, 8, '你好1111', WrappingIndent.Same, 'keepAll');
319
});
320
});
321
322