关于前端:你不知道的-VSCode-代码高亮原理

45次阅读

共计 13358 个字符,预计需要花费 34 分钟才能阅读完成。

全文 5000 字,解读 vscode 背地的代码高亮实现原理,欢送点赞关注转发。

Vscode 的代码高亮、代码补齐、错误诊断、跳转定义等语言性能由两种扩大计划协同实现,包含:

  • 基于词法剖析技术,辨认分词 token 并利用高亮款式
  • 基于可编程语言个性接口,辨认代码语义并利用高亮款式,此外还能实现错误诊断、智能提醒、格式化等性能

两种计划的性能领域逐级递增,相应地技术复杂度与实现老本也逐级升高,本文将概要介绍两种计划的工作过程与特点,各自实现什么工作,相互这么写作,并结合实际案例一步步揭开 vscode 代码高亮性能的实现原理:

Vscode 插件根底

介绍 vscode 代码高亮原理之前,有必要先相熟一下 vscode 的底层架构。与 Webpack 类似,vscode 自身只是实现了一套架子,架子外部的命令、款式、状态、调试等性能都以插件模式提供,vscode 对外提供了五种拓展能力:

其中,代码高亮性能由 语言扩大 类插件实现,依据实现形式又能够细分为:

  • 申明式:以特定 JSON 构造申明一堆匹配词法的正则,无需编写逻辑代码即可增加如块级匹配、主动缩进、语法高亮等语言个性,vscode 内置的 extendsions/css、extendsions/html 等插件都是基于申明式接口实现的
  • 编程式:vscode 运行过程中会监听用户行为,在特定行为产生后触发事件回调,编程式语言扩大须要监听这些事件,动态分析文本内容并按特定格局返回代码信息

申明式性能高,能力弱;编程式性能低,能力强。语言插件开发者通常能够混用,用申明式接口在最短时间内辨认出词法 token,提供根本的语法高亮性能;之后用编程式接口动态分析内容,提供更高级个性比方错误诊断、智能提醒等。

Vscode 中的申明式语言扩大基于 TextMate 词法剖析引擎实现;编程式语言扩大则基于语义剖析接口、vscode.language.* 接口、Language Server Protocol 协定三种形式实现,上面开展介绍每种技术计划的根本逻辑。

词法高亮

词法剖析 (Lexical Analysis) 是计算机学科中将字符序列转换为 标记 (token) 序列的过程,而 标记(token) 是形成源代码的最小单位,词法剖析技术在编译、IDE 等畛域有十分宽泛的利用。

比方 vscode 的词法引擎剖析出 token 序列后再依据 token 的类型利用高亮款式,这个过程能够简略划分为分词、款式利用两个步骤。

参考资料:

  • https://macromates.com/manual…\_grammars
  • https://code.visualstudio.com…

分词

分词过程实质上将一长串代码递归地拆解为具备特定含意、分类的字符串片段,比方 +-*/% 等操作符;var/const 等关键字;1234"tecvan" 类型的常量值等,简略说就是从一段文本中辨认出,什么中央有一个什么词。

Vscode 的词法剖析基于 TextMate 引擎实现,性能比较复杂,能够简略划分为三个方面:基于正则的分词、复合分词规定、嵌套分词规定。

根本规定

Vscode 底层的 TextMate 引擎基于 正则 匹配实现分词性能,运行时逐行扫描文本内容,用预约义的 rule 汇合测试文本行中是否蕴含匹配特定正则的内容,例如对于上面的规定配置:

{
    "patterns": [
        {
            "name": "keyword.control",
            "match": "\b(if|while|for|return)\b"
        }
    ]
}

示例中,patterns 用于定义规定汇合,match 属性定于用于匹配 token 的正则,name 属性申明该 token 的分类(scope),TextMate 分词过程遇到匹配 match 正则的内容时,会将其看作独自 token 解决并分类为 name 申明的 keyword.control 类型。

上述示例会将 if/while/for/return 关键词辨认为 keyword.control 类型,但无奈辨认其它关键字:

在 TextMate 语境中,scope 是一种 . 宰割的层级构造,例如 keywordkeyword.control 造成父子层级,这种层级构造在款式解决逻辑中能实现一种相似 css 选择器的匹配,前面会讲到细节。

复合分词

