从源码来看,VSCode主体只是个Editor(核心部分可在Web环境独立运行,叫Monaco),并不提供任何语言特性相关的功能,比如:
这些统统没有,都是由插件提供的,对JS的支持也是这样
VS Code内置插件中,与JavaScript有关的只有一个vscode/extensions/javascript/,而且是个纯粹的语言支持型插件:
"contributes": {
// 语言id
"languages": [],
// 语法
"grammars": [],
// 代码片段
"snippets": [],
// 语言相关配置文件校验规则及提示
"jsonValidation": []
}
P.S.关于jsonValidation
的作用,见Json Schema with VS Code
一堆配置文件显然提供不了跳转定义之类的强力功能,因此,还有两个TypeScript相关的插件:
javascript
插件,提供TS语言语法支持其中typescript-language-features
是VS Code能够理解JS/TS(以及JSX/TSX)代码语义,并支持跳转到定义等功能的关键:
"activationEvents": [
"onLanguage:javascript",
"onLanguage:javascriptreact",
"onLanguage:typescript",
"onLanguage:typescriptreact",
"onLanguage:jsx-tags",
"onLanguage:jsonc"
]
./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部分是features
、server
和typescriptServiceClient
:
具体的,该插件激活时主要发生了这3件事情:
TypeScriptServerPlugin
,并在Client ready之后注册TypeScriptServiceClient
,立即创建TSServer进程LanguageProvider
,负责对接VSCode功能入口vscode.languages.registerCompletionItemProvider
接补全提示其中比较有意思的是注册TypeScriptServerPlugin,创建TSServer,以及Client与Server之间的通信
只在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运行在单独的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进程也有此限制,具体见四.进程模型
由于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与TS密不可分,如图:
其中,最重要的3块是:
tsc
))
CLI工具,对输入文件进行编译转换,再输出到文件而TSServer作为独立的进程服务(Standalone Server (tsserver
)),在Compiler和Service之上建立了一层封装,以JSON协议的形式暴露接口,具体见Using the Language Service API
所以,TSServer具有tsc
的完整能力,还有面向编辑器的语言服务支持,非常适合编辑器后台进程之类的应用场景
至此,一切都明了了。最关键的语义分析能力及数据支持来自下层TSServer,因此,跳转到定义的大致流程是这样的:
typescript-language-features
注册的对应Feature实现P.S.VSCode中其它JS语义相关的功能与之类似,都依靠TSServer提供支持