>개발 도구 >VSCode >VSCode 코드 강조 원칙에 대한 심층 분석

VSCode 코드 강조 원칙에 대한 심층 분석

青灯夜游
青灯夜游앞으로
2021-06-08 11:12:243707검색

이 기사에서는 VSCode코드 강조 원칙에 대한 심층 분석을 제공합니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.

VSCode 코드 강조 원칙에 대한 심층 분석

전체 텍스트는 5,000단어로, vscode의 구현 원리를 강조하는 코드를 설명합니다. 좋아요, 팔로우 및 전달을 환영합니다.

코드 강조, 코드 완성, 오류 진단 및 점프 정의와 같은 Vscode의 언어 기능은 다음을 포함한 두 가지 확장 솔루션에 의해 공동으로 구현됩니다.

  • 어휘 분석 기술을 기반으로 단어 분할 토큰 식별 및 강조 스타일 적용
  • 기반 프로그래밍 가능한 언어 기능 인터페이스에서 코드 의미를 인식하고 강조 스타일을 적용합니다. 또한 오류 진단, 지능형 프롬프트, 서식 지정 및 기타 기능도 실현할 수 있습니다

[추천 학습: "vscode tutorial"]

두 가지. 솔루션 기능 범위가 단계적으로 증가하고 이에 따라 기술적 복잡성과 구현 비용도 단계적으로 증가합니다. 이 기사에서는 두 솔루션의 작업 프로세스와 특성, 각각 수행하는 작업을 간략하게 소개하고 서로를 이렇게 작성합니다. 실제 사례를 바탕으로 단계별로 공개합니다. vscode 코드 하이라이팅 기능의 구현 원리:

VSCode 코드 강조 원칙에 대한 심층 분석

Vscode 플러그인 기본

vscode 코드 하이라이팅의 원리를 소개하기 전에 먼저 vscode의 기본 아키텍처. Webpack과 마찬가지로 vscode 자체는 쉘프 세트만 구현합니다. 쉘프 내부의 명령, 스타일, 상태, 디버깅 및 기타 기능은 모두 플러그인 형태로 제공됩니다.

VSCode 코드 강조 원칙에 대한 심층 분석

그중 코드 강조 기능은 언어 확장 클래스 플러그인으로 구현되며 구현 방법에 따라 세분화될 수 있습니다.

  • 선언적: 특정 JSON 구조에서 일련의 정규 일치 어휘 규칙을 선언합니다. , 논리 코드를 작성하지 않고도 블록 수준 일치를 추가할 수 있으며 자동 들여쓰기, 구문 강조 및 기타 언어 기능, vscode의 내장 확장/css, 확장/html 및 기타 플러그인은 모두 선언적 인터페이스를 기반으로 구현됩니다
  • 프로그래밍 방식: vscode는 특정 작업 중에 사용자 동작을 모니터링합니다. 이벤트 콜백은 동작이 발생한 후 트리거됩니다. 프로그래밍 언어 확장은 이러한 이벤트를 모니터링하고 텍스트 내용을 동적으로 분석하고 특정 형식으로 코드 정보를 반환해야 합니다. 선언적 성능이 높습니다. , 능력은 약하지만 프로그래밍 성능은 낮지만 능력은 강합니다. 언어 플러그인 개발자는 일반적으로 선언적 인터페이스를 사용하여 가능한 가장 짧은 시간에 어휘 토큰을 식별하고 기본 구문 강조 기능을 제공한 다음 프로그래밍 인터페이스를 사용하여 콘텐츠를 동적으로 분석하고 오류 진단과 같은 고급 기능을 제공하는 등 다양한 방법을 사용할 수 있습니다. , 지능형 프롬프트 등
Vscode의 선언적 언어 확장은 TextMate 어휘 분석 엔진을 기반으로 구현됩니다. 프로그래밍 언어 확장은 의미 분석 인터페이스, vscode.언어.* 인터페이스 및 언어 서버를 기반으로 구현됩니다. 프로토콜 프로토콜은 다음과 같이 각 기술 솔루션의 기본 로직을 확장하고 소개합니다.

어휘 강조

vscode.language.* 接口、Language Server Protocol 协议三种方式实现,下面展开介绍每种技术方案的基本逻辑。

词法高亮

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

