ist jetzt verfügbar! Lesen Sie über die neuen Funktionen und Fehlerbehebungen vom November.

Leitfaden für Sprachserver-Erweiterungen

Wie Sie im Thema Programmatic Language Features gesehen haben, ist es möglich, Sprachfunktionen direkt über die languages.* API zu implementieren. Language Server Extension bietet jedoch einen alternativen Weg zur Implementierung solcher Sprachunterstützung.

Dieses Thema

Warum Language Server?

Ein Language Server ist eine spezielle Art von Visual Studio Code-Erweiterung, die das Bearbeitungserlebnis für viele Programmiersprachen ermöglicht. Mit Language Servern können Sie Autovervollständigung, Fehlerprüfung (Diagnosen), Sprung zur Definition und viele andere Sprachfunktionen implementieren, die in VS Code unterstützt werden.

Bei der Implementierung der Unterstützung für Sprachfunktionen in VS Code haben wir jedoch drei häufige Probleme festgestellt

Erstens werden Language Server normalerweise in ihren nativen Programmiersprachen implementiert, was eine Herausforderung bei der Integration mit VS Code darstellt, das eine Node.js-Laufzeitumgebung hat.

Darüber hinaus können Sprachfunktionen ressourcenintensiv sein. Zum Beispiel benötigt ein Language Server, um eine Datei korrekt zu validieren, das Parsen einer großen Anzahl von Dateien, das Erstellen von Abstract Syntax Trees dafür und die Durchführung einer statischen Programmanalyse. Diese Operationen können erhebliche CPU- und Speicherauslastung verursachen, und wir müssen sicherstellen, dass die Leistung von VS Code unbeeinträchtigt bleibt.

Schließlich kann die Integration mehrerer Sprach-Toolings mit mehreren Code-Editoren erheblichen Aufwand bedeuten. Aus Sicht der Sprach-Toolings müssen sie sich an Code-Editoren mit unterschiedlichen APIs anpassen. Aus Sicht der Code-Editoren können sie keine einheitliche API von den Sprach-Toolings erwarten. Dies macht die Implementierung von Sprachunterstützung für M Sprachen in N Code-Editoren zur Arbeit von M * N.

Um diese Probleme zu lösen, hat Microsoft das Language Server Protocol spezifiziert, das die Kommunikation zwischen Sprach-Tooling und Code-Editor standardisiert. Auf diese Weise können Language Server in jeder Sprache implementiert und in ihrem eigenen Prozess ausgeführt werden, um Leistungskosten zu vermeiden, da sie über das Language Server Protocol mit dem Code-Editor kommunizieren. Darüber hinaus können alle LSP-kompatiblen Sprach-Toolings mit mehreren LSP-kompatiblen Code-Editoren integriert werden, und alle LSP-kompatiblen Code-Editoren können problemlos mehrere LSP-kompatible Sprach-Toolings nutzen. LSP ist ein Gewinn für Sprach-Tooling-Anbieter und Code-Editor-Anbieter!

LSP Languages and Editors

In diesem Leitfaden werden wir

  • Erklären, wie eine Language Server-Erweiterung in VS Code mit dem bereitgestellten Node SDK erstellt wird.
  • Erklären, wie die Language Server-Erweiterung ausgeführt, debuggt, protokolliert und getestet wird.
  • Sie auf einige fortgeschrittene Themen zu Language Servern verweisen.

Implementierung eines Language Servers

Übersicht

In VS Code besteht ein Language Server aus zwei Teilen

  • Language Client: Eine normale VS Code-Erweiterung, die in JavaScript / TypeScript geschrieben ist. Diese Erweiterung hat Zugriff auf die gesamte VS Code Namespace API.
  • Language Server: Ein Sprachanalysetool, das in einem separaten Prozess ausgeführt wird.

