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
Closed

Language Service Plugins with Proxies #11976

RyanCavanaugh opened this issue Nov 1, 2016 · 6 comments
Assignees
Labels
Discussion Issues which may not have code impact

Comments

@RyanCavanaugh
Copy link
Member

RyanCavanaugh 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 added the Discussion Issues which may not have code impact label Nov 1, 2016
@RyanCavanaugh RyanCavanaugh self-assigned this Nov 1, 2016
@weswigham
Copy link
Member

weswigham 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
Copy link
Member

@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
Copy link
Member Author

@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
Copy link

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
Copy link

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
Copy link
Member Author

Closing for housekeeping purposes

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests

3 participants