上述示例配置对象在 TextMate 语境下被称作 Language Rule,除了 match 用于匹配单行内容,还能够应用 begin + end 属性对匹配更简单的跨行场景。从 beginend 所辨认到的范畴内,都认为是 name 类型的 token,比方在 vuejs/vetur 插件的 syntaxes/vue.tmLanguage.json 文件中有这么一段配置:

{
    "name": "Vue",
    "scopeName": "source.vue",
    "patterns": [
        {"begin": "(<)(style)(?![^/>]*/>\\s*$)",
          // 虚构字段,不便解释
          "name": "tag.style.vue",
          "beginCaptures": {
            "1": {"name": "punctuation.definition.tag.begin.html"},
            "2": {"name": "entity.name.tag.style.html"}
          },
          "end": "(</)(style)(>)",
          "endCaptures": {
            "1": {"name": "punctuation.definition.tag.begin.html"},
            "2": {"name": "entity.name.tag.style.html"},
            "3": {"name": "punctuation.definition.tag.end.html"}
          }
        }
    ]
}

配置中,begin 用于匹配 <style> 语句,end 用于匹配 </style> 语句,且 <style></style> 整个语句被赋予 scope 为 tag.style.vue。此外,语句中字符被 beginCapturesendCaptures 属性调配成不同的 scope 类型:

这里从 beginbeginCaptures,从 endendCaptures 造成了某种程度的复合构造,从而实现一次匹配多行内容。

规定嵌套

在上述 begin + end 根底上,TextMate 还反对以子 patterns 形式定义嵌套的语言规定,例如:

{
    "name": "lng",
    "patterns": [
        {
            "begin": "^lng`",
            "end": "`",
            "name": "tecvan.lng.outline",
            "patterns": [
                {
                    "match": "tec",
                    "name": "tecvan.lng.prefix"
                },
                {
                    "match": "van",
                    "name": "tecvan.lng.name"
                }
            ]
        }
    ],
    "scopeName": "tecvan"
}

配置辨认 lng` ` 之间的字符串,并分类为 tecvan.lng.outline。之后,递归解决两者之间的内容并依照子 patterns 规定匹配出更具体的 token,例如对于:

lng`awesome tecvan`

可辨认出分词:

  • lng`awesome tecvan` ,scope 为 tecvan.lng.outline
  • tec,scope 为 tecvan.lng.prefix
  • van,scope 为 tecvan.lng.name

TextMate 还反对语言级别的嵌套,例如:

{
    "name": "lng",
    "patterns": [
        {
            "begin": "^lng`",
            "end": "`",
            "name": "tecvan.lng.outline",
            "contentName": "source.js"
        }
    ],
    "scopeName": "tecvan"
}

基于上述配置,lng` ` 之间的内容都会辨认为 contentName 指定的 source.js 语句。

款式

词法高亮实质上就是先按上述规定将原始文本拆解成多个具类的 token 序列,之后依照 token 的类型适配不同的款式。TextMate 在分词根底上提供了一套依照 token 类型字段 scope 配置款式的性能构造,例如:

{
    "tokenColors": [
        {
            "scope": "tecvan",
            "settings": {"foreground": "#eee"}
        },
        {
            "scope": "tecvan.lng.prefix",
            "settings": {"foreground": "#F44747"}
        },
        {
            "scope": "tecvan.lng.name",
            "settings": {"foreground": "#007acc",}
        }
    ]
}

示例中,scope 属性反对一种被称作 Scope Selectors 的匹配模式,这种模式与 css 选择器相似,反对:

  • 元素抉择,例如 scope = tecvan.lng.prefix 可能匹配 tecvan.lng.prefix 类型的 token;特地的 scope = tecvan 可能匹配 tecvan.lngtecvan.lng.prefix 等子类型的 token
  • 后辈抉择,例如 scope = text.html source.js 用于匹配 html 文档中的 JavaScript 代码
  • 分组抉择,例如 scope = string, comment 用于匹配字符串或备注

插件开发者能够自定义 scope 也能够抉择复用 TextMate 内置的许多 scope,包含 comment、constant、entity、invalid、keyword 等,残缺列表请查阅 官网。

settings 属性则用于设置该 token 的体现款式,反对 foreground、background、bold、italic、underline 等款式属性。

实例解析

