[JAVA] Premiers pas avec Language Server Protocol avec LSP4J

Qu'est-ce que le protocole Language Server?

Language Server Protocl (LSP) est un protocole qui définit l'interaction entre les IDE et les éditeurs de texte et les outils liés aux langages de programmation. .. Cela vous permet d'implémenter un ** serveur de langue ** et d'ajouter la prise en charge de cette langue sans avoir à créer d'extensions distinctes pour chaque éditeur.

Historiquement, il était basé sur ce que Microsoft a développé pour TypeScript et est maintenant publié en tant que spécification distincte.

Que faire dans cet article

Ici, en tant que sujet, nous allons implémenter une fonction qui complète les valeurs de la même colonne dans un fichier CSV comme Excel (bien que VS Code complète les mots dans le fichier par défaut, donc ce n'est pas pratique du tout) .. Le but étant d'apprendre les bases de LSP et LSP4J, l'implémentation de fonctions complémentaires est bâclée. Créez le client pour Visual Studio Code. Le serveur utilise LSP4J.

Source

Notions de base sur LSP

Comme mentionné précédemment, LSP est un protocole qui définit l'interaction entre les ** clients ** et les ** serveurs de langage **. Le serveur de langage fournit diverses fonctions liées au langage de programmation, telles que l'achèvement des entrées et la notification des erreurs de syntaxe. Le client est généralement un éditeur de texte, mais vous pouvez utiliser autre chose.

Le protocole est divisé en une partie d'en-tête de type HTTP et une partie de corps au format JSON. Le contenu de la partie du corps est au format JSONP. Cependant, la bibliothèque se chargera de ce domaine, et si vous lisez les spécifications, c'est tout, donc je ne vais pas l'expliquer en détail.

Il existe trois types de messages échangés: ** demande **, ** réponse ** et ** notification **. Une demande est un message qui envoie une demande et vous demande d'envoyer une réponse. Les notifications sont des messages sans réponse qui ne nécessitent pas de réponse. Grâce à l'échange de ce message, nous échangerons diverses choses entre nous.

client

Visual Studio Code est un éditeur typique pour LSP. En tant que client, créez des extensions VS Code.

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/"
    }
}

Vous devez déclarer à VS Code que "cette extension gère les fichiers CSV". ʻActivationEvents: ["onLanguage: csv"] peut déclarer que cette extension est activée lors de l'édition d'un fichier CSV. De plus, VS Code ne reconnaît pas les fichiers CSV par défaut. Dans la partie ci-dessous contribue.languages, déclarez que le fichier * .csv` est reconnu comme un fichier CSV.

Référence

Définissez script pour exécuter VS Code en tant qu'hôte d'extension. Le chemin est codé en dur, alors changez-le.

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);
}

Le chemin Java et l'emplacement du JAR sont codés en dur, mais en production, c'est via config.

Le troisième argument du constructeur LanguageClient spécifie un objet qui hérite de ServerOptions. Ici, nous utilisons ʻExecutable pour exécuter réellement le programme. En fait, LSP ne spécifie pas comment la communication est échangée. ʻExecutable communique entre le serveur et le client en utilisant les E / S standard.

Lorsque vous souhaitez utiliser la communication par socket.

function serverOptions(): Thenable<StreamInfo> {
    const client = require("net").connect(8080)
    return Promise.resolve({
        writer: client,
        reader: client
    })
}

(J'ai confirmé que cela fonctionne, mais je ne suis pas familier avec node.js, donc je ne sais pas si c'est une implémentation "correcte".)

serveur

LSP4J est l'un des projets Eclipse et est une bibliothèque pour gérer LSP en Java. Serveur de langage Eclipse JDT utilisé par VS Code Java Extension développé par RedHat /eclipse.jdt.ls) l'utilise.

dependencies {
    compile 'org.eclipse.lsp4j:org.eclipse.lsp4j:0.4.1'
}

Tout d'abord, nous définirons la méthode principale.

SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();

Puisque LSP4J utilise java.util.logging, il est configuré pour passer par SLF4J. De plus, VS Code affichera la sortie d'erreur standard pour le débogage, définissez donc la destination de sortie de Logback sur 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 {  /* ... */ }

Le serveur est créé en implémentant l'interface LanguageServer. LanguageClientAware est une interface qui indique que la classe d'implémentation veut envoyer des données au client. Nous voulons également envoyer au client dans la plupart des cas, nous implémentons donc également LanguageClientAware.

var server = new MyLanguageServer();
var launcher = LSPLauncher.createServerLauncher( server, System.in, System.out );

var client = launcher.getRemoteProxy();

server.connect( client );
launcher.startListening();

Démarrez le serveur avec la méthode LSPLauncher.createServerLauncher (). Puisque l'entrée / sortie standard est utilisée pour la communication cette fois, «System.in» et «System.out» sont utilisés.

var serverSocket = new ServerSocket( 8080 );
var socket = serverSocket.accept();
var launcher = LSPLauncher.createServerLauncher( server, socket.getInputStream(), socket.getOutputStream() );

Bien sûr, vous pouvez utiliser d'autres moyens tels que des prises.

var client = launcher.getRemoteProxy();
server.connect( client );
launcher.startListening();

Vous pouvez obtenir une instance qui représente le client avec la méthode getRemoteProxy (). Appelez LanguageClientAware.connect (LanguageClient) pour permettre à LanguageServer d'appeler le client.

Enfin, utilisez startListening () pour démarrer la réponse. Cette méthode bloque. Le serveur est arrêté lorsque le flux d'entrée atteint la fin (c'est-à-dire ʻInputStream.read () retourne -1`).