比如 vscode 的词法引擎分析出 token 序列后再根据 token 的类型应用高亮样式,这个过程可以简单划分为分词、样式应用两个步骤。

参考资料:

  • https://macromates.com/manual/en/language%5C_grammars
  • https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide

分词

分词过程本质上将一长串代码递归地拆解为具有特定含义、分类的字符串片段,比如 +-*/% 等操作符;var/const 等关键字;1234"tecvan"

어휘 분석은 컴퓨터 과학에서 문자 시퀀스를 마크(토큰)

시퀀스로 변환하는 프로세스이며, 🎜토큰(토큰)🎜은 소스 코드를 구성하는 가장 작은 단위. 어휘 분석 기술은 컴파일, IDE 및 기타 분야에서 널리 사용됩니다. 🎜🎜예를 들어 vscode의 어휘 엔진은 토큰 시퀀스를 분석한 다음 토큰 유형에 따라 강조 스타일을 적용합니다. 이 프로세스는 간단히 단어 분할과 스타일 적용의 두 단계로 나눌 수 있습니다. 🎜🎜🎜참고 자료: 🎜🎜🎜https://macromates.com/manual/en/언어%5C_grammars
🎜🎜🎜https://code.visualstudio.com/api/언어-extensions/syntax-highlight - guide🎜🎜🎜🎜

🎜단어 분할🎜

🎜단어 분할 프로세스는 본질적으로 긴 코드 문자열을 특정 의미와 분류를 가진 문자열 조각으로 재귀적으로 분해합니다. 예를 들어 , +-*/%와 같은 연산자, var/const 또는 "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 类型,但无法识别其它关键字:

VSCode 코드 강조 원칙에 대한 심층 분석

在 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></style> 语句,end 用于匹配 语句,且 <style></style> 整个语句被赋予 scope 为 tag.style.vue 。此外,语句中字符被 beginCapturesendCaptures 属性分配成不同的 scope 类型:

VSCode 코드 강조 원칙에 대한 심층 분석

这里从 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 等样式属性。

实例解析

看完原理我们来拆解一个实际案例: github.com/mrmlnc/vsco…json5 是 JSON 扩展协议,旨在使人类更易于手动编写和维护,支持备注、单引号、十六进制数字等特性,这些拓展特性需要使用 vscode-json5 插件实现高亮效果:

VSCode 코드 강조 원칙에 대한 심층 분석

上图中,左边是没有启动 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 命令并回车:

VSCode 코드 강조 원칙에 대한 심층 분석

命令运行后就可以看到分词 token 的语言、scope、样式等信息。

编程式语言扩展

词法分析引擎 TextMate 本质上是一种基于正则的静态词法分析器,优点是接入方式标准化,成本低且运行效率较高,缺点是静态代码分析很难实现某些上下文相关的 IDE 功能,例如对于下面的代码:

VSCode 코드 강조 원칙에 대한 심층 분석

注意代码第一行函数参数 languageModes 与第二行函数体内的 languageModes 是同一实体但是没有实现相同的样式,视觉上没有形成联动。

为此,vscode 在 TextMate 引擎之外提供了三种更强大也更复杂的语言特性扩展机制:

  • 使用 DocumentSemanticTokensProvider 实现可编程的语义分析
  • 使用 vscode.languages.* 下的接口监听各类编程行为事件,在特定时间节点实现语义分析
  • 根据 Language Server Protocol 协议实现一套完备的语言特性分析服务器

相比于上面介绍的声明式的词法高亮,语言特性接口更灵活,能够实现诸如错误诊断、候选词、智能提示、定义跳转等高级功能。

参考资料:

  • https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide
  • https://code.visualstudio.com/api/language-extensions/programmatic-language-features
  • https://code.visualstudio.com/api/language-extensions/language-server-extension-guide

DocumentSemanticTokensProvider 分词

简介

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

注意区分一下,TextMate 中的扫描是引擎驱动的,逐行匹配正则,而 Sematic Tokens Provider 场景下扫描规则、匹配规则都交由插件开发者自行实现,灵活性增强但相对的开发成本也会更高。

实现上,Sematic Tokens Providervscode.DocumentSemanticTokensProvider 接口定义,开发者可以按需实现两个方法:

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

我们来看个完整的示例:

import * as vscode from &#39;vscode&#39;;

