Repository: microsoft/vscode (TypeScript)
Date: 2022-05-25 08:07:15+00:00
Task type: Bug Fix

Issue:
"l·l" Ligature in "Source Code Pro" incorrectly invoked when render whitespace is enabled
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- Please search existing issues to avoid creating duplicates. -->
<!-- Also please test using the latest insiders build to make sure your issue has not already been fixed: https://code.visualstudio.com/insiders/ -->

<!-- Use Help > Report Issue to prefill these. -->
- VSCode Version: 1.49.0
- OS Version: Windows 10

Steps to Reproduce:

1. Set `editor.renderWhitespace` to `all`.
2. Set `editor.fontFamily` to `Source Code Pro` (ensure you have the font installed on your system).
3. Set `editor.fontLigatures` to `false` (this issue is not reproducible if this setting is not set to `false`).
4. Open a new editor and type `l l` (a lowercase letter "l", followed by a space, followed by another lowercase letter "l").

Screenshot of this bug:

![](https://user-images.githubusercontent.com/45960387/93015483-79a27b80-f5ec-11ea-9c66-9a726a86d20a.png)

When `editor.renderWhitespace` is set to `all`, the whitespace character (U+0020) is rendered as the middle dot unicode character, `·` (U+00B7). Coincidentally, "Source Code Pro" has a ligature that comprise of the characters `l·l` (see the screenshot above), so this might be the reason why the ligature is invoked.

<!-- Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes



Issue discussion:
This does not reproduce when running with `--disable-features=LayoutNG`
Workaround: ~~configure `"editor.fontLigatures": "'liga' off, 'calt' off"`~~
configure `"editor.fontLigatures": "'ccmp' off"`
OK. I have actually found out quite a number of things while investigating this:
- https://wakamaifondue.com/ can be used to upload a font and get a list of all the font features settings available in that font
- Source Code Pro has 37 available font feature settings: `'case', 'ccmp', 'cv01', 'cv02', 'cv04', 'cv06', 'cv07', 'cv08', 'cv09', 'cv10', 'cv11', 'cv12', 'cv14', 'cv15', 'cv16', 'cv17', 'dnom', 'frac', 'locl', 'numr', 'onum', 'ordn', 'salt', 'sinf', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'subs', 'sups', 'zero', 'mark', 'mkmk', 'size'`
- By doing a binary search on them, it is the font feature setting named `'ccmp'` that is causing `l·l` to be rendered with a different glyph.
- Here is what I could [find about `ccmp`](https://docs.microsoft.com/en-us/typography/opentype/spec/features_ae#tag-ccmp)
- It looks like Source Code Pro registers a ligature for the character sequence `l·l`. The `·` is the character we use to render whitespace.
- This ligature is no longer disabled via `'liga' off, 'calt' off` as it used to be the case in the past.

**TL;DR**: This has changed since VS Code 1.49 because we have upgraded to Electron 9, which ships `layoutNG`. I am not convinced this is a bug, it might just be something unique about `Source Code Pro`. The workaround is to use `"editor.fontLigatures": "'ccmp' off"`
> **TL;DR**: This has changed since VS Code 1.49 because we have upgraded to Electron 9, which ships `layoutNG`. I am not convinced this is a bug, it might just be something unique about `Source Code Pro`. The workaround is to use `"editor.fontLigatures": "'ccmp' off"`

I run into this issue as well. If this is a not a bug, what is it? I don't mind having fine control over font features (that sounds great actually). However, there really should be a **simple** setting that effectively disables all use of font ligatures. Something like `"editor.fontLigatures": "all off"`, but it would be even better if `false` actually meant `"all off"`.
The fact that the editor uses an actual text character (`·`) to implement whitespace indicators seems like an implementation detail. I'd say since the user didn't intentionally insert `·`, its a bug if ligatures fire on that character sequence. Maybe not easy to fix (I don't know), but I do think this is a bug. The user entered `l l` (elle space elle) not `l·l` (elle dot elle) and the ligature is not intended to trigger for `l l`. 

Furthermore, the caret positioning for the rest of the line becomes incorrect:
See this result with the problematic ligature turned on:
![image](https://user-images.githubusercontent.com/409140/94058022-58345300-fd95-11ea-9192-36d3083f0493.png)
See this result once `"editor.fontLigatures": "'ccmp' off"` is applied:
![image](https://user-images.githubusercontent.com/409140/94058125-7e59f300-fd95-11ea-8af5-3bb0985f947b.png)

Furthermore, the caret position jumps around when faced with navigating the line after the ligature occurs, and selection also assumes it is rendered as if the ccmp feature is off:
![image](https://user-images.githubusercontent.com/409140/94058281-ae08fb00-fd95-11ea-9830-28cb084391ed.png)


I had the same issue.
@alexdima 's fix solved the issue.
```
"editor.fontLigatures": "'ccmp' off"
```
Copying here what I commented in adobe-fonts/source-code-pro#260:
> IMHO that should be solved by VSCode by switching to the proper Unicode character, U+2027, instead of reusing U+00B7, which is an orthographic character in the Catalan language and other related Romance languages like Occitan. VSCode shouldn't ask font developers to make their fonts substandard in a human language to accomodate for their software's quirks.
@fitojb makes a reasonable point, but switching will make the visible whitespace harder to see.

![image](https://user-images.githubusercontent.com/6726799/106584178-cd7f1400-653d-11eb-867c-5d0d1120811b.png)



Another screenshot with some more options plus a first line showing how the current choice (MIDDLE DOT) looks when visible whitespace is enabled. This time I have also used "Zoom In". Compare lines 1 and 3 to see how the dimming of visible whitespace affects the visibility of the chosen character.

Theme: Dark+

![image](https://user-images.githubusercontent.com/6726799/106587907-15079f00-6542-11eb-914d-84a1a685ed6b.png)

Theme: Light+

![image](https://user-images.githubusercontent.com/6726799/106588075-484a2e00-6542-11eb-98d9-58faa84305e5.png)

My test file:
[visibleWhitespace.txt](https://github.com/microsoft/vscode/files/5910578/visibleWhitespace.txt)
> @fitojb makes a reasonable point, but switching will make the visible whitespace harder to see.

That depends on what font user is using.
This could be mitigated by whitespace color preference/setting.
Possibly there could be a preference/setting to customize which glyphs are used for whitespaces.
And/or how about glyph replacement map: allow individual glyphs to be remapped to another glyph.

I'm a bit confused. The text being rendered is already different from the text being interacted with (e.g. there's a dot there, where there isn't in the text). So why can't we just render `&NoBreak;<dot>&NoBreak;` (or similar with ZWSP, etc. -- any of the zero-width space characters) to break up the ligature? I don't actually know Unicode well enough to say which would be the best, NoBreak was just the first one I thought of.

That way we preserve the aesthetic, which admittedly I do like the best out of the options, while still not causing problems for fonts which define the Catalan ligature, AFAIK without needing to tweak rendering code significantly.

ETA: my hesitation isn't sarcasm, I'm just not familiar enough with VS Code's internals to know whether this is really an option.
Just a reminder that this bug is still a problem (deb package version 1.56.2-1620838498).
This is still a problem, and having to set ligatures to `"'ccmp' off"` is not a great fix. I assumed that setting ligatures to `false` would turn ligatures off, not ... ignore settings related to them? I guess? (I'm not actually sure what it does).
More intuitive or expressive options for `editor.fontLigatures` would be a good thing, as would not having ligatures show up due to rendered whitespace.
@jonnyhoff me too
Just updated to latest VSCode.  This issue remains unfixed.
Also suffering from this.
> I'm a bit confused. The text being rendered is already different from the text being interacted with (e.g. there's a dot there, where there isn't in the text). So why can't we just render `&NoBreak;<dot>&NoBreak;` (or similar with ZWSP, etc. -- any of the zero-width space characters) to break up the ligature? I don't actually know Unicode well enough to say which would be the best, NoBreak was just the first one I thought of.
> 
> That way we preserve the aesthetic, which admittedly I do like the best out of the options, while still not causing problems for fonts which define the Catalan ligature, AFAIK without needing to tweak rendering code significantly.
> 
> ETA: my hesitation isn't sarcasm, I'm just not familiar enough with VS Code's internals to know whether this is really an option.

`U+200C` ZERO WIDTH NON-JOINER (`&zwnj;`) should be a better option, which break the ligatures and keep the original width. from [wikipedia](https://en.wikipedia.org/wiki/Zero-width_non-joiner):

> The zero-width non-joiner (ZWNJ) (‌) is a non-printing character used in the computerization of writing systems that make use of ligatures. When placed between two characters that would otherwise be connected into a ligature, a ZWNJ causes them to be printed in their final and initial forms, respectively. This is also an effect of a space character, but a ZWNJ is used when it is desirable to keep the words closer together or to connect a word with its morpheme.


Solution patch:
diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts
--- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts
+++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts
@@ -991,7 +991,9 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
 				} else { // must be CharCode.Space
 					charWidth = 1;
 
+					sb.write1(0x200C); // ZERO WIDTH NON-JOINER
 					sb.write1(renderSpaceCharCode); // &middot; or word separator middle dot
+					sb.write1(0x200C); // ZERO WIDTH NON-JOINER
 				}
 
 				charOffsetInPart += charWidth;


Test patch:
diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts
--- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts
+++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts
@@ -201,7 +201,7 @@ suite('viewLineRenderer.renderLine', () => {
 		]);
 		const expectedOutput = [
 			'<span class="mtkz" style="width:40px">\u2192\u00a0\u00a0\u00a0</span>',
-			'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+			'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 			'<span class="mtk2">export</span>',
 			'<span class="mtk3">\u00a0</span>',
 			'<span class="mtk4">class</span>',
@@ -212,8 +212,8 @@ suite('viewLineRenderer.renderLine', () => {
 			'<span class="mtk9">\u00a0</span>',
 			'<span class="mtk10">//\u00a0</span>',
 			'<span class="mtk11">http://test.com</span>',
-			'<span class="mtkz" style="width:20px">\u00b7\u00b7</span>',
-			'<span class="mtkz" style="width:30px">\u00b7\u00b7\u00b7</span>'
+			'<span class="mtkz" style="width:20px">\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:30px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>'
 		].join('');
 
 		const info: CharacterMappingInfo[] = [
@@ -565,7 +565,7 @@ suite('viewLineRenderer.renderLine', () => {
 		]);
 
 		const expectedOutput = [
-			'<span class="mtkw">\u00b7\u00b7\u00b7\u00b7</span>',
+			'<span class="mtkw">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 			'<span class="mtk2">[</span>',
 			'<span style="unicode-bidi:isolate" class="mtk3">"🖨️\u00a0چاپ\u00a0فاکتور"</span>',
 			'<span class="mtk2">,</span>',
@@ -1108,10 +1108,10 @@ suite('viewLineRenderer.renderLine 2', () => {
 			null,
 			[
 				'<span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'<span class="mtk2">He</span>',
 				'<span class="mtk3">llo\u00a0world!</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'</span>',
 			].join('')
 		);
@@ -1130,12 +1130,12 @@ suite('viewLineRenderer.renderLine 2', () => {
 			null,
 			[
 				'<span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'<span class="mtk2">He</span>',
 				'<span class="mtk3">llo\u00a0world!</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'</span>',
 			].join('')
 		);
@@ -1177,15 +1177,15 @@ suite('viewLineRenderer.renderLine 2', () => {
 			null,
 			[
 				'<span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u2192\u00a0</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u2192\u00a0</span>',
 				'<span class="mtkz" style="width:40px">\u2192\u00a0\u00a0\u00a0</span>',
-				'<span class="mtkz" style="width:20px">\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:20px">\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'<span class="mtk2">He</span>',
 				'<span class="mtk3">llo\u00a0world!</span>',
-				'<span class="mtkz" style="width:20px">\u00b7\uffeb</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u2192\u00a0</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\uffeb</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:20px">\u200c\u00b7\u200c\uffeb</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u2192\u00a0</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\uffeb</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'</span>',
 			].join('')
 		);
@@ -1206,13 +1206,13 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[
 				'<span>',
 				'<span class="">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0</span>',
-				'<span class="mtkz" style="width:20px">\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:20px">\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'<span class="mtk2">He</span>',
 				'<span class="mtk3">llo\u00a0world!</span>',
-				'<span class="mtkz" style="width:20px">\u00b7\uffeb</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u2192\u00a0</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\uffeb</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:20px">\u200c\u00b7\u200c\uffeb</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u2192\u00a0</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\uffeb</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'</span>',
 			].join('')
 		);
@@ -1233,10 +1233,10 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[
 				'<span>',
 				'<span class="">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0</span>',
-				'<span class="mtkw">\u00b7\u00b7</span>',
+				'<span class="mtkw">\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'<span class="mtk2">He</span>',
 				'<span class="mtk3">llo\u00a0world!</span>',
-				'<span class="mtkw">\u00b7\uffeb\u00b7\u00b7\u2192\u00a0\u00b7\u00b7\u00b7\uffeb\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkw">\u200c\u00b7\u200c\uffeb\u200c\u00b7\u200c\u200c\u00b7\u200c\u2192\u00a0\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\uffeb\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'</span>',
 			].join('')
 		);
@@ -1257,11 +1257,11 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[
 				'<span>',
 				'<span class="mtk1">it</span>',
-				'<span class="mtkz" style="width:20px">\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:20px">\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'<span class="mtk1">it</span>',
 				'<span class="mtk2">\u00a0</span>',
 				'<span class="mtk3">it</span>',
-				'<span class="mtkz" style="width:20px">\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:20px">\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'<span class="mtk3">it</span>',
 				'</span>',
 			].join('')
@@ -1282,10 +1282,10 @@ suite('viewLineRenderer.renderLine 2', () => {
 			null,
 			[
 				'<span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk0">Hel</span>',
 				'<span class="mtk1">lo</span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk2">world!</span>',
 				'<span class="mtkz" style="width:30px">\u2192\u00a0\u00a0</span>',
 				'</span>',
@@ -1329,10 +1329,10 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[new LineRange(0, 14)],
 			[
 				'<span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk0">Hel</span>',
 				'<span class="mtk1">lo</span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk2">world!</span>',
 				'<span class="mtkz" style="width:30px">\u2192\u00a0\u00a0</span>',
 				'</span>',
@@ -1354,7 +1354,7 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[new LineRange(0, 5)],
 			[
 				'<span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk0">Hel</span>',
 				'<span class="mtk1">lo</span>',
 				'<span class="mtk2">\u00a0world!\u00a0\u00a0\u00a0</span>',
@@ -1378,7 +1378,7 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[new LineRange(0, 5), new LineRange(9, 14)],
 			[
 				'<span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk0">Hel</span>',
 				'<span class="mtk1">lo</span>',
 				'<span class="mtk2">\u00a0world!</span>',
@@ -1403,7 +1403,7 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[new LineRange(9, 14), new LineRange(0, 5)],
 			[
 				'<span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk0">Hel</span>',
 				'<span class="mtk1">lo</span>',
 				'<span class="mtk2">\u00a0world!</span>',
@@ -1425,9 +1425,9 @@ suite('viewLineRenderer.renderLine 2', () => {
 			[new LineRange(0, 1), new LineRange(1, 2), new LineRange(2, 3)],
 			[
 				'<span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk0">*</span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'<span class="mtk0">S</span>',
 				'</span>',
 			].join('')
@@ -1473,7 +1473,7 @@ suite('viewLineRenderer.renderLine 2', () => {
 				'<span class="mtk0">\u00a0Hel</span>',
 				'<span class="mtk1">lo</span>',
 				'<span class="mtk2">\u00a0world!</span>',
-				'<span class="mtkz" style="width:30px">\u00b7\u2192\u00a0</span>',
+				'<span class="mtkz" style="width:30px">\u200c\u00b7\u200c\u2192\u00a0</span>',
 				'</span>',
 			].join('')
 		);
@@ -1496,8 +1496,8 @@ suite('viewLineRenderer.renderLine 2', () => {
 				'<span class="mtk1">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0</span>',
 				'<span class="mtk2">He</span>',
 				'<span class="mtk3">llo\u00a0world!</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u00b7\u00b7\u00b7</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span>',
 				'</span>',
 			].join('')
 		);
@@ -1516,8 +1516,8 @@ suite('viewLineRenderer.renderLine 2', () => {
 			null,
 			[
 				'<span>',
-				'<span class="mtkz" style="width:40px">\u00b7\u2192\u00a0\u00a0</span>',
-				'<span class="mtkz" style="width:10px">\u00b7</span>',
+				'<span class="mtkz" style="width:40px">\u200c\u00b7\u200c\u2192\u00a0\u00a0</span>',
+				'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 				'</span>',
 			].join('')
 		);
@@ -1925,9 +1925,9 @@ suite('viewLineRenderer.renderLine 2', () => {
 		const expected = [
 			'<span>',
 			'<span class="mtk3">asd</span>',
-			'<span class="mtkw">\u00b7</span>',
+			'<span class="mtkw">\u200c\u00b7\u200c</span>',
 			'<span class="mtk3">=</span>',
-			'<span class="mtkw">\u00b7</span>',
+			'<span class="mtkw">\u200c\u00b7\u200c</span>',
 			'<span class="mtk3">"擦"</span>',
 			'<span class="mtkw">\u2192\u00a0\u2192\u00a0\u00a0\u00a0</span>',
 			'<span class="mtk3">#asd</span>',
@@ -2184,40 +2184,40 @@ suite('viewLineRenderer.renderLine 2', () => {
 
 		const expected = [
 			'<span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 			'<span class="mtk15">else</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 			'<span class="mtk15">if</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 			'<span class="mtk1">(</span>',
 			'<span class="mtk16">$s</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 			'<span class="mtk1">=</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 			'<span class="mtk6">08</span>',
 			'<span class="mtk1">)</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 			'<span class="mtk15">then</span>',
-			'<span class="mtkz" style="width:10px">\u00b7</span>',
+			'<span class="mtkz" style="width:10px">\u200c\u00b7\u200c</span>',
 			'<span class="mtk11">\'\\b\'</span>',
 			'</span>'
 		].join('');
@@ -2318,7 +2318,7 @@ suite('viewLineRenderer.renderLine 2', () => {
 
 		const expected = [
 			'<span>',
-			'<span class="mtkw">····</span><span class="mtk2">if</span><span class="ced-1-TextEditorDecorationType2-17c14d98-3 ced-1-TextEditorDecorationType2-3"></span><span class="ced-1-TextEditorDecorationType2-17c14d98-4 ced-1-TextEditorDecorationType2-4"></span><span class="ced-ghost-text-1-4"></span>',
+			'<span class="mtkw">\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c\u200c\u00b7\u200c</span><span class="mtk2">if</span><span class="ced-1-TextEditorDecorationType2-17c14d98-3 ced-1-TextEditorDecorationType2-3"></span><span class="ced-1-TextEditorDecorationType2-17c14d98-4 ced-1-TextEditorDecorationType2-4"></span><span class="ced-ghost-text-1-4"></span>',
 			'</span>'
 		].join('');