VSCode跳转到定义内部实现_VSCode插件开发笔记4

       感谢支持ayqy我的订阅号,每周义务推送1篇(only unique one)原创精品博文,话题包括但不限于前端、Node、Android、数学(WebGL)、语文(课外书读后感)、英语(文档翻译)        
       若是以为弱水三千,一瓢太少,能够去 http://blog.ayqy.net 看个痛快    
   javascript

写在前面

从源码来看,VSCode主体只是个Editor(核心部分可在Web环境独立运行,叫Monaco),并不提供任何语言特性相关的功能,好比:前端

  • 语法支持:语法校验、高亮、格式化、Lint检查等等java

  • 编辑体验:跳转到定义、智能提示、自动补全、查找引用、变量重命名等等node

这些通通没有,都是由插件提供的,对JS的支持也是这样react

一.内置插件

VS Code内置插件中,与JavaScript有关的只有一个vscode/extensions/javascript/,并且是个纯粹的语言支持型插件:typescript

"contributes": {
  // 语言id
  "languages": [],
  // 语法
  "grammars": [],
  // 代码片断
  "snippets": [],
  // 语言相关配置文件校验规则及提示
  "jsonValidation": []
}

P.S.关于jsonValidation的做用,见Json Schema with VS Codejson

一堆配置文件显然提供不了跳转定义之类的强力功能,所以,还有两个TypeScript相关的插件:api

  • typescript-basics:相似于javascript插件,提供TS语言语法支持安全

  • typescript-language-features:提供语言特性相关的高级支持,如跳转、查找声明/引用、补全提示、outline/breadcrumb等涉及代码语义的高级功能微信

其中typescript-language-features是VS Code可以理解JS/TS(以及JSX/TSX)代码语义,并支持跳转到定义等功能的关键

"activationEvents": [
  "onLanguage:javascript",
  "onLanguage:javascriptreact",
  "onLanguage:typescript",
  "onLanguage:typescriptreact",
  "onLanguage:jsx-tags",
  "onLanguage:jsonc"
]

二.typescript-language-features

结构

./src
├── commands.ts   # TS相关自定义command
├── extension.ts  # 插件入口
├── features  # 各类语言特性,如高亮、折叠、跳转到定义等
├── languageProvider.ts # 对接VSCode功能入口
├── protocol.const.ts   # TS语言元素常量
├── protocol.d.ts # tsserver接口协议
├── server.ts     # 管理tsserver进进程
├── test
├── typeScriptServiceClientHost.ts  # 负责管理Client
├── typescriptService.ts        # 定义Client接口形态
├── typescriptServiceClient.ts  # Client具体实现
├── typings
└── utils

P.S.参考源码版本v1.28.2,最新的源码目录结构已经变了,但思路同样

其中最重要的3部分是featuresservertypescriptServiceClient

  • Feature:对接VSCode,为高亮、折叠、跳转等Editor功能入口提供具体实现

  • Server:接入TSServer,以得到理解JS代码语义的能力,为语义相关的功能提供数据源

  • Client:与Server交互(按照既定接口协议),发起请求,并接收响应数据

启动流程

具体的,该插件激活时主要发生了这3件事情:

  1. 找出全部插件添加的TypeScriptServerPlugin,并在Client ready以后注册

  2. 建立TypeScriptServiceClientHost

    1. 建立TypeScriptServiceClient,当即建立TSServer进程

    2. 建立LanguageProvider,负责对接VSCode功能入口

  3. TSServer ready以后,开始链接VSCode与TSServer

    1. LanguageProvider注册VSCode各项功能,例如vscode.languages.registerCompletionItemProvider接补全提示

    2. 当即触发诊断(语法校验、类型检查等)

其中比较有意思的是注册TypeScriptServerPlugin,建立TSServer,以及Client与Server之间的通讯

注册TypeScriptServerPlugin

只在TS v2.3.0+才注册外部Plugin,经过命令行参数传入:

if (apiVersion.gte(API.v230)) {
  const pluginPaths = this._pluginPathsProvider.getPluginPaths();

  if (plugins.length) {
    args.push('--globalPlugins', plugins.map(x => x.name).join(','));

    if (currentVersion.path === this._versionProvider.defaultVersion.path) {
      pluginPaths.push(...plugins.map(x => x.path));
    }
  }

  if (pluginPaths.length !== 0) {
    args.push('--pluginProbeLocations', pluginPaths.join(','));
  }
}

由于TSServer plugin API是在TS v2.3.0推出的:

TypeScript 2.3 officially makes a language server plugin API available. This API allows plugins to augment the regular editing experience that TypeScript already delivers. What all of this means is that you can get an enhanced editing experience for many different workloads.

也就是说,VSCode的宇宙级JS编辑体验,都得益于下层的TypeScript

One of TypeScript’s goals is to deliver a state-of-the-art editing experience to the JavaScript world.

(摘自Announcing TypeScript 2.3)

