Nahaufnahme einer Person, die mit einem Tablet auf einer Decke sitzt.

TechWiese Blog

Visual Studio Code (8): Tasks, Erstellen und Debuggen

15. Februar 2016

Portrait Bild von Tobias Kahlert

Tobias Kahlert

Dieser Blogbeitrag ist ein Repost und stammt im Original aus dem Know-how-Bereich von TechWiese, dessen Artikel in diesem Blog aufgegangen sind.

Visual Studio Code unterstützt zwar Syntax-Highlighting für zahlreiche Sprachen, aber zum Programmieren gehören noch andere Aufgaben. Dazu zählt das Erstellen des eigentlichen Programms aus dem Quellcode oder das Finden von Fehlern mit Hilfe eines Debuggers. VS Code kann nicht für jede Sprache eingebaute Tools  anbieten. Dafür verfügt es aber über allgemeine Schnittstellen, mit denen externe Tools wie Compiler und Debugger angeschlossen werden können.

Programmieren mit Tasks

Um externe Programme zum Kompilieren oder für andere Aufgaben verwenden zu können, stellt VS Code das Konzept von Tasks zur Verfügung. Dabei kann ein Programm konfiguriert werden, welches sich dann mit verschiedenen Aufgaben (Tasks) beauftragen lässt. Beispielhaft wird im Folgenden gezeigt, wie ein Projekt mit Tasks aussehen kann.

Das Projekt benutzt node.js und soll einen Parser laden, mit diesem dann eine URL in ihre Einzelteile zerlegt und das Ergebnis ausgeben wird. Der Parser wird mit PEG.js aus einer Grammatik erzeugt, die in einer separaten Datei gespeichert ist. Das Projekt hat zunächst folgende Struktur:

 .
 ├── node_modules/                    // Ordner für node.js Abhängigkeiten
 ├── index.js                         // Eigentlicher Programmcode
 ├── package.json                     // Node.js Modul Informationen
 └── url_grammar.pegjs                // Definition des URL Parsers

„url_grammar.pegjs“ enthält die Definition des Parsers und muss zuvor zu JavaScript kompiliert werden. Die index.js importiert anschließend den unter „url_grammar.js“ abgespeicherten Parser und benutzt ihn, um eine einfache URL zu parsen.

// parser laden
var urlparser = require("./url_grammar.js");
 
// parsen von "test.de/app"
 var url = urlparser.parse("test.de/app");
 
// Ergebnis ist ein JSON-Objekt, das nun auf der Konsole ausgegeben werden soll
 var niceJson = JSON.stringify(url, undefined, 2);
console.log(niceJson);
Abbildung 1: Grammatik des URL Parsers
Abbildung 1: Grammatik des URL Parsers

Wie auch Visual Studio unterstützt VS Code die Build-Tastenkombination „Strg+Shift+B“. In diesem Fall soll sie bewirken, dass die URL-Grammatik in den Parser übersetzt wird. Sie kann aber zum Beispiel auch die Kompilation von C++ oder TypeScript Code starten. Beim Drücken besagter Tastenkombination meldet VS Code aber zunächst, dass kein Task Runner konfiguriert wurde.

Abbildung 2: Meldung, dass noch kein Task Runner konfiguriert wurde
Abbildung 2: Meldung, dass noch kein Task Runner konfiguriert wurde

Das leuchtet ein, denn woher soll VS Code wissen, was zu tun ist? Es bietet aber an, einen Task Runner zu konfigurieren und erstellt eine Datei namens „tasks.json“ im Unterordner „.vscode/“. Diese ermöglicht es, einen Task Runner zu definieren. Dabei muss ein Hauptbefehl angegeben werden, der beim Starten eines Tasks ausgeführt wird. Verschiedene dieser Tasks können dann in einer Liste beschrieben werden. Alle Tasks starten den Hauptbefehl, sind aber in der Lage, unterschiedliche Argumente zu übergeben. Um eine Vielzahl von Grammatiken auf einmal zu übersetzen und um später Problem Matcher für peg.js zu verwenden (siehe unten), wird ein zusätzliches Build-Script „build.js“ verwendet. Dieses führt nur die Kompilation für die URL-Grammtik durch. Das Skript selber ist auch in JavaScript geschrieben und kann deshalb direkt mit node.js gestartet werden.

 var execute = require("child_process").spawn;
  