Wie oben kurz erwähnt, gibt es zwei Vorteile bei der Ausführung des Language Servers in einem separaten Prozess

  • Das Analysetool kann in jeder Sprache implementiert werden, solange es gemäß dem Language Server Protocol mit dem Language Client kommunizieren kann.
  • Da Sprachanalysetools oft CPU- und Speicherkosten verursachen, vermeidet die Ausführung in einem separaten Prozess Leistungskosten.

Hier ist eine Illustration von VS Code, das zwei Language Server-Erweiterungen ausführt. Der HTML Language Client und der PHP Language Client sind normale VS Code-Erweiterungen, die in TypeScript geschrieben sind. Jeder von ihnen instanziiert einen entsprechenden Language Server und kommuniziert über LSP mit ihnen. Obwohl der PHP Language Server in PHP geschrieben ist, kann er immer noch über LSP mit dem PHP Language Client kommunizieren.

LSP Illustration

Dieser Leitfaden zeigt Ihnen, wie Sie einen Language Client / Server mit unserem Node SDK erstellen. Das übrige Dokument setzt voraus, dass Sie mit der VS Code Extension API vertraut sind.

LSP Sample - Ein einfacher Language Server für Plain-Text-Dateien

Lassen Sie uns eine einfache Language Server-Erweiterung erstellen, die Autovervollständigung und Diagnosen für Plain-Text-Dateien implementiert. Wir werden auch die Synchronisierung von Konfigurationen zwischen Client / Server behandeln.

Wenn Sie lieber direkt in den Code springen möchten

  • lsp-sample: Stark dokumentierter Quellcode für diesen Leitfaden.
  • lsp-multi-server-sample: Eine stark dokumentierte, erweiterte Version von lsp-sample, die pro Arbeitsbereichsordner eine andere Serverinstanz startet, um die Funktion für mehrere Arbeitsbereiche in VS Code zu unterstützen.

Klonen Sie das Repository Microsoft/vscode-extension-samples und öffnen Sie das Beispiel

> git clone https://github.com/microsoft/vscode-extension-samples.git
> cd vscode-extension-samples/lsp-sample
> npm install
> npm run compile
> code .

Das obige installiert alle Abhängigkeiten und öffnet den lsp-sample-Arbeitsbereich, der sowohl den Client- als auch den Servercode enthält. Hier ist ein grober Überblick über die Struktur von lsp-sample

.
├── client // Language Client
│   ├── src
│   │   ├── test // End to End tests for Language Client / Server
│   │   └── extension.ts // Language Client entry point
├── package.json // The extension manifest
└── server // Language Server
    └── src
        └── server.ts // Language Server entry point

Erklärung des 'Language Client'

Werfen wir zunächst einen Blick auf /package.json, das die Fähigkeiten des Language Client beschreibt. Es gibt zwei interessante Abschnitte

Schauen Sie sich zuerst den Abschnitt configuration an

"configuration": {
    "type": "object",
    "title": "Example configuration",
    "properties": {
        "languageServerExample.maxNumberOfProblems": {
            "scope": "resource",
            "type": "number",
            "default": 100,
            "description": "Controls the maximum number of problems produced by the server."
        }
    }
}

Dieser Abschnitt trägt configuration-Einstellungen zu VS Code bei. Das Beispiel erklärt, wie diese Einstellungen beim Start und bei jeder Änderung der Einstellungen an den Language Server gesendet werden.

Hinweis: Wenn Ihre Erweiterung mit VS Code-Versionen vor 1.74.0 kompatibel ist, müssen Sie onLanguage:plaintext im Feld activationEvents von /package.json deklarieren, um VS Code anzuweisen, die Erweiterung zu aktivieren, sobald eine Plain-Text-Datei geöffnet wird (z. B. eine Datei mit der Erweiterung .txt).

"activationEvents": []

Der eigentliche Quellcode des Language Client und die entsprechende package.json befinden sich im Ordner /client. Der interessante Teil in der Datei /client/package.json ist, dass sie die vscode Extension Host API über das Feld engines referenziert und eine Abhängigkeit zur vscode-languageclient Bibliothek hinzufügt.

