Skip to content

Commit b3318bc

Browse files
authored
Merge pull request microsoft#136347 from microsoft/alex/116939
Escape unicode directional formatting characters when rendering control characters
2 parents 4bbec28 + d2c24cc commit b3318bc

File tree

5 files changed

+114
-4
lines changed

5 files changed

+114
-4
lines changed

src/vs/editor/browser/viewParts/lines/viewLines.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
100% { background-color: none }
1515
}*/
1616

17+
.mtkcontrol {
18+
color: rgb(255, 255, 255) !important;
19+
background: rgb(150, 0, 0) !important;
20+
}
21+
1722
.monaco-editor.no-user-select .lines-content,
1823
.monaco-editor.no-user-select .view-line,
1924
.monaco-editor.no-user-select .view-lines {

src/vs/editor/common/config/editorOptions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ export interface IEditorOptions {
575575
renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all';
576576
/**
577577
* Enable rendering of control characters.
578-
* Defaults to false.
578+
* Defaults to true.
579579
*/
580580
renderControlCharacters?: boolean;
581581
/**
@@ -4637,8 +4637,8 @@ export const EditorOptions = {
46374637
{ description: nls.localize('renameOnType', "Controls whether the editor auto renames on type."), markdownDeprecationMessage: nls.localize('renameOnTypeDeprecate', "Deprecated, use `editor.linkedEditing` instead.") }
46384638
)),
46394639
renderControlCharacters: register(new EditorBooleanOption(
4640-
EditorOption.renderControlCharacters, 'renderControlCharacters', false,
4641-
{ description: nls.localize('renderControlCharacters', "Controls whether the editor should render control characters.") }
4640+
EditorOption.renderControlCharacters, 'renderControlCharacters', true,
4641+
{ description: nls.localize('renderControlCharacters', "Controls whether the editor should render control characters."), restricted: true }
46424642
)),
46434643
renderFinalNewline: register(new EditorBooleanOption(
46444644
EditorOption.renderFinalNewline, 'renderFinalNewline', true,

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,9 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput
500500
// We can never split RTL text, as it ruins the rendering
501501
tokens = splitLargeTokens(lineContent, tokens, !input.isBasicASCII || input.fontLigatures);
502502
}
503+
if (input.renderControlCharacters && !input.isBasicASCII) {
504+
tokens = extractControlCharacters(lineContent, tokens);
505+
}
503506

504507
return new ResolvedRenderLineInput(
505508
input.useMonospaceOptimizations,
@@ -621,6 +624,67 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces:
621624
return result;
622625
}
623626

627+
function isControlCharacter(charCode: number): boolean {
628+
if (charCode < 32) {
629+
return (charCode !== CharCode.Tab);
630+
}
631+
if (charCode === 127) {
632+
// DEL
633+
return true;
634+
}
635+
636+
if (
637+
(charCode >= 0x202A && charCode <= 0x202E)
638+
|| (charCode >= 0x2066 && charCode <= 0x2069)
639+
|| (charCode >= 0x200E && charCode <= 0x200F)
640+
|| charCode === 0x061C
641+
) {
642+
// Unicode Directional Formatting Characters
643+
// LRE U+202A LEFT-TO-RIGHT EMBEDDING
644+
// RLE U+202B RIGHT-TO-LEFT EMBEDDING
645+
// PDF U+202C POP DIRECTIONAL FORMATTING
646+
// LRO U+202D LEFT-TO-RIGHT OVERRIDE
647+
// RLO U+202E RIGHT-TO-LEFT OVERRIDE
648+
// LRI U+2066 LEFT‑TO‑RIGHT ISOLATE
649+
// RLI U+2067 RIGHT‑TO‑LEFT ISOLATE
650+
// FSI U+2068 FIRST STRONG ISOLATE
651+
// PDI U+2069 POP DIRECTIONAL ISOLATE
652+
// LRM U+200E LEFT-TO-RIGHT MARK
653+
// RLM U+200F RIGHT-TO-LEFT MARK
654+
// ALM U+061C ARABIC LETTER MARK
655+
return true;
656+
}
657+
658+
return false;
659+
}
660+
661+
function extractControlCharacters(lineContent: string, tokens: LinePart[]): LinePart[] {
662+
let result: LinePart[] = [];
663+
let lastLinePart: LinePart = new LinePart(0, '', 0);
664+
let charOffset = 0;
665+
for (const token of tokens) {
666+
const tokenEndIndex = token.endIndex;
667+
for (; charOffset < tokenEndIndex; charOffset++) {
668+
const charCode = lineContent.charCodeAt(charOffset);
669+
if (isControlCharacter(charCode)) {
670+
if (charOffset > lastLinePart.endIndex) {
671+
// emit previous part if it has text
672+
lastLinePart = new LinePart(charOffset, token.type, token.metadata);
673+
result.push(lastLinePart);
674+
}
675+
lastLinePart = new LinePart(charOffset + 1, 'mtkcontrol', token.metadata);
676+
result.push(lastLinePart);
677+
}
678+
}
679+
if (charOffset > lastLinePart.endIndex) {
680+
// emit previous part if it has text
681+
lastLinePart = new LinePart(tokenEndIndex, token.type, token.metadata);
682+
result.push(lastLinePart);
683+
}
684+
}
685+
return result;
686+
}
687+
624688
/**
625689
* Whitespace is rendered by "replacing" tokens with a special-purpose `mtkw` type that is later recognized in the rendering phase.
626690
* Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (&rarr; or &middot;) do not have the same width as &nbsp;.
@@ -1005,6 +1069,11 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
10051069
} else if (renderControlCharacters && charCode === 127) {
10061070
// DEL
10071071
sb.write1(9249);
1072+
} else if (renderControlCharacters && isControlCharacter(charCode)) {
1073+
sb.appendASCIIString('[U+');
1074+
sb.appendASCIIString(to4CharHex(charCode));
1075+
sb.appendASCIIString(']');
1076+
producedCharacters = 8;
10081077
} else {
10091078
sb.write1(charCode);
10101079
}
@@ -1049,3 +1118,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
10491118

10501119
return new RenderLineOutput(characterMapping, containsRTL, containsForeignElements);
10511120
}
1121+
1122+
function to4CharHex(n: number): string {
1123+
return n.toString(16).toUpperCase().padStart(4, '0');
1124+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2114,6 +2114,38 @@ suite('viewLineRenderer.renderLine 2', () => {
21142114
assert.deepStrictEqual(actual.html, expected);
21152115
});
21162116

2117+
test('issue #116939: Important control characters aren\'t rendered', () => {
2118+
const actual = renderViewLine(new RenderLineInput(
2119+
false,
2120+
false,
2121+
`transferBalance(5678,${String.fromCharCode(0x202E)}6776,4321${String.fromCharCode(0x202C)},"USD");`,
2122+
false,
2123+
false,
2124+
false,
2125+
0,
2126+
createViewLineTokens([createPart(42, 3)]),
2127+
[],
2128+
4,
2129+
0,
2130+
10,
2131+
10,
2132+
10,
2133+
10000,
2134+
'none',
2135+
true,
2136+
false,
2137+
null
2138+
));
2139+
2140+
const expected = [
2141+
'<span>',
2142+
'<span class="mtk3">transferBalance(5678,</span><span class="mtkcontrol">[U+202E]</span><span class="mtk3">6776,4321</span><span class="mtkcontrol">[U+202C]</span><span class="mtk3">,"USD");</span>',
2143+
'</span>'
2144+
].join('');
2145+
2146+
assert.deepStrictEqual(actual.html, expected);
2147+
});
2148+
21172149
test('issue #124038: Multiple end-of-line text decorations get merged', () => {
21182150
const actual = renderViewLine(new RenderLineInput(
21192151
true,

src/vs/monaco.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3226,7 +3226,7 @@ declare namespace monaco.editor {
32263226
renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all';
32273227
/**
32283228
* Enable rendering of control characters.
3229-
* Defaults to false.
3229+
* Defaults to true.
32303230
*/
32313231
renderControlCharacters?: boolean;
32323232
/**

0 commit comments

Comments
 (0)