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.
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.
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.
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.
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".)
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`).
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.
Notez que l'ERREUR est un avertissement que certains services ne sont pas implémentés, alors ne vous inquiétez pas.
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.
L'achèvement des entrées a fonctionné correctement.
Recommended Posts