看完原理咱们来拆解一个理论案例:https://github.com/mrmlnc/vsc…,json5 是 JSON 扩大协定,旨在使人类更易于手动编写和保护,反对备注、单引号、十六进制数字等个性,这些拓展个性须要应用 vscode-json5 插件实现高亮成果:

上图中,右边是没有启动 vscode-json5 的成果,左边是启动后的成果。

vscode-json5 插件源码很简略,两个关键点:

  • package.json 文件中申明插件的 contributes 属性,能够了解为插件的入口:
  "contributes": {
    // 语言配置
    "languages": [{
      "id": "json5",
      "aliases": ["JSON5", "json5"],
      "extensions": [".json5"],
      "configuration": "./json5.configuration.json"
    }],
    // 语法配置
    "grammars": [{
      "language": "json5",
      "scopeName": "source.json5",
      "path": "./syntaxes/json5.json"
    }]
  }
  • 在语法配置文件 ./syntaxes/json5.json 中依照 TextMate 的要求定义 Language Rule:
{
    "scopeName": "source.json5",
    "fileTypes": ["json5"],
    "name": "JSON5",
    "patterns": [{ "include": "#array"},
        {"include": "#constant"}
        // ...
    ],
    "repository": {
        "array": {
            "begin": "\\[",
            "beginCaptures": {"0": { "name": "punctuation.definition.array.begin.json5"}
            },
            "end": "\\]",
            "endCaptures": {"0": { "name": "punctuation.definition.array.end.json5"}
            },
            "name": "meta.structure.array.json5"
            // ...
        },
        "constant": {"match": "\\b(?:true|false|null|Infinity|NaN)\\b",
            "name": "constant.language.json5"
        } 
        // ...
    }
}

OK,完结了,没了,就是这么简略,之后 vscode 就能够依据这份配置适配 json5 的语法高亮规定。

调试工具

Vscode 内置了一套 scope inspect 工具,用于调试 TextMate 检测出的 token、scope 信息,应用时只须要将编辑器光标 focus 到特定 token 上,快捷键 ctrl + shift + p 关上 vscode 命令面板后输入 Developer: Inspect Editor Tokens and Scopes 命令并回车:

命令运行后就能够看到分词 token 的语言、scope、款式等信息。

编程式语言扩大

词法剖析引擎 TextMate 实质上是一种基于正则的动态词法分析器,长处是接入形式标准化,成本低且运行效率较高,毛病是动态代码剖析很难实现某些上下文相干的 IDE 性能,例如对于上面的代码:

留神代码第一行函数参数 languageModes 与第二行函数体内的 languageModes 是同一实体然而没有实现雷同的款式,视觉上没有造成联动。

为此,vscode 在 TextMate 引擎之外提供了三种更弱小也更简单的语言个性扩大机制:

  • 应用 DocumentSemanticTokensProvider 实现可编程的语义剖析
  • 应用 vscode.languages.* 下的接口监听各类编程行为事件,在特定工夫节点实现语义剖析
  • 依据 Language Server Protocol 协定实现一套齐备的语言个性剖析服务器

相比于下面介绍的申明式的词法高亮,语言个性接口更灵便,可能实现诸如错误诊断、候选词、智能提醒、定义跳转等高级性能。

参考资料:

  • https://code.visualstudio.com…
  • https://code.visualstudio.com…
  • https://code.visualstudio.com…

DocumentSemanticTokensProvider 分词

简介

Sematic Tokens Provider 是 vscode 内置的一种对象协定,它须要自行扫描代码文件内容,而后以整数数组模式返回语义 token 序列,通知 vscode 在文件的哪一行、那一列、多长的区间内是一个什么类型的 token。

留神辨别一下,TextMate 中的扫描是引擎驱动的,逐行匹配正则,而 Sematic Tokens Provider 场景下扫描规定、匹配规定都交由插件开发者自行实现,灵活性加强但绝对的开发成本也会更高。

实现上,Sematic Tokens Providervscode.DocumentSemanticTokensProvider 接口定义,开发者能够按需实现两个办法:

  • provideDocumentSemanticTokens:全量分析代码文件语义
  • provideDocumentSemanticTokensEdits:增量剖析正在编辑模块的语义

咱们来看个残缺的示例:

import * as vscode from 'vscode';

