VSCode 里的 GoToDefinition 是如何实现的

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

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

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

1.png

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

因为自己是基于 Typescript 开发的,因此内置了对 JavaScriptTypeScriptNode.js 的支持;git

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

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

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

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

vscode 版本 1.41.1async

内置 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

相关文章
相关标签/搜索