"engines": {
    "vscode": "^1.52.0"
},
"dependencies": {
    "vscode-languageclient": "^7.0.0"
}

Wie bereits erwähnt, wird der Client als normale VS Code-Erweiterung implementiert und hat Zugriff auf die gesamte VS Code-Namespace-API.

Unten sehen Sie den Inhalt der entsprechenden extension.ts-Datei, die der Einstiegspunkt der lsp-sample-Erweiterung ist

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';

import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind
} from 'vscode-languageclient/node';

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  // The server is implemented in node
  let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
  // The debug options for the server
  // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
  let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };

  // If the extension is launched in debug mode then the debug server options are used
  // Otherwise the run options are used
  let serverOptions: ServerOptions = {
    run: { module: serverModule, transport: TransportKind.ipc },
    debug: {
      module: serverModule,
      transport: TransportKind.ipc,
      options: debugOptions
    }
  };

  // Options to control the language client
  let clientOptions: LanguageClientOptions = {
    // Register the server for plain text documents
    documentSelector: [{ scheme: 'file', language: 'plaintext' }],
    synchronize: {
      // Notify the server about file changes to '.clientrc files contained in the workspace
      fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
    }
  };

  // Create the language client and start the client.
  client = new LanguageClient(
    'languageServerExample',
    'Language Server Example',
    serverOptions,
    clientOptions
  );

  // Start the client. This will also launch the server
  client.start();
}

export function deactivate(): Thenable<void> | undefined {
  if (!client) {
    return undefined;
  }
  return client.stop();
}

Erklärung des 'Language Server'

Hinweis: Die 'Server'-Implementierung, die aus dem GitHub-Repository geklont wurde, enthält die endgültige Schritt-für-Schritt-Implementierung. Um dem Walkthrough zu folgen, können Sie eine neue server.ts-Datei erstellen oder den Inhalt der geklonten Version ändern.

Im Beispiel ist der Server ebenfalls in TypeScript implementiert und wird mit Node.js ausgeführt. Da VS Code bereits eine Node.js-Laufzeitumgebung mitbringt, ist es nicht notwendig, eine eigene bereitzustellen, es sei denn, Sie haben spezielle Anforderungen an die Laufzeitumgebung.

Der Quellcode für den Language Server befindet sich unter /server. Der interessante Abschnitt in der package.json-Datei des Servers ist

"dependencies": {
    "vscode-languageserver": "^7.0.0",
    "vscode-languageserver-textdocument": "^1.0.1"
}

Dies integriert die vscode-languageserver Bibliotheken.

Unten sehen Sie eine Serverimplementierung, die den bereitgestellten Textdokumentmanager verwendet, der Textdokumente synchronisiert, indem er inkrementelle Deltas von VS Code an den Server sendet.

import {
  createConnection,
  TextDocuments,
  Diagnostic,
  DiagnosticSeverity,
  ProposedFeatures,
  InitializeParams,
  DidChangeConfigurationNotification,
  CompletionItem,
  CompletionItemKind,
  TextDocumentPositionParams,
  TextDocumentSyncKind,
  InitializeResult
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);

// Create a simple text document manager.
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;

connection.onInitialize((params: InitializeParams) => {
  let capabilities = params.capabilities;

  // Does the client support the `workspace/configuration` request?
  // If not, we fall back using global settings.
  hasConfigurationCapability = !!(
    capabilities.workspace && !!capabilities.workspace.configuration
  );
  hasWorkspaceFolderCapability = !!(
    capabilities.workspace && !!capabilities.workspace.workspaceFolders
  );
  hasDiagnosticRelatedInformationCapability = !!(
    capabilities.textDocument &&
    capabilities.textDocument.publishDiagnostics &&
    capabilities.textDocument.publishDiagnostics.relatedInformation
  );

  const result: InitializeResult = {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      // Tell the client that this server supports code completion.
      completionProvider: {
        resolveProvider: true
      }
    }
  };
  if (hasWorkspaceFolderCapability) {
    result.capabilities.workspace = {
      workspaceFolders: {
        supported: true
      }
    };
  }
  return result;
});

