VS Code Extensions

Einleitung

In der Welt der Softwareentwicklung ist die Wahl der richtigen integrierten Entwicklungsumgebung (IDE) von entscheidender Bedeutung für die Produktivität und den Erfolg eines Entwicklers. Wir haben uns für unser viertes Projekt Visual Studio Code und dessen Erweiterungen angeschaut.

In diesem Bericht konzentrieren wir uns speziell auf die VS-Code extensions. Diese Erweiterungen bieten viele nützliche Funktionen, die das Implementieren und Schreiben von Code deutlich vereinfachen und verbessern können. Von Syntax-Hervorhebung über intelligente Codevervollständigung bis hin zu leistungsstarken Debugging-Tools gibt es eine Vielzahl von Erweiterungen, die den Entwicklungsworkflow optimieren und die Effizienz steigern können.

Indem wir einen genaueren Blick auf die Welt der VS Code-Erweiterungen werfen, möchten wir die Vielfalt und den Nutzen dieser Tools hervorheben. Wir werden sowohl die allgemeine Struktur der Extensions anschauen, als auch einige Beispiel-Extensions untersuchen. Außerdem werden wir auch unsere eigene Erweiterung implementieren und anschließend dann veröffentlichen.

Versuch

Entwicklungsumgebung

Das Entwickeln einer VS Code Erweiterung kann mit dem command npx --package yo --package generator-code -- yo code gestartet werden. Dort kann man die Einstellungen der Extension interaktiv festlegen und sofort eine Instanz von VS Code starten.

Zum Entwickeln gibt es die launch Task “Run Extension” die die Extension im Debug Modus startet und bei Änderungen automatisch neu lädt.

../_images/vs-ext-quickstart.png

Generierte Projektstruktur

Struktur

Der Einstiegspunkt der Extension ist die extension.ts Datei. Hier wird die Extension initialisiert und die Commands und Funktionalitäten registriert.

Mit den named exports activate und deactivate wird die Extension gestartet und beendet. Die activate Funktion registriert die Commands und Funktionalitäten, die die Extension bereitstellt.

Dabei werden auf einen extensionContext disposables registriert, die bei Deaktivierung der Extension wieder entfernt werden.

activate function of extension.ts
export function activate(context: vscode.ExtensionContext) {
	context.subscriptions.push(
        vscode.commands.registerCommand('helloworld.helloWorld', () => {
            showHelloWorldMessage();
        })
    );
}

export function showHelloWorldMessage() {
    vscode.window.showInformationMessage('Hello World from HelloWorld!');
}

WebViews in VS Code - TODO Extension

WebViews in Visual Studio Code ermöglichen es Entwicklern, benutzerdefinierte Web-basierte Inhalte direkt in der IDE zu integrieren. Diese WebViews sind im Grunde genommen eingebettete Webseiten innerhalb der VS Code Umgebung, die mit HTML, CSS und JavaScript gestaltet werden können. Sie bieten eine reichhaltige Benutzeroberfläche, die über die standardmäßigen UI-Komponenten von VS Code hinausgeht, und können zur Anzeige von dynamischen Informationen, Formularen, interaktiven Tools und mehr verwendet werden.

Als Beispiel haben wir eine einfache TODO Extension entwickelt, die alle Dateien in den geöffneten Ordnern von VS Code nach TODO-Kommentaren durchsucht und in einem übersichtlichen Format in eine WebView in der Activity Bar anzeigt.

TODO Kommentar TODO Extension

package.json

Die in der Extension verwendeten Elemente wie Views, Commands, Einstellungen müssen inder Datei package.json angegeben werden.

"contributes": {
    "viewsContainers": {
      // --snip--
    },
    "views": {
      // --snip--
    },
    "commands": [
     // --snip--
    ],
    "menus": {
      // --snip--
    },
    "configuration": {
      // --snip--
    }
  },

extension.ts

Die Extension registriert einen TodoViewProvider, der für die Darstellung der Webansicht verantwortlich ist, und einen Befehl zum Aktualisieren der TODO-Liste. Dafür wird die ID aus der package.json verwendet.

const provider = new TodoViewProvider(context.extensionUri);
context.subscriptions.push(
  vscode.window.registerWebviewViewProvider(TodoViewProvider.viewType, provider)
);
context.subscriptions.push(
  vscode.commands.registerCommand("todo.refresh", async () => {
    await provider.sendTodoFiles();
  })
);

Der TodoViewProvider implementiert die WebviewViewProvider Klasse. Er generiert HTML für die Webansicht und reagiert auf Nachrichten von dieser Webansicht, wie z.B. das Öffnen einer Datei oder das Zentrieren einer bestimmten Zeile im Editor.