P.S.之因此存在低版本TS的状况,是由于VSCode容许使用外部TS(内置的固然是高版本)

建立TSServer

TSServer运行在单独的Node进程:

public spawn(
  version: TypeScriptVersion,
  configuration: TypeScriptServiceConfiguration,
  pluginManager: PluginManager
): TypeScriptServer {
  const apiVersion = version.version || API.defaultVersion;

  const { args, cancellationPipeName, tsServerLogFile } = this.getTsServerArgs(configuration, version, pluginManager);

  // fork一个tsserver进程
  // 内置的TSServer位于extensions/node_modules/typescript/lib/tsserver.js
  const childProcess = electron.fork(version.tsServerPath, args, this.getForkOptions());

  return new TypeScriptServer(childProcess, tsServerLogFile, cancellationPipeName, this._logger, this._telemetryReporter, this._tracer);
}

其中,electron.fork是对原生fork()的封装,限制了Electron API访问:

import cp = require('child_process');

export function fork(modulePath, args, options): cp.ChildProcess {
  const newEnv = generatePatchedEnv(process.env, modulePath);
  return cp.fork(modulePath, args, {
    silent: true,
    cwd: options.cwd,
    env: newEnv,
    execArgv: options.execArgv
  });
}

与原生cp.fork()的区别在于对环境变量的Patch:

function generatePatchedEnv(env: any, modulePath: string): any {
  const newEnv = Object.assign({}, env);

  newEnv['ELECTRON_RUN_AS_NODE'] = '1';
  newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..');

  // Ensure we always have a PATH set
  newEnv['PATH'] = newEnv['PATH'] || process.env.PATH;

  return newEnv;
}

其中ELECTRON_RUN_AS_NODE用来限制访问Electron API

ELECTRON_RUN_AS_NODE: Starts the process as a normal Node.js process.

主要出于UI定制限制与安全性考虑,不然第三方VSCode插件能够经过typescriptServerPlugins扩展点访问Electron API,篡改UI

P.S.普通插件所处的Node进程也有此限制,具体见四.进程模型

Client与Server通讯

因为TSServer跑在子进程中,API调用存在跨进程的问题,所以TSServer定义了一套JSON协议protocol.d.ts,主要包括API名以及消息格式:

// 命令
const enum CommandTypes {
    Definition = "definition",
    Format = "format",
    References = "references",
    // ...
}

// 基本消息格式
interface Message {
    seq: number;
    type: "request" | "response" | "event";
}

// 请求消息格式
interface Request extends Message {
    type: "request";
    command: string;
    arguments?: any;
}

// 响应消息格式
interface Response extends Message {
    type: "response";
    request_seq: number;
    success: boolean;
    command: string;
    message?: string;
    body?: any;
    metadata?: unknown;
}

经过标准输入/输出收发消息,具体见Message format:

tsserver listens on stdin and writes messages back to stdout.

P.S.关于进程间通讯的更多信息,请查看1.经过stdin/stdout传递json

三.TSServer

TSServer与TS密不可分,如图:

其中,最重要的3块是:

  • 编译器核心(Core TypeScript Compiler)

    实现了一个完整的编译器,包括词法分析、类型校验、语法分析、代码生成等

  • 面向编辑器的语言服务(Language Service)

    提供语句补全、API提示、代码格式化、文件内跳转、配色、断点位置校验等,还有一些更场景化的API,如增量编译,具体见Standalone Server (tsserver)

  • 独立的编译器(Standalone Compiler (tsc))

    CLI工具,对输入文件进行编译转换,再输出到文件

而TSServer做为独立的进程服务(Standalone Server (tsserver)),在Compiler和Service之上创建了一层封装,以JSON协议的形式暴露接口,具体见Using the Language Service API

因此,TSServer具备tsc的完整能力,还有面向编辑器的语言服务支持,很是适合编辑器后台进程之类的应用场景

四.总结

至此,一切都明了了。最关键的语义分析能力及数据支持来自下层TSServer,所以,跳转到定义的大体流程是这样的:

  1. 用户在VSCode界面点击Go to Definition

  2. 触发内置插件typescript-language-features注册的对应Feature实现

  3. Feature经过Client发起对TSServer的请求

  4. TSServer查相关AST找出Definitions,并按照既定协议格式输出

  5. Client接到响应,取出数据,传递给Feature

  6. Feature把原始数据转换成VSCode展示须要的格式

  7. VSCode拿到数据,让光标移动到Editor指定位置。砰,就跳过去了

P.S.VSCode中其它JS语义相关的功能与之相似,都依靠TSServer提供支持

参考资料

  • Microsoft/vscode 1.28.2

  • Architectural Overview

联系ayqy      

若是在文章中发现了什么问题,请查看原文并留下评论,ayqy看到就会回复的(不建议直接回复公众号,看不到的啦)

特别要紧的问题,能够直接微信联系ayqywx      


本文分享自微信公众号 - 前端向后(backward-fe)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索