connection.onInitialized(() => {
  if (hasConfigurationCapability) {
    // Register for all configuration changes.
    connection.client.register(DidChangeConfigurationNotification.type, undefined);
  }
  if (hasWorkspaceFolderCapability) {
    connection.workspace.onDidChangeWorkspaceFolders(_event => {
      connection.console.log('Workspace folder change event received.');
    });
  }
});

// The example settings
interface ExampleSettings {
  maxNumberOfProblems: number;
}

// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
let globalSettings: ExampleSettings = defaultSettings;

// Cache the settings of all open documents
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

// Only keep settings for open documents
documents.onDidClose(e => {
  documentSettings.delete(e.document.uri);
});

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
  validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

connection.onDidChangeWatchedFiles(_change => {
  // Monitored files have change in VS Code
  connection.console.log('We received a file change event');
});

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

// Listen on the connection
connection.listen();

Hinzufügen einer einfachen Validierung

Um der Validierung von Dokumenten zum Server hinzuzufügen, fügen wir einen Listener zum Textdokumentmanager hinzu, der aufgerufen wird, wenn sich der Inhalt eines Textdokuments ändert. Es liegt dann am Server zu entscheiden, wann der beste Zeitpunkt für die Validierung eines Dokuments ist. In der Beispielimplementierung validiert der Server das Plain-Text-Dokument und markiert alle Vorkommen von Wörtern, die in GROSSBUCHSTABEN geschrieben sind. Der entsprechende Codeausschnitt sieht so aus

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(async change => {
  let textDocument = change.document;
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
});

Diagnostik-Tipps und -Tricks

  • Wenn die Start- und Endpositionen gleich sind, unterstreicht VS Code das Wort an dieser Position mit einer wellenförmigen Linie.
  • Wenn Sie bis zum Ende der Zeile mit einer wellenförmigen Linie unterstreichen möchten, setzen Sie das Zeichen der Endposition auf Number.MAX_VALUE.

Um den Language Server auszuführen, führen Sie die folgenden Schritte aus

  • Drücken Sie ⇧⌘B (Windows, Linux Ctrl+Shift+B), um die Build-Aufgabe zu starten. Die Aufgabe kompiliert sowohl den Client als auch den Server.
  • Öffnen Sie die Run-Ansicht, wählen Sie die Startkonfiguration Launch Client und drücken Sie die Schaltfläche Start Debugging, um eine zusätzliche Instanz von VS Code (Extension Development Host) zu starten, die den Erweiterungscode ausführt.
  • Erstellen Sie eine test.txt-Datei im Stammverzeichnis und fügen Sie den folgenden Inhalt ein
TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY host. ANY OS. Open Source.

Die Instanz des Extension Development Host sieht dann so aus

Validating a text file

Debugging von Client und Server

Das Debugging des Client-Codes ist so einfach wie das Debugging einer normalen Erweiterung. Setzen Sie einen Haltepunkt im Client-Code und debuggen Sie die Erweiterung, indem Sie F5 drücken.

Debugging the client

Da der Server vom LanguageClient gestartet wird, der in der Erweiterung (Client) läuft, müssen wir einen Debugger an den laufenden Server anhängen. Wechseln Sie dazu zur Ansicht Run and Debug, wählen Sie die Startkonfiguration Attach to Server und drücken Sie F5. Dies wird den Debugger an den Server anhängen.

Debugging the server

Logging-Unterstützung für Language Server

Wenn Sie vscode-languageclient zur Implementierung des Clients verwenden, können Sie eine Einstellung [langId].trace.server festlegen, die den Client anweist, die Kommunikation zwischen Language Client / Server in einem Kanal mit dem name des Language Client zu protokollieren.

Für lsp-sample können Sie diese Einstellung setzen: "languageServerExample.trace.server": "verbose". Gehen Sie nun zum Kanal "Language Server Example". Sie sollten die Protokolle sehen.