class TodoViewProvider implements vscode.WebviewViewProvider {
  resolveWebviewView(webviewView: vscode.WebviewView) {
    webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
    webviewView.webview.onDidReceiveMessage(async (data) => {
      switch (data.type) {
        case "openFile": {
          const document = await vscode.workspace.openTextDocument(data.file);
          await vscode.window.showTextDocument(document);
          break;
        }
        case "openTodo": {
          const document = await vscode.workspace.openTextDocument(data.file);
          const editor = await vscode.window.showTextDocument(document);
          const line = document.lineAt(data.line);
          editor.revealRange(line.range, vscode.TextEditorRevealType.InCenter);
          break;
        }
      }
    });
  }
}

Die Methode _getTodos durchsucht rekursiv die Verzeichnisse und Dateien eines Arbeitsbereichs nach TODO-Kommentaren. Sie filtert Dateien und Verzeichnisse basierend auf Benutzereinstellungen und extrahiert TODOs aus den Dateien.

TODO settings
async _getTodos(filePath: string, result: TodoFile = {}) {
  const files = await fs.readdir(filePath);
  await Promise.all(
    files.map(async (file) => {
      const fullPath = path.join(filePath, file);
      const stats = await fs.stat(fullPath);
      if (stats.isFile() && this._isCorrectFile(file)) {
        const lines = (await fs.readFile(fullPath, "utf-8")).split("\n");
        const todos: Todo[] = [];
        lines.forEach((line, index) => {
          const matches = line.trim().toUpperCase().match(/\/\/.*TODOS*:*\s(.*)/);
          if (matches) {
            todos.push({ todo: matches[1], codeLine: index, fileName: file });
          }
        });
        if (todos.length > 0) {
          result[fullPath] = todos;
        }
      }
    })
  );
  return result;
}

media - JS und CSS

Die Stylesheets und der JavaScript-Code der in der WebView ausgeführt wird befinden sich in einem separaten media Ordner und kommuniziert über die VS Code API mit der VS Code Extension.

Die API ermögicht es, Nachrichten zwischen der WebView und der VS Code Extension zu senden. Diese Funktionen senden Nachrichten an VS Code, um eine Datei zu öffnen oder zu einem bestimmten Todo in einer Datei zu springen. Sie nutzen vscode.postMessage um eine Nachricht mit einem bestimmten Typ und weiteren Daten zu senden.

const vscode = acquireVsCodeApi();

function handleFileClick(file) {
  vscode.postMessage({ type: "openFile", file });
}

function handleTodoClick(file, line) {
  vscode.postMessage({ type: "openTodo", file, line });
}

Diese Funktion wird verwendet, um die Benutzeroberfläche zu aktualisieren, indem sie die Todo-Dateien durchläuft und für jede Datei und jedes Todo entsprechende HTML-Elemente erstellt. Jedes Element erhält Event-Listener, die handleFileClick oder handleTodoClick aufrufen, wenn darauf geklickt wird.

function render(todoFiles) {
  const body = document.getElementsByTagName("body")[0];
  body.innerHTML = "";

  todoFiles.forEach((folder) => {
    const files = Object.values(folder);
    files.forEach((todos, i) => {
      const filePath = Object.keys(folder)[i];
      const fileName = document.createElement("h3");
      fileName.textContent = todos[0].fileName;
      fileName.classList.add("clickable");
      fileName.addEventListener("click", () => handleFileClick(filePath));
      body.appendChild(fileName);

      todos.forEach((todo) => {
        const listItem = document.createElement("li");
        listItem.textContent = todo.todo;
        listItem.classList.add("clickable");
        listItem.addEventListener("click", () =>
          handleTodoClick(filePath, todo.codeLine)
        );
        body.appendChild(listItem);
      });

      const lineBreak = document.createElement("br");
      body.appendChild(lineBreak);
    });
  });
}

Diese Funktion initialisiert die WebView, indem sie auf Nachrichten von VS Code hört und den Zustand der WebView verwaltet. Sie lädt die Todo-Dateien, wenn sie noch nicht geladen wurden.

(function main() {
  const savedState = vscode.getState();
  window.addEventListener("message", (event) => {
    const message = event.data;
    if (message.type === "todoFiles") {
      vscode.setState({ todoFiles: message.todoFiles });
      render(message.todoFiles);
    }
  });

  if (savedState && savedState.todoFiles) {
    render(savedState.todoFiles);
  } else {
    vscode.postMessage({ type: "load" });
  }
})();

Language Server Protocol

Die Innovation, die VS Code zum bekanntesten Editor gemacht hat, war jedoch die Erfindung des Language Server Protocols (LSP).

Microsoft hat um 2014 immer mehr Probleme gehabt den Compiler und die IDE-Tool Feature-comparable zu behalten. Beide Produkte tun das gleiche jedoch auf verschiedene Art und Weise und mit anderem Output. Der neue Compiler für C# sollte daher diese beiden Welten vereinen. Parallel dazu entstand aber auch der Web Code Editor Monaco (später VS Code), daher brauchte Microsoft eine Lösung für Desktop und Web.