const tokenTypes = ['class', 'interface', 'enum', 'function', 'variable'];
const tokenModifiers = ['declaration', 'documentation'];
const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers);

const provider: vscode.DocumentSemanticTokensProvider = {
  provideDocumentSemanticTokens(document: vscode.TextDocument): vscode.ProviderResult<vscode.SemanticTokens> {const tokensBuilder = new vscode.SemanticTokensBuilder(legend);
    tokensBuilder.push(new vscode.Range(new vscode.Position(0, 3), new vscode.Position(0, 8)),
      tokenTypes[0],
      [tokenModifiers[0]]
    );
    return tokensBuilder.build();}
};

const selector = {language: 'javascript', scheme: 'file'};

vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend);

置信大多数读者对这段代码都会感觉生疏,我想了很久,感觉还是从函数输入的角度开始讲起比拟容易了解,也就是上例代码第 17 行 tokensBuilder.build()

输入构造

provideDocumentSemanticTokens 函数要求返回一个整数数组,数组项按 5 位为一组别离示意:

  • 5 * i 位,token 所在行绝对于上一个 token 的偏移
  • 5 * i + 1 位,token 所在列绝对于上一个 token 的偏移
  • 5 * i + 2 位,token 长度
  • 5 * i + 3 位,token 的 type 值
  • 5 * i + 4 位,token 的 modifier 值

咱们须要了解这是一个地位强相干的整数数组,数组中每 5 个项形容一个 token 的地位、类型。token 地位由所在行、列、长度三个数字组成,而为了压缩数据的大小 vscode 无意设计成绝对位移的模式,例如对于这样的代码:

const name as

如果只是简略地按空格宰割,那么这里能够解析出三个 token:constnameas,对应的形容数组为:

[
// 对应第一个 token:const
0, 0, 5, x, x,
// 对应第二个 token:name
0, 6, 4, x, x,
// 第三个 token:as
0, 5, 2, x, x
]

留神这里是以绝对前一个 token 地位的模式形容的,比方 as 字符对应的 5 个数字的语义为:绝对前一个 token 偏移 0 行、5 列,长度为 2,类型为 xx。

剩下的第 5 * i + 3 位与第 5 * i + 4 位别离形容 token 的 type 与 modifier,其中 type 批示 token 的类型,例如 comment、class、function、namespace 等等;modifier 是类型根底上的润饰器,能够近似了解为子类型,比方对于 class 有可能是 abstract 的,也有可能是从规范库导出 defaultLibrary。

type、modifier 的具体数值须要开发者自行定义,例如上例中:

const tokenTypes = ['class', 'interface', 'enum', 'function', 'variable'];
const tokenModifiers = ['declaration', 'documentation'];
const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers);

// ...

vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend);

首先通过 vscode. SemanticTokensLegend 类构建 type、modifier 的外部示意 legend 对象,之后应用 vscode.languages.registerDocumentSemanticTokensProvider 接口与 provider 一起注册到 vscode 中。

语义剖析

上例中 provider 的次要作用就是遍历剖析文件内容,返回合乎上述规定的整数数组,vscode 对具体的分析方法并没有做限定,只是提供了用于构建 token 形容数组的工具 SemanticTokensBuilder,例如上例中:

const provider: vscode.DocumentSemanticTokensProvider = {
  provideDocumentSemanticTokens(document: vscode.TextDocument): vscode.ProviderResult<vscode.SemanticTokens> {const tokensBuilder = new vscode.SemanticTokensBuilder(legend);
    tokensBuilder.push(new vscode.Range(new vscode.Position(0, 3), new vscode.Position(0, 8)),
      tokenTypes[0],
      [tokenModifiers[0]]
    );
    return tokensBuilder.build();}
};

代码应用 SemanticTokensBuilder 接口构建并返回了一个 [0, 3, 5, 0, 0] 的数组,即第 0 行,第 3 列,长度为 5 的字符串,type =0,modifier = 0,运行成果:

除了这一段被辨认出的 token 外,其它字符都被认为不可辨认。

小结

实质上,DocumentSemanticTokensProvider 只是提供了一套毛糙的 IOC 接口,开发者能做的事件比拟无限,所以当初大多数插件都没有采纳这种计划,读者了解即可,不用深究。

Language API

简介

