在编辑器领域里, “跳转到定义” 这个功能是很多语言服务里最常用的一个,那么在 VSCode 的世界里它是如何同时实现并适配到很多不同语言里去的呢?
首先我们先看一下 VSCode 的官方定义 👇;
也就是说它本身只是个轻量的源代码编辑器,并没有提供语言的自动语法校验、格式化、智能提示和补全等,统统都是依靠其强大的插件系统来完成;
由于本身是基于 Typescript
开发的,所以内置了对 JavaScript
,TypeScript
和 Node.js
的支持;
那么 “跳转到定义” 这个功能同样也是由对应的语言服务插件来提供支持;
本文以 Typescript
为例,来看看内置的 Typescript
语言服务插件;
在这之前需要先熟知一下关于 LSP (Language Server Protocol) 语言服务协议, 在本博客的 WebIDE 技术相关资料整理 这篇文章有提到;
通俗的讲就是语言服务单独运行在一个进程里,通过 JSON RPC
作为协议与客户端通信,为其提供如跳转定义、自动补全等通用语言功能,例如 ts 的类型检查、类型跳转、自动补全等都需要有对应的 ts 语言服务端实现并与 Client 端通信,官方文档有更为详细的阐述;
vscode 版本 1.41.1
内置插件目录位于 VSCode 项目根目录的 extensions
目录,里面和 ts 或 js 有关的插件有
...
├── javascript
├── typescript-basics
└── typescript-language-features
...
其中 javascript
和 typescript-basics
里只有一些 json 格式的描述文件;
那么重点看 typescript-language-features
插件, 目录 👇;
└── src
├── commands
├── features
| ├── ...
│ ├── definitionProviderBase.ts
│ ├── definitions.ts
| ├── ...
├── test
├── tsServer
├── typings
└── utils
其中我们看到了 features
目录下目测和 definitions 有关的两个文件了;
看来 “跳转到定义” 这个功能铁定和这个插件有必然的联系;
在了解了 LSP
之后可以快速找到这个插件的 Client 实现和 Server 实现;
其中 Client 端的实现有
├── typescriptService.ts // 接口定义
├── typescriptServiceClient.ts // Client 具体实现
├── typeScriptServiceClientHost.ts // 管理 Client
这三个文件
而 Server 端的实现在 ./src/tsServer/server.ts
;
既然是插件,那么我们看看它 package.json
里 activationEvents
字段检查一下激活条件是什么
"activationEvents": [
"onLanguage:javascript",
"onLanguage:javascriptreact",
"onLanguage:typescript",
"onLanguage:typescriptreact",
"onLanguage:jsx-tags",
"onCommand:typescript.reloadProjects",
"onCommand:javascript.reloadProjects",
"onCommand:typescript.selectTypeScriptVersion",
"onCommand:javascript.goToProjectConfig",
"onCommand:typescript.goToProjectConfig",
"onCommand:typescript.openTsServerLog",
"onCommand:workbench.action.tasks.runTask",
"onCommand:_typescript.configurePlugin",
"onLanguage:jsonc"
],
只有在打开的文件是 js 或 ts 等才会得以激活,那么我们看看 extension.ts
文件的 activate
函数
export function activate(
context: vscode.ExtensionContext
): Api {
const pluginManager = new PluginManager();
context.subscriptions.push(pluginManager);
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
const onCompletionAccepted = new vscode.EventEmitter<vscode.CompletionItem>();
context.subscriptions.push(onCompletionAccepted);
const lazyClientHost = createLazyClientHost(context, pluginManager, commandManager, item => {
onCompletionAccepted.fire(item);
});
registerCommands(commandManager, lazyClientHost, pluginManager);
context.subscriptions.push(vscode.workspace.registerTaskProvider('typescript', new TscTaskProvider(lazyClientHost.map(x => x.serviceClient))));
context.subscriptions.push(new LanguageConfigurationManager());
import('./features/tsconfig').then(module => {
context.subscriptions.push(module.register());
});
context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager));
return getExtensionApi(onCompletionAccepted.event, pluginManager);
}
前面是注册一些基操的命令,重点在 createLazyClientHost
函数,开始构造了 Client 端管理的实例,该函数核心是 new 了 TypeScriptServiceClientHost
在 TypeScriptServiceClientHost
类的构造函数里核心为
// more ...
this.client = this._register(new TypeScriptServiceClient(
workspaceState,
version => this.versionStatus.onDidChangeTypeScriptVersion(version),
pluginManager,
logDirectoryProvider,
allModeIds));
// more ...
for (const description of descriptions) {
const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted);
this.languages.push(manager);
this._register(manager);
this.languagePerId.set(description.id, manager);
}
注册了 TypeScriptServiceClient
实例和 LanguageProvider
语言功能
其中 LanguageProvider
构造函数核心为
client.onReady(() => this.registerProviders());
开始注册一些功能实现,核心为
private async registerProviders(): Promise<void> {
const selector = this.documentSelector;
const cachedResponse = new CachedResponse();
await Promise.all([
// more import ...
import('./features/definitions').then(provider => this._register(provider.register(selector, this.client))),
// more import ...
]);
}
就是在这里开始导入 definitions
功能, 我们来看看 definitions.ts
文件
末尾为
// more ...
export function register(
selector: vscode.DocumentSelector,
client: ITypeScriptServiceClient,
) {
return vscode.languages.registerDefinitionProvider(selector,
new TypeScriptDefinitionProvider(client));
}
实例化了 TypeScriptDefinitionProvider
类, 该类定义为
export default class TypeScriptDefinitionProvider extends DefinitionProviderBase implements vscode.DefinitionProvider
继承了 DefinitionProviderBase
和实现了 vscode.DefinitionProvider
接口;
其中核心部分是 TypeScriptDefinitionProviderBase
基类的 getSymbolLocations
方法, 核心语句为
protected async getSymbolLocations(
definitionType: 'definition' | 'implementation' | 'typeDefinition',
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Location[] | undefined> {
// more ...
const response = await this.client.execute(definitionType, args, token);
// more ...
}
执行 Client 的 execute 方法并返回响应数据, 在 execute 内部是启动 Server 服务,调用了 service 方法
private service(): ServerState.Running {
if (this.serverState.type === ServerState.Type.Running) {
return this.serverState;
}
if (this.serverState.type === ServerState.Type.Errored) {
throw this.serverState.error;
}
const newState = this.startService();
if (newState.type === ServerState.Type.Running) {
return newState;
}
throw new Error('Could not create TS service');
}
其中 startService
函数才是真正调用 ts 语言服务端的过程,里面有一段为
// more ...
if (!fs.existsSync(currentVersion.tsServerPath)) {
vscode.window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', currentVersion.path));
this.versionPicker.useBundledVersion();
currentVersion = this.versionPicker.currentVersion;
}
// more ...
读取当前 ts 版本的 server 文件路径,判断是否存在,而 currentVersion 的 tsServerPath 变量为
public get tsServerPath(): string {
return path.join(this.path, 'tsserver.js');
}
咱们翻山越岭。。。终于找到了最为核心的一段,该 tsserver.js
文件是 extension
目录下 node_modules
目录的 typescript 模块编译后的 lib 包文件,为其提供了语法功能,我们要找的 “跳转到定义” 的 ts 实现就是在这里;
而实现原理就是在 typescript 仓库里;
"跳转到定义" 的原理实现就是在其 src/services/goToDefinition.ts
目录下,感兴趣的可以在前往仔细研究研究 goToDefinition.ts
因此,实际上 VSCode 对于 Typescript 语言的 “跳转到定义” 实现流程步骤可以分为
- 检查当前打开的文件所对应的语言环境,若为 ts 或 js 等则注册
typescript-language-features
插件 - 用户执行
Go to Definition
方法 - 插件 Client 端发起 Service 端请求
- 插件 Service 端发起对 Typescript 核心文件
tsserver
的请求并接收到响应 - Client 端接收到 Service 端响应返回给 features 里的 definitions
- definitions 转换成 VSCode 所需的格式并响应
- VSCode 收到响应跳转到对应文件的对应位置
done