Daraus entstand das Language Server Protocol, das die Kommunikation zwischen einem Editor und einem Language Server/Compiler definiert.

https://code.visualstudio.com/assets/api/language-extensions/language-server-extension-guide/lsp-languages-editors.png

Der Language Server kommuniziert mit dem Editor über JSON-RPC und kann daher mehrere Editoren unterstützen außerdem kann der Server in jeder beliebigen Programmiersprache geschrieben werden vorzugsweise Sprachen, die zu WebAssembly kompiliert werden können.

Der Editor kann mit mehreren Language Servern kommunizieren und so mehrere Sprachen unterstützen.

Extension

An sich muss die Extension nur den Client für den Language Server bereitstellen und den Server starten, wenn der richtige Typ von Datei geöffnet wird.

let serverOptions: ServerOptions = {
  run: { module: serverModule, transport: TransportKind.ipc },
};
let clientOptions: LanguageClientOptions = {
  // Register the server for plain text documents
  documentSelector: [{ scheme: "file", language: "plaintext" }],
};
client = new LanguageClient(/* snip */);

Server

Für den Server stellt Microsoft einige Libraries für verschiedene Sprachen wie Typescript zur Verfügung. Es gibt aber auch Implementierungen von Drittanbietern.

Für den Versuch benutzten wir die vscode-languageserver Library von Microsoft.

Der Code besteht in unserem kleinen Beispiel aus wichtigen zwei Teilen. Zum einen die dynamische Konfiguration von LSP Features, die ich nicht beschreiben werde.

Zum anderen die Implementierung der einzelnen Features wie completion oder hover. Wir haben uns im Versuch auf completion und linting beschränkt.

documents.onDidChangeContent((change) => {
  validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // The validator creates diagnostics for all uppercase words length 2 and more
  const text = textDocument.getText();
  const pattern = /\/\/.*TODO/i;

  let problems = 0;
  const diagnostics: Diagnostic[] = getDiagnostics(text, pattern);
  // In getDiagnostics werden dann die einzelnen code Stellen mit Fehlern zurückgegeben
  // bei uns sind das alle TODO Kommentare ohne Ticket Nummer

  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });

alt text

Bei die completion wird bei uns nur die Ticket Nummer imaginär vervollständigt.

connection.onCompletion(
  async (
    _textDocumentPosition: TextDocumentPositionParams
  ): Promise<CompletionItem[]> => {
    return (await getTicketNumbers()).map((ticket) => ({
      label: ticket,
      kind: CompletionItemKind.Text,
      data: 1,
    }));
  }
);
connection.onCompletionResolve((item: CompletionItem): CompletionItem => {
  return item;
});

Hier müsste man noch darauf achten welcher Code voransteht.

alt text

Testen von Erweiterungen

Visual Studio Code unterstützt das Ausführen und Debuggen von Tests für Erweiterungen. Diese Tests werden innerhalb einer speziellen Instanz von VS Code ausgeführt, die als der Extension Development Host bezeichnet wird, und haben vollen Zugriff auf die VS Code API. Diese Tests werden als Integrationstests bezeichnet, da sie über Unit-Tests hinausgehen, die ohne eine VS Code-Instanz ausgeführt werden können.

Wenn der Yeoman Generator zum Erstellen der Erweiterung verwendet wurde, werden Integrationstests bereits automatisch erstellt. Für die Demonstration der Test wird zur Einfachheit die Hello World Extension (/4_VS_CODE_EXT/helloworld) verwendet.

export function activate(context: vscode.ExtensionContext) {

	// Use the console to output diagnostic information (console.log) and errors (console.error)
	// This line of code will only be executed once when your extension is activated
	console.log('Congratulations, your extension "helloworld" is now active!');

	context.subscriptions.push(
        vscode.commands.registerCommand('helloworld.helloWorld', () => {
            showHelloWorldMessage();
        })
    );
}

Erster Schritt: Das Test-CLI

Das VS Code-Team hat ein Befehlszeilen-Tool zum Ausführen von Erweiterungstests veröffentlicht. Das Test-CLI bietet eine schnelle Einrichtung und ermöglicht es auch, Tests der VS Code-Benutzeroberfläche mithilfe des Extension Test Runners einfach auszuführen und zu debuggen. Das CLI verwendet ausschließlich Mocha im Hintergrund.

Um zu beginnen, musst zuerst das @vscode/test-cli-Modul installiert werden, sowie das @vscode/test-electron-Modul, das Tests in VS Code Desktop ausführen kann:

npm install --save-dev @vscode/test-cli @vscode/test-electron

