How is GoToDefinition implemented in VSCode

In the editor world, “jump to definition” is one of the most commonly used language services, so how does it work in the VSCode world and adapt to many different languages at the same time?

First let’s take a look at VSCode’s official definition 👇;

That is to say, it itself is only a lightweight source code editor, and does not provide automatic syntax verification, formatting, intelligent hints and completion of the language, all rely on its powerful plug-in system to complete;

Built in support of JavaScript, Typescript, and Node.js.

The “jump to definition” function is also supported by the corresponding language service plug-in;

This article uses Typescript as an example to take a look at the built-in Typescript language service plug-ins.

Before this, we need to be familiar with the Language Server Protocol (LSP), which is mentioned in this article about WebIDE technology.

Generally speaking, the language service runs separately in a process and communicates with the Client through JSON RPC as a protocol, providing it with general language functions such as jump definition and automatic completion. For example, ts type check, type jump and automatic completion need corresponding TS language server to implement and communicate with the Client. The official documentation goes into more detail;

Vscode version 1.41.1

Built-in Typescript plug-ins

The built-in plugins directory is located in the extensions directory of the VSCode project root, and contains plugins related to ts or js

. ├─ ├─ Typescript-language-exercises...Copy the code

Javascript and typescript-Basics have only json description files;

Focus on typescript-language-features at 👇;

└ ─ ─ the SRC ├ ─ ─ commands ├ ─ ─ the features | ├ ─ ─... │ │ ├ ─ ─ definitionProviderBase. Ts ├ ─ ─ definitions. Ts | ├ ─ ─... ├ ─ ─testHeavy Exercises ── Heavy Exercises ── heavy exercisesCopy the code

There are two visual files related to Definitions in the Features directory.

The “jump to definition” feature must be related to this plugin;

After understanding LSP, we can quickly find the Client and Server implementation of this plug-in.

The implementation of the Client side is

├ ─ ─ typescriptService. Ts / / interface definition ├ ─ ─ typescriptServiceClient. Ts / / Client specific implementation ├ ─ ─typeScriptServiceClientHost. Ts/Client/managementCopy the code

These three files

The Server is implemented in./ SRC /tsServer/server.ts;

Start the process

Json. What are the activation conditions

  "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"
  ],
Copy the code

This function can only be activated if the file is js or ts, so let’s look at the extension

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);
}
Copy the code

Some base operating command, is registered in front of the focus on createLazyClientHost function, start the instance of the Client side management structure, the core function is new the TypeScriptServiceClientHost

In the core of TypeScriptServiceClientHost constructor of a class

// 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);
}

Copy the code

The TypeScriptServiceClient instance and LanguageProvider language functions are registered

The LanguageProvider constructor core is

client.onReady((a)= > this.registerProviders());
Copy the code

Start registering some feature implementations, core for

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 ...
    ]);
}
Copy the code

This is where we start importing the Definitions functionality, so let’s take a look at the definitions

At the end of

// more ...
export function register(selector: vscode.DocumentSelector, client: ITypeScriptServiceClient,) {
	return vscode.languages.registerDefinitionProvider(selector,
		new TypeScriptDefinitionProvider(client));
}
Copy the code

Instantiated TypeScriptDefinitionProvider class, the class is defined as

export default class TypeScriptDefinitionProvider extends DefinitionProviderBase implements vscode.DefinitionProvider
Copy the code

Inherited DefinitionProviderBase and implements vscode. DefinitionProvider interface;

Which core part is TypeScriptDefinitionProviderBase getSymbolLocations method of the base class for core sentence

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 ...
    }
Copy the code

Execute the execute method of the Client and return the response data. Inside the Execute, the Server service is started and the service method is invoked

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');
}
Copy the code

StartService function is the real process of calling the TS language server, and there is a section for

// 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 ...
Copy the code

Read the server file path of the current TS version and check whether it exists. The tsServerPath variable of currentVersion is

public get tsServerPath(): string {
    return path.join(this.path, 'tsserver.js');
}
Copy the code

Let’s go over the mountains… The tsserver.js file is a lib package compiled from the typescript module in the node_modules directory in the extension directory, providing syntax for the tsserver.js file. The “jump to definition” TS implementation we’re looking for is right here;

This is done in the typescript repository;

The principle of “jump to define” implementation is in the SRC/services/goToDefinition ts directory, interested can go to study research in goToDefinition. Ts

conclusion

Therefore, VSCode’s jump to definition implementation process for Typescript can actually be divided into two steps

  1. Check the locale of the currently opened file, register it if it is TS or JS, etctypescript-language-featuresThe plug-in
  2. The user to performGo to Definitionmethods
  3. The plug-in Client initiates a Service request
  4. The plug-in Service side initiates Typescript core filestsserverRequest and receive the response
  5. The Client receives the Service response back to definitions in Features
  6. Definitions are converted to the format required by VSCode and responded to
  7. When VSCode receives a response, it jumps to the corresponding location of the corresponding file

done