原文 blog: VSCode 里的 GoToDefinition 是如何实现的javascript
在编辑器领域里, “跳转到定义” 这个功能是不少语言服务里最经常使用的一个,那么在 VSCode 的世界里它是如何同时实现并适配到不少不一样语言里去的呢?java
首先咱们先看一下 VSCode 的官方定义 👇;node
也就是说它自己只是个轻量的源代码编辑器,并无提供语言的自动语法校验、格式化、智能提示和补全等,通通都是依靠其强大的插件系统来完成;react
因为自己是基于 Typescript
开发的,因此内置了对 JavaScript
,TypeScript
和 Node.js
的支持;git
那么 “跳转到定义” 这个功能一样也是由对应的语言服务插件来提供支持;github
本文以 Typescript
为例,来看看内置的 Typescript
语言服务插件;typescript
在这以前须要先熟知一下关于 LSP (Language Server Protocol) 语言服务协议, 在本博客的 WebIDE 技术相关资料整理 这篇文章有提到;json
通俗的讲就是语言服务单独运行在一个进程里,经过 JSON RPC
做为协议与客户端通讯,为其提供如跳转定义、自动补全等通用语言功能,例如 ts 的类型检查、类型跳转、自动补全等都须要有对应的 ts 语言服务端实现并与 Client 端通讯,官方文档有更为详细的阐述;bash
vscode 版本 1.41.1async
内置插件目录位于 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 语言的 “跳转到定义” 实现流程步骤能够分为
typescript-language-features
插件Go to Definition
方法tsserver
的请求并接收到响应done