相对而言,vscode.languages.* 系列 API 所提供的语言扩大能力可能更合乎前端开发者的思维习惯。vscode.languages.* 托管了一系列用户交互行为的解决、归类逻辑,并以事件接口方式凋谢进去,插件开发者只需监听这些事件,依据参数推断语言个性,并按规定返回后果即可。

Vscode Language API 提供了很多事件接口,比如说:

  • registerCompletionItemProvider:提供代码补齐提醒

  • registerHoverProvider:光标停留在 token 上时触发

  • registerSignatureHelpProvider:提供函数签名提醒

残缺的列表请查阅 https://code.visualstudio.com… 一文。

Hover 示例

Hover 性能实现分两步,首先须要在 package.json 中申明 hover 个性:

{
    ...
    "main": "out/extensions.js",
    "capabilities" : {
        "hoverProvider" : "true",
        ...
    }
}

之后,须要在 activate 函数中调用 registerHoverProvider 注册 hover 回调:

export function activate(ctx: vscode.ExtensionContext): void {
    ...
    vscode.languages.registerHoverProvider('language name', {provideHover(document, position, token) {return { contents: ['aweome tecvan'] };
        }
    });
    ...
}

运行后果:

其它个性性能的写法与此类似,感兴趣的同学倡议到官网自行查阅。

Language Server Protocol

简介

上述基于语言扩大插件的代码高亮办法有一个类似的问题:难以在编辑器间复用,同一个语言,须要依据编辑器环境、语言反复编写性能类似的反对插件,那么对于 n 种语言,m 中编辑器,这外面的开发成本就是 n * m

为了解决这个问题,微软提出了一种叫做 Language Server Protocol 的标准协议,语言性能插件与编辑器之间不再间接通信,而是通过 LSP 做一层隔离:

减少 LSP 层带来两个益处:

  • LSP 层的开发语言、环境等与具体 IDE 所提供的 host 环境脱耦
  • 语言插件的外围性能只须要编写一次,就能够复用到反对 LSP 协定的 IDE 中

尽管 LSP 与上述 Language API 能力上简直雷同,但借助这两个长处大大晋升了插件的开发效率,目前很多 vscode 语言类插件都曾经迁徙到 LSP 实现,包含 vetur、eslint、Python for VSCode 等出名插件。

Vscode 中的 LSP 架构蕴含两局部:

  • Language Client: 一个规范 vscode 插件,实现与 vscode 环境的交互,例如 hover 事件首先会传递到 client,再由 client 传递到背地的 server
  • Language Server: 语言个性的外围实现,通过 LSP 协定与 Language Client 通信,留神 Server 实例会以独自过程形式运行

做个类比,LSP 就是通过架构优化的 Language API,原来由单个 provider 函数实现的性能拆解为 Client + Server 两端跨语言架构,Client 与 vscode 交互并实现申请转发;Server 执行代码剖析动作,并提供高亮、补全、提醒等性能,如下图:

简略示例

LSP 略微有一点点简单,倡议读者先拉下 vscode 官网示例比照学习:

git clone https://github.com/microsoft/vscode-extension-samples.git
cd vscode-extension-samples/lsp-sample
yarn
yarn compile
code .

vscode-extension-samples/lsp-sample 的次要代码文件有:

.
├── client // Language Client
│   ├── src
│   │   └── extension.ts // Language Client 入口文件
├── package.json 
└── server // Language Server
    └── src
        └── server.ts // Language Server 入口文件

样例代码中有几个关键点:

  1. package.json 中申明激活条件与插件入口
  2. 编写入口文件 client/src/extension.ts,启动 LSP 服务
  3. 编写 LSP 服务即 server/src/server.ts,实现 LSP 协定

逻辑上,vscode 会在加载插件时依据 package.json 的配置判断激活条件,之后加载、运行插件入口,启动 LSP 服务器。插件启动后,后续用户在 vscode 的交互行为会以规范事件,如 hover、completion、signature help 等形式触发插件的 client,client 再依照 LSP 协定转发到 server 层。

上面咱们拆开看看三个模块的细节。

入口配置

示例 vscode-extension-samples/lsp-sample 中的 package.json 有两个要害配置:

{
    "activationEvents": ["onLanguage:plaintext"],
    "main": "./client/out/extension",
}