LSP Log

Verwendung von Konfigurationseinstellungen im Server

Beim Schreiben des Client-Teils der Erweiterung haben wir bereits eine Einstellung definiert, um die maximale Anzahl der gemeldeten Probleme zu steuern. Wir haben auch Code auf der Serverseite geschrieben, um diese Einstellungen vom Client zu lesen.

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

Alles, was wir jetzt tun müssen, ist, auf Konfigurationsänderungen auf der Serverseite zu hören und bei Änderung einer Einstellung die offenen Textdokumente erneut zu validieren. Um die Validierungslogik der Dokumentenänderungsereignisbehandlung wiederzuverwenden, extrahieren wir den Code in eine validateTextDocument-Funktion und modifizieren den Code so, dass er eine maxNumberOfProblems-Variable berücksichtigt.

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

Die Handhabung der Konfigurationsänderung erfolgt durch Hinzufügen eines Benachrichtigungs-Handlers für Konfigurationsänderungen zur Verbindung. Der entsprechende Code sieht so aus

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

Wenn Sie den Client erneut starten und die Einstellung ändern, um maximal 1 Problem zu melden, führt dies zu folgender Validierung

Maximum One Problem

Hinzufügen zusätzlicher Sprachfunktionen

Die erste interessante Funktion, die ein Language Server normalerweise implementiert, ist die Validierung von Dokumenten. In diesem Sinne zählt sogar ein Linter als Language Server und in VS Code werden Linter normalerweise als Language Server implementiert (siehe eslint und jshint für Beispiele). Aber es gibt mehr bei Language Servern. Sie können Code-Vervollständigung, Alle Referenzen finden oder Gehe zu Definition bereitstellen. Der folgende Beispielcode fügt dem Server Code-Vervollständigung hinzu. Er schlägt die beiden Wörter 'TypeScript' und 'JavaScript' vor.

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

Die Felder data werden verwendet, um ein Vervollständigungselement im Resolve-Handler eindeutig zu identifizieren. Die data-Eigenschaft ist für das Protokoll transparent. Da das zugrunde liegende Nachrichtenübertragungsprotokoll auf JSON basiert, sollte das data-Feld nur Daten enthalten, die serialisierbar von und zu JSON sind.

Alles, was fehlt, ist VS Code mitzuteilen, dass der Server Code-Vervollständigungsanfragen unterstützt. Um dies zu tun, kennzeichnen Sie die entsprechende Fähigkeit im Initialisierungs-Handler.

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            ...
            // Tell the client that the server supports code completion
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});

Der folgende Screenshot zeigt den abgeschlossenen Code, der in einer Plain-Text-Datei ausgeführt wird.

Code Complete

Testen des Language Servers

Um einen qualitativ hochwertigen Language Server zu erstellen, müssen wir eine gute Testsuite erstellen, die seine Funktionalitäten abdeckt. Es gibt zwei gängige Methoden zum Testen von Language Servern

  • Unit Test: Dies ist nützlich, wenn Sie spezifische Funktionalitäten in Language Servern testen möchten, indem Sie alle Informationen, die an ihn gesendet werden, simulieren. Die HTML- / CSS- / JSON Language Server von VS Code verfolgen diesen Ansatz zum Testen. Die LSP npm-Module verwenden ebenfalls diesen Ansatz. Sehen Sie sich hier einige Unit-Tests an, die mit dem npm-Protokollmodul geschrieben wurden.
  • End-to-End-Test: Dies ähnelt den VS Code Extension Tests. Der Vorteil dieses Ansatzes ist, dass er den Test ausführt, indem eine VS Code-Instanz mit einem Arbeitsbereich instanziiert wird, die Datei geöffnet, der Language Client / Server aktiviert und VS Code-Befehle ausgeführt werden. Dieser Ansatz ist überlegen, wenn Sie Dateien, Einstellungen oder Abhängigkeiten (wie z. B. node_modules) haben, die schwer oder unmöglich zu simulieren sind. Die beliebte Python-Erweiterung verfolgt diesen Ansatz zum Testen.

