/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { CharCode } from 'vs/base/common/charCode';
import * as strings from 'vs/base/common/strings';
import { IViewLineTokens } from 'vs/editor/common/core/lineTokens';
import { MetadataConsts } from 'vs/editor/common/modes';
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
import { CharacterMapping, RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel';
import { ViewLineToken, ViewLineTokens } from 'vs/editor/test/common/core/viewLineToken';
function createViewLineTokens(viewLineTokens: ViewLineToken[]): IViewLineTokens {
return new ViewLineTokens(viewLineTokens);
}
function createPart(endIndex: number, foreground: number): ViewLineToken {
return new ViewLineToken(endIndex, (
foreground << MetadataConsts.FOREGROUND_OFFSET
) >>> 0);
}
suite('viewLineRenderer.renderLine', () => {
function assertCharacterReplacement(lineContent: string, tabSize: number, expected: string, expectedCharOffsetInPart: number[][], expectedPartLengts: number[]): void {
let _actual = renderViewLine(new RenderLineInput(
false,
true,
lineContent,
false,
strings.isBasicASCII(lineContent),
false,
0,
createViewLineTokens([new ViewLineToken(lineContent.length, 0)]),
[],
tabSize,
0,
-1,
'none',
false,
false
));
assert.equal(_actual.html, '' + expected + '');
assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts);
}
test('replaces spaces', () => {
assertCharacterReplacement(' ', 4, '\u00a0', [[0, 1]], [1]);
assertCharacterReplacement(' ', 4, '\u00a0\u00a0', [[0, 1, 2]], [2]);
assertCharacterReplacement('a b', 4, 'a\u00a0\u00a0b', [[0, 1, 2, 3, 4]], [4]);
});
test('escapes HTML markup', () => {
assertCharacterReplacement('ab', 4, 'a>b', [[0, 1, 2, 3]], [3]);
assertCharacterReplacement('a&b', 4, 'a&b', [[0, 1, 2, 3]], [3]);
});
test('replaces some bad characters', () => {
assertCharacterReplacement('a\0b', 4, 'ab', [[0, 1, 2, 3]], [3]);
assertCharacterReplacement('a' + String.fromCharCode(CharCode.UTF8_BOM) + 'b', 4, 'a\ufffdb', [[0, 1, 2, 3]], [3]);
assertCharacterReplacement('a\u2028b', 4, 'a\ufffdb', [[0, 1, 2, 3]], [3]);
});
test('handles tabs', () => {
assertCharacterReplacement('\t', 4, '\u00a0\u00a0\u00a0\u00a0', [[0, 4]], [4]);
assertCharacterReplacement('x\t', 4, 'x\u00a0\u00a0\u00a0', [[0, 1, 4]], [4]);
assertCharacterReplacement('xx\t', 4, 'xx\u00a0\u00a0', [[0, 1, 2, 4]], [4]);
assertCharacterReplacement('xxx\t', 4, 'xxx\u00a0', [[0, 1, 2, 3, 4]], [4]);
assertCharacterReplacement('xxxx\t', 4, 'xxxx\u00a0\u00a0\u00a0\u00a0', [[0, 1, 2, 3, 4, 8]], [8]);
});
function assertParts(lineContent: string, tabSize: number, parts: ViewLineToken[], expected: string, expectedCharOffsetInPart: number[][], expectedPartLengts: number[]): void {
let _actual = renderViewLine(new RenderLineInput(
false,
true,
lineContent,
false,
true,
false,
0,
createViewLineTokens(parts),
[],
tabSize,
0,
-1,
'none',
false,
false
));
assert.equal(_actual.html, '' + expected + '');
assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts);
}
test('empty line', () => {
assertParts('', 4, [], '\u00a0', [], []);
});
test('uses part type', () => {
assertParts('x', 4, [createPart(1, 10)], 'x', [[0, 1]], [1]);
assertParts('x', 4, [createPart(1, 20)], 'x', [[0, 1]], [1]);
assertParts('x', 4, [createPart(1, 30)], 'x', [[0, 1]], [1]);
});
test('two parts', () => {
assertParts('xy', 4, [createPart(1, 1), createPart(2, 2)], 'xy', [[0], [0, 1]], [1, 1]);
assertParts('xyz', 4, [createPart(1, 1), createPart(3, 2)], 'xyz', [[0], [0, 1, 2]], [1, 2]);
assertParts('xyz', 4, [createPart(2, 1), createPart(3, 2)], 'xyz', [[0, 1], [0, 1]], [2, 1]);
});
test('overflow', () => {
let _actual = renderViewLine(new RenderLineInput(
false,
true,
'Hello world!',
false,
true,
false,
0,
createViewLineTokens([
createPart(1, 0),
createPart(2, 1),
createPart(3, 2),
createPart(4, 3),
createPart(5, 4),
createPart(6, 5),
createPart(7, 6),
createPart(8, 7),
createPart(9, 8),
createPart(10, 9),
createPart(11, 10),
createPart(12, 11),
]),
[],
4,
10,
6,
'boundary',
false,
false
));
let expectedOutput = [
'H',
'e',
'l',
'l',
'o',
'\u00a0',
'…'
].join('');
assert.equal(_actual.html, '' + expectedOutput + '');
assertCharacterMapping(_actual.characterMapping,
[
[0],
[0],
[0],
[0],
[0],
[0, 1],
],
[1, 1, 1, 1, 1, 1]
);
});
test('typical line', () => {
let lineText = '\t export class Game { // http://test.com ';
let lineParts = createViewLineTokens([
createPart(5, 1),
createPart(11, 2),
createPart(12, 3),
createPart(17, 4),
createPart(18, 5),
createPart(22, 6),
createPart(23, 7),
createPart(24, 8),
createPart(25, 9),
createPart(28, 10),
createPart(43, 11),
createPart(48, 12),
]);
let expectedOutput = [
'\u2192\u00a0\u00a0\u00a0',
'\u00b7\u00b7\u00b7\u00b7',
'export',
'\u00a0',
'class',
'\u00a0',
'Game',
'\u00a0',
'{',
'\u00a0',
'//\u00a0',
'http://test.com',
'\u00b7\u00b7',
'\u00b7\u00b7\u00b7'
].join('');
let expectedOffsetsArr = [
[0],
[0, 1, 2, 3],
[0, 1, 2, 3, 4, 5],
[0],
[0, 1, 2, 3, 4],
[0],
[0, 1, 2, 3],
[0],
[0],
[0],
[0, 1, 2],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
[0, 1],
[0, 1, 2, 3],
];
let _actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
true,
false,
0,
lineParts,
[],
4,
10,
-1,
'boundary',
false,
false
));
assert.equal(_actual.html, '' + expectedOutput + '');
assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [4, 4, 6, 1, 5, 1, 4, 1, 1, 1, 3, 15, 2, 3]);
});
test('issue #2255: Weird line rendering part 1', () => {
let lineText = '\t\t\tcursorStyle:\t\t\t\t\t\t(prevOpts.cursorStyle !== newOpts.cursorStyle),';
let lineParts = createViewLineTokens([
createPart(3, 1), // 3 chars
createPart(15, 2), // 12 chars
createPart(21, 3), // 6 chars
createPart(22, 4), // 1 char
createPart(43, 5), // 21 chars
createPart(45, 6), // 2 chars
createPart(46, 7), // 1 char
createPart(66, 8), // 20 chars
createPart(67, 9), // 1 char
createPart(68, 10), // 2 chars
]);
let expectedOutput = [
'\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0',
'cursorStyle:',
'\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0',
'(',
'prevOpts.cursorStyle\u00a0',
'!=',
'=',
'\u00a0newOpts.cursorStyle',
')',
',',
].join('');
let expectedOffsetsArr = [
[0, 4, 8], // 3 chars
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // 12 chars
[0, 4, 8, 12, 16, 20], // 6 chars
[0], // 1 char
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], // 21 chars
[0, 1], // 2 chars
[0], // 1 char
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // 20 chars
[0], // 1 char
[0, 1] // 2 chars
];
let _actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
true,
false,
0,
lineParts,
[],
4,
10,
-1,
'none',
false,
false
));
assert.equal(_actual.html, '' + expectedOutput + '');
assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]);
});
test('issue #2255: Weird line rendering part 2', () => {
let lineText = ' \t\t\tcursorStyle:\t\t\t\t\t\t(prevOpts.cursorStyle !== newOpts.cursorStyle),';
let lineParts = createViewLineTokens([
createPart(4, 1), // 4 chars
createPart(16, 2), // 12 chars
createPart(22, 3), // 6 chars
createPart(23, 4), // 1 char
createPart(44, 5), // 21 chars
createPart(46, 6), // 2 chars
createPart(47, 7), // 1 char
createPart(67, 8), // 20 chars
createPart(68, 9), // 1 char
createPart(69, 10), // 2 chars
]);
let expectedOutput = [
'\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0',
'cursorStyle:',
'\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0',
'(',
'prevOpts.cursorStyle\u00a0',
'!=',
'=',
'\u00a0newOpts.cursorStyle',
')',
',',
].join('');
let expectedOffsetsArr = [
[0, 1, 4, 8], // 4 chars
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // 12 chars
[0, 4, 8, 12, 16, 20], // 6 chars
[0], // 1 char
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], // 21 chars
[0, 1], // 2 chars
[0], // 1 char
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // 20 chars
[0], // 1 char
[0, 1] // 2 chars
];
let _actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
true,
false,
0,
lineParts,
[],
4,
10,
-1,
'none',
false,
false
));
assert.equal(_actual.html, '' + expectedOutput + '');
assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]);
});
test('issue Microsoft/monaco-editor#280: Improved source code rendering for RTL languages', () => {
let lineText = 'var קודמות = \"מיותר קודמות צ\'ט של, אם לשון העברית שינויים ויש, אם\";';
let lineParts = createViewLineTokens([
createPart(3, 6),
createPart(13, 1),
createPart(66, 20),
createPart(67, 1),
]);
let expectedOutput = [
'var',
'\u00a0קודמות\u00a0=\u00a0',
'"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"',
';'
].join('');
let _actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
false,
true,
0,
lineParts,
[],
4,
10,
-1,
'none',
false,
false
));
assert.equal(_actual.html, '' + expectedOutput + '');
assert.equal(_actual.containsRTL, true);
});
test('issue #6885: Splits large tokens', () => {
// 1 1 1
// 1 2 3 4 5 6 7 8 9 0 1 2
// 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234
let _lineText = 'This is just a long line that contains very interesting text. This is just a long line that contains very interesting text.';
function assertSplitsTokens(message: string, lineText: string, expectedOutput: string[]): void {
let lineParts = createViewLineTokens([createPart(lineText.length, 1)]);
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
true,
false,
0,
lineParts,
[],
4,
10,
-1,
'none',
false,
false
));
assert.equal(actual.html, '' + expectedOutput.join('') + '', message);
}
// A token with 49 chars
{
assertSplitsTokens(
'49 chars',
_lineText.substr(0, 49),
[
'This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains\u00a0very\u00a0inter',
]
);
}
// A token with 50 chars
{
assertSplitsTokens(
'50 chars',
_lineText.substr(0, 50),
[
'This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains\u00a0very\u00a0intere',
]
);
}
// A token with 51 chars
{
assertSplitsTokens(
'51 chars',
_lineText.substr(0, 51),
[
'This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains\u00a0very\u00a0intere',
's',
]
);
}
// A token with 99 chars
{
assertSplitsTokens(
'99 chars',
_lineText.substr(0, 99),
[
'This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains\u00a0very\u00a0intere',
'sting\u00a0text.\u00a0This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contain',
]
);
}
// A token with 100 chars
{
assertSplitsTokens(
'100 chars',
_lineText.substr(0, 100),
[
'This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains\u00a0very\u00a0intere',
'sting\u00a0text.\u00a0This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains',
]
);
}
// A token with 101 chars
{
assertSplitsTokens(
'101 chars',
_lineText.substr(0, 101),
[
'This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains\u00a0very\u00a0intere',
'sting\u00a0text.\u00a0This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains',
'\u00a0',
]
);
}
});
test('issue #21476: Does not split large tokens when ligatures are on', () => {
// 1 1 1
// 1 2 3 4 5 6 7 8 9 0 1 2
// 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234
let _lineText = 'This is just a long line that contains very interesting text. This is just a long line that contains very interesting text.';
function assertSplitsTokens(message: string, lineText: string, expectedOutput: string[]): void {
let lineParts = createViewLineTokens([createPart(lineText.length, 1)]);
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
true,
false,
0,
lineParts,
[],
4,
10,
-1,
'none',
false,
true
));
assert.equal(actual.html, '' + expectedOutput.join('') + '', message);
}
// A token with 101 chars
{
assertSplitsTokens(
'101 chars',
_lineText.substr(0, 101),
[
'This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0contains\u00a0very\u00a0',
'interesting\u00a0text.\u00a0This\u00a0is\u00a0just\u00a0a\u00a0long\u00a0line\u00a0that\u00a0',
'contains\u00a0',
]
);
}
});
test('issue #20624: Unaligned surrogate pairs are corrupted at multiples of 50 columns', () => {
let lineText = 'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷';
let lineParts = createViewLineTokens([createPart(lineText.length, 1)]);
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
false,
false,
0,
lineParts,
[],
4,
10,
-1,
'none',
false,
false
));
let expectedOutput = [
'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷',
];
assert.equal(actual.html, '' + expectedOutput.join('') + '');
});
test('issue #6885: Does not split large tokens in RTL text', () => {
let lineText = 'את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר.';
let lineParts = createViewLineTokens([createPart(lineText.length, 1)]);
let expectedOutput = [
'את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.'
];
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineText,
false,
false,
true,
0,
lineParts,
[],
4,
10,
-1,
'none',
false,
false
));
assert.equal(actual.html, '' + expectedOutput.join('') + '');
assert.equal(actual.containsRTL, true);
});
test('issue #19673: Monokai Theme bad-highlighting in line wrap', () => {
let lineText = ' MongoCallback): void {';
let lineParts = createViewLineTokens([
createPart(17, 1),
createPart(18, 2),
createPart(24, 3),
createPart(26, 4),
createPart(27, 5),
createPart(28, 6),
createPart(32, 7),
createPart(34, 8),
]);
let expectedOutput = [
'\u00a0\u00a0\u00a0\u00a0',
'MongoCallback',
'<',
'string',
'>)',
':',
'\u00a0',
'void',
'\u00a0{'
].join('');
let _actual = renderViewLine(new RenderLineInput(
true,
true,
lineText,
false,
true,
false,
4,
lineParts,
[],
4,
10,
-1,
'none',
false,
false
));
assert.equal(_actual.html, '' + expectedOutput + '');
});
function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void {
assertCharPartOffsets(actual, expectedCharPartOffsets);
let expectedCharAbsoluteOffset: number[] = [], currentPartAbsoluteOffset = 0;
for (let partIndex = 0; partIndex < expectedCharPartOffsets.length; partIndex++) {
const part = expectedCharPartOffsets[partIndex];
for (let i = 0; i < part.length; i++) {
const charIndex = part[i];
expectedCharAbsoluteOffset.push(currentPartAbsoluteOffset + charIndex);
}
currentPartAbsoluteOffset += expectedPartLengths[partIndex];
}
let actualCharOffset: number[] = [];
let tmp = actual.getAbsoluteOffsets();
for (let i = 0; i < tmp.length; i++) {
actualCharOffset[i] = tmp[i];
}
assert.deepEqual(actualCharOffset, expectedCharAbsoluteOffset);
}
function assertCharPartOffsets(actual: CharacterMapping, expected: number[][]): void {
let charOffset = 0;
for (let partIndex = 0; partIndex < expected.length; partIndex++) {
let part = expected[partIndex];
for (let i = 0; i < part.length; i++) {
let charIndex = part[i];
// here
let _actualPartData = actual.charOffsetToPartData(charOffset);
let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData);
let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData);
assert.deepEqual(
{ partIndex: actualPartIndex, charIndex: actualCharIndex },
{ partIndex: partIndex, charIndex: charIndex },
`character mapping for offset ${charOffset}`
);
// here
let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex);
assert.equal(
actualOffset,
charOffset,
`character mapping for part ${partIndex}, ${charIndex}`
);
charOffset++;
}
}
assert.equal(actual.length, charOffset);
}
});
suite('viewLineRenderer.renderLine 2', () => {
function testCreateLineParts(fontIsMonospace: boolean, lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace: 'none' | 'boundary' | 'all', expected: string): void {
let actual = renderViewLine(new RenderLineInput(
fontIsMonospace,
true,
lineContent,
false,
true,
false,
fauxIndentLength,
createViewLineTokens(tokens),
[],
4,
10,
-1,
renderWhitespace,
false,
false
));
assert.deepEqual(actual.html, expected);
}
test('issue #18616: Inline decorations ending at the text length are no longer rendered', () => {
let lineContent = 'https://microsoft.com';
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineContent,
false,
true,
false,
0,
createViewLineTokens([createPart(21, 3)]),
[new LineDecoration(1, 22, 'link', InlineDecorationType.Regular)],
4,
10,
-1,
'none',
false,
false
));
let expected = [
'',
'https://microsoft.com',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #19207: Link in Monokai is not rendered correctly', () => {
let lineContent = '\'let url = `http://***/_api/web/lists/GetByTitle(\\\'Teambuildingaanvragen\\\')/items`;\'';
let actual = renderViewLine(new RenderLineInput(
true,
true,
lineContent,
false,
true,
false,
0,
createViewLineTokens([
createPart(49, 6),
createPart(51, 4),
createPart(72, 6),
createPart(74, 4),
createPart(84, 6),
]),
[
new LineDecoration(13, 51, 'detected-link', InlineDecorationType.Regular)
],
4,
10,
-1,
'none',
false,
false
));
let expected = [
'',
'\'let\u00a0url\u00a0=\u00a0`',
'http://***/_api/web/lists/GetByTitle(',
'\\',
'\'',
'Teambuildingaanvragen',
'\\\'',
')/items`;\'',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('createLineParts simple', () => {
testCreateLineParts(
false,
'Hello world!',
[
createPart(12, 1)
],
0,
'none',
[
'',
'Hello\u00a0world!',
'',
].join('')
);
});
test('createLineParts simple two tokens', () => {
testCreateLineParts(
false,
'Hello world!',
[
createPart(6, 1),
createPart(12, 2)
],
0,
'none',
[
'',
'Hello\u00a0',
'world!',
'',
].join('')
);
});
test('createLineParts render whitespace - 4 leading spaces', () => {
testCreateLineParts(
false,
' Hello world! ',
[
createPart(4, 1),
createPart(6, 2),
createPart(20, 3)
],
0,
'boundary',
[
'',
'\u00b7\u00b7\u00b7\u00b7',
'He',
'llo\u00a0world!',
'\u00b7\u00b7\u00b7\u00b7',
'',
].join('')
);
});
test('createLineParts render whitespace - 8 leading spaces', () => {
testCreateLineParts(
false,
' Hello world! ',
[
createPart(8, 1),
createPart(10, 2),
createPart(28, 3)
],
0,
'boundary',
[
'',
'\u00b7\u00b7\u00b7\u00b7',
'\u00b7\u00b7\u00b7\u00b7',
'He',
'llo\u00a0world!',
'\u00b7\u00b7\u00b7\u00b7',
'\u00b7\u00b7\u00b7\u00b7',
'',
].join('')
);
});
test('createLineParts render whitespace - 2 leading tabs', () => {
testCreateLineParts(
false,
'\t\tHello world!\t',
[
createPart(2, 1),
createPart(4, 2),
createPart(15, 3)
],
0,
'boundary',
[
'',
'\u2192\u00a0\u00a0\u00a0',
'\u2192\u00a0\u00a0\u00a0',
'He',
'llo\u00a0world!',
'\u2192\u00a0\u00a0\u00a0',
'',
].join('')
);
});
test('createLineParts render whitespace - mixed leading spaces and tabs', () => {
testCreateLineParts(
false,
' \t\t Hello world! \t \t \t ',
[
createPart(6, 1),
createPart(8, 2),
createPart(31, 3)
],
0,
'boundary',
[
'',
'\u00b7\u00b7\u2192\u00a0',
'\u2192\u00a0\u00a0\u00a0',
'\u00b7\u00b7',
'He',
'llo\u00a0world!',
'\u00b7\uffeb',
'\u00b7\u00b7\u2192\u00a0',
'\u00b7\u00b7\u00b7\uffeb',
'\u00b7\u00b7\u00b7\u00b7',
'',
].join('')
);
});
test('createLineParts render whitespace skips faux indent', () => {
testCreateLineParts(
false,
'\t\t Hello world! \t \t \t ',
[
createPart(4, 1),
createPart(6, 2),
createPart(29, 3)
],
2,
'boundary',
[
'',
'\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0',
'\u00b7\u00b7',
'He',
'llo\u00a0world!',
'\u00b7\uffeb',
'\u00b7\u00b7\u2192\u00a0',
'\u00b7\u00b7\u00b7\uffeb',
'\u00b7\u00b7\u00b7\u00b7',
'',
].join('')
);
});
test('createLineParts does not emit width for monospace fonts', () => {
testCreateLineParts(
true,
'\t\t Hello world! \t \t \t ',
[
createPart(4, 1),
createPart(6, 2),
createPart(29, 3)
],
2,
'boundary',
[
'',
'\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0',
'\u00b7\u00b7',
'He',
'llo\u00a0world!',
'\u00b7\uffeb\u00b7\u00b7\u2192\u00a0\u00b7\u00b7\u00b7\uffeb\u00b7\u00b7\u00b7\u00b7',
'',
].join('')
);
});
test('createLineParts render whitespace in middle but not for one space', () => {
testCreateLineParts(
false,
'it it it it',
[
createPart(6, 1),
createPart(7, 2),
createPart(13, 3)
],
0,
'boundary',
[
'',
'it',
'\u00b7\u00b7',
'it',
'\u00a0',
'it',
'\u00b7\u00b7',
'it',
'',
].join('')
);
});
test('createLineParts render whitespace for all in middle', () => {
testCreateLineParts(
false,
' Hello world!\t',
[
createPart(4, 0),
createPart(6, 1),
createPart(14, 2)
],
0,
'all',
[
'',
'\u00b7',
'Hel',
'lo',
'\u00b7',
'world!',
'\u2192\u00a0\u00a0',
'',
].join('')
);
});
test('createLineParts can handle unsorted inline decorations', () => {
let actual = renderViewLine(new RenderLineInput(
false,
true,
'Hello world',
false,
true,
false,
0,
createViewLineTokens([createPart(11, 0)]),
[
new LineDecoration(5, 7, 'a', InlineDecorationType.Regular),
new LineDecoration(1, 3, 'b', InlineDecorationType.Regular),
new LineDecoration(2, 8, 'c', InlineDecorationType.Regular),
],
4,
10,
-1,
'none',
false,
false
));
// 01234567890
// Hello world
// ----aa-----
// bb---------
// -cccccc----
assert.deepEqual(actual.html, [
'',
'H',
'e',
'll',
'o\u00a0',
'w',
'orld',
'',
].join(''));
});
test('issue #11485: Visible whitespace conflicts with before decorator attachment', () => {
let lineContent = '\tbla';
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineContent,
false,
true,
false,
0,
createViewLineTokens([createPart(4, 3)]),
[new LineDecoration(1, 2, 'before', InlineDecorationType.Before)],
4,
10,
-1,
'all',
false,
true
));
let expected = [
'',
'\u2192\u00a0\u00a0\u00a0',
'bla',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #32436: Non-monospace font + visible whitespace + After decorator causes line to "jump"', () => {
let lineContent = '\tbla';
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineContent,
false,
true,
false,
0,
createViewLineTokens([createPart(4, 3)]),
[new LineDecoration(2, 3, 'before', InlineDecorationType.Before)],
4,
10,
-1,
'all',
false,
true
));
let expected = [
'',
'\u2192\u00a0\u00a0\u00a0',
'b',
'la',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #30133: Empty lines don\'t render inline decorations', () => {
let lineContent = '';
let actual = renderViewLine(new RenderLineInput(
false,
true,
lineContent,
false,
true,
false,
0,
createViewLineTokens([createPart(0, 3)]),
[new LineDecoration(1, 2, 'before', InlineDecorationType.Before)],
4,
10,
-1,
'all',
false,
true
));
let expected = [
'',
'',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
' 1. 🙏',
false,
false,
false,
0,
createViewLineTokens([createPart(7, 3)]),
[new LineDecoration(7, 8, 'inline-folded', InlineDecorationType.After)],
2,
10,
10000,
'none',
false,
false
));
let expected = [
'',
'\u00a0\u00a01.\u00a0',
'🙏',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #37401: Allow both before and after decorations on empty line', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
'',
false,
true,
false,
0,
createViewLineTokens([createPart(0, 3)]),
[
new LineDecoration(1, 2, 'before', InlineDecorationType.Before),
new LineDecoration(0, 1, 'after', InlineDecorationType.After),
],
2,
10,
10000,
'none',
false,
false
));
let expected = [
'',
'',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #38935: GitLens end-of-line blame no longer rendering', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
'\t}',
false,
true,
false,
0,
createViewLineTokens([createPart(2, 3)]),
[
new LineDecoration(3, 3, 'ced-TextEditorDecorationType2-5e9b9b3f-3 ced-TextEditorDecorationType2-3', InlineDecorationType.Before),
new LineDecoration(3, 3, 'ced-TextEditorDecorationType2-5e9b9b3f-4 ced-TextEditorDecorationType2-4', InlineDecorationType.After),
],
4,
10,
10000,
'none',
false,
false
));
let expected = [
'',
'\u00a0\u00a0\u00a0\u00a0}',
'',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #22832: Consider fullwidth characters when rendering tabs', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
'asd = "擦"\t\t#asd',
false,
false,
false,
0,
createViewLineTokens([createPart(15, 3)]),
[],
4,
10,
10000,
'none',
false,
false
));
let expected = [
'',
'asd\u00a0=\u00a0"擦"\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0#asd',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #22832: Consider fullwidth characters when rendering tabs (render whitespace)', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
'asd = "擦"\t\t#asd',
false,
false,
false,
0,
createViewLineTokens([createPart(15, 3)]),
[],
4,
10,
10000,
'all',
false,
false
));
let expected = [
'',
'asd',
'\u00b7',
'=',
'\u00b7',
'"擦"',
'\u2192\u00a0\u2192\u00a0\u00a0\u00a0',
'#asd',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #22352: COMBINING ACUTE ACCENT (U+0301)', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
'12345689012345678901234568901234567890123456890abába',
false,
false,
false,
0,
createViewLineTokens([createPart(53, 3)]),
[],
4,
10,
10000,
'none',
false,
false
));
let expected = [
'',
'12345689012345678901234568901234567890123456890abába',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #22352: Partially Broken Complex Script Rendering of Tamil', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
' JoyShareல் பின்தொடர்ந்து, விடீயோ, ஜோக்குகள், அனிமேசன், நகைச்சுவை படங்கள் மற்றும் செய்திகளை பெறுவீர்',
false,
false,
false,
0,
createViewLineTokens([createPart(100, 3)]),
[],
4,
10,
10000,
'none',
false,
false
));
let expected = [
'',
'\u00a0JoyShareல்\u00a0பின்தொடர்ந்து,\u00a0விடீயோ,\u00a0ஜோக்குகள்,\u00a0',
'அனிமேசன்,\u00a0நகைச்சுவை\u00a0படங்கள்\u00a0மற்றும்\u00a0செய்திகளை\u00a0',
'பெறுவீர்',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #42700: Hindi characters are not being rendered properly', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
' वो ऐसा क्या है जो हमारे अंदर भी है और बाहर भी है। जिसकी वजह से हम सब हैं। जिसने इस सृष्टि की रचना की है।',
false,
false,
false,
0,
createViewLineTokens([createPart(105, 3)]),
[],
4,
10,
10000,
'none',
false,
false
));
let expected = [
'',
'\u00a0वो\u00a0ऐसा\u00a0क्या\u00a0है\u00a0जो\u00a0हमारे\u00a0अंदर\u00a0भी\u00a0है\u00a0और\u00a0बाहर\u00a0भी\u00a0है।\u00a0',
'जिसकी\u00a0वजह\u00a0से\u00a0हम\u00a0सब\u00a0हैं।\u00a0जिसने\u00a0इस\u00a0सृष्टि\u00a0की\u00a0रचना\u00a0की\u00a0',
'है।',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
'This is a long line which never uses more than two spaces. ',
true,
true,
false,
0,
createViewLineTokens([createPart(59, 3)]),
[],
4,
10,
10000,
'boundary',
false,
false
));
let expected = [
'',
'This\u00a0is\u00a0a\u00a0long\u00a0line\u00a0which\u00a0never\u00a0uses\u00a0more\u00a0than\u00a0two\u00a0spaces.\u00a0',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #33525: Long line with ligatures takes a long time to paint decorations', () => {
let actual = renderViewLine(new RenderLineInput(
false,
false,
'append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to',
false,
true,
false,
0,
createViewLineTokens([createPart(194, 3)]),
[],
4,
10,
10000,
'none',
false,
true
));
let expected = [
'',
'append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0',
'append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0',
'append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0',
'append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0append\u00a0data\u00a0to\u00a0',
'append\u00a0data\u00a0to',
''
].join('');
assert.deepEqual(actual.html, expected);
});
test('issue #33525: Long line with ligatures takes a long time to paint decorations - not possible', () => {
let actual = renderViewLine(new RenderLineInput(
false,
false,
'appenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatato',
false,
true,
false,
0,
createViewLineTokens([createPart(194, 3)]),
[],
4,
10,
10000,
'none',
false,
true
));
let expected = [
'',
'appenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatato',
''
].join('');
assert.deepEqual(actual.html, expected);
});
function createTestGetColumnOfLinePartOffset(lineContent: string, tabSize: number, parts: ViewLineToken[], expectedPartLengths: number[]): (partIndex: number, partLength: number, offset: number, expected: number) => void {
let renderLineOutput = renderViewLine(new RenderLineInput(
false,
true,
lineContent,
false,
true,
false,
0,
createViewLineTokens(parts),
[],
tabSize,
10,
-1,
'none',
false,
false
));
return (partIndex: number, partLength: number, offset: number, expected: number) => {
let charOffset = renderLineOutput.characterMapping.partDataToCharOffset(partIndex, partLength, offset);
let actual = charOffset + 1;
assert.equal(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset);
};
}
test('getColumnOfLinePartOffset 1 - simple text', () => {
let testGetColumnOfLinePartOffset = createTestGetColumnOfLinePartOffset(
'hello world',
4,
[
createPart(11, 1)
],
[11]
);
testGetColumnOfLinePartOffset(0, 11, 0, 1);
testGetColumnOfLinePartOffset(0, 11, 1, 2);
testGetColumnOfLinePartOffset(0, 11, 2, 3);
testGetColumnOfLinePartOffset(0, 11, 3, 4);
testGetColumnOfLinePartOffset(0, 11, 4, 5);
testGetColumnOfLinePartOffset(0, 11, 5, 6);
testGetColumnOfLinePartOffset(0, 11, 6, 7);
testGetColumnOfLinePartOffset(0, 11, 7, 8);
testGetColumnOfLinePartOffset(0, 11, 8, 9);
testGetColumnOfLinePartOffset(0, 11, 9, 10);
testGetColumnOfLinePartOffset(0, 11, 10, 11);
testGetColumnOfLinePartOffset(0, 11, 11, 12);
});
test('getColumnOfLinePartOffset 2 - regular JS', () => {
let testGetColumnOfLinePartOffset = createTestGetColumnOfLinePartOffset(
'var x = 3;',
4,
[
createPart(3, 1),
createPart(4, 2),
createPart(5, 3),
createPart(8, 4),
createPart(9, 5),
createPart(10, 6),
],
[3, 1, 1, 3, 1, 1]
);
testGetColumnOfLinePartOffset(0, 3, 0, 1);
testGetColumnOfLinePartOffset(0, 3, 1, 2);
testGetColumnOfLinePartOffset(0, 3, 2, 3);
testGetColumnOfLinePartOffset(0, 3, 3, 4);
testGetColumnOfLinePartOffset(1, 1, 0, 4);
testGetColumnOfLinePartOffset(1, 1, 1, 5);
testGetColumnOfLinePartOffset(2, 1, 0, 5);
testGetColumnOfLinePartOffset(2, 1, 1, 6);
testGetColumnOfLinePartOffset(3, 3, 0, 6);
testGetColumnOfLinePartOffset(3, 3, 1, 7);
testGetColumnOfLinePartOffset(3, 3, 2, 8);
testGetColumnOfLinePartOffset(3, 3, 3, 9);
testGetColumnOfLinePartOffset(4, 1, 0, 9);
testGetColumnOfLinePartOffset(4, 1, 1, 10);
testGetColumnOfLinePartOffset(5, 1, 0, 10);
testGetColumnOfLinePartOffset(5, 1, 1, 11);
});
test('getColumnOfLinePartOffset 3 - tab with tab size 6', () => {
let testGetColumnOfLinePartOffset = createTestGetColumnOfLinePartOffset(
'\t',
6,
[
createPart(1, 1)
],
[6]
);
testGetColumnOfLinePartOffset(0, 6, 0, 1);
testGetColumnOfLinePartOffset(0, 6, 1, 1);
testGetColumnOfLinePartOffset(0, 6, 2, 1);
testGetColumnOfLinePartOffset(0, 6, 3, 1);
testGetColumnOfLinePartOffset(0, 6, 4, 2);
testGetColumnOfLinePartOffset(0, 6, 5, 2);
testGetColumnOfLinePartOffset(0, 6, 6, 2);
});
test('getColumnOfLinePartOffset 4 - once indented line, tab size 4', () => {
let testGetColumnOfLinePartOffset = createTestGetColumnOfLinePartOffset(
'\tfunction',
4,
[
createPart(1, 1),
createPart(9, 2),
],
[4, 8]
);
testGetColumnOfLinePartOffset(0, 4, 0, 1);
testGetColumnOfLinePartOffset(0, 4, 1, 1);
testGetColumnOfLinePartOffset(0, 4, 2, 1);
testGetColumnOfLinePartOffset(0, 4, 3, 2);
testGetColumnOfLinePartOffset(0, 4, 4, 2);
testGetColumnOfLinePartOffset(1, 8, 0, 2);
testGetColumnOfLinePartOffset(1, 8, 1, 3);
testGetColumnOfLinePartOffset(1, 8, 2, 4);
testGetColumnOfLinePartOffset(1, 8, 3, 5);
testGetColumnOfLinePartOffset(1, 8, 4, 6);
testGetColumnOfLinePartOffset(1, 8, 5, 7);
testGetColumnOfLinePartOffset(1, 8, 6, 8);
testGetColumnOfLinePartOffset(1, 8, 7, 9);
testGetColumnOfLinePartOffset(1, 8, 8, 10);
});
test('getColumnOfLinePartOffset 5 - twice indented line, tab size 4', () => {
let testGetColumnOfLinePartOffset = createTestGetColumnOfLinePartOffset(
'\t\tfunction',
4,
[
createPart(2, 1),
createPart(10, 2),
],
[8, 8]
);
testGetColumnOfLinePartOffset(0, 8, 0, 1);
testGetColumnOfLinePartOffset(0, 8, 1, 1);
testGetColumnOfLinePartOffset(0, 8, 2, 1);
testGetColumnOfLinePartOffset(0, 8, 3, 2);
testGetColumnOfLinePartOffset(0, 8, 4, 2);
testGetColumnOfLinePartOffset(0, 8, 5, 2);
testGetColumnOfLinePartOffset(0, 8, 6, 2);
testGetColumnOfLinePartOffset(0, 8, 7, 3);
testGetColumnOfLinePartOffset(0, 8, 8, 3);
testGetColumnOfLinePartOffset(1, 8, 0, 3);
testGetColumnOfLinePartOffset(1, 8, 1, 4);
testGetColumnOfLinePartOffset(1, 8, 2, 5);
testGetColumnOfLinePartOffset(1, 8, 3, 6);
testGetColumnOfLinePartOffset(1, 8, 4, 7);
testGetColumnOfLinePartOffset(1, 8, 5, 8);
testGetColumnOfLinePartOffset(1, 8, 6, 9);
testGetColumnOfLinePartOffset(1, 8, 7, 10);
testGetColumnOfLinePartOffset(1, 8, 8, 11);
});
});