Implémentation de Language Server

En implémentant chaque méthode des interfaces «LanguageServer» et «LanguageClientAware», nous décrirons le rappel lorsqu'un processus spécifique est demandé par le client.

private LanguageClient client;

@Override
public void connect( LanguageClient client ) {
    this.client = client;
}

Gardez le LanguageClient dans le champ. Implémentons Hello World pour le moment.

@Override
public void initialized( InitializedParams params ) {
    client.logMessage( new MessageParams( MessageType.Info, "hello, world" ) );
}

notification initialisée est une notification envoyée par le client au serveur une seule fois après le démarrage. Lorsque nous recevrons cela, nous enverrons au client une notification logMessage appelée «bonjour, monde».

J'espère que vous devriez voir le journal sur VS Code.

キャプチャ.PNG

Notez que l'ERREUR est un avertissement que certains services ne sont pas implémentés, alors ne vous inquiétez pas.

Implémentation de TextDocumentService

Nous allons effectivement implémenter la fonction de complétion de fichier CSV. Mais avant cela, vous devez enregistrer Capability et dire au client que" ce serveur de langue peut gérer l'achèvement des entrées ".

@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 déclare que le serveur doit synchroniser tout le fichier. Il est possible de ne recevoir que les parties modifiées par ʻIncrémental, mais comme il est difficile à mettre en œuvre, sélectionnez Complet` cette fois.

Maintenant, implémentons TextDocumentservice.

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() );
}

Synchronisez côté serveur lorsque le fichier est ouvert ou mis à jour.

@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 ) {  //La dernière nouvelle ligne du fichier ne figure pas dans la liste
            return Either.forLeft( List.of() );
        }
        var currentRow = src.get( currentLineIndex );
        var currentRowString = currentRow.stream().collect( joining( "," ) );
        var currentRowBeforeCursor = currentRowString
                //La source n'a peut-être pas été mise à jour en raison de problèmes de synchronisation de synchronisation
                .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 );
    } );
}

Lorsque la demande d'achèvement d'entrée «achèvement» est reçue, le processus d'achèvement est exécuté. Cette fois, la mise en œuvre n'est pas importante, elle est donc appropriée. Veuillez ne pas y faire référence.

無題.png

L'achèvement des entrées a fonctionné correctement.

Recommended Posts

Premiers pas avec Language Server Protocol avec LSP4J
Premiers pas avec DBUnit
Premiers pas avec Ruby
Premiers pas avec Swift
Premiers pas avec Doma-Transactions
Premiers pas avec le traitement Doma-Annotation
Premiers pas avec Java Collection
Premiers pas avec JSP et servlet
Premiers pas avec les bases de Java
Premiers pas avec Spring Boot
Premiers pas avec les modules Ruby
Premiers pas avec Java_Chapitre 5_Exercices pratiques 5_4
[Google Cloud] Premiers pas avec Docker
Premiers pas avec Docker avec VS Code
Présentation de Swift / C Bridge avec l'histoire du portage d'Echo Server à l'aide de libuv
Premiers pas avec Doma-Criteria API Cheet Sheet
Premiers pas avec Ruby pour les ingénieurs Java
Premiers pas avec Docker pour Mac (installation)
Introduction au test de paramétrage dans JUnit
Introduction à Java à partir de 0 Partie 1
Premiers pas avec Ratpack (4) - Routage et contenu statique
Premiers pas avec la création d'ensembles de ressources avec ListResoueceBundle
Premiers pas avec Java_Chapter 8_A propos des "Instances" et des "Classes"
Liens et mémos pour démarrer avec Java (pour moi-même)
Premiers pas avec Doma-Using Projection avec l'API Criteira
Premiers pas avec les sous-requêtes utilisant Doma avec l'API Criteria
Premiers pas avec Java 1 Assembler des éléments similaires
Premiers pas avec Kotlin à envoyer aux développeurs Java
Premiers pas avec Doma-Using Joins avec l'API Criteira
Premiers pas avec Doma - Introduction à l'API Criteria
J'ai essayé de démarrer avec Gradle sur Heroku
Prise en main des programmes Java à l'aide de Visual Studio Code
Premiers pas avec les anciens ingénieurs Java (Stream + Lambda)
Commencez avec Gradle
Premiers pas avec Java et création d'un éditeur Ascii Doc avec JavaFX
Premiers pas avec Doma-Dynamic Construction de clauses WHERE avec l'API Criteria
Premiers pas avec Reactive Streams et l'API JDK 9 Flow
Vérifiez la communication entre Android et le serveur node.js avec des tampons de protocole
Premiers pas avec Git Hub Container Registry au lieu de Docker Hub