Sprachlehrer für Visual Studio Code
Für Microsofts Sourcecode-Editor lassen sich ohne viel Aufwand Erweiterungen erstellen, die beim Entwickeln mit weniger verbreiteten Programmiersprachen helfen.
- Nils Andresen
Hilfen wie Syntax-Hervorhebung, Autovervollständigung und gute Code-Schnipsel sind für viele Entwickler das A und O einer Entwicklungsumgebung. Visual Studio Code bietet nicht nur viele Erweiterungen für zahlreiche Programmiersprachen, sondern ermöglicht darüber hinaus das Erstellen eigener Extensions, wenn die Programmiersprache der Wahl nicht auf der Liste steht. Der Artikel zeigt am Beispiel der Template-Sprache von FirstSpirit, wie Entwickler eine einfache eigene Erweiterung für die IDE erstellen können.
Das Produkt FirstSpirit ist ein Enterprise-CMS der Firma e-Spirit. Damit können Redakteure Inhalte nicht nur in Standard-Eingabekomponenten erfassen, sondern Entwickler können zusätzlich spezielle Eingabekomponenten erstellen. Die Anwendung gibt die von den Redakteuren erfassten Informationen anhand von Vorlagen aus, für deren Erstellung FirstSpirit über eine eigene Template-Sprache verfügt.
Erste Schritte zur Vorbereitung
Zum Erstellen einer Erweiterung für Visual Studio Code sieht die Dokumentation die Werkzeuge npm, Yeoman und Visual Studio Code Extension Generator vor. Nach der Installation von npm installiert folgende Zeile Yeoman und den Generator auf dem System:
npm install -g yo generator-code
yo code
Der Extension Generator erstellt das Grundgerüst in einer passenden Dateistruktur, die er mit einem einfachen Beispiel befüllt. Dazu fragt er ein paar Punkte ab (s. Abb. 1).
Alle im Artikel gezeigten Beispiele sind unter Windows 10 und Ubuntu getestet und sollten sich auf jedem anderen aktuellen Linux sowie unter macOS ebenso durchführen lassen.
Unter Linux und macOS installieren die Befehle npm install -g ...
die genannten Komponenten systemweit mit dem Parameter -g
und gegebenenfalls einem vorgestellten sudo
für administrative Rechte. Auf Windows Systemen muss das Kommandofenster administrative Rechte erhalten.
Zusammenhänge und Überblick
Das mit dem Generator erstellte Paket enthält drei wichtige Dateien: package.json mit den Grundeinstellungen des Projekts, language-configuration.json mit der eigentlichen Konfiguration der Sprache und fs.tmLanguage.json im Unterordner syntaxes mit der Syntax der Sprache im TextMate-Format.
package.json enthält neben den Grundeinstellungen des Projekts unter dem Punkt contributes
die Elemente, die die Erweiterung zu Visual Studio Code beisteuert (s. Abb. 2).
Der erste Punkt unter contributes
ist languages
, der die Programmiersprache(n) definiert. Den in id
definierten Wert referenzieren die folgenden Elemente. configuration
verweist auf die Konfiguration der angegebenen Sprache – im konkreten Fall auf die Datei language-configuration.json.
Der Punkt grammars
definiert die Grammatik der Sprache. Dessen Unterpunkt language
gibt die id
der Sprache an, für die die Grammatik gilt. path
verweist auf die Datei mit der Definition der Grammatik.
Die Datei language-configuration.json enthält die Konfiguration der Sprache in folgenden Abschnitten:
comments
bestimmt das Format der Kommentare, unterteilt inlineComment
blockComment
für einzeilige beziehungsweise Blockkommentare. Die Angaben verwendet Visual Studio Code für die Standardfunktion zum Ein- und Auskommentieren wie Shift | Alt | A zum Umschalten von Blockkommentaren. Da FirstSpirit keine Zeilenkommentare kennt, fehlt die Angabe im Beispiel. Blockkommentare starten mit$--
und enden auf--$
.brackets
beschreiben die gültigen Klammerpaare. Der Editor verwendet die Angabe unter anderem, um das passende Gegenstück zu einer selektierten Klammer hervorzuheben.autoClosingPairs
definiert Kombinationen, bei denen Visual Studio Code den zweiten Teil automatisch einfügt, nachdem Entwickler den ersten Teil getippt haben.- Die Angabe unter
surroundingPairs
beschreibt Klammerpaare, die einen gewählten Bereich einschließen. Wenn beispielsweise(
und)
als Paar angegeben sind, können Entwickler einen beliebigen Text markieren und "(" eingeben, damit der Editor den Bereich mit dem gewählten Klammerpaar einschließt.
Die language-configuration.json sieht für die FirstSpirit-Definition folgendermaßen aus:
{
"comments": {
"blockComment": [ "$--", "--$" ]
},
"brackets": [
["{", "}"], ["[", "]"], ["(", ")"]
],
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"],
{ "open": "$--", "close": "--$",
"notIn": ["comment"] },
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
}
Farben für die Syntax
Die Definition von Syntaxhervorhebungen in Form der farblichen Markierungen für einzelne Befehle und Schlüsselworte erfolgen in einer Erweiterung für Visual Studio Code in zwei Teilen:
- dem Zerlegen des Editor-Inhalts in Tokens, also Worte oder Abschnitte, die eine bestimmte Bedeutung haben und
- der Farbgebung.
Beides geschieht in der Datei fs.tmLanguage.json. Der Inhalt der Datei ist im TextMate-Format angelegt: ein JSON-Format, mit dem sich reguläre Ausdrücke (nach Oniguruma) verwenden lassen, um den Text in Tokens zu zerlegen und ihnen Namen beziehungsweise Scopes zuzuweisen. Dabei handelt es sich um die Einfärbung (ähnlich wie CSS-Klassen), die einem Farbschema entsprechen müssen.
Um der Grammatik der Sprache nicht zusätzlich eine eigene Farbgebung hinzuzufügen und den Nutzern der Erweiterung eigene Farben aufzudrängen, ist es sinnvoll, sich an den existierenden Namen beziehungsweise Scopes zu orientieren, die sich unter anderem in den Theme-Dateien wie light_plus.json für Visual Studio Code finden.
Eine einfache Regel sieht folgendermaßen aus:
"closetag": {
"name": "keyword.control.fs",
"match": "\\$CMS_END_([^\\$]+)\\$",
"captures": {
"1": {
"name": "entity.name.class.fs"
}
}
}
- Das Schlüsselwort der Regel (hier
closetag
) kann beliebig ausfallen, muss aber innerhalb der Datei eindeutig sein. name
bezeichnet den Scope und damit die Färbung des erkannten Tokens. Es stellt eine Hierarchie dar und muss laut Konvention in der ID der Sprache (fs
) enden.keyword.control
bezeichnet die Standardfarbe für Schlüsselworte, die den Programmablauf steuern (im konkreten Beispiel lila).match
bestimmt den regulären Ausdruck zum Erkennen des Tokens: Im konkreten Fall alle Bereiche in der Form$CMS_END_Kommando$
, wobei der TeilKommando
– alles nach dem zweiten Unterstrich und vor dem zweiten Dollar-Zeichen – zusätzlich in eine sogenannte Capture Group gefasst ist, die an den runden Klammern erkennbar ist.- Mit
capture
lassen sich die Capture Groups des regulären Ausdrucks mit abweichenden Namen beziehungsweise Scopes versehen. Capture Groups beginnen in der Zählung immer bei 0, wobei die 0-Gruppe den ganzen erkannten Ausdruck darstellt. Im konkreten Fall wird einzig die 1-Gruppe mitentity.name.class.fs
benannt. Der erste Teil,entity.name.class
, bezeichnet die Standardfarbe für eine Klasse, die im konkreten Beispiel grün ist.fs
ist erneut die ID der aktuellen Sprache.
Alternativ können Entwickler zwei reguläre Ausdrücke angeben: begin
beschreibt den Anfang und end
das Ende des Tokens. Falls in dieser Syntax Capture Groups vorkommen sollen, müsste entsprechend beginCaptures
und endCaptures
das captures
ersetzen:
"comment": {
"name": "comment.line.fs",
"begin": "\\$--",
"end": "--\\$"
}
Patchwork mit Codeschnipseln
Bei Codeschnipseln handelt es sich um vorbereitete Fragmente, die Entwickler über eine Abkürzung in den bestehenden Code einfügen können. Die Definition erfolgt unter snippets
im Abschnitt contributes
in der Datei package.json:
"snippets": [{
"language": "fs",
"path": "./snippets/fs.json"
}]
Erneut beschreibt language
die ID der Programmiersprache und path
verweist auf die Datei, die die Schnipsel beschreibt. Entwickler müssen sie manuell anlegen, um darin die Definition der Schnipsel im TextMate-Format zu deklarieren. Folgender Abschnitt definiert ein $CMS_FOR()
-Codeschnipsel.
"cms_for": {
"prefix": "cms-for",
"body": [
"\\$CMS_FOR(${1:identifier}, ${2:object})\\$",
"$0",
"\\$CMS_END_FOR\\$"
],
"description": "For Loop"
}
Der Code fügt drei Zeilen mit zwei Platzhaltern ein und platziert anschließend den Cursor in der zweiten Zeile zwischen $CMS_FOR()$
und $CMS_END_FOR$
(s. Abb. 3).
Der Schlüssel für den Schnipsel (cms_for
) ist beliebig wählbar, muss aber in der Datei eindeutig sein. description
gibt einen Text an, den Visual Studio Code als Beschreibung bei der Auswahl des Schnipsels darstellt. prefix
definiert das Kürzel, das das Einfügen im Editor auslöst. body
gibt den Inhalt als Array an: Jede Zeile des Codeschnipsels entspricht einem Element im Array.
Der Inhalt des Schnipsels kann auf einige Variablen wie den Namen der aktuell im Editor geöffneten Datei zugreifen. Eine vollständige Liste der Variablen ist in der Dokumentation verfügbar. Zusätzlich lassen sich Tabulator-Sprungpunkte für Platzhalter definieren, die nach dem Einfügen des Code-Schnipsels noch ausgefüllt werden müssen.
Der spezielle Sprungpunkt $0
bestimmt die Position des Cursors, nach dem Einfügen des Schnipsels und Auffüllen aller Platzhalter. Standardmäßig steht der Cursor hinter dem letzten Zeichen des Codeschnipsels.
Deklaratives Falten
Code-Folding beschreibt das Zu- und Aufklappen bestimmter Bereiche im Editor, um den Inhalt für die besseren Übersicht auszublenden. Visual Studio Code bietet einen integrierten Mechanismus, für den es die Einrückungstiefe des Quellcodes nutzt. Diese Form des Ausblendens funktioniert immer und ist nicht abschaltbar.
Eine zusätzliche Regel lässt sich in der Sprachkonfiguration erstellen. Entwickler können in der language-configuration.json unter folding
mit regulären Ausdrücken für die Start- und die Endzeile eines Bereichs eine zusätzliche Regel für das Code-Folding einrichten:
"folding": {
"markers": {
"start": "^.*\\$CMS_(?!END)[^\\$]+\\$.*$",
"end": "^.*\\$CMS_END_[^\\$]+\\$.*$"
}
}
Der Nachteil ist, dass sich auf diese einfache Weise keine komplexeren Sachverhalte abbilden lassen. Dass für die Beispielsprache das Code-Folding für Bereiche von $CMS_SET()$
bis $CMS_END_SET$
und von $CMS_IF()$
bis $CMS_END_IF$
, aber nicht für Bereiche von $CMS_SET()$
bis $CMS_END_IF$
gelten soll, lässt sich nicht in zwei einfachen regulären Ausdrücken abbilden. Visual Studio Code kennt jedoch zwei erweiterte Formen der Definition: Das Anbinden einer API-Schnittstelle und der Einsatz eines Sprachservers.
Abbildung 4 zeigt einen weiteren Nachteil des gezeigten Vorgehens: Die regulären Ausdrücke sorgen für Code-Folding von $CMS_ELSE$
bis $CMS_END_IF$
. Dadurch ist $CMS_IF$
frei, um es bis zu $CMS_END_FOR$
einzuklappen. Ein weiteres Code-Folding ergibt sich somit zwischen den nicht zusammenhängenden Ausdrücken $CMS_FOR$
aus der obersten Zeile und dem $CMS_END$
.
Zeit für Tests
Nach dem Erstellen der Erweiterung über das yeoman-template liegt eine vorbereitete launch.json im .vscode-Ordner der Erweiterung zum Testen bereit. Der Befehl Debug: Start Debugging
(F5) startet eine neue Instanz von Visual Studio Code mit der installierten Erweiterung aus dem Entwicklungsordner.
Dadurch können Entwickler in einer Instanz von Visual Studio Code die Erweiterung schreiben, während sie in einer weiteren Instanz die Anpassungen direkt testen. Allerdings müssen sie nach Änderungen an der Erweiterung zunächst die Testinstanz über den Befehl Developer: Reload Window
neu laden.
Programmierte Funktionen
Zusätzlich zu den deklarativen Funktionen lassen sich auch API-Funktionen implementieren. Alle API-Endpunkte stehen im Bereich vscode.languages.* unter programmatic-language-features
in der Dokumentation über Dokumentation zur Verfügung (s. Abb. 5).
Derzeit existieren 19 Endpunkte, die sich ansprechen lassen. Beispielhaft kommt im Folgenden der für das Code-Folding zum Tragen. Um in TypeScript statt JavaScript zu entwickeln, gilt es zunächst, die passenden Dependencies hinzuzufügen:
npm install --save-dev typescript vscode @types/node
Die Dokumentation über Aktivierungs-Events beschreibt unter anderem, dass Entwickler ein activationEvent
benennen und eine activate()
-Methode aus dem main
-Modul implementieren müssen. Für eine sprachspezifische Erweiterung können sie das onLanguage
-Event verwenden. Es aktiviert die Erweiterung beim Laden einer Datei in der passenden Programmiersprache:
"main": "./js/index",
"activationEvents": [
"onLanguage:fs"
],
Die Sprach-ID findet sich dabei in der Zeile "onLanguage:fs"
, während "main": "./js/index"
auf die JavaScript-Datei mit der zugehörigen activate()
-Methode verweist. Wer in TypeScript entwickelt, muss die Ausgabe in das korrekte Verzeichnis (js/) umleiten. Dazu dient die Zeile "outdir": "js"
in der tsconfig.json:
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "js",
"lib": [
"es6"
],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true
}
}
Das Umwandeln von TypeScript in JavaScript übernimmt der tsc-compiler, der sich, deklariert als scripts
in package.json, am einfachsten über npm run build aufrufen lässt.
Programmiertes Falten
Das Code-Folding lässt sich über folgende Methode registrieren:
registerFoldingRangeProvider(
selector: DocumentSelector,
provider: FoldingRangeProvider)
Der Parameter DocumentSelector
ist im einfachsten Fall die id
aus der languages
-Definition. Der zweite Parameter legt den Provider für das Code-Folding fest. Der Rückgabewert des Aufrufs ist eine Referenz auf die Registrierung, über die sich der Provider wieder entfernen lässt, wenn die Erweiterung ihn nicht mehr benötigt wird. Visual Studio Code bringt dafür einen eigenen Mechanismus mit: Der ExtensionContext
der activate()
-Methode hat ein subscriptions
-Array, mit dem Visual Studio Code eigenständig Registrierungen sauber entfernt. Der Provider lässt sich mit folgenden Codezeilen registrieren:
import { ExtensionContext,languages } from "vscode";
import { FsFoldingProvider } from "./foldingProvider"
export function activate(context: ExtensionContext): void {
console.log("FS-Extension is activated!");
context.subscriptions.push(
languages.registerFoldingRangeProvider(
"fs", new FsFoldingProvider));
}
Der Provider ist in der API-Dokumentation beschrieben und muss die folgende Methode implementieren:
provideFoldingRanges(document: TextDocument,
context: FoldingContext,
token: CancellationToken):
ProviderResult<FoldingRange[]>
document: TextDocument
ist eine Referenz auf das Dokument. context: FoldingContext
ist derzeit ungenutzt und in der Dokumentation als "for future use" beschrieben. Der letzte Parameter token: CancellationToken
ist für asynchrone oder lang andauernde Operationen ausgelegt. Anhand des Tokens lässt sich mit token.isCancellationRequested
überprüfen, ob die aktuelle Berechnung noch notwendig ist. Eine weitere Methode hierfür ist die Callback-Funktion token.onCancellationRequested
.
Folgender Codeausschnitt zeigt eine einfache Implementierung eines Code-Folding-Providers, der jeweils drei Zeilen einer Datei einklappt (s. Abb. 6):
import {
FoldingRangeProvider,
TextDocument,
FoldingContext,
CancellationToken,
ProviderResult,
FoldingRange } from 'vscode';
export class FsFoldingProvider
implements FoldingRangeProvider
{
provideFoldingRanges(document: TextDocument,
context: FoldingContext,
token: CancellationToken):
ProviderResult<FoldingRange[]>
{
if(token.isCancellationRequested){
return null;
}
// Immer drei Zeilen einklappen:
const folds = Math.floor(document.lineCount / 3);
const ranges:FoldingRange[] = [];
for(let i=0; i<folds; i++){
let start = i * 3;
let end = start + 2;
ranges.push(new FoldingRange(start, end));
}
return ranges;
}
}
Obwohl der einfache Provider nicht wirklich sinnvoll erscheint, erfüllt er einen ersten Zweck: Der Code ist simpel und birgt wenig Potential für Fehler. Auf die Weise lässt sich das Einbinden auf einfache Weise testen.
Folgender Code führt das Code-Folding für tatsächliche Codeblöcke durch, hat aber nicht das in Abb. 4 gezeigte Problem des falschen Code-Foldings.
export class FsFoldingProvider implements FoldingRangeProvider {
private foldingPairs: IFoldingPair[] = [
{ from: new RegExp("\\$CMS_FOR\\(", "i"),
to: new RegExp("\\$CMS_END_FOR\\$", "i") },
{ from: new RegExp("\\$CMS_SET\\([^,]+\\)", "i"),
to: new RegExp("\\$CMS_END_SET\\$", "i") },
{ from: new RegExp("\\$CMS_IF\\(", "i"),
to: new RegExp("\\$CMS_END_IF\\$", "i") },
{ from: new RegExp("\\$CMS_TRIM\\(", "i"),
to: new RegExp("\\$CMS_END_TRIM\\$", "i") },
{ from: new RegExp("\\$CMS_SWITCH\\(", "i"),
to: new RegExp("\\$CMS_END_SWITCH\\$", "i") }
];
provideFoldingRanges(document: TextDocument,
context: FoldingContext,
token: CancellationToken):
ProviderResult<FoldingRange[]>
{
const ranges:FoldingRange[] = [];
const foldStack:IFoldingPairHit[] = [];
for(let i =0; i<document.lineCount; i++) {
if(token.isCancellationRequested){
return null;
}
let line = document.lineAt(i).text;
let startHit:IFoldingPairHit|null=null;
let startHitAt:number=-1;
this.foldingPairs.forEach((p, n) => {
const startIdx = line.search(p.from);
const endIdx = line.search(p.to);
if(startIdx >= 0 && endIdx >= 0 &&
startIdx < endIdx){
return; // can not fold "in" a line
}
if(startIdx >= 0) {
if(startIdx < startHitAt || startHitAt < 0){
startHit = {
pair: p,
line: i
}
startHitAt = startIdx
}
}
if(endIdx >= 0 && foldStack.length > 0){
// found an end - compare to the top of the stack
let topStart:IFoldingPairHit = foldStack.pop()!;
if(topStart.pair.from === p.from) {
// we have a match
ranges.push(new FoldingRange(topStart.line, i));
} else {
// ignore - put top back on stack.
foldStack.push(topStart);
}
}
});
if(startHit !== null){
foldStack.push(startHit!);
}
}
return ranges;
}
}
Verteilung und Marktplatz
Das Tool vsce erstellt aus dem gesamten Code für die Erweiterung ein Paket:
npm install -g vsce
vsce package -o ..\bin\fs-lang-1.2.3.vsix
Nach der Installation in der ersten Zeile ruft die zweite das Tool auf, um die vsix-Datei zu erstellen. Der Parameter -o ..\bin\fs-lang-1.2.3.vsix
gibt den Dateinamen an. Wenn er fehlt, erstellt vsce eine Datei mit dem Namen [paketname]-[paketversion].vsix im Wurzelverzeichnis der Anwendung. Leider erlaubt das Tool nicht die reine Angabe eines Ausgabeverzeichnisses wie -o ..\bin\
, sondern für andere Verzeichnisse müssen Entwickler immer den vollen Dateipfad übergeben.
vsce erstellt aus dem Projekt eine installierbare vsix-Datei, die sich sowohl aus Visual Studio Code heraus (s. Abb. 7) als auch über die Kommandozeile installieren lässt:
code --install-extension fs-lang-1.2.3.vsix
Um das Paket im Visual Studio Extension Marketplace bereitzustellen, benötigen Entwickler zunächst eine PublisherId. Sofern sie noch keine besitzen, können sie eine im Marktplatz von Visual Studio Code erstellen, was allerdings ein Windows-Konto voraussetzt. Anschließend können sie in der Übersicht des Publishers neue Erweiterungen registrieren (s. Abb. 8).
Nach dem Hochladen des vsix-Pakets und einer kurzen systeminternen Prüfung steht es zum Download im Marketplace und direkt in Visual Studio Code zur Verfügung.
Polyglotte Entwicklungsumgebung
Visual Studio Code kennt zahlreiche Programmiersprachen und ermöglicht darüber hinaus das Erstellen passender Extensions für weniger bekannte Sprachen. Dabei haben Entwickler den vollen Zugriff auf die Syntaxhervorhebung, Autovervollständigung und sogar erweiterte Funktionen wie das Code-Folding.
Eigene Erweiterungen lassen sich nicht nur komfortabel schreiben, sondern auch direkt aus der Umgebung heraus testen. Mit einem zusätzlichen Werkzeug können Entwickler ihre eigene Erweiterung anderen Programmierern im Visual Studio Code Marketplace zur Verfügung stellen.
Sowohl Visual Studio Code selbst als auch die Extensions sind unter Windows, Linux und macOS lauffähig, sodass sich ohne Mehraufwand eine Entwicklung für mehrere Systeme problemlos realisieren lässt.
Nils Andresen
ist Softwarearchitekt und Berater bei der adesso SE. Er arbeitet in verschiedenen Projekten im Umfeld von Microsoft SharePoint, Azure und O365.
(rme)