const tokenTypes = [&#39;class&#39;, &#39;interface&#39;, &#39;enum&#39;, &#39;function&#39;, &#39;variable&#39;];
const tokenModifiers = [&#39;declaration&#39;, &#39;documentation&#39;];
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: &#39;javascript&#39;, scheme: &#39;file&#39; };

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 = [&#39;class&#39;, &#39;interface&#39;, &#39;enum&#39;, &#39;function&#39;, &#39;variable&#39;];
const tokenModifiers = [&#39;declaration&#39;, &#39;documentation&#39;];
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,运行效果:

VSCode 코드 강조 원칙에 대한 심층 분석

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

小结

本质上,DocumentSemanticTokensProvider 只是提供了一套粗糙的 IOC 接口,开发者能做的事情比较有限,所以现在大多数插件都没有采用这种方案,读者理解即可,不必深究。

Language API

简介

相对而言,vscode.languages.* 系列 API 所提供的语言扩展能力可能更符合前端开发者的思维习惯。vscode.languages.* 托管了一系列用户交互行为的处理、归类逻辑,并以事件接口方式开放出来,插件开发者只需监听这些事件,根据参数推断语言特性,并按规则返回结果即可。

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

  • registerCompletionItemProvider: 提供代码补齐提示

VSCode 코드 강조 원칙에 대한 심층 분석

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

VSCode 코드 강조 원칙에 대한 심층 분석

  • registerSignatureHelpProvider:提供函数签名提示

VSCode 코드 강조 원칙에 대한 심층 분석

完整的列表请查阅 https://code.visualstudio.com/api/language-extensions/programmatic-language-features#show-hovers 一文。

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(&#39;language name&#39;, {
        provideHover(document, position, token) {
            return { contents: [&#39;aweome tecvan&#39;] };
        }
    });
    ...
}

运行结果:

1VSCode 코드 강조 원칙에 대한 심층 분석

其它特性功能的写法与此相似,感兴趣的同学建议到官网自行查阅。

Language Server Protocol

简介

上述基于语言扩展插件的代码高亮方法有一个相似的问题:难以在编辑器间复用,同一个语言,需要根据编辑器环境、语言重复编写功能相似的支持插件,那么对于 n 种语言,m 中编辑器,这里面的开发成本就是 n * m

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

1VSCode 코드 강조 원칙에 대한 심층 분석

增加 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 执行代码分析动作,并提供高亮、补全、提示等功能,如下图:

1VSCode 코드 강조 원칙에 대한 심층 분석

简单示例

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 入口文件

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

  • package.json 中声明激活条件与插件入口

  • 编写入口文件 client/src/extension.ts,启动 LSP 服务

  • 编写 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(&#39;server&#39;, &#39;out&#39;, &#39;server.js&#39;)
            ), 
            // 通讯协议,支持 stdio、ipc、pipe、socket
            transport: TransportKind.ipc 
        },
    };

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

    // 使用 Server、Client 配置创建代理对象
    const client = new LanguageClient(
        &#39;languageServerExample&#39;,
        &#39;Language Server Example&#39;,
        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: &#39;ex&#39;
        };
        diagnostics.push(diagnostic);
    }

    // 发送错误诊断信息
    // vscode 会自动完成错误提示渲染
    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

LSP Server 代码的主要流程:

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

运行效果:

VSCode 코드 강조 원칙에 대한 심층 분석

小结

通览样例代码,LSP 客户端服务器之间的通讯过程都已经封装在 LanguageClientconnection 等对象中,插件开发者并不需要关心底层实现细节,也不需要深入理解 LSP 协议即可基于这些对象暴露的接口、事件等实现简单的代码高亮效果。

总结

Vscode 用插件方式提供了多种语言扩展接口,分声明式、编程式两类,在实际项目中通常会混合使用这两种技术,用基于 TextMate 的声明式接口迅速识别出代码中的词法;再用编程式接口如 LSP 补充提供诸如错误提示、代码补齐、跳转定义等高级功能。

这段时间看了不少开源 vscode 插件,其中 Vue 官方提供的 Vetur 插件学习是这方面的典型案例,学习价值极高,建议对这方面有兴趣的读者可以自行前往分析学习 vscode 语言扩展类插件的写法。

更多编程相关知识,请访问:编程入门!!

위 내용은 VSCode 코드 강조 원칙에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제