Es ist möglich, Unit-Tests mit jedem von Ihnen gewählten Test-Framework durchzuführen. Hier beschreiben wir, wie End-to-End-Tests für Language Server Extension durchgeführt werden.

Öffnen Sie .vscode/launch.json, und Sie finden ein E2E-Testziel.

{
  "name": "Language Server E2E Test",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceRoot}",
    "--extensionTestsPath=${workspaceRoot}/client/out/test/index",
    "${workspaceRoot}/client/testFixture"
  ],
  "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}

Wenn Sie dieses Debug-Ziel ausführen, wird eine VS Code-Instanz mit client/testFixture als aktuellem Arbeitsbereich gestartet. VS Code führt dann alle Tests in client/src/test aus. Als Debugging-Tipp können Sie Haltepunkte in TypeScript-Dateien in client/src/test setzen, und diese werden erreicht.

Schauen wir uns die Datei completion.test.ts an.

import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';

suite('Should do completion', () => {
  const docUri = getDocUri('completion.txt');

  test('Completes JS/TS in txt file', async () => {
    await testCompletion(docUri, new vscode.Position(0, 0), {
      items: [
        { label: 'JavaScript', kind: vscode.CompletionItemKind.Text },
        { label: 'TypeScript', kind: vscode.CompletionItemKind.Text }
      ]
    });
  });
});

async function testCompletion(
  docUri: vscode.Uri,
  position: vscode.Position,
  expectedCompletionList: vscode.CompletionList
) {
  await activate(docUri);

  // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion
  const actualCompletionList = (await vscode.commands.executeCommand(
    'vscode.executeCompletionItemProvider',
    docUri,
    position
  )) as vscode.CompletionList;

  assert.ok(actualCompletionList.items.length >= 2);
  expectedCompletionList.items.forEach((expectedItem, i) => {
    const actualItem = actualCompletionList.items[i];
    assert.equal(actualItem.label, expectedItem.label);
    assert.equal(actualItem.kind, expectedItem.kind);
  });
}

In diesem Test

  • Aktivieren Sie die Erweiterung.
  • Führen Sie den Befehl vscode.executeCompletionItemProvider mit einer URI und einer Position aus, um die Vervollständigungsanfrage zu simulieren.
  • Vergleichen Sie die zurückgegebenen Vervollständigungselemente mit unseren erwarteten Vervollständigungselementen.

Tauchen wir ein wenig tiefer in die Funktion activate(docURI) ein. Sie ist in client/src/test/helper.ts definiert.

import * as vscode from 'vscode';
import * as path from 'path';

export let doc: vscode.TextDocument;
export let editor: vscode.TextEditor;
export let documentEol: string;
export let platformEol: string;

/**
 * Activates the vscode.lsp-sample extension
 */
export async function activate(docUri: vscode.Uri) {
  // The extensionId is `publisher.name` from package.json
  const ext = vscode.extensions.getExtension('vscode-samples.lsp-sample')!;
  await ext.activate();
  try {
    doc = await vscode.workspace.openTextDocument(docUri);
    editor = await vscode.window.showTextDocument(doc);
    await sleep(2000); // Wait for server activation
  } catch (e) {
    console.error(e);
  }
}

async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Im Aktivierungsteil

  • Holen Sie sich die Erweiterung über {publisher.name}.{extensionId}, wie in package.json definiert.
  • Öffnen Sie das angegebene Dokument und zeigen Sie es im aktiven Texteditor an.
  • Warten Sie 2 Sekunden, damit wir sicher sind, dass der Language Server instanziiert ist.

Nach der Vorbereitung können wir die VS Code-Befehle für jede Sprachfunktion ausführen und das zurückgegebene Ergebnis vergleichen.

Es gibt noch einen weiteren Test, der die gerade implementierte Diagnosefunktion abdeckt. Schauen Sie ihn sich unter client/src/test/diagnostics.test.ts an.

