Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ELS – The Ember Language Server
Search
Tobias Bieniek
October 11, 2018
Programming
0
260
ELS – The Ember Language Server
Tobias Bieniek
October 11, 2018
Tweet
Share
More Decks by Tobias Bieniek
See All by Tobias Bieniek
Please wait… Oh, it didn't work!
turbo87
0
220
Abstract Syntax Forestry
turbo87
0
63
Help! How do you let others know what’s going on when you’re 8000 feet up in the air in a plane without an engine?!
turbo87
0
110
Internationali[sz]ation: It's easy in Ember!
turbo87
1
410
EuregioCup 2018 - Selbstbriefing
turbo87
1
64
The Next Generation of Testing in Ember.js
turbo87
3
1k
750 km FAI-Dreieck mit der Clubklasse
turbo87
3
66
High Level DOM Assertions for QUnit
turbo87
0
130
EuregioCup 2017 - Selbstbriefing
turbo87
1
45
Other Decks in Programming
See All in Programming
マイコンでもRustのtestがしたい その2/KernelVM Tokyo 18
tnishinaga
2
2.2k
Strands Agents で実現する名刺解析アーキテクチャ
omiya0555
1
120
書き捨てではなく継続開発可能なコードをAIコーディングエージェントで書くために意識していること
shuyakinjo
1
270
Amazon Q CLI開発で学んだAIコーディングツールの使い方
licux
3
180
令和最新版手のひらコンピュータ
koba789
13
7.6k
iOS開発スターターキットの作り方
akidon0000
0
240
What's new in Adaptive Android development
fornewid
0
140
202507_ADKで始めるエージェント開発の基本 〜デモを通じて紹介〜(奥田りさ)The Basics of Agent Development with ADK — A Demo-Focused Introduction
risatube
PRO
6
1.4k
「リーダーは意思決定する人」って本当?~ 学びを現場で活かす、リーダー4ヶ月目の試行錯誤 ~
marina1017
0
220
Reactの歴史を振り返る
tutinoko
1
180
Portapad紹介プレゼンテーション
gotoumakakeru
1
130
ワープロって実は計算機で
pepepper
2
1.3k
Featured
See All Featured
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
183
54k
Optimizing for Happiness
mojombo
379
70k
The Art of Programming - Codeland 2020
erikaheidi
54
13k
Designing for humans not robots
tammielis
253
25k
VelocityConf: Rendering Performance Case Studies
addyosmani
332
24k
How to Ace a Technical Interview
jacobian
278
23k
Done Done
chrislema
185
16k
XXLCSS - How to scale CSS and keep your sanity
sugarenia
248
1.3M
Being A Developer After 40
akosma
90
590k
Imperfection Machines: The Place of Print at Facebook
scottboms
267
13k
Build The Right Thing And Hit Your Dates
maggiecrowley
37
2.8k
Adopting Sorbet at Scale
ufuk
77
9.5k
Transcript
ELS the Ember Language Server
Turbo87 TobiasBieniek
simplabs based in Munich consulting all over ! * Stickers
available after the talk
Disclaimer this talk contains simplified code examples! the real code
is available at https://github.com/emberwatch/ember-language-server
What text editor do you use? " # $ %
$ & ' ( ) * " *
None
Goto Class
Goto Definition
IntelliJ-only
vim Emacs VS Code IntelliJ JavaScript ❓ ❓ ❓ ❓
CSS ❓ ❓ ❓ ❓ Ember.js ❓ ❓ ❓ ❓ Rust ❓ ❓ ❓ ❓
JavaScript ✅ CSS ✅ Ember.js ✅ Rust ✅ vim ✅
Emacs ✅ VS Code ✅ IntelliJ ✅
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
HTTP JSON API
ELS JSON-RPC Language Server Protocol HTTP JSON API
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, }
vscode-languageserver
Syntax Highlighting
Syntax Highlighting (sorry, not responsible for that...) https://github.com/Microsoft/language-server-protocol/issues/33#issuecomment-231883169
Autocomplete
Request { "method": "textDocument/completion", "params": { "textDocument": { "uri": "file:
///Users/tbieniek/Code/crates.io/app/templates/index.hbs", }, "position": { "line": 5, "character": 18, }, }, }
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
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
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
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
AST Explorer https://astexplorer.net
{{button onclick=(a)}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression Cursor
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
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
let node = findNodeAtPosition(ast, position); let isSubExpressionPath = ( node.type
=== 'PathExpression' && node.parent.type === 'SubExpression' ); if (isSubExpressionPath) { return SUB_EXPRESSION_HELPERS; }
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, }));
Autocomplete ✅
Autocomplete #2
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
import glob from 'fast-glob'; let components = glob.sync(['**/*.js'], { cwd:
`${projectRoot}/app/components`, }); let templates = glob.sync(['**/*.hbs'], { cwd: `${projectRoot}/app/templates/components`, });
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));
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));
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, }));
Autocomplete #2 ✅
Goto Definition
Request { "method": "textDocument/definition", "params": { "textDocument": { "uri": "file:
///Users/tbieniek/Code/crates.io/app/templates/crate.hbs", }, "position": { "line": 92, "character": 14, }, }, }
{{crate-readme rendered=crate.readme}} Program -> MustacheStatement -> PathExpression -> Hash ->
HashPair -> SubExpression -> PathExpression
let node = findNodeAtPosition(ast, position); let isMustacheStatementPath = ( node.type
=== 'PathExpression' && node.parent.type === 'MustacheStatement' ); if (!isMustacheStatementPath) { return; }
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
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: ... }, }); }
What else can we build with this?
{{#link-to "index"}}Home{{/link-to}} • app/controllers/index.js • app/routes/index.js • app/templates/index.hbs
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
import Component from '@ember/component'; import { inject as service }
from '@ember/service'; export default Component.extend({ session: service('session'), }); • app/services/session.js
{{t "my-name-is"}} • translations/de.json • translations/en.json • translations/fr.json • translations/nl.json
Caveats (roadblocks and deviations)
Compiler vs. Editor
Compiler > must produce 100% correct output > expects 100%
valid input
Editor > is allowed to produce incomplete output > should
handle incomplete/invalid input
@glimmer/syntax > written for the template compiler > can not
handle incomplete/invalid input {{#link-to "|
ELS Editor Support
ELS Editor Support > VSCode ... done (vscode-ember extension) >
Atom ... done (ide-ember package) > emacs ... work-in-progress > vim ... )
Wishlist (not yet supported by LSP)
File Type Icons
Goto Related File from the crates controller
Goto Related File > cycle through controller, route, template >
or component and template > or model, adapter, serializer > or to the related test file
Inline Translations
emberwatch/ember-language-server #topic-editors
ELS the Ember Language Server Thanks!