Skip to content

Commit 7fa7f21

Browse files
committed
Fixes microsoft#37208: Do not allow inline decorations to break surrogate pairs
1 parent bf036aa commit 7fa7f21

4 files changed

Lines changed: 60 additions & 14 deletions

File tree

src/vs/editor/common/viewLayout/lineDecorations.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { InlineDecoration } from 'vs/editor/common/viewModel/viewModel';
88
import { Constants } from 'vs/editor/common/core/uint';
9+
import * as strings from 'vs/base/common/strings';
910

1011
export class LineDecoration {
1112
_lineDecorationBrand: void;
@@ -172,7 +173,7 @@ export class LineDecorationsNormalizer {
172173
/**
173174
* Normalize line decorations. Overlapping decorations will generate multiple segments
174175
*/
175-
public static normalize(lineDecorations: LineDecoration[]): DecorationSegment[] {
176+
public static normalize(lineContent: string, lineDecorations: LineDecoration[]): DecorationSegment[] {
176177
if (lineDecorations.length === 0) {
177178
return [];
178179
}
@@ -184,16 +185,34 @@ export class LineDecorationsNormalizer {
184185

185186
for (let i = 0, len = lineDecorations.length; i < len; i++) {
186187
let d = lineDecorations[i];
188+
let startColumn = d.startColumn;
189+
let endColumn = d.endColumn;
190+
let className = d.className;
191+
192+
// If the position would end up in the middle of a high-low surrogate pair, we move it to before the pair
193+
if (startColumn > 1) {
194+
const charCodeBefore = lineContent.charCodeAt(startColumn - 2);
195+
if (strings.isHighSurrogate(charCodeBefore)) {
196+
startColumn--;
197+
}
198+
}
199+
200+
if (endColumn > 1) {
201+
const charCodeBefore = lineContent.charCodeAt(endColumn - 2);
202+
if (strings.isHighSurrogate(charCodeBefore)) {
203+
endColumn--;
204+
}
205+
}
187206

188-
let currentStartOffset = d.startColumn - 1;
189-
let currentEndOffset = d.endColumn - 2;
207+
let currentStartOffset = startColumn - 1;
208+
let currentEndOffset = endColumn - 2;
190209

191210
nextStartOffset = stack.consumeLowerThan(currentStartOffset, nextStartOffset, result);
192211

193212
if (stack.count === 0) {
194213
nextStartOffset = currentStartOffset;
195214
}
196-
stack.insert(currentEndOffset, d.className);
215+
stack.insert(currentEndOffset, className);
197216
}
198217

199218
stack.consumeLowerThan(Constants.MAX_SAFE_SMALL_INTEGER, nextStartOffset, result);

src/vs/editor/common/viewLayout/viewLineRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ function _applyRenderWhitespace(lineContent: string, len: number, tokens: LinePa
531531
*/
532532
function _applyInlineDecorations(lineContent: string, len: number, tokens: LinePart[], _lineDecorations: LineDecoration[]): LinePart[] {
533533
_lineDecorations.sort(LineDecoration.compare);
534-
const lineDecorations = LineDecorationsNormalizer.normalize(_lineDecorations);
534+
const lineDecorations = LineDecorationsNormalizer.normalize(lineContent, _lineDecorations);
535535
const lineDecorationsLen = lineDecorations.length;
536536

537537
let lineDecorationIndex = 0;

src/vs/editor/test/common/viewLayout/lineDecorations.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ suite('Editor ViewLayout - ViewLineParts', () => {
1717

1818
test('Bug 9827:Overlapping inline decorations can cause wrong inline class to be applied', () => {
1919

20-
var result = LineDecorationsNormalizer.normalize([
20+
var result = LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
2121
new LineDecoration(1, 11, 'c1', false),
2222
new LineDecoration(3, 4, 'c2', false)
2323
]);
@@ -31,7 +31,7 @@ suite('Editor ViewLayout - ViewLineParts', () => {
3131

3232
test('issue #3462: no whitespace shown at the end of a decorated line', () => {
3333

34-
var result = LineDecorationsNormalizer.normalize([
34+
var result = LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
3535
new LineDecoration(15, 21, 'vs-whitespace', false),
3636
new LineDecoration(20, 21, 'inline-folded', false),
3737
]);
@@ -55,31 +55,31 @@ suite('Editor ViewLayout - ViewLineParts', () => {
5555

5656
test('ViewLineParts', () => {
5757

58-
assert.deepEqual(LineDecorationsNormalizer.normalize([
58+
assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
5959
new LineDecoration(1, 2, 'c1', false),
6060
new LineDecoration(3, 4, 'c2', false)
6161
]), [
6262
new DecorationSegment(0, 0, 'c1'),
6363
new DecorationSegment(2, 2, 'c2')
6464
]);
6565

66-
assert.deepEqual(LineDecorationsNormalizer.normalize([
66+
assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
6767
new LineDecoration(1, 3, 'c1', false),
6868
new LineDecoration(3, 4, 'c2', false)
6969
]), [
7070
new DecorationSegment(0, 1, 'c1'),
7171
new DecorationSegment(2, 2, 'c2')
7272
]);
7373

74-
assert.deepEqual(LineDecorationsNormalizer.normalize([
74+
assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
7575
new LineDecoration(1, 4, 'c1', false),
7676
new LineDecoration(3, 4, 'c2', false)
7777
]), [
7878
new DecorationSegment(0, 1, 'c1'),
7979
new DecorationSegment(2, 2, 'c1 c2')
8080
]);
8181

82-
assert.deepEqual(LineDecorationsNormalizer.normalize([
82+
assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
8383
new LineDecoration(1, 4, 'c1', false),
8484
new LineDecoration(1, 4, 'c1*', false),
8585
new LineDecoration(3, 4, 'c2', false)
@@ -88,7 +88,7 @@ suite('Editor ViewLayout - ViewLineParts', () => {
8888
new DecorationSegment(2, 2, 'c1 c1* c2')
8989
]);
9090

91-
assert.deepEqual(LineDecorationsNormalizer.normalize([
91+
assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
9292
new LineDecoration(1, 4, 'c1', false),
9393
new LineDecoration(1, 4, 'c1*', false),
9494
new LineDecoration(1, 4, 'c1**', false),
@@ -98,7 +98,7 @@ suite('Editor ViewLayout - ViewLineParts', () => {
9898
new DecorationSegment(2, 2, 'c1 c1* c1** c2')
9999
]);
100100

101-
assert.deepEqual(LineDecorationsNormalizer.normalize([
101+
assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
102102
new LineDecoration(1, 4, 'c1', false),
103103
new LineDecoration(1, 4, 'c1*', false),
104104
new LineDecoration(1, 4, 'c1**', false),
@@ -109,7 +109,7 @@ suite('Editor ViewLayout - ViewLineParts', () => {
109109
new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*')
110110
]);
111111

112-
assert.deepEqual(LineDecorationsNormalizer.normalize([
112+
assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [
113113
new LineDecoration(1, 4, 'c1', false),
114114
new LineDecoration(1, 4, 'c1*', false),
115115
new LineDecoration(1, 4, 'c1**', false),

src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,33 @@ suite('viewLineRenderer.renderLine 2', () => {
11101110
assert.deepEqual(actual.html, expected);
11111111
});
11121112

1113+
test('issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character', () => {
1114+
1115+
let actual = renderViewLine(new RenderLineInput(
1116+
true,
1117+
' 1. 🙏',
1118+
false,
1119+
0,
1120+
[createPart(7, 3)],
1121+
[new LineDecoration(7, 8, 'inline-folded', true)],
1122+
2,
1123+
10,
1124+
10000,
1125+
'none',
1126+
false,
1127+
false
1128+
));
1129+
1130+
let expected = [
1131+
'<span>',
1132+
'<span class="mtk3">\u00a0\u00a01.\u00a0</span>',
1133+
'<span class="mtk3 inline-folded">🙏</span>',
1134+
'</span>'
1135+
].join('');
1136+
1137+
assert.deepEqual(actual.html, expected);
1138+
});
1139+
11131140
function createTestGetColumnOfLinePartOffset(lineContent: string, tabSize: number, parts: ViewLineToken[], expectedPartLengths: number[]): (partIndex: number, partLength: number, offset: number, expected: number) => void {
11141141
let renderLineOutput = renderViewLine(new RenderLineInput(
11151142
false,

0 commit comments

Comments
 (0)