[JAVA] Erste Schritte mit dem Language Server Protocol mit LSP4J

Was ist das Sprachserverprotokoll?

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.

Was ist in diesem Artikel zu tun?

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.

Quelle

LSP-Grundlagen

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.

Klient

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.

Referenz

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.)

Server

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).

Implementierung von 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.

キャプチャ.PNG

Beachten Sie, dass der FEHLER eine Warnung ist, dass einige Dienste nicht implementiert sind. Machen Sie sich also keine Sorgen.

Implementierung von 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.

無題.png

Die Eingabe wurde ordnungsgemäß ausgeführt.

Recommended Posts

Erste Schritte mit dem Language Server Protocol mit LSP4J
Erste Schritte mit DBUnit
Erste Schritte mit Ruby
Erste Schritte mit Swift
Erste Schritte mit Doma-Transaktionen
Erste Schritte mit der Verarbeitung von Doma-Annotationen
Erste Schritte mit Java Collection
Erste Schritte mit JSP & Servlet
Erste Schritte mit Java Basics
Erste Schritte mit Spring Boot
Erste Schritte mit Ruby-Modulen
Erste Schritte mit Java_Kapitel 5_Praktische Übungen 5_4
[Google Cloud] Erste Schritte mit Docker
Erste Schritte mit Docker mit VS-Code
Einführung in Swift / C Bridge mit der Geschichte der Portierung von Echo Server mit libuv
Erste Schritte mit dem Doma-Criteria API Cheet Sheet
Erste Schritte mit Ruby für Java-Ingenieure
Erste Schritte mit Docker für Mac (Installation)
Einführung in den Parametrisierungstest in JUnit
Einführung in Java ab 0 Teil 1
Erste Schritte mit Ratpack (4) -Routing & Static Content
Erste Schritte mit dem Erstellen von Ressourcenpaketen mit ListResoueceBundle
Erste Schritte mit Java_Kapitel 8_Über "Instanzen" und "Klassen"
Links & Memos für den Einstieg in Java (für mich)
Erste Schritte mit der Doma-Projektion mit der Criteira-API
Erste Schritte mit Java 1 Ähnliche Dinge zusammenstellen
Erste Schritte mit Kotlin zum Senden an Java-Entwickler
Erste Schritte mit Doma-Using Joins mit der Criteira-API
Erste Schritte mit Doma-Einführung in die Kriterien-API
Ich habe versucht, mit Gradle auf Heroku zu beginnen
Erste Schritte mit Java-Programmen mit Visual Studio Code
Erste Schritte mit älteren Java-Ingenieuren (Stream + Lambda)
Beginnen Sie mit Gradle
Erste Schritte mit Java und Erstellen eines Ascii Doc-Editors mit JavaFX
Erste Schritte mit Doma-Dynamic Erstellen von WHERE-Klauseln mit der Kriterien-API
Erste Schritte mit Reactive Streams und der JDK 9 Flow API
Überprüfen Sie die Kommunikation von Android zum node.js-Server mit Protokollpuffern
Erste Schritte mit der Git Hub Container Registry anstelle von Docker Hub