Language Service Plugins with Proxies

Goals / non-goals

  • Support plugins which can change completions, quick info, diagnostics, etc, returned from the language service
  • Add new entry points as needed to checker
  • Allow per-project configuration of plugins
  • Automatically load and enable plugins with no extra coding from editor-side code
  • Non-goal: Support new syntax, new typechecking behavior, etc (too complex)
  • Non-goal: Commandline plugins (we may expand this model to tsc if it's successful)
  • Non-goal: Scale to a large number of plugins
  • Non-goal: Support projects not configured using tsconfig.json

Architecture Overview

When an editor performs a language service operation, the following steps occur:

  • Editor sends a request to TS Server
  • TS Server decodes the message and determines which function to invoke ("decode and dispatch")
  • Each dispatching function, if needed, finds the project associated with the message ("find project")
  • The dispatching function gets the language service from the project
  • The method on the language service instance is invoked
  • The response is encoded and sent back to the editor
        send           decode and            find             --- proxy inserted here
       message         dispatch            project           vvv
[editor] -> [TS Server] ----> getFormatting ----> Project A ----> Language Service A
                        \---> getCompletions ---> Project B ----> Language Service B
                        \---> getQuickInfo   ---> Project C ----> Language Service C
                        \---> ...

The change here is to insert a proxy (more accurately a decorator) between the project and the language service. Projects backed by tsconfig.json files will, upon creation of their language service, wrap the LS instance by invoking a factory method on the plugins listed in the config file.

Configuration

A new "plugins" section is added to tsconfig.json

{
    "compilerOptions": {
        "strictNullChecks": false,
        "plugins": [
            { "name": "myPlugin" }
        ]
    },
    "files": ["sample.ts"]
}

This configures the my-plugin plugin

Loading

These plugins are loaded as node modules from the folder where the tsconfig.json file is.

Initialization

Immediately after creating its language service, a tsconfig.json-based project will wrap the language service in the plugin proxy by calling its create method:

class ConfiguredProject {
  init() {
    // psuedo-ish code of what happens using the above config file
    let myLanguageService = createLanguageService(); // Normal LS creation
    // Literals here are actually loaded from config file, not hardcoded
    const plugin = require("./my-plugin");
    // Pass in the entry from tsconfig so plugin can read its own config object
    myLanguageService = plugin.create(myLanguageService, this, { name: "myPlugin" });
  }
}

The implementation of myPlugin might look like this

export function create(oldLS, project, config) {
  const newLS = ts.createLanguageServiceProxy(oldLS);
  newLS.getQuickInfo = function() {
    const x = oldLS.getQuickInfo.apply(oldLS, arguments);
    // do something interesting with 'x' here
    return x;
  }
  return newLS;
}

/cc @chuckjaz