Nach der Installation der Module ist der Befehlszeilenbefehl vscode-test verfügbar, den man in den Abschnitt “scripts” in der package.json hinzufügen muss:

{
  "name": "helloworld",
  "scripts": {
    "test": "vscode-test"
  }
}

Schritt 2: Testkonfiguration schreiben

“vscode-test” sucht nach einer .vscode-test.js/mjs/cjs-Datei relativ zum aktuellen Arbeitsverzeichnis. Diese Datei enthält die Konfiguration für den Testrunner. Die Konfiguration sieht bei unserem Beispiel so aus:

import { defineConfig } from '@vscode/test-cli';

export default defineConfig({
	files: 'out/test/**/*.test.js',
});

Schritt 3: Tests schreiben

Nachdem man alles installiert und eingestellt hat, ist es Zeit die Tests zu schreiben. Es wurde für die Hello-World Erweiterung ein Test geschrieben, der prüfen soll, ob genau “Hello World from HelloWorld!” ausgegeben wird und die dazugehörige Funktion einmal aufgerufen wurde.

test('Hello World command', () => {
        const showInformationMessageSpy = sinon.spy(vscode.window, 'showInformationMessage');

        myExtension.showHelloWorldMessage();

        assert.strictEqual(showInformationMessageSpy.calledOnce, true);
        assert.strictEqual(showInformationMessageSpy.calledWith('Hello World from HelloWorld!'), true);

        showInformationMessageSpy.restore();
    });

export function showHelloWorldMessage() {
    vscode.window.showInformationMessage('Hello World from HelloWorld!');
}

Wenn die Integrationstests durch npm run test oder yarn test aufgerufen werden, werden die folgende Schritte durchgeführt:

  1. Die neueste Version von VS Code wird herunter geladen und entpackt.

  2. Es werden die von der Erweiterungstestlauf-Skript angegebenen Mocha-Tests ausgeführt.

Test output der Hello-World extension: VS Code test output

Veröffentlichung der Erweiterung

Die Erweiterung kann auf dem offiziellen Vs Code Extensions Marketplace veröffentlicht werden. ( https://marketplace.visualstudio.com/vscode ) Alternativ kann die Erweiterung lokal, in eine .vsix Datei verwandelt werden und privat weitergegeben werden.

In beiden Fällen wird das vsce npm Paket genutzt, dieses kann mit npm install -g @vscode/vsce installiert werden. Kommandos für die Veröffentlichung und Paketerzeugung sind respektiv vsce publish und vsce package.

Lokale Veröffentlichung (.vsix Datei)

Für die private Verteilung der Erweiterung genügt es lokal eine .vsix Datei zu erzeugen und verteilen. Diese Art der Veröffentlichung ist für interne Nutzung geeignet, wenn die Erweiterung nicht Öffentlich zugänglich sein soll.

Vorgehen

Projekt einpacken mit vsce package im root Verzeichnis der Erweiterung.

.vsix Datei kann installiert werden durch code --install-extension my-extension-0.0.1.vsix oder in Vs Code unter das “…”-Menü des Erweiterungen Tabs.

install .vsix

Marketplace Veröffentlichung

Das Extension Marketplace nutzt Azure DevOps für seine Services. Für die Veröffentlichung einer Erweiterung muss eine Organisation in Azure DevOps und dann ein persönlicher Access Token erstellt werden. Dieses Token muss in die package.json des Erweiterungs-Projektes mit dem Key “publisher” aufgenommen werden.

Vorgehen

  1. Azure DevOps Konto und Organisation erstellen. ( https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/create-organization?view=azure-devops )

  2. Im Konto unter Nutzereinstellungen “Personal Access Token” wählen, einen erstellen und aufschreiben. azure devops konto

  3. Auf der Marketplace Website mit demselben Nutzerkonto einen Publisher erstellen. Name uns ID sind zu spezifizieren, dann “erstellen” wählen. ( https://marketplace.visualstudio.com/manage/createpublisher?managePageRedirect=true ) extension publisher konto

  4. Den Publisher verifizieren durch vsce login <publisher id> und dem Access Code. publisher verification

  5. Den Namen des Publishers in das package.json aufnehmen mit dem Key “publisher”.

  6. Wenn alles passt, kann eine Erweiterung mit vsce publish veröffentlicht werden. published extension


Quellen

Veröffentlichung der Erweiterung

https://code.visualstudio.com/api/working-with-extensions/publishing-extension https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/create-organization?view=azure-devops https://marketplace.visualstudio.com/manage/createpublisher?managePageRedirect=true https://code.visualstudio.com/api/extension-guides/webview https://github.com/microsoft/vscode-extension-samples/blob/main/webview-view-sample/package.json https://code.visualstudio.com/api/language-extensions/language-server-extension-guide