// Erstellt eine peg.js Grammatik
function buildGrammar(file) {
    // Ausgabe, welche Datei als nächstes generiert wird; diese Information wird später im Problem Matcher verwendet
    console.info("Building: " + file);
 
    // Führt die Kompilation durch und gibt Fehler und Informationen aus
    execute("node", ["node_modules/pegjs/bin/pegjs", file])
        .stderr.pipe(process.stdout);
 }
  
 buildGrammar("url_grammar.pegjs");

Aus diesem Grund wird der Hauptbefehl in der „tasks.json“ auf „node“ gesetzt und als Argument in der Taskbeschreibung das Build-Skript angegeben. Dieses führt node.js anschließend aus.

 {
    // Version des Task Runners
    "version": "0.1.0",
    // Der Haupt-Befehl, der für alle Tasks ausgeführt werden soll
    "command": "node",
    // Verhalten der Ausgabe
    "showOutput": "silent",
    // Liste mit Tasks
    "tasks": [
        {
            // Name des Tasks
            "taskName": "build",
            // Argument, welches an node gegeben werden soll
            "args": [ "build.js" ],
            // Gibt an, ob es sich um den Standard-Build-Befehl handelt und er mit Strg+Shift+B aufgerufen werden kann
            "isBuildCommand": true
        }
    ]
 }

Mit dem Kommando „Tasks: Run Task“ kann ein Task zum Ausführen ausgesucht werden. Da hier nur ein Task definiert wurde, wird nur „build“ angezeigt. DerTask wurde dabei als Build-Task markiert und daher kann alternativ auch das Kommando „Tasks: Run Build Task“ oder die Tastenkombination „Strg+Shift+B“ benutzt werden.

Abbildung 3: Liste mit Tasks, die gestartet werden können.
Abbildung 3: Liste mit Tasks, die gestartet werden können.

Daraufhin öffnet sich am unteren Rand eine Konsole, auf der die Ausgabe des Build-Skripts ausgegeben wird. Im Hintergrund wurde die Datei „url_grammar.js“ aus der „url_grammar.pegjs“ erzeugt.

Feedback von externen Programmen mit Problem Matchern

Es gibt aber noch einiges Verbesserungs-Potenzial. Momentan öffnet sich noch bei jedem Ausführen von „build“ die Task-Ausgabe. Außerdem muss man bei einem Fehler die Fehlerstelle mühsam selber suchen.  Die Fehlerliste von VS Code zeigt darüber hinaus keine Fehler an. Das ergibt auch keinen Sinn, denn woher soll VS Code wissen, ob der Task fehlerhaft durchgelaufen ist oder nicht. Hier schaffen so genannte Problem Matcher Abhilfe. Sie helfen VS Code, Fehler und Informationen aus der Ausgabe des Tasks herauszulesen.

Definiert wird ein Problem Matcher direkt im Task. Er besteht aus einem regulären Ausdruck, der die Struktur einer Fehlerzeile in der Ausgabe des Tasks beschreibt. Dabei können bestimmte Teile wie die Zeilen- /Spaltennummer in RegEx-Gruppen gefangen und einer Bedeutung zugewiesen werden. Für das obige Build-Skript sieht die Ausgabe bei einem Fehler zum Beispiel so aus:

Building: url_grammar.pegjs
20:13: Referenced rule "Alha" does not exist.

Die erste Zeile enthält den Namen der Datei, bei der der Fehler auftritt und die zweite Zeile die Position und die Nachricht. Der zugehörige Problem Matcher muss diese Informationen einfangen. Tipp: Beim Erstellen der regulären Ausdrücke hilft es, diese vorher auf regex101 auszuprobieren.

"problemMatcher": {
                // Gibt an wie gefundene Dateinamen aufgelöst werden sollen
                "fileLocation": "relative",
                // Beschreibt zeilenweise die Ausgabe des Tasks
                "pattern": [
                    {
                        // Die erste Zeile der Ausgabe enthält welche Datei erstellt wird
                        "regexp": "Building: (.+)$",
                        "file": 1
                    },
                    {
                        // In der zweiten Zeile der Ausgabe sind Zeile, Spalte und Nachricht zu finden
                        "regexp": "(\\d+):(\\d+): (.+)$",
                        "line": 1,
                        "column": 2,
                        "message": 3
                    }
                ]
            }

Führt man nun den Build-Task aus, öffnet sich erstens keine Ausgabe mehr und zweitens werden gefundene Fehler in der Fehlerliste angezeigt und im Code Rot unterstrichen. Fährt man mit der Maus über rot unterstrichenen Text, wird außerdem ein Fehlertext angezeigt.

