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
250
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
210
Abstract Syntax Forestry
turbo87
0
59
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
90
Internationali[sz]ation: It's easy in Ember!
turbo87
1
390
EuregioCup 2018 - Selbstbriefing
turbo87
1
62
The Next Generation of Testing in Ember.js
turbo87
3
980
750 km FAI-Dreieck mit der Clubklasse
turbo87
3
61
High Level DOM Assertions for QUnit
turbo87
0
120
EuregioCup 2017 - Selbstbriefing
turbo87
1
43
Other Decks in Programming
See All in Programming
EventSourcingの理想と現実
wenas
6
2.3k
.NET のための通信フレームワーク MagicOnion 入門 / Introduction to MagicOnion
mayuki
1
1.5k
Jakarta EE meets AI
ivargrimstad
0
150
Pinia Colada が実現するスマートな非同期処理
naokihaba
4
220
【Kaigi on Rails 2024】YOUTRUST スポンサーLT
krpk1900
1
330
Realtime API 入門
riofujimon
0
150
as(型アサーション)を書く前にできること
marokanatani
10
2.6k
A Journey of Contribution and Collaboration in Open Source
ivargrimstad
0
900
cmp.Or に感動した
otakakot
2
140
3 Effective Rules for Using Signals in Angular
manfredsteyer
PRO
1
100
Outline View in SwiftUI
1024jp
1
330
よくできたテンプレート言語として TypeScript + JSX を利用する試み / Using TypeScript + JSX outside of Web Frontend #TSKaigiKansai
izumin5210
6
1.7k
Featured
See All Featured
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
4
370
The Language of Interfaces
destraynor
154
24k
[RailsConf 2023] Rails as a piece of cake
palkan
52
4.9k
The World Runs on Bad Software
bkeepers
PRO
65
11k
Six Lessons from altMBA
skipperchong
27
3.5k
Optimising Largest Contentful Paint
csswizardry
33
2.9k
StorybookのUI Testing Handbookを読んだ
zakiyama
27
5.3k
The Cost Of JavaScript in 2023
addyosmani
45
6.7k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
169
50k
Rebuilding a faster, lazier Slack
samanthasiow
79
8.7k
A better future with KSS
kneath
238
17k
For a Future-Friendly Web
brad_frost
175
9.4k
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!