Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ELS – The Ember Language Server

ELS – The Ember Language Server

Tobias Bieniek

October 11, 2018
Tweet

More Decks by Tobias Bieniek

Other Decks in Programming

Transcript

  1. Disclaimer this talk contains simplified code examples! the real code

    is available at 
 https://github.com/emberwatch/ember-language-server
  2. vim Emacs VS Code IntelliJ JavaScript ❓ ❓ ❓ ❓

    CSS ❓ ❓ ❓ ❓ Ember.js ❓ ❓ ❓ ❓ Rust ❓ ❓ ❓ ❓
  3. JavaScript ✅ CSS ✅ Ember.js ✅ Rust ✅ vim ✅

    Emacs ✅ VS Code ✅ IntelliJ ✅
  4. JavaScript javascript- typescript- langserver CSS vscode-css- languageserver Ember.js ember-language- server

    Rust rust-language- server vim vim-lsp Emacs emacs-lsp VS Code builtin! IntelliJ
  5. JSON-RPC 2.0 Request {
 "jsonrpc": "2.0",
 "id": 42,
 "method": "add",


    "params": {
 "a": 1,
 "b": 2,
 },
 } Response {
 "jsonrpc": "2.0",
 "id": 42,
 "result": 3,
 "error": null,
 }
  6. Request {
 "method": "textDocument/completion",
 "params": {
 "textDocument": {
 "uri": "file:

    ///Users/tbieniek/Code/crates.io/app/templates/index.hbs",
 },
 "position": {
 "line": 5,
 "character": 18,
 },
 },
 }
  7. import URI from 'vscode-uri';
 let uri = URI.parse(
 'file: ///Users/tbieniek/Code/crates.io/app/templates/index.hbs'


    ) let path = uri.fsPath; // -> /Users/tbieniek/Code/crates.io/app/templates/index.hbs
  8. import fs from 'fs'; let content = fs.readFileSync( '/Users/tbieniek/Code/crates.io/app/templates/index.hbs', {

    encoding: 'utf-8' }, ); let lines = content.split('\n'); let line = line[5]; let character = line[18]; // -> a
  9. import fs from 'fs'; import { preprocess } from '@glimmer/syntax';

    let content = fs.readFileSync( '/Users/tbieniek/Code/crates.io/app/templates/index.hbs', { encoding: 'utf-8' }, ); let ast = preprocess(content); // -> Abstract Syntax T ree
  10. path: { type: 'PathExpression', original: 'a', this: false, parts: [

    'a', ], data: false, loc: { start: { line: 5, column: 18, }, end: { line: 5, column: 19, }, }, } Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression
  11. path: { type: 'PathExpression', original: 'a', this: false, parts: [

    'a', ], data: false, loc: { start: { line: 5, column: 18, }, end: { line: 5, column: 19, }, }, } position: {
 line: 5,
 character: 18,
 } Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression
  12. let node = findNodeAtPosition(ast, position); let isSubExpressionPath = ( node.type

    === 'PathExpression' && node.parent.type === 'SubExpression' ); if (isSubExpressionPath) { return SUB_EXPRESSION_HELPERS; }
  13. const SUB_EXPRESSION_HELPER_NAMES = [ 'action', 'component', 'concat', 'get', 'if', 'unless',

    ]; const SUB_EXPRESSION_HELPERS = SUB_EXPRESSION_HELPER_NAMES
 .map(name => ({ label: name, kind: CompletionItemKind.Function, }));
  14. import path from 'path'; import findUp from 'find-up'; let projectRoot

    = findUp.sync('ember-cli-build.js', { cwd: path.dirname( '/Users/tbieniek/Code/crates.io/app/templates/index.hbs' ), }); // -> /Users/tbieniek/Code/crates.io
  15. import glob from 'fast-glob'; let components = glob.sync(['**/*.js'], {
 cwd:

    `${projectRoot}/app/components`,
 }); let templates = glob.sync(['**/*.hbs'], {
 cwd: `${projectRoot}/app/templates/components`,
 });
  16. import glob from 'fast-glob'; let components = glob.sync(['**/*.js'], {
 cwd:

    `${projectRoot}/app/components`,
 }); let templates = glob.sync(['**/*.hbs'], {
 cwd: `${projectRoot}/app/templates/components`,
 }); let names = components .concat(templates) .map(filePath => stripExtension(filePath));
  17. import glob from 'fast-glob'; let components = glob.sync(['**/*.js'], {
 cwd:

    `${projectRoot}/app/components`,
 }); let templates = glob.sync(['**/*.hbs'], {
 cwd: `${projectRoot}/app/templates/components`,
 }); let names = components .concat(templates) .map(filePath => stripExtension(filePath)); let uniqueNames = Array.from(new Set(names));
  18. let names = components .concat(templates) .map(filePath => stripExtension(filePath)); let uniqueNames

    = Array.from(new Set(names)); return uniqueNames.map(name => ({ label: name, kind: CompletionItemKind.Class, }));
  19. Request {
 "method": "textDocument/definition",
 "params": {
 "textDocument": {
 "uri": "file:

    ///Users/tbieniek/Code/crates.io/app/templates/crate.hbs",
 },
 "position": {
 "line": 92,
 "character": 14,
 },
 },
 }
  20. let node = findNodeAtPosition(ast, position); let isMustacheStatementPath = ( node.type

    === 'PathExpression' && node.parent.type === 'MustacheStatement' ); if (!isMustacheStatementPath) { return; }
  21. import path from 'path'; import findUp from 'find-up'; let projectRoot

    = findUp.sync('ember-cli-build.js', { cwd: path.dirname( '/Users/tbieniek/Code/crates.io/app/templates/crate.hbs' ), }); // -> /Users/tbieniek/Code/crates.io
  22. import DS from 'ember-data'; export default DS.Model.extend({ crates: DS.hasMany('crate'), });

    • app/adapters/crate.js • app/models/crate.js • app/serializers/crate.js
  23. import Component from '@ember/component'; import { inject as service }

    from '@ember/service'; export default Component.extend({ session: service('session'), }); • app/services/session.js
  24. @glimmer/syntax > written for the template compiler > can not

    handle incomplete/invalid input {{#link-to "|
  25. ELS Editor Support > VSCode ... done (vscode-ember extension) >

    Atom ... done (ide-ember package) > emacs ... work-in-progress > vim ... )
  26. Goto Related File > cycle through controller, route, template >

    or component and template > or model, adapter, serializer > or to the related test file