VSCode 里的 GoToDefinition 是如何实现的

3,672 阅读5分钟

原文 blog: VSCode 里的 GoToDefinition 是如何实现的

在编辑器领域里, “跳转到定义” 这个功能是很多语言服务里最常用的一个,那么在 VSCode 的世界里它是如何同时实现并适配到很多不同语言里去的呢?

首先我们先看一下 VSCode 的官方定义 👇;

1.png

也就是说它本身只是个轻量的源代码编辑器,并没有提供语言的自动语法校验、格式化、智能提示和补全等,统统都是依靠其强大的插件系统来完成;

由于本身是基于 Typescript 开发的,所以内置了对 JavaScriptTypeScriptNode.js 的支持;

那么 “跳转到定义” 这个功能同样也是由对应的语言服务插件来提供支持;

本文以 Typescript 为例,来看看内置的 Typescript 语言服务插件;

在这之前需要先熟知一下关于 LSP (Language Server Protocol) 语言服务协议, 在本博客的 WebIDE 技术相关资料整理 这篇文章有提到;

通俗的讲就是语言服务单独运行在一个进程里,通过 JSON RPC 作为协议与客户端通信,为其提供如跳转定义、自动补全等通用语言功能,例如 ts 的类型检查、类型跳转、自动补全等都需要有对应的 ts 语言服务端实现并与 Client 端通信,官方文档有更为详细的阐述;

vscode 版本 1.41.1

内置 Typescript 插件

内置插件目录位于 VSCode 项目根目录的 extensions 目录,里面和 ts 或 js 有关的插件有

...
├── javascript
├── typescript-basics
└── typescript-language-features
...

其中 javascripttypescript-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.jsonactivationEvents 字段检查一下激活条件是什么

  "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 语言的 “跳转到定义” 实现流程步骤可以分为

  1. 检查当前打开的文件所对应的语言环境,若为 ts 或 js 等则注册 typescript-language-features 插件
  2. 用户执行 Go to Definition 方法
  3. 插件 Client 端发起 Service 端请求
  4. 插件 Service 端发起对 Typescript 核心文件 tsserver 的请求并接收到响应
  5. Client 端接收到 Service 端响应返回给 features 里的 definitions
  6. definitions 转换成 VSCode 所需的格式并响应
  7. VSCode 收到响应跳转到对应文件的对应位置

done