其中:

  • activationEvents:申明插件的激活条件,代码中的 onLanguage:plaintext 意为关上 txt 文本文件时激活
  • main:插件的入口文件

Client 样例

示例 vscode-extension-samples/lsp-sample 中的 Client 入口代码,要害局部如下:

export function activate(context: ExtensionContext) {
    // Server 配置信息
    const serverOptions: ServerOptions = {
        run: { 
            // Server 模块的入口文件
            module: context.asAbsolutePath(path.join('server', 'out', 'server.js')
            ), 
            // 通信协定,反对 stdio、ipc、pipe、socket
            transport: TransportKind.ipc 
        },
    };

    // Client 配置
    const clientOptions: LanguageClientOptions = {
        // 与 packages.json 文件的 activationEvents 相似
        // 插件的激活条件
        documentSelector: [{scheme: 'file', language: 'plaintext'}],
        // ...
    };

    // 应用 Server、Client 配置创立代理对象
    const client = new LanguageClient(
        'languageServerExample',
        'Language Server Example',
        serverOptions,
        clientOptions
    );

    client.start();}

代码脉络很清晰,先是定义 Server、Client 配置对象,之后创立并启动了 LanguageClient 实例。从实例能够看到,Client 这一层能够做的很薄,在 Node 环境下大部分转发逻辑都被封装在 LanguageClient 类中,开发者无需关怀细节。

Server 样例

示例 vscode-extension-samples/lsp-sample 中的 Server 代码实现了错误诊断、代码补全性能,作为学习样例来说稍显简单,所以我只摘抄出错误诊断局部的代码:

// Server 层所有通信都应用 createConnection 创立的 connection 对象实现
const connection = createConnection(ProposedFeatures.all);

// 文档对象管理器,提供文档操作、监听接口
// 匹配 Client 激活规定的文档对象都会主动增加到 documents 对象中
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

// 监听文档内容变更事件
documents.onDidChangeContent(change => {validateTextDocument(change.document);
});

// 校验
async function validateTextDocument(textDocument: TextDocument): Promise<void> {const text = textDocument.getText();
    // 匹配全大写的单词
    const pattern = /\b[A-Z]{2,}\b/g;
    let m: RegExpExecArray | null;

    // 这里判断,如果一个单词外面全都是大写字符,则报错
    const diagnostics: Diagnostic[] = [];
    while ((m = pattern.exec(text))) {
        const diagnostic: Diagnostic = {
            severity: DiagnosticSeverity.Warning,
            range: {start: textDocument.positionAt(m.index),
                end: textDocument.positionAt(m.index + m[0].length)
            },
            message: `${m[0]} is all uppercase.`,
            source: 'ex'
        };
        diagnostics.push(diagnostic);
    }

    // 发送错误诊断信息
    // vscode 会主动实现谬误提醒渲染
    connection.sendDiagnostics({uri: textDocument.uri, diagnostics});
}

LSP Server 代码的次要流程:

  • 调用 createConnection 建设与 vscode 主过程的通信链路,后续所有的信息交互都基于 connection 对象实现。
  • 创立 documents 对象,并依据须要监听文档事件如上例中的 onDidChangeContent
  • 在事件回调中剖析代码内容,依据语言规定返回错误诊断信息,例如示例中应用正则判断单词是否全副为大写字母,是的话应用 connection.sendDiagnostics 接口发送谬误提示信息

运行成果:

小结

通览样例代码,LSP 客户端服务器之间的通信过程都曾经封装在 LanguageClientconnection 等对象中,插件开发者并不需要关怀底层实现细节,也不须要深刻了解 LSP 协定即可基于这些对象裸露的接口、事件等实现简略的代码高亮成果。

总结

Vscode 用插件形式提供了多种语言扩大接口,分申明式、编程式两类,在理论我的项目中通常会混合应用这两种技术,用基于 TextMate 的申明式接口迅速辨认出代码中的词法;再用编程式接口如 LSP 补充提供诸如谬误提醒、代码补齐、跳转定义等高级性能。

这段时间看了不少开源 vscode 插件,其中 Vue 官网提供的 Vetur 插件学习是这方面的典型案例,学习价值极高,倡议对这方面有趣味的读者能够自行返回剖析学习 vscode 语言扩大类插件的写法。

正文完
 0