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

相關文章
相關標籤/搜索