Abbildung 4: Fehler werden in der Fehlerliste und in der Statusbar angezeigt. Im Code werden sie markiert.
Abbildung 4: Fehler werden in der Fehlerliste und in der Statusbar angezeigt. Im Code werden sie markiert.

Neben der Möglichkeit eigene Problem Matcher zu schreiben, gibt es noch eine Reihe von vordefinierten Problem Matchern. Eine Liste ist in der offiziellen Dokumentation zu finden.

Ausführen und Debuggen

Ist das Erstellen eines Programms erfolgreich abgeschlossen, möchte man dieses auch ausführen und testen. Visual Studio Code bietet weiterhin die Möglichkeit, Anwendungen mit dem eingebauten Debugger zu debuggen. Außerdem lassen sich Debugger für weitere Sprachen über den Marketplace nachrüsten.

Ausführen von Programmen

Normalerweise lässt sich das Debuggen mit einem Druck auf F5 beginnen. Beim ersten Mal wird VS Code allerdings nach der gewünschten Debug-Umgebung fragen. Hier wird eine Liste von vordefinierten und nachträglich installierten Debuggern angezeigt. Da obiges Projekt Node verwendet, muss selbiges als Debug-Umgebung ausgewählt werden. Es wird eine Datei namens „launch.json“ im Ordner „.vscode“ mit den gewünschten Einstellungen erzeugt.

Abbildung 5: Beim ersten Start muss eine Debug Umgebung gewählt werden.
Abbildung 5: Beim ersten Start muss eine Debug Umgebung gewählt werden.

Beim nächsten Klick auf F5 startet das Programm und eine Konsole mit der Ausgabe des Programms öffnet sich.

Abbildung 6: Erster Start des Programmes.
Abbildung 6: Erster Start des Programmes.

Debuggen von Programmen

Das Node.js Projekt wird nicht nur gestartet, sondern zusätzlich wird auch ein Debugger angehängt. Dieser wird an Breakpoints anhalten, die an der linken Seite des Codes gesetzt werden können, und weitere Untersuchungen des momentanen Programmzustandes ermöglichen. Am oberen Rand des Fensters erscheint die Steuerungsleiste für den Debugger mit folgenden Knöpfen:

Debuggen Pause  / Debuggen Start Pausiert oder führt das Debugging fort

Debuggen Next Springt zur nächsten Anweisung

Debuggen Next In Springt in die nächste Funktion hinein

Debuggen Current Out Springt aus der momentanen Funktion heraus

Debuggen Start New Startet das Debugging neu

Debuggen End Beendend den Debugger

Die Debugging-Ansicht auf der linken Seite ist in vier Bereiche geteilt. Im oberen Bereich werden Variablen angezeigt, die im momentanen Kontext erreichbar sind. Im Bereich darunter befinden sich Variablen und ihr Inhalt, die explizit beobachtet werden. Variablen, die sich nach dem letzten Anhalten des Debuggers verändert haben, werden mit einem blauen Feld hinterlegt. Der Inhalt von Variablen kann ebenfalls eingesehen werden, wenn man mit der Maus über die entsprechenden Stellen im Code-Fenster fährt. Dort wird die derzeitige Zeile gelb markiert. Im dritten Bereich wird der Call Stack angezeigt. Er enthält eine Liste mit momentan aufgerufenen Funktionen, wobei die aktuellste ganz oben gelistet ist. Der unterste Bereich enthält eine Liste mit Breakpoints. Diese können hier deaktiviert oder ganz gelöscht werden. Ganz oben in dieser Liste befinden sich zwei spezielle Breakpoints: „All Exceptions“ ist standardmäßig deaktiviert und bewirkt, dass der Debugger hält, sobald irgendeine Exception geworfen wird. „Uncaught exceptions“ hält nur, wenn eine Exception geworfen wurde und es keinen umschließenden try-catch-Block gibt.

Abbildung 7: Debug Ansicht
Abbildung 7: Debug Ansicht

Eine Besonderheit stellt noch die Debug Konsole dar. Hat der Debugger gehalten, können hier beliebige Ausdrücke evaluiert werden. Diese können auch die im Kontext vorhandenen Variablen miteinschließen.

Abbildung 8: Evaluierung von Ausdrücken in der Debug Konsole.
Abbildung 8: Evaluierung von Ausdrücken in der Debug Konsole.

Wichtige Ressourcen

Visual Studio Code Artikelserie

Grundlagen

Extensions

Weitere Beiträge