Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Language Service Plugins with Proxies #11976

Closed
RyanCavanaugh opened this issue Nov 1, 2016 · 6 comments

Comments

@RyanCavanaugh
Copy link
Member

commented Nov 1, 2016

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

@RyanCavanaugh RyanCavanaugh self-assigned this Nov 1, 2016

@weswigham

This comment has been minimized.

Copy link
Member

commented Nov 1, 2016

If I'm not mistaken, the wrapped/decorated language service model was already how @chuckjaz's proposal worked (and what my prototype implemented), though the API exposed here asks for monkeypatching rather than exposing hooks, and uses the single-registration-function plugin shape rather than expected-export-shape API.

@weswigham

This comment has been minimized.

Copy link
Member

commented Nov 1, 2016

@RyanCavanaugh How do multiple language service plugins stack up in this model? It's unclear on the order they are constructed/executed in and the precedence of their return values.

@RyanCavanaugh

This comment has been minimized.

Copy link
Member Author

commented Nov 1, 2016

@weswigham they'd wrap to arbitrary depth. For ordering, I guess this does have to be an array, though it makes me sad. Updating.

@angelozerr

This comment has been minimized.

Copy link

commented Feb 3, 2017

It's perhaps out of scope of this issue but I tell me how to extends tsserver commands to add it with plugin. For instance I would like to have a new tsserver command to return a list of Angular2 modules that I would like to use with a Angular2 wizard to generate an angular-cli generate command which waits a module. In this wizard I would like to display a list with available modules that I could retrieve with tsserver command. See angelozerr/angular-eclipse#34

@angelozerr

This comment has been minimized.

Copy link

commented Feb 13, 2017

Just for your information, I have created https://github.com/angelozerr/tsserver-plugins to consume language service proxy (like angular2 and tslint) with any version of TypeScript tsserver.

@RyanCavanaugh

This comment has been minimized.

Copy link
Member Author

commented Aug 12, 2017

Closing for housekeeping purposes

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
3 participants
You can’t perform that action at this time.