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

Du willst eine Language Extension bauen

Du willst eine Language Extension bauen

Syntax Highlighting kann doch nicht so schwierig sein, oder? Und wenn das funktioniert, bauen wir schnell noch Autocomplete dazu …
In diesem Talk nehme ich euch mit auf meine Reise durch die Extension-Systeme verschiedener Editoren. Wir schauen hinter die Kulissen von Syntax Highlighting und Language Servern und darauf, welche überraschenden Unterschiede und Fallstricke zwischen den Editor-Ökosystemen warten.

Avatar for Dominic Lüchinger

Dominic Lüchinger

October 16, 2026

More Decks by Dominic Lüchinger

Other Decks in Programming

Transcript

  1. 👋 Ich bin Dominic     🛠️ Software

    Engineer – Platformen und API sind mein Zuhause 🚀 DevOps erfahren - bringe Systeme zum Fliegen 🔍 Observability ist kein Fremdwort – tauchen gern in die Tiefe 🗣️ Immer auf Entdeckungsreise – Neues lernen und Wissen weitergeben
  2. "Kann doch nicht so schwierig sein, oder?" examples/diagnostics/broken.yaml processors: batch:

    transform: log_statements: - context: log statements: - set(body, "bear") where attributes["http.path"] == "/animal" exporters: debug: bogus_exporter_type: endpoint: http://nowhere service: pipelines: traces: receivers: [otlp] processors: [batch, transform] exporters: [does_not_exist] nonsense_signal: receivers: [otlp] exporters: [debug] Drei Fehler drin. ⚠️ Keiner leuchtet.
  3. Sieht aus wie YAML. Ist ein Komponentengraph. Sieht aus wie

    Ist eigentlich Freitext-Strings Querverweise zwischen receivers / processors / exporters / pipelines Eine Sprache 7 Distributionen (contrib, k8s, datadog, elastic …) mit je eigenen Komponenten Ein String-Wert Env-Var-Substitution oder eingebettetes OTTL — je nach Schlüssel Generische YAML-Tools (Red Hat YAML Extension, SchemaStore) kennen diese Domänen-Semantik nicht.
  4. "Ist doch nur Syntax Highlighting" otelcol-yaml.tmLanguage.json — echte Regel {

    } "match": "^(\\s*)(condition|statement)\\s*:\\s+(.+?)\\s*(#.*)?$", "captures": { "2": { "name": "entity.name.tag.yaml" }, "3": { "name": "meta.embedded.block.ottl" } } Regex an, Scope zugewiesen, Farbe an.
  5. Hübsch gefärbt. Immer noch kaputt. exporters: debug: bogus_exporter_type: # <-

    unbekannte Komponente endpoint: http://nowhere service: pipelines: traces: exporters: [does_not_exist] # <- nirgends definiert
  6. Das M×N-Problem Ohne LSP: M Sprachen × N Editoren =

    M×N Integrationen. Mit LSP: M + N.
  7. Wir erfinden nichts Neues 80+ Editoren & IDEs sprechen LSP

    bereits. VS Code, alle JetBrains-IDEs, Vim, Emacs, Sublime, Zed, Helix, Eclipse, Theia, JupyterLab … microsoft.github.io/language-server-protocol/implementors/tools
  8. Eine Session, Schritt für Schritt Erst der Handshake (Capabilities austauschen),

    dann beliebig viele Requests — z.B. der Hover von eben.
  9. Ein Hover, Byte für Byte otelcol-config.yaml 1 --2 3 receivers:

    4 otl¦p: 5 protocols: ... Request → Server { "jsonrpc": "2.0", "id": 1, "method": "textDocument/hover", "params": { "textDocument": { "uri": "file:///otelcol-config.yaml" }, "position": { "line": 4, "character": 5 } } } Response → Client { "jsonrpc": "2.0", "id": 1, "result": { "contents": "**otlp** receiver\nStability: beta" } } Spec: Language Server Protocol 3.18 (aktuelle Version) — microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/
  10. Das volle Spektrum an Capabilities rangeFormatting didChangeConfiguration selectionRange executeCommand documentHighlight

    codeAction showMessageRequest signatureHelp linkedEditingRange prepareRename pullDiagnostics implementation codeLens inlineValue onTypeFormatting semanticTokens completion formatting markdown typeHierarchy moniker willCreateFiles references hover definition showMessage telemetry inlayHint didRenameFiles willDeleteFiles workspaceSymbol applyEdit workDoneProgress synchronization inlineCompletion diagnostics foldingRange typeDefinition workspaceFolders rename documentLink documentSymbol callHierarchy declaration showDocument workspaceEdit didChangeWatchedFiles didChangeWorkspaceFolders fileOperations notebookDocumentSync
  11. So sieht das in echtem Code aus src/server/server.ts — gekürzt,

    echter Code aus otelcol-lang // import { createConnection, ProposedFeatures } from "vscode-languageserver/node"; // const connection = createConnection(ProposedFeatures.all); // Handshake: Server sagt, was er kann connection.onInitialize(() => ({ capabilities: { hoverProvider: true, completionProvider: { triggerCharacters: [":", " ", "\n", "-"] }, }, })); // Nur abgefragt, weil hoverProvider:true oben steht connection.onHover((params) => { const doc = documents.get(params.textDocument.uri); if (!doc) return null; return computeHover(model, doc.uri, componentsIndex, params.position); });
  12. Was der Server kann Hover Diagnostics Inkl. Codeowners, Warnings und

    FeatureGates aus metadata.yaml Undefined/ambiguous refs, unused components, Signal-Kompatibilität Completion Navigation Kontextsensitiv — 5 Branches je nach Cursor-Position Semantic Tokens, Go-to-Definition, Find References Config Sets 7 Distributionen Querverweise über mehrere YAML-Dateien hinweg Schema-Index (componentsIndex) pro Distribution: contrib, k8s, datadog, elastic …
  13. Ein Server, alle Editoren Einziges Bundle dist/server/server.js (stdio-Einstiegspunkt) ↓ IPC

    VS Code LSP4IJ JetBrains WASM+PATH Zed PATH Helix Der Server ist überall derselbe. Aber eine Extension ist mehr als ein LSP-Client.
  14. Eine Datei, Schritt für Schritt otelcol-config.yaml receivers: otlp: protocols: grpc:

    endpoint: 0.0.0.0:4317 service: pipelines: traces: receivers: [otlp] exporters: [otlp/foo] DocModel — was buildModel() extrahiert components.receiver: { "otlp" → { idRange: 2:3-2:7, configRange: 2:3-5:34 } } pipelines: [{ id: "traces", signal: "traces", receivers: [{ id: "otlp", range: 10:18-10:22 }], exporters: [{ id: "otlp/foo", range: 11:18-… }] }] buildModel() läuft einmal pro geöffneter/geänderter Datei (src/server/yaml-model.ts, 712 Zeilen). Jede extrahierte ID behält ihre exakte Zeilen/Spalten-Range im Originaltext — die Grundlage für Hover, Go-to-Definition und präzise Diagnostics.
  15. Vom Byte zum Index Event didOpen / didChange → Parsing,

    pro Datei buildModel() → DocModel → Vereinigung bei Config-Sets buildSetModel() → SetModel → + Schema componentsIndex pro Distribution → LSP-Antwort hover.ts · completion.ts · …
  16. Was eine Extension wirklich braucht Editor Manifest Highlighting Publishing VS

    Code package.json TextMate .tmLanguage.json vsce publish → Marketplace, Token VSCE_PAT JetBrains plugin.xml + build.gradle.kts TextMate-Bundle (IntelliJ liest TextMate direkt) Gradle publishPlugin, Token Zed extension.toml + Cargo.toml (Rust → Tree-sitter-Queries (.scm) Helix languages.toml Tree-sitter-Queries (.scm) WASM) JETBRAINS_MARKETPLACE_TOKEN PR gegen zed-industries/extensions — kein Token, Code-Review statt Marketplace-Key Keine Registry — Nutzer fügt die Config selbst ein Der LSP-Server ist überall derselbe Code. Manifest, Highlighting-Format und PublishingWeg sind viermal komplett eigener Code.
  17. Was sonst noch alles ins package.json muss Block Zweck Beispiel

    aus otelcol-lang Marketplace-Metadaten Name, Publisher, Icon, Kategorien, Mindest-VS-Code-Version publisher: "otelery", engines.vscode: "^1.91.0" contributes.languages Language-ID, Aliase, Dateiendungen, firstLine-Regex firstLine: "^#\s*(otelcol|opentelemetrycollector|configset-otelcol:)" contributes.grammars TextMate-Grammar-Zuordnung und Embedded Languages embeddedLanguages: { "meta.embedded.block.ottl": "ottl" } — die OTTL-Injection von vorhin contributes.configuration Settings-Schema, erscheint in der VS-Code-Settings-UI 9 Einstellungen: otelcol.distribution, otelcol.ottlLspPath, … semanticTokenScopes + configurationDefaults Default-Farben für eigene TokenTypen, die kein Theme kennt 13 Farbregeln von Hand — sonst bleibt alles weiss activationEvents Wann die Extension überhaupt aufwacht onLanguage:otelcol, onLanguage:yaml Dasselbe Problem wie bei JetBrains eben (Semantic Tokens ohne Farbe) — nur hier über package.json gelöst statt über Code.
  18. Was der Extension-Code sonst noch macht Dateien erkennen Server finden

    & starten Erkennen & umklassifizieren (Sniffer- bzw. FileType-Hook) Gebündelt, PATH oder npm-Autoinstall Einstellungen durchreichen Fehlende Features nachrüsten Client-Settings an den Server (otelcol.distribution, …) Was der Host nicht automatisch bietet, z.B. JetBrains' Semantic-Token-Farbzuordnung Dev-Loop Bundle-Cache & Versionsprüfung Server-Binary beobachten, bei Änderung automatisch neu verbinden JetBrains extrahiert + hash-prüft den Server aus dem Plugin-JAR
  19. Gemeinsamer Kern vs. Editor-Glue-Code Zeilen Code, nicht Zeilen Config —

    Manifeste, TOML und Queries zählen hier nicht mit. 3’904 Gemeinsamer LSP-Kern 656 JetBrains Zed 222 VS Code 182 Helix 0
  20. Live-Demo: VS Code • Config mit Tippfehler in einer Receiver-Referenz

    öffnen → Diagnostic erscheint • Hover auf eine Komponente → Doku & Codeowner • Im Pipeline-Block Strg+Space → kontextsensitive Vorschläge • Go-to-Definition über Dateigrenzen hinweg
  21. Live-Demo: JetBrains • Dieselbe Datei öffnen • Diagnostic, Hover, Completion

    wiederholen • Pointe: identisches Verhalten, komplett andere Plattform-API dahinter
  22. Wer lässt sich automatisiert testen? Editor Mechanismus Tiefe @vscode/test-cli, echter

    Echte Feature-Tests, auch gegen das gepackte .vsix BasePlatformTestCase + echtes LSP4IJ 7 Kotlin-Testklassen, echte FeatureTests Zed Cargo-Tests gegen TOML/Queries Nur statisch, kein echter Editor in CI Helix hx --health otelcol Rauchtest, keine Feature-Tests VS Code JetBrains Extension Host
  23. Dieselbe Idee, zweimal weitergereicht Protokoll Von Seit Transport Verbindet LSP

    Microsoft 2016 JSON-RPC, stdio/IPC Editor und Language Server MCP Anthropic Nov. 2024 JSON-RPC 2.0, stdio/HTTP KI-App und Tools/Resources/Prompts ACP Zed Industries Aug. 2025 JSON-RPC, stdio Editor und Coding-Agent "Just as LSP unbundled language intelligence from monolithic IDEs, ACP's goal is to switch agents without switching your editor." — Nathan Sobo, CEO Zed JetBrains ist seit Oktober 2025 Co-Entwickler von ACP.
  24. Es ist machbar — hier ist die Landkarte Geteilter Kern

    Klein anfangen Handarbeit bleibt Ein geteilter Kern (YAMLKlassifizierer + LSP-Server) plus dünne Editor-Shims reicht für echte Reichweite über VS Code, JetBrains, Zed und Helix. Dort anfangen, wo die Editor-API am großzügigsten ist (VS Code), dann erweitern — nicht umgekehrt. LSP löst das IntelligenzProblem. Start, Discovery und Bundling pro Editor bleiben Handarbeit — und waren die eigentliche Arbeit.
  25. VS Code klassifiziert um. Die anderen nicht. Eine Datei muss

    erst als otelcol erkannt werden, bevor der LSP-Server sie überhaupt sieht. Jeder Editor löst das anders: Mechanismus VS Code JetBrains Zed Helix Offene YAML-Datei zur Laufzeit umklassifizieren ja ja nur bei Namens-Match nein Erste-Zeile-Muster (firstLine) nativ im Editor Sniffer-Port nur bei Namens-Match keine Entsprechung Reine Dateiname/GlobErkennung ja ja ja einzige Methode Ohne passenden Dateinamen bleibt eine Config in Zed/Helix für immer generisches YAML — egal was drinsteht.
  26. Wir waren nicht die Ersten mit "otel*.yaml" • Alter Sidecar-Dateiname:

    otelcol-configset.yaml. • SchemaStore.org — der Katalog hinter der Red-Hat-YAML-Extension (VS Code) und JetBrains' eingebautem Schema-Katalog — hat bereits einen startverankerten Glob otel*.yaml registriert. Für ein komplett anderes Schema: die OpenTelemetry-SDK-Declarative-Configuration. • Ergebnis: beide Editoren haben automatisch das falsche Schema auf unsere Datei angewendet — Validierungsfehler aus einem fremden Projekt, völlig ungefragt. • Fix: umbenannt zu configset.otelcol.yaml / # configset-otelcol: — behält unser eigenes Suffix, meidet aber den SchemaStore-Glob.
  27. Jedes Ökosystem tickt anders LSP standardisiert, wie man mit dem

    Server spricht — nicht, wie man ihn startet, findet oder bündelt.
  28. JetBrains: GUI-Start sieht ein anderes PATH Auf macOS/Linux fehlt Homebrew/nvm

    im PATH des IDE-Launchers — node wird nicht gefunden, obwohl im Terminal alles läuft. OtelcolLspServerFactory — Auflösungsreihenfolge -Dotelcol.lsp.command → fertiges Kommando, --stdio angehängt -Dotelcol.lsp.node → expliziter Node-Binary-Pfad Shell-PATH via EnvironmentUtil → findet Homebrew/nvm/node literal "node" → letzter Ausweg, erbt gestripptes PATH
  29. Zed: Server bündeln ist verboten VS Code & JetBrains packen

    den Server direkt in .vsix/.zip. Zeds Extension-Policy verbietet das — der Server muss zur Laufzeit aufgelöst werden. Tier Quelle Verhalten 1 lsp.otelcol.binary.path Explizite Einstellung gewinnt 2 worktree.which(...) Globale Installation auf PATH 3 npm_install_package(...) Auto-Install aus der npm-Registry Bug A Zed ignoriert das #!/usr/bin/env node-Shebang beim Spawnen von .js Bug B npm-Paketname ≠ Bin-Name — eine alte Fehlermeldung nannte sogar das falsche Paket.