Skip to content

API usage patterns for complex editor extensions #2824

@andrewbranch

Description

@andrewbranch

TypeScript 6.0 and earlier allowed loading third-party plugin code directly into TS Server, exposing language service and project system internals for proxying and patching. This was often used in coordination with editor extensions, sometimes in tandem with an additional custom language server, to provide language support for languages that embed or transform into TypeScript.

In the Go implementation, we can't dynamically link third-party code into the server process. Instead, we provide an IPC-based API and a TypeScript client library, currently under development, for communicating with the server. The API will expose most of the read-only features of today's ts.Program and ts.TypeChecker APIs; I want to use this issue to discuss and explore what other features the API needs in order to replace TS Server plugins.

Case study: Vue

I'm going to present my very basic understanding of how Vue's language support integrates with TypeScript today. There are three components:

  1. @vue/typescript-plugin, the TS Server plugin
  2. @vue/language-server, an additional LSP server responsible mostly for serving requests in the template and CSS parts of .vue files
  3. The VS Code extension that registers the TS plugin, spawns the Vue language server, and brokers communication between the two.

When the TS Server plugin initializes, it tells TypeScript to include the .vue extension in its projects and overrides TS Server's view of .vue file contents with valid TypeScript syntax. It then intercepts requests and responses for those files in order to map positions between locations in the original .vue file content and locations in the virtual TypeScript-only content. It also overrides module resolution to allow import {} from "./MyComponent.vue" to resolve, and readDirectory to include .vue files in path completions. Finally, it installs some custom request handlers that provide information from the TypeScript language service to @vue/typescript-plugin, proxied through the VS Code extension.

Proposal

I'd like to get feedback on a system where, instead of making the TypeScript LSP server serve requests for non-TypeScript files directly, we let an auxiliary language server, through API usage, compute or proxy whatever information it needs from the TypeScript language service. Roughly how I imagine that looking:

  • The Vue language server would tell the TypeScript API to open / keep open any projects for which it wants to include .vue files, and would "open" the virtual files, then continue to manage their state in a similar way in textDocument/didChange and workspace/didChangeWatchedFiles handlers.

    const snapshot = await api.updateSnapshot({
      openProject: projectTsconfigUri,
      openFiles: projectVueFiles.map(file => ({
        uri: `virtual://@vue/language-server/${file.basePath}`,
        content: getServiceScriptContent(file),
        scriptKind: ScriptKind.TS,
        defaultProject: projectTsconfigUri,
        version: file.version,
      }))
    })
  • Language service handlers in the Vue language server could access these virtual files through Program/TypeChecker APIs for semantic analysis as well as initiate standard LSP requests for them.

    connection.onCompletion(async (params) => {
      const { uri, position } = params.textDocument;
      using snapshot = await api.updateSnapshot();
      const { project, checker } = await snapshot.getDefaultProjectForFile(uri);
      const sourceFile = program.getSourceFile(uri);
      
      const virtualFilePosition = mapToVirtualFilePosition(uri, position);
      const node = getTokenAtPosition(sourceFile, virtualFilePosition);
      const tsCompletions = await snapshot.lspRequest("textDocument/completion", {
        textDocument: { uri },
        position: virtualFilePosition,
      });
    
      if (shouldProvideSpecialVueCompletions(node)) {
        const vueCompletions = await getVueCompletionsUsingSemanticAPIs(node, checker, params);
        return [...tsCompletions, ...vueCompletions];
      }
    });
  • For module resolution, we'd likely want to provide some kind of static mapping API that can be updated as needed—having callbacks into client code for module resolution is something we want to avoid. Whatever the format, it should cover module resolution, auto imports, and path completions—it seems silly that today's TS Server plugins can specify extra file extensions and external files but still have to patch readDirectory to get them to show up in path completions.

This approach shifts the responsibility and control for servicing LSP requests for non-TypeScript files to the language servers that actually own those files. With the exception of module resolution, most of what other language servers need from TypeScript falls pretty cleanly out of existing LSP functionality (opening and updating a virtual file is the same thing as opening an untitled file in an editor; we just need to add a project association) or existing/planned API functionality.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions