Slide 1

Slide 1 text

ELS the Ember Language Server

Slide 2

Slide 2 text

Turbo87 TobiasBieniek

Slide 3

Slide 3 text

simplabs based in Munich consulting all over ! * Stickers available after the talk

Slide 4

Slide 4 text

Disclaimer this talk contains simplified code examples! the real code is available at 
 https://github.com/emberwatch/ember-language-server

Slide 5

Slide 5 text

What text editor do you use? " # $ % $ & ' ( ) * " *

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Goto Class

Slide 8

Slide 8 text

Goto Definition

Slide 9

Slide 9 text

IntelliJ-only

Slide 10

Slide 10 text

vim Emacs VS Code IntelliJ JavaScript ❓ ❓ ❓ ❓ CSS ❓ ❓ ❓ ❓ Ember.js ❓ ❓ ❓ ❓ Rust ❓ ❓ ❓ ❓

Slide 11

Slide 11 text

JavaScript ✅ CSS ✅ Ember.js ✅ Rust ✅ vim ✅ Emacs ✅ VS Code ✅ IntelliJ ✅

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

HTTP JSON API

Slide 14

Slide 14 text

ELS JSON-RPC Language Server Protocol HTTP JSON API

Slide 15

Slide 15 text

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,
 }

Slide 16

Slide 16 text

vscode-languageserver

Slide 17

Slide 17 text

Syntax Highlighting

Slide 18

Slide 18 text

Syntax Highlighting (sorry, not responsible for that...) https://github.com/Microsoft/language-server-protocol/issues/33#issuecomment-231883169

Slide 19

Slide 19 text

Autocomplete

Slide 20

Slide 20 text

Request {
 "method": "textDocument/completion",
 "params": {
 "textDocument": {
 "uri": "file: ///Users/tbieniek/Code/crates.io/app/templates/index.hbs",
 },
 "position": {
 "line": 5,
 "character": 18,
 },
 },
 }

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 25

Slide 25 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 26

Slide 26 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 27

Slide 27 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 28

Slide 28 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 29

Slide 29 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 30

Slide 30 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 31

Slide 31 text

AST Explorer https://astexplorer.net

Slide 32

Slide 32 text

{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression Cursor

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

let node = findNodeAtPosition(ast, position); let isSubExpressionPath = ( node.type === 'PathExpression' && node.parent.type === 'SubExpression' ); if (isSubExpressionPath) { return SUB_EXPRESSION_HELPERS; }

Slide 36

Slide 36 text

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, }));

Slide 37

Slide 37 text

Autocomplete ✅

Slide 38

Slide 38 text

Autocomplete #2

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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));

Slide 42

Slide 42 text

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));

Slide 43

Slide 43 text

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, }));

Slide 44

Slide 44 text

Autocomplete #2 ✅

Slide 45

Slide 45 text

Goto Definition

Slide 46

Slide 46 text

Request {
 "method": "textDocument/definition",
 "params": {
 "textDocument": {
 "uri": "file: ///Users/tbieniek/Code/crates.io/app/templates/crate.hbs",
 },
 "position": {
 "line": 92,
 "character": 14,
 },
 },
 }

Slide 47

Slide 47 text

{{crate-readme
 rendered=crate.readme}} Program -> MustacheStatement -> PathExpression -> Hash -> HashPair -> SubExpression -> PathExpression

Slide 48

Slide 48 text

let node = findNodeAtPosition(ast, position); let isMustacheStatementPath = ( node.type === 'PathExpression' && node.parent.type === 'MustacheStatement' ); if (!isMustacheStatementPath) { return; }

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

import URI from 'vscode-uri'; let componentPath = `${projectRoot}/app/components/${node.original}.js`; if (fs.existsSync(componentPath)) { results.push({ uri: URI.file(componentPath), range: { start: ..., end: ... }, }); }

Slide 51

Slide 51 text

What else can
 we build with this?

Slide 52

Slide 52 text

{{#link-to "index"}}Home{{/link-to}} • app/controllers/index.js • app/routes/index.js • app/templates/index.hbs

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

import Component from '@ember/component'; import { inject as service } from '@ember/service'; export default Component.extend({ session: service('session'), }); • app/services/session.js

Slide 55

Slide 55 text

{{t "my-name-is"}} • translations/de.json • translations/en.json • translations/fr.json • translations/nl.json

Slide 56

Slide 56 text

Caveats (roadblocks and deviations)

Slide 57

Slide 57 text

Compiler vs. Editor

Slide 58

Slide 58 text

Compiler > must produce 100% correct output > expects 100% valid input

Slide 59

Slide 59 text

Editor > is allowed to produce incomplete output > should handle incomplete/invalid input

Slide 60

Slide 60 text

@glimmer/syntax > written for the template compiler > can not handle incomplete/invalid input {{#link-to "|

Slide 61

Slide 61 text

ELS Editor Support

Slide 62

Slide 62 text

ELS Editor Support > VSCode ... done (vscode-ember extension) > Atom ... done (ide-ember package) > emacs ... work-in-progress > vim ... )

Slide 63

Slide 63 text

Wishlist (not yet supported by LSP)

Slide 64

Slide 64 text

File Type Icons

Slide 65

Slide 65 text

Goto Related File from the crates controller

Slide 66

Slide 66 text

Goto Related File > cycle through controller, route, template > or component and template > or model, adapter, serializer > or to the related test file

Slide 67

Slide 67 text

Inline Translations

Slide 68

Slide 68 text

emberwatch/ember-language-server #topic-editors

Slide 69

Slide 69 text

ELS the Ember Language Server Thanks!