原文 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