Fortgeschrittene Themen

Bisher hat dieser Leitfaden behandelt

  • Eine kurze Übersicht über Language Server und Language Server Protocol.
  • Architektur einer Language Server-Erweiterung in VS Code.
  • Die lsp-sample-Erweiterung und wie man sie entwickelt/debuggt/inspeziert/testet.

Es gibt noch einige fortgeschrittene Themen, die wir in diesen Leitfaden nicht aufnehmen konnten. Wir werden Links zu diesen Ressourcen für weiteres Studium der Language Server-Entwicklung bereitstellen.

Zusätzliche Language Server-Funktionen

Die folgenden Sprachfunktionen werden derzeit in einem Language Server neben Code-Vervollständigungen unterstützt

  • Document Highlights: hebt alle 'gleichen' Symbole in einem Textdokument hervor.
  • Hover: liefert Hover-Informationen für ein Symbol, das in einem Textdokument ausgewählt wurde.
  • Signature Help: liefert Signaturhilfe für ein Symbol, das in einem Textdokument ausgewählt wurde.
  • Goto Definition: bietet Unterstützung für den Sprung zur Definition eines Symbols, das in einem Textdokument ausgewählt wurde.
  • Goto Type Definition: bietet Unterstützung für den Sprung zur Typ-/Schnittstellendefinition eines Symbols, das in einem Textdokument ausgewählt wurde.
  • Goto Implementation: bietet Unterstützung für den Sprung zur Implementierungsdefinition eines Symbols, das in einem Textdokument ausgewählt wurde.
  • Find References: findet alle projektweiten Referenzen für ein Symbol, das in einem Textdokument ausgewählt wurde.
  • List Document Symbols: listet alle Symbole auf, die in einem Textdokument definiert sind.
  • List Workspace Symbols: listet alle projektweiten Symbole auf.
  • Code Actions: berechnet Befehle zur Ausführung (typischerweise Verschönerung/Refactoring) für ein gegebenes Textdokument und einen gegebenen Bereich.
  • CodeLens: berechnet CodeLens-Statistiken für ein gegebenes Textdokument.
  • Document Formatting: dies beinhaltet das Formatieren ganzer Dokumente, Dokumentbereiche und das Formatieren beim Tippen.
  • Rename: projektweites Umbenennen eines Symbols.
  • Document Links: berechnet und löst Links innerhalb eines Dokuments auf.
  • Document Colors: berechnet und löst Farben innerhalb eines Dokuments auf, um einen Farbwähler im Editor bereitzustellen.

Das Thema Programmatic Language Features beschreibt jede der oben genannten Sprachfunktionen und gibt Anleitungen, wie sie entweder über das Language Server Protocol oder durch direkte Verwendung der Erweiterbarkeits-API aus Ihrer Erweiterung implementiert werden können.

Inkrementelle Textdokumentensynchronisierung

Das Beispiel verwendet den einfachen Textdokumentmanager, der vom vscode-languageserver-Modul bereitgestellt wird, um Dokumente zwischen VS Code und dem Language Server zu synchronisieren.

Dies hat zwei Nachteile

  • Viele Daten werden übertragen, da der gesamte Inhalt eines Textdokuments wiederholt an den Server gesendet wird.
  • Wenn eine vorhandene Sprachbibliothek verwendet wird, unterstützen solche Bibliotheken in der Regel inkrementelle Dokumentenaktualisierungen, um unnötiges Parsen und die Erstellung von Abstract Syntax Trees zu vermeiden.

Das Protokoll unterstützt daher auch inkrementelle Dokumentensynchronisierung.

Um die inkrementelle Dokumentensynchronisierung zu nutzen, muss ein Server drei Benachrichtigungs-Handler installieren

  • onDidOpenTextDocument: wird aufgerufen, wenn ein Textdokument in VS Code geöffnet wird.
  • onDidChangeTextDocument: wird aufgerufen, wenn sich der Inhalt eines Textdokuments in VS Code ändert.
  • onDidCloseTextDocument: wird aufgerufen, wenn ein Textdokument in VS Code geschlossen wird.

Unten sehen Sie einen Codeausschnitt, der veranschaulicht, wie diese Benachrichtigungs-Handler an einer Verbindung angehängt und die richtige Fähigkeit bei der Initialisierung zurückgegeben wird.

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            // Enable incremental document sync
            textDocumentSync: TextDocumentSyncKind.Incremental,
            ...
        }
    };
});

connection.onDidOpenTextDocument((params) => {
    // A text document was opened in VS Code.
    // params.uri uniquely identifies the document. For documents stored on disk, this is a file URI.
    // params.text the initial full content of the document.
});

connection.onDidChangeTextDocument((params) => {
    // The content of a text document has change in VS Code.
    // params.uri uniquely identifies the document.
    // params.contentChanges describe the content changes to the document.
});

connection.onDidCloseTextDocument((params) => {
    // A text document was closed in VS Code.
    // params.uri uniquely identifies the document.
});

/*
Make the text document manager listen on the connection
for open, change and close text document events.

Comment out this line to allow `connection.onDidOpenTextDocument`,
`connection.onDidChangeTextDocument`, and `connection.onDidCloseTextDocument` to handle the events
*/
// documents.listen(connection);

Verwendung der VS Code API direkt zur Implementierung von Sprachfunktionen

Während Language Server viele Vorteile haben, sind sie nicht die einzige Option, um die Bearbeitungsfähigkeiten von VS Code zu erweitern. In Fällen, in denen Sie einige einfache Sprachfunktionen für einen Dokumententyp hinzufügen möchten, sollten Sie vscode.languages.register[LANGUAGE_FEATURE]Provider als Option in Betracht ziehen.

Hier ist ein completions-sample, das vscode.languages.registerCompletionItemProvider verwendet, um einige Snippets als Vervollständigungen für Plain-Text-Dateien hinzuzufügen.

Weitere Beispiele, die die Verwendung der VS Code API veranschaulichen, finden Sie unter https://github.com/microsoft/vscode-extension-samples.

Fehlertoleranter Parser für Language Server

Meistens ist der Code im Editor unvollständig und syntaktisch falsch, aber Entwickler erwarten trotzdem, dass Autovervollständigung und andere Sprachfunktionen funktionieren. Daher ist ein fehlerverzeihender Parser für einen Language Server notwendig: Der Parser generiert aussagekräftige ASTs aus teilweise vollständigem Code, und der Language Server stellt Sprachfunktionen basierend auf dem AST bereit.

Als wir die PHP-Unterstützung in VS Code verbesserten, stellten wir fest, dass der offizielle PHP-Parser nicht fehlerverzeihend war und nicht direkt im Language Server wiederverwendet werden konnte. Daher haben wir an Microsoft/tolerant-php-parser gearbeitet und detaillierte Notizen hinterlassen, die Language Server-Autoren helfen könnten, die einen fehlerverzeihenden Parser implementieren müssen.

Häufig gestellte Fragen

Wenn ich versuche, mich an den Server anzuhängen, erhalte ich "cannot connect to runtime process (timeout after 5000 ms)"?

Sie sehen diesen Timeout-Fehler, wenn der Server nicht läuft, wenn Sie versuchen, den Debugger anzuhängen. Der Client startet den Language Server, also stellen Sie sicher, dass Sie den Client gestartet haben, um einen laufenden Server zu haben. Möglicherweise müssen Sie auch Ihre Client-Haltepunkte deaktivieren, wenn sie das Starten des Servers beeinträchtigen.

Ich habe diesen Leitfaden und die LSP-Spezifikation gelesen, aber ich habe immer noch ungelöste Fragen. Wo kann ich Hilfe bekommen?

Bitte öffnen Sie ein Issue unter https://github.com/microsoft/language-server-protocol.

© . This site is unofficial and not affiliated with Microsoft.