Language Server Protocl (LSP) ist ein Protokoll, das die Interaktion zwischen IDEs und Texteditoren sowie Tools für Programmiersprachen definiert. .. Auf diese Weise können Sie einen ** Sprachserver ** implementieren und Unterstützung für diese Sprache hinzufügen, ohne für jeden Editor separate Erweiterungen erstellen zu müssen.
In der Vergangenheit basierte es auf dem, was Microsoft für TypeScript entwickelt hat, und wird jetzt als separate Spezifikation veröffentlicht.
Als Thema implementieren wir hier eine Funktion, die die Werte derselben Spalte in der CSV-Datei wie Excel ergänzt (obwohl VS Code standardmäßig die Wörter in der Datei ergänzt, sodass dies überhaupt nicht praktikabel ist). .. Da der Zweck darin besteht, die Grundlagen von LSP und LSP4J zu erlernen, ist die Implementierung komplementärer Funktionen schlampig. Erstellen Sie den Client für Visual Studio Code. Der Server verwendet LSP4J.
Wie bereits erwähnt, ist LSP ein Protokoll, das die Interaktion zwischen ** Clients ** und ** Sprachservern ** definiert. Der Sprachserver bietet verschiedene Funktionen in Bezug auf die Programmiersprache, z. B. die Vervollständigung der Eingabe und die Benachrichtigung über Syntaxfehler. Der Client ist im Allgemeinen ein Texteditor, aber Sie können auch einen anderen verwenden.
Das Protokoll ist in einen HTTP-ähnlichen Header-Teil und einen JSON-formatierten Body-Teil unterteilt. Der Inhalt des Körperteils liegt im JSONP-Format vor. Die Bibliothek wird sich jedoch um diesen Bereich kümmern, und wenn Sie die Spezifikationen lesen, ist das alles, sodass ich es nicht im Detail erklären werde.
Es werden drei Arten von Nachrichten ausgetauscht: ** Anfrage **, ** Antwort ** und ** Benachrichtigung **. Eine Anfrage ist eine Nachricht, die eine Anfrage sendet und Sie auffordert, eine Antwort zu senden. Benachrichtigungen sind unbeantwortete Nachrichten, für die keine Antwort erforderlich ist. Durch den Austausch dieser Botschaft werden wir verschiedene Dinge miteinander austauschen.
Visual Studio Code ist ein typischer Editor für LSP. Erstellen Sie als Client VS-Code-Erweiterungen.
package.json
{
"activationEvents": [
"onLanguage:csv"
],
"contributes": {
"languages": [
{
"id": "csv",
"extensions": [
".csv"
],
"aliases": [
"CSV",
"Csv",
"csv"
]
}
]
},
"dependencies": {
"vscode": "^1.1.18",
"vscode-languageclient": "^4.2.1"
},
"devDependencies": {
"@types/node": "^10.3.1",
"typescript": "^2.9.1"
},
"scripts": {
"compile": "tsc -p ./",
"update-vscode": "node ./node_modules/vscode/bin/install",
"launch": "code --extensionDevelopmentPath=F:/work/TeachYourselfLsp4j/client/"
}
}
Sie müssen gegenüber VS Code deklarieren, dass "diese Erweiterung CSV-Dateien verarbeitet". Mit activityEvents: [" onLanguage: csv "]
können Sie deklarieren, diese Erweiterung beim Bearbeiten einer CSV-Datei zu aktivieren. Außerdem erkennt VS Code CSV-Dateien standardmäßig nicht. Deklarieren Sie im folgenden Teil "contrib.languages", dass die Datei "* .csv" als CSV-Datei erkannt wird.
Stellen Sie "script" so ein, dass VS Code als Erweiterungshost ausgeführt wird. Der Pfad ist fest codiert und muss geändert werden.
export function activate(context: ExtensionContext) {
const serverOptions: Executable = {
command: "C:/PROGRA~1/Zulu/zulu-10/bin/java",
args: ["-jar", "F:/work/TeachYourselfLsp4j/server/build/libs/TeachYourselfLsp4j-1.0-SNAPSHOT.jar"]
}
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'csv' }]
}
const disposable = new LanguageClient('myLanguageServer', 'MyLanguageServer', serverOptions, clientOptions).start();
context.subscriptions.push(disposable);
}
Der Java-Pfad und der JAR-Speicherort sind fest codiert, in der Produktion jedoch über die Konfiguration.
Das dritte Argument des Konstruktors "LanguageClient" gibt ein Objekt an, das "ServerOptions" erbt. Hier verwenden wir Executable
, um das Programm tatsächlich auszuführen.
Tatsächlich gibt LSP nicht an, wie Kommunikation ausgetauscht wird. Executable
kommuniziert zwischen dem Server und dem Client über Standard-E / A.
Wenn Sie die Socket-Kommunikation verwenden möchten.
function serverOptions(): Thenable<StreamInfo> {
const client = require("net").connect(8080)
return Promise.resolve({
writer: client,
reader: client
})
}
(Ich habe bestätigt, dass es funktioniert, bin aber mit node.js nicht vertraut, daher weiß ich nicht, ob es sich um eine "richtige" Implementierung handelt.)
LSP4J ist eines der Eclipse-Projekte und eine Bibliothek für den Umgang mit LSP in Java. Eclipse JDT Language Server, verwendet von VS Code Java Extension, entwickelt von RedHat /eclipse.jdt.ls) verwendet es.
dependencies {
compile 'org.eclipse.lsp4j:org.eclipse.lsp4j:0.4.1'
}
Zunächst definieren wir die Hauptmethode.
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
Da LSP4J "java.util.logging" verwendet, ist es so eingestellt, dass es SLF4J durchläuft. Außerdem zeigt VS Code die Standardfehlerausgabe für das Debuggen an. Setzen Sie daher das Logback-Ausgabeziel auf "System.err".
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger {%mdc} - %msg%n</pattern>
</encoder>
</appender>
public final class MyLanguageServer implements LanguageServer, LanguageClientAware { /* ... */ }
Der Server wird durch Implementierung der LanguageServer-Schnittstelle erstellt. LanguageClientAware
ist eine Schnittstelle, die angibt, dass die Implementierungsklasse Daten an den Client senden möchte. In den meisten Fällen möchten wir auch an den Client senden, daher implementieren wir auch "LanguageClientAware".
var server = new MyLanguageServer();
var launcher = LSPLauncher.createServerLauncher( server, System.in, System.out );
var client = launcher.getRemoteProxy();
server.connect( client );
launcher.startListening();
Starten Sie den Server mit der Methode LSPLauncher.createServerLauncher ()
. Da diesmal die Standardeingabe / -ausgabe für die Kommunikation verwendet wird, werden "System.in" und "System.out" verwendet.
var serverSocket = new ServerSocket( 8080 );
var socket = serverSocket.accept();
var launcher = LSPLauncher.createServerLauncher( server, socket.getInputStream(), socket.getOutputStream() );
Natürlich können Sie auch andere Mittel wie Steckdosen verwenden.
var client = launcher.getRemoteProxy();
server.connect( client );
launcher.startListening();
Sie können eine Instanz abrufen, die den Client mit der Methode "getRemoteProxy ()" darstellt. Rufen Sie "LanguageClientAware.connect (LanguageClient)" auf, damit "LanguageServer" den Client aufrufen kann.
Verwenden Sie abschließend startListening ()
, um die Antwort zu starten. Diese Methode blockiert. Der Server wird beendet, wenn der Eingabestream sein Ende erreicht hat (dh "InputStream.read ()" gibt "-1" zurück).
Language Server
Durch die Implementierung jeder Methode der Schnittstellen "LanguageServer" und "LanguageClientAware" beschreiben wir den Rückruf, wenn der Client einen bestimmten Prozess anfordert.
private LanguageClient client;
@Override
public void connect( LanguageClient client ) {
this.client = client;
}
Halten Sie den LanguageClient
im Feld.
Lassen Sie uns Hello World vorerst implementieren.
@Override
public void initialized( InitializedParams params ) {
client.logMessage( new MessageParams( MessageType.Info, "hello, world" ) );
}
initialisierte Benachrichtigung ist eine Benachrichtigung, die vom Client nur einmal nach dem Start an den Server gesendet wird. Wenn wir dies erhalten, senden wir dem Client eine logMessage-Benachrichtigung mit dem Namen "Hallo Welt".
Hoffentlich sollten Sie das Protokoll auf VS Code sehen.
Beachten Sie, dass der FEHLER eine Warnung ist, dass einige Dienste nicht implementiert sind. Machen Sie sich also keine Sorgen.
TextDocumentService
Wir werden die CSV-Dateivervollständigungsfunktion tatsächlich implementieren. Zuvor müssen Sie jedoch "Capability" registrieren und dem Client mitteilen, dass "dieser Sprachserver die Eingabe vervollständigen kann".
@Override
public CompletableFuture<InitializeResult> initialize( InitializeParams params ) {
var capabilities = new ServerCapabilities();
capabilities.setTextDocumentSync( TextDocumentSyncKind.Full );
var completionOptions = new CompletionOptions();
completionOptions.setResolveProvider( true );
capabilities.setCompletionProvider( completionOptions );
var result = new InitializeResult( capabilities );
return CompletableFuture.completedFuture( result );
}
TextDocumentSyncKind.Full
deklariert, dass der Server die gesamte Datei synchronisieren muss. Es ist möglich, nur die Teile zu empfangen, die durch "Inkrementell" geändert wurden. Da die Implementierung jedoch schwierig ist, wählen Sie diesmal "Voll".
Lassen Sie uns nun "TextDocumentservice" implementieren.
public final class MyTextDocumentService implements TextDocumentService { /* ... */ }
@Override
public void didOpen( DidOpenTextDocumentParams params ) {
updateDocument( params.getTextDocument().getUri() );
}
@Override
public void didChange( DidChangeTextDocumentParams params ) {
updateDocument( params.getTextDocument().getUri() );
}
Synchronisieren Sie auf der Serverseite, wenn die Datei geöffnet oder aktualisiert wird.
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion( CompletionParams position ) {
return CompletableFutures.computeAsync( checker -> {
var src = this.src.get();
var currentLineIndex = position.getPosition().getLine();
if ( src.size() <= currentLineIndex ) { //Die letzte neue Zeile in der Datei befindet sich nicht in der Liste
return Either.forLeft( List.of() );
}
var currentRow = src.get( currentLineIndex );
var currentRowString = currentRow.stream().collect( joining( "," ) );
var currentRowBeforeCursor = currentRowString
//Die Quelle wurde möglicherweise aufgrund von Problemen mit dem Synchronisationszeitpunkt nicht aktualisiert
.substring( 0, Math.min( currentRowString.length(), position.getPosition().getCharacter() ) );
var currentColumn = (int) currentRowBeforeCursor
.chars()
.filter( c -> c == ',' )
.count();
var wordsInSameColumn = src.stream()
.filter( l -> l.size() > currentColumn )
.map( l -> l.get( currentColumn ) )
.filter( s -> !s.isEmpty() )
.distinct()
.collect( toList() );
logger.debug( "{}", wordsInSameColumn );
var response = wordsInSameColumn.stream()
.map( CompletionItem::new )
.collect( toList() );
return Either.forLeft( response );
} );
}
Wenn die Eingabe-Abschlussanforderung "Abschluss" empfangen wird, wird der Abschlussprozess ausgeführt. Diesmal ist die Implementierung nicht wichtig, daher ist sie angemessen. Bitte beziehen Sie sich nicht darauf.
Die Eingabe wurde ordnungsgemäß ausgeführt.
Recommended Posts