语法突出显示方面的优化
2017 年 2 月 8 日 - Alexandru Dima
Visual Studio Code 1.9 版本包含一个很酷的性能改进,我们一直在为此努力,我想讲述它的故事。
TL;DR TextMate 主题在 VS Code 1.9 中看起来更像作者的意图,同时渲染速度更快,内存消耗更少。
语法突出显示
语法突出显示通常由两个阶段组成。令牌被分配给源代码,然后它们成为主题的目标,被分配颜色,瞧,您的源代码就用颜色渲染出来了。这是将文本编辑器变成代码编辑器的功能之一。
VS Code(以及 Monaco Editor 中)的令牌化逐行运行,从上到下,单次通过。令牌化器可以在令牌化行的末尾存储一些状态,这些状态将在令牌化下一行时传回。这是许多令牌化引擎(包括 TextMate 语法)使用的一种技术,它允许编辑器在用户进行编辑时仅重新令牌化行的小子集。
大多数时候,在一行上键入只会导致该行被重新令牌化,因为令牌化器返回相同的结束状态,并且编辑器可以假定以下行不会获得新令牌
![Tokenization Single Line](/assets/blogs/2017/02/08/tokenization-1.gif)
更少见的情况是,在一行上键入会导致当前行和下面的一些行(直到遇到相等的结束状态)被重新令牌化/重绘
![Tokenization Multiple Lines](/assets/blogs/2017/02/08/tokenization-2.gif)
我们过去如何表示令牌
VS Code 中编辑器的代码是在 VS Code 存在之前很久就编写的。它以 Monaco Editor 的形式在各种 Microsoft 项目(包括 Internet Explorer 的 F12 工具)中发布。我们有一个要求是减少内存使用量。
过去,我们是手工编写令牌化器(即使在今天,在浏览器中解释 TextMate 语法也是不可行的,但这又是另一个故事)。对于下面的行,我们将从我们手工编写的令牌化器中获得以下令牌
![Line offsets](/assets/blogs/2017/02/08/line-offsets.png)
tokens = [
{ startIndex: 0, type: 'keyword.js' },
{ startIndex: 8, type: '' },
{ startIndex: 9, type: 'identifier.js' },
{ startIndex: 11, type: 'delimiter.paren.js' },
{ startIndex: 12, type: 'delimiter.paren.js' },
{ startIndex: 13, type: '' },
{ startIndex: 14, type: 'delimiter.curly.js' }
];
保留该令牌数组在 Chrome 中占用 648 字节,因此存储这样的对象在内存方面非常昂贵(每个对象实例都必须为指向其原型、其属性列表等保留空间)。我们当前的机器确实有大量 RAM,但对于 15 个字符的行存储 648 字节是不可接受的。
因此,当时,我们提出了一种二进制格式来存储令牌,这种格式一直使用到包括 VS Code 1.8。考虑到会存在重复的令牌类型,我们将它们收集在一个单独的映射(每个文件)中,执行类似以下操作
// 0 1 2 3 4
map = ['', 'keyword.js', 'identifier.js', 'delimiter.paren.js', 'delimiter.curly.js'];
tokens = [
{ startIndex: 0, type: 1 },
{ startIndex: 8, type: 0 },
{ startIndex: 9, type: 2 },
{ startIndex: 11, type: 3 },
{ startIndex: 12, type: 3 },
{ startIndex: 13, type: 0 },
{ startIndex: 14, type: 4 }
];
然后,我们将 startIndex
(32 位)和 type
(16 位)编码到 JavaScript 数字的 53 位尾数位中的 48 位。我们的令牌数组最终将如下所示,并且 map 数组将为整个文件重用
tokens = [
// type startIndex
4294967296, // 0000000000000001 00000000000000000000000000000000
8, // 0000000000000000 00000000000000000000000000001000
8589934601, // 0000000000000010 00000000000000000000000000001001
12884901899, // 0000000000000011 00000000000000000000000000001011
12884901900, // 0000000000000011 00000000000000000000000000001100
13, // 0000000000000000 00000000000000000000000000001101
17179869198 // 0000000000000100 00000000000000000000000000001110
];
保留此令牌数组在 Chrome 中占用 104 字节。元素本身应该只占用 56 字节(7 个 64 位数字),其余的可能可以通过 v8 存储与数组相关的其他元数据来解释,或者可能是以 2 的幂分配后备存储。但是,内存节省是显而易见的,并且随着每行令牌数量的增加而变得更好。我们对这种方法感到满意,并且我们一直使用这种表示形式。
注意:可能存在更紧凑的令牌存储方式,但是以二进制可搜索的线性格式存储令牌在内存使用和访问性能方面为我们提供了最佳的权衡。
令牌 <-> 主题匹配
我们认为遵循浏览器最佳实践是个好主意,例如将样式留给 CSS,因此在渲染上述行时,我们将使用 map
解码二进制令牌,然后使用类似以下的令牌类型进行渲染
<span class="token keyword js">function</span>
<span class="token"> </span>
<span class="token identifier js">f1</span>
<span class="token delimiter paren js">(</span>
<span class="token delimiter paren js">)</span>
<span class="token"> </span>
<span class="token delimiter curly js">{</span>
我们将我们的主题 在 CSS 中 编写(例如 Visual Studio 主题)
...
.monaco-editor.vs .token.delimiter { color: #000000; }
.monaco-editor.vs .token.keyword { color: #0000FF; }
.monaco-editor.vs .token.keyword.flow { color: #AF00DB; }
...
结果非常好,我们可以翻转某个地方的类名,立即将新主题应用于编辑器。
TextMate 语法
为了 VS Code 的发布,我们有大约 10 个手工编写的令牌化器,主要用于 Web 语言,这肯定不足以满足通用桌面代码编辑器的需求。输入 TextMate 语法,这是一种描述性的指定令牌化规则的形式,已被许多编辑器采用。但是,有一个问题,TextMate 语法的工作方式与我们手工编写的令牌化器不太一样。
TextMate 语法通过使用 begin/end 状态或 while 状态,可以推送跨越多个令牌的作用域。以下是在 JavaScript TextMate 语法下的相同示例(为简洁起见忽略空格)
VS Code 1.8 中的 TextMate 语法
如果我们通过作用域堆栈进行剖析,则每个令牌基本上都会获得一个作用域名数组,我们将从令牌化器获得类似以下的结果
![Line offsets](/assets/blogs/2017/02/08/line-offsets.png)
tokens = [
{ startIndex: 0, scopes: ['source.js', 'meta.function.js', 'storage.type.function.js'] },
{ startIndex: 8, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 9,
scopes: [
'source.js',
'meta.function.js',
'meta.definition.function.js',
'entity.name.function.js'
]
},
{
startIndex: 11,
scopes: [
'source.js',
'meta.function.js',
'meta.parameters.js',
'punctuation.definition.parameters.js'
]
},
{ startIndex: 13, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 14,
scopes: [
'source.js',
'meta.function.js',
'meta.block.js',
'punctuation.definition.block.js'
]
}
];
所有令牌类型都是字符串,而我们的代码尚未准备好处理字符串数组,更不用说对令牌的二进制编码的影响了。因此,我们继续使用以下策略将作用域数组“近似地”* 表示为单个字符串
- 忽略最不具体的作用域(即
source.js
);它很少增加任何价值。 - 在
"."
上拆分每个剩余作用域。 - 删除重复的唯一片段。
- 使用稳定的排序函数(不一定是词典排序)对剩余片段进行排序。
- 在
"."
上连接片段。
tokens = [
{ startIndex: 0, type: 'meta.function.js.storage.type' },
{ startIndex: 9, type: 'meta.function.js' },
{ startIndex: 9, type: 'meta.function.js.definition.entity.name' },
{ startIndex: 11, type: 'meta.function.js.definition.parameters.punctuation' },
{ startIndex: 13, type: 'meta.function.js' },
{ startIndex: 14, type: 'meta.function.js.definition.punctuation.block' }
];
*: 我们所做的是完全错误的,“近似”是一个非常好的词来形容它:)。
这些令牌随后将“适应”并遵循与手动编写的令牌化器相同的代码路径(进行二进制编码),然后也将以相同的方式渲染
<span class="token meta function js storage type">function</span>
<span class="token meta function js"> </span>
<span class="token meta function js definition entity name">f1</span>
<span class="token meta function js definition parameters punctuation">()</span>
<span class="token meta function js"> </span>
<span class="token meta function js definition punctuation block">{</span>
TextMate 主题
TextMate 主题与 作用域选择器 一起工作,作用域选择器选择具有特定作用域的令牌,并将主题信息(例如颜色、粗体等)应用于它们。
给定一个具有以下作用域的令牌
// C B A
scopes = ['source.js', 'meta.definition.function.js', 'entity.name.function.js'];
以下是一些简单的选择器,它们将匹配,并按其排名(降序)排序
选择器 | C | B | A |
---|---|---|---|
source | source.js | meta.definition.function.js | entity.name.function.js |
source.js | source.js | meta.definition.function.js | entity.name.function.js |
meta | source.js | meta.definition.function.js | entity.name.function.js |
meta.definition | source.js | meta.definition.function.js | entity.name.function.js |
meta.definition.function | source.js | meta.definition.function.js | entity.name.function.js |
entity | source.js | meta.definition.function.js | entity.name.function.js |
entity.name | source.js | meta.definition.function.js | entity.name.function.js |
entity.name.function | source.js | meta.definition.function.js | entity.name.function.js |
entity.name.function.js | source.js | meta.definition.function.js | entity.name.function.js |
观察:
entity
胜过meta.definition.function
,因为它匹配了更具体的作用域(分别是A
超过B
)。
观察:
entity.name
胜过entity
,因为它们都匹配相同的作用域(A
),但是entity.name
比entity
更具体。
父选择器
为了使事情变得更复杂,TextMate 主题还支持父选择器。以下是一些使用简单选择器和父选择器的示例(再次按其排名降序排序)
选择器 | C | B | A |
---|---|---|---|
meta | source.js | meta.definition.function.js | entity.name.function.js |
source meta | source.js | meta.definition.function.js | entity.name.function.js |
source.js meta | source.js | meta.definition.function.js | entity.name.function.js |
meta.definition | source.js | meta.definition.function.js | entity.name.function.js |
source meta.definition | source.js | meta.definition.function.js | entity.name.function.js |
entity | source.js | meta.definition.function.js | entity.name.function.js |
source entity | source.js | meta.definition.function.js | entity.name.function.js |
meta.definition entity | source.js | meta.definition.function.js | entity.name.function.js |
entity.name | source.js | meta.definition.function.js | entity.name.function.js |
source entity.name | source.js | meta.definition.function.js | entity.name.function.js |
观察:
source entity
胜过entity
,因为它们都匹配相同的作用域(A
),但是source entity
也匹配父作用域(C
)。
观察:
entity.name
胜过source entity
,因为它们都匹配相同的作用域(A
),但是entity.name
比entity
更具体。
注意:还有第三种选择器,即涉及排除作用域的选择器,我们在此处不讨论。我们没有添加对此类选择器的支持,并且我们注意到它在野外很少使用。
VS Code 1.8 中的 TextMate 主题
以下是两个 Monokai 主题规则(此处以 JSON 格式表示以求简洁;原始格式为 XML)
...
// Function name
{ "scope": "entity.name.function", "fontStyle": "", "foreground":"#A6E22E" }
...
// Class name
{ "scope": "entity.name.class", "fontStyle": "underline", "foreground":"#A6E22E" }
...
在 VS Code 1.8 中,为了匹配我们的“近似”作用域,我们将生成以下动态 CSS 规则
...
/* Function name */
.entity.name.function { color: #A6E22E; }
...
/* Class name */
.entity.name.class { color: #A6E22E; text-decoration: underline; }
...
然后,我们将留给 CSS 来将“近似”作用域与“近似”规则进行匹配。但是 CSS 匹配规则与 TextMate 选择器匹配规则不同,尤其是在排名方面。CSS 排名基于匹配的类名数量,而 TextMate 选择器排名对作用域特异性有明确的规则。
这就是为什么 VS Code 中的 TextMate 主题看起来还可以,但永远不像作者的意图那样。有时,差异会很小,但有时这些差异会完全改变主题的感觉。
一些星星连成一线
随着时间的推移,我们已经逐步淘汰了我们手工编写的令牌化器(最后一个,用于 HTML,仅在几个月前)。因此,在今天的 VS Code 中,所有文件都使用 TextMate 语法进行令牌化。对于 Monaco Editor,我们已迁移到使用 Monarch(一种描述性令牌化引擎,在本质上类似于 TextMate 语法,但更具表现力,并且可以在浏览器中运行)用于大多数受支持的语言,并且我们为手动令牌化器添加了一个包装器。总而言之,这意味着支持新的令牌化格式将需要更改 3 个令牌提供程序(TextMate、Monarch 和手动包装器),并且不超过 10 个。
几个月前,我们回顾了我们在 VS Code 核心中读取令牌类型的所有代码,我们注意到这些消费者只关心字符串、正则表达式或注释。例如,括号匹配逻辑会忽略包含作用域 "string"
、"comment"
或 "regex"
的令牌。
最近,我们从我们的内部合作伙伴(Microsoft 内部使用 Monaco Editor 的其他团队)那里获得了许可,他们不再需要在 Monaco Editor 中支持 IE9 和 IE10。
可能最重要的是,编辑器最受投票的功能是 minimap 支持。为了在合理的时间内渲染 minimap,我们不能使用 DOM 节点和 CSS 匹配。我们可能会使用 canvas,并且我们将需要知道 JavaScript 中每个令牌的颜色,以便我们可以用正确的颜色绘制这些小字母。
也许我们最大的突破是,我们不需要存储令牌,也不需要存储它们的作用域,因为令牌仅在主题匹配它们或括号匹配跳过字符串方面产生影响。
最后,VS Code 1.9 中的新功能
表示 TextMate 主题
以下是一个非常简单的主题的外观
theme = [
{ "foreground": "#F8F8F2" },
{ "scope": "var", "foreground": "#F8F8F2" },
{ "scope": "var.identifier", "foreground": "#00FF00", "fontStyle": "bold" },
{ "scope": "meta var.identifier", "foreground": "#0000FF" },
{ "scope": "constant", "foreground": "#100000", "fontStyle": "italic" },
{ "scope": "constant.numeric", "foreground": "#200000" },
{ "scope": "constant.numeric.hex", "fontStyle": "bold" },
{ "scope": "constant.numeric.oct", "fontStyle": "underline" },
{ "scope": "constant.numeric.dec", "foreground": "#300000" },
];
加载它时,我们将为主题中出现的每个唯一颜色生成一个 id,并将其存储到颜色映射中(类似于我们上面对令牌类型所做的那样)
// 1 2 3 4 5 6
colorMap = ["reserved", "#F8F8F2", "#00FF00", "#0000FF", "#100000", "#200000", "#300000"]
theme = [
{ "foreground": 1 },
{ "scope": "var", "foreground": 1, },
{ "scope": "var.identifier", "foreground": 2, "fontStyle": "bold" },
{ "scope": "meta var.identifier", "foreground": 3 },
{ "scope": "constant", "foreground": 4, "fontStyle": "italic" },
{ "scope": "constant.numeric", "foreground": 5 },
{ "scope": "constant.numeric.hex", "fontStyle": "bold" },
{ "scope": "constant.numeric.oct", "fontStyle": "underline" },
{ "scope": "constant.numeric.dec", "foreground": 6 },
];
然后,我们将从主题规则中生成一个 Trie 数据结构,其中每个节点都保存已解析的主题选项
![Theme Trie](/assets/blogs/2017/02/08/trie.png)
观察:
constant.numeric.hex
和constant.numeric.oct
的节点包含将前景色更改为5
的指令,因为它们从constant.numeric
继承了此指令。
观察:
var.identifier
的节点保存着额外的父规则meta var.identifier
,并将相应地回答查询。
当我们想知道如何主题化作用域时,我们可以查询这个 trie。
例如
查询 | 结果 |
---|---|
constant | 将前景色设置为 4,字体样式设置为 italic |
constant.numeric | 将前景色设置为 5,字体样式设置为 italic |
constant.numeric.hex | 将前景色设置为 5,字体样式设置为 bold |
var | 将前景色设置为 1 |
var.baz | 将前景色设置为 1(匹配 var) |
baz | 不执行任何操作(不匹配) |
var.identifier | 如果存在父作用域 meta,则将前景色设置为 3,字体样式设置为 bold, 否则,将前景色设置为 2,字体样式设置为 bold |
令牌化的更改
VS Code 中使用的所有 TextMate 令牌化代码都位于一个单独的项目 vscode-textmate 中,该项目可以独立于 VS Code 使用。我们更改了在 vscode-textmate
中表示作用域堆栈的方式,使其成为 不可变的链表,该链表还存储完全解析的 metadata
。
当将新作用域推送到作用域堆栈上时,我们将在主题 trie 中查找新作用域。然后,我们可以根据我们从作用域堆栈继承的内容以及主题 trie 返回的内容,立即计算作用域列表的完全解析的所需前景色或字体样式。
一些示例
作用域堆栈 | 元数据 |
---|---|
["source.js"] | 前景色为 1,字体样式为常规(没有作用域选择器的默认规则) |
["source.js","constant"] | 前景色为 4,字体样式为 italic |
["source.js","constant","baz"] | 前景色为 4,字体样式为 italic |
["source.js","var.identifier"] | 前景色为 2,字体样式为 bold |
["source.js","meta","var.identifier"] | 前景色为 3,字体样式为 bold |
当从作用域堆栈中弹出时,无需计算任何内容,因为我们可以只使用与先前作用域列表元素一起存储的元数据。
这是表示作用域列表中元素的 TypeScript 类
export class ScopeListElement {
public readonly parent: ScopeListElement;
public readonly scope: string;
public readonly metadata: number;
...
}
我们存储 32 位元数据
/**
* - -------------------------------------------
* 3322 2222 2222 1111 1111 1100 0000 0000
* 1098 7654 3210 9876 5432 1098 7654 3210
* - -------------------------------------------
* xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
* bbbb bbbb bfff ffff ffFF FTTT LLLL LLLL
* - -------------------------------------------
* - L = LanguageId (8 bits)
* - T = StandardTokenType (3 bits)
* - F = FontStyle (3 bits)
* - f = foreground color (9 bits)
* - b = background color (9 bits)
*/
最后,令牌不再从令牌化引擎作为对象发出
// These are generated using the Monokai theme.
tokens_before = [
{ startIndex: 0, scopes: ['source.js', 'meta.function.js', 'storage.type.function.js'] },
{ startIndex: 8, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 9,
scopes: [
'source.js',
'meta.function.js',
'meta.definition.function.js',
'entity.name.function.js'
]
},
{
startIndex: 11,
scopes: [
'source.js',
'meta.function.js',
'meta.parameters.js',
'punctuation.definition.parameters.js'
]
},
{ startIndex: 13, scopes: ['source.js', 'meta.function.js'] },
{
startIndex: 14,
scopes: [
'source.js',
'meta.function.js',
'meta.block.js',
'punctuation.definition.block.js'
]
}
];
// Every even index is the token start index, every odd index is the token metadata.
// We get fewer tokens because tokens with the same metadata get collapsed
tokens_now = [
// bbbbbbbbb fffffffff FFF TTT LLLLLLLL
0,
16926743, // 000000010 000001001 001 000 00010111
8,
16793623, // 000000010 000000001 000 000 00010111
9,
16859159, // 000000010 000000101 000 000 00010111
11,
16793623 // 000000010 000000001 000 000 00010111
];
它们使用以下方式渲染
<span class="mtk9 mtki">function</span>
<span class="mtk1"> </span>
<span class="mtk5">f1</span>
<span class="mtk1">() {</span>
令牌直接从令牌化器作为 Uint32Array 返回。我们保留后备 ArrayBuffer,对于上面的示例,它在 Chrome 中占用 96 字节。元素本身应该只占用 32 字节(8 个 32 位数字),但同样,我们可能观察到了一些 v8 元数据开销。
一些数字
为了获得以下测量结果,我选择了三个具有不同特性和不同语法的文件
文件名 | 文件大小 | 行数 | 语言 | 观察 |
---|---|---|---|---|
checker.ts | 1.18 MB | 22,253 | TypeScript | TypeScript 编译器中使用的实际源文件 |
bootstrap.min.css | 118.36 KB | 12 | CSS | 最小化的 CSS 文件 |
sqlite3.c | 6.73 MB | 200,904 | C | SQLite 的串联分发文件 |
我在 Windows 上的一台功能强大的台式机上(使用 32 位 Electron)运行了测试。
我不得不对源代码进行一些更改,以便将苹果与苹果进行比较,例如确保在两个 VS Code 版本中使用完全相同的语法,在两个版本中都关闭丰富的语言功能,或者取消 VS Code 1.8 中的 100 堆栈深度限制,该限制在 VS Code 1.9 中不再存在等等。我还必须将 bootstrap.min.css 拆分为多行,以使每行都在 20k 个字符以下。
令牌化时间
令牌化以产生式的方式在 UI 线程上运行,因此我必须添加一些代码以强制其同步运行,以便测量以下时间(显示 10 次运行的中位数)
文件名 | 文件大小 | VS Code 1.8 | VS Code 1.9 | 加速 |
---|---|---|---|---|
checker.ts | 1.18 MB | 4606.80 毫秒 | 3939.00 毫秒 | 14.50% |
bootstrap.min.css | 118.36 KB | 776.76 毫秒 | 416.28 毫秒 | 46.41% |
sqlite3.c | 6.73 MB | 16010.42 毫秒 | 10964.42 毫秒 | 31.52% |
![Tokenization times](/assets/blogs/2017/02/08/tokenization-times.png)
尽管令牌化现在也进行了主题匹配,但节省的时间可以用对每一行进行单次遍历来解释。而在以前,将有一个令牌化过程,一个辅助过程将作用域“近似”为字符串,以及第三个过程将令牌进行二进制编码,而现在令牌是从 TextMate 令牌化引擎直接以二进制编码方式生成的。需要垃圾回收的生成对象数量也已大大减少。
内存使用量
折叠消耗大量内存,尤其是对于大型文件(这是另一个时间的优化),因此我收集了以下关闭折叠的堆快照数字。这显示了模型持有的内存,不包括原始文件字符串
文件名 | 文件大小 | VS Code 1.8 | VS Code 1.9 | 内存节省 |
---|---|---|---|---|
checker.ts | 1.18 MB | 3.37 MB | 2.61 MB | 22.60% |
bootstrap.min.css | 118.36 KB | 267.00 KB | 201.33 KB | 24.60% |
sqlite3.c | 6.73 MB | 27.49 MB | 21.22 MB | 22.83% |
![Memory usage](/assets/blogs/2017/02/08/tokenization-memory.png)
减少的内存使用量可以用不再保留令牌映射、具有相同元数据的连续令牌的折叠以及使用
ArrayBuffer
作为后备存储来解释。我们可以通过始终将仅包含空格的令牌折叠到前一个令牌中来进一步改进此处,因为空格渲染什么颜色都无关紧要(空格是不可见的)。
新的 TextMate 作用域检查器小部件
我们添加了一个新的小部件来帮助创作和调试主题或语法:您可以使用开发人员:检查编辑器令牌和作用域在命令面板中运行它 (⇧⌘P (Windows, Linux Ctrl+Shift+P))。
验证更改
在此编辑器组件中进行更改会带来一些严重的风险,因为我们方法中的任何错误(在新 trie 创建代码、新的二进制编码格式等中)都可能导致用户可见的巨大差异。
在 VS Code 中,我们有一个集成套件,用于断言我们发布的五种主题(Light、Light+、Dark、Dark+、High Contrast)中所有编程语言的颜色。当对我们的主题之一进行更改以及更新特定语法时,这些测试都非常有帮助。73 个集成测试中的每一个都包含一个 fixture 文件(例如 test.c)和五种主题的预期颜色 (test_c.json),并且它们在我们 CI 构建 的每次提交时运行。
为了验证令牌化更改,我们使用旧的基于 CSS 的方法从这些测试中收集了颜色化结果,涵盖了我们发布的 14 个主题(不仅仅是我们编写的五个主题)。然后,在每次更改之后,我们使用新的基于 trie 的逻辑运行相同的测试,并使用自定义构建的可视化差异(和补丁)工具,我们将查看每个颜色差异并找出颜色更改的根本原因。我们使用这种技术至少捕获了 2 个错误,并且我们能够更改我们的五个主题,以在 VS Code 版本之间获得最小的颜色更改
之前和之后
以下是各种颜色主题在 VS Code 1.8 和现在的 VS Code 1.9 中的外观
Monokai 主题
Quiet Light 主题
Red Theme
结论
我希望您会欣赏从升级到 VS Code 1.9 中获得的额外 CPU 时间和 RAM,并且我们可以继续帮助您以高效和愉快的方式进行编码。
编码愉快!
Alexandru Dima,VS Code 团队成员 @alexdima123