[JAVA] Getting Started with Language Server Protocol with LSP4J

What is Language Server Protocol?

Language Server Protocl (LSP) is a protocol that defines the interaction between IDEs and text editors and tools related to programming languages. .. This allows you to implement a single ** language server ** and add support for that language without having to create separate extensions for each editor.

Historically, it was based on what Microsoft developed for TypeScript and is now published as an independent specification.

What to do in this article

Here, as a subject, we will implement a function to complete the values of the same column in the CSV file like Excel (although VS Code will complete the words in the file by default, so it is not practical at all) .. Since the purpose is to learn the basics of LSP and LSP4J, the implementation of complementary functions is sloppy. Create the client for Visual Studio Code. The server uses LSP4J.

Source

LSP basics

As mentioned earlier, LSP is a protocol that defines the interaction between ** clients ** and ** language servers **. The language server provides various features related to the programming language, such as input completion and syntax error notification. The client is generally a text editor, but you can use other than that.

The protocol is divided into an HTTP-like header part and a JSON-formatted body part. The contents of the body part are in JSONP format. However, the library will take care of this area, and if you read the specifications, that's all, so I won't explain it in detail.

There are three types of messages exchanged: ** Request **, ** Response **, and ** Notification **. A request is a message that sends some request and asks you to send a response. Notifications are unanswered messages that do not require a reply. Through the exchange of this message, we will exchange various things with each other.

client

Visual Studio Code is a typical editor for LSP. As a client, create a VS Code extension.

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

You need to declare in VS Code that "this extension handles CSV files". ʻActivationEvents: ["onLanguage: csv"]allows you to declare that this extension is enabled when you edit the CSV file. Also, VS Code doesn't recognize CSV files by default. In the part belowcontributes.languages, declare that the * .csv` file is recognized as a CSV file.

Reference

Set script to run VS Code as an extension host. The path is hard coded, so change it.

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

The Java path and JAR location are hard-coded, but in production it is via config.

The third argument of the LanguageClient constructor specifies an object that inherits ServerOptions. Here we are using ʻExecutable to actually execute the program. In fact, LSP does not specify how communication is exchanged. ʻExecutable communicates between the server and the client using standard I / O.

If you want to use socket communication.

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

(I've confirmed that it works, but I'm not familiar with node.js, so I don't know if it's a "correct" implementation.)

server

LSP4J is one of the Eclipse projects and is a library for handling LSP in Java. Eclipse JDT Language Server used by VS Code Java Extension developed by RedHat /eclipse.jdt.ls) is using it.

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

First, let's define the main method.

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

Since LSP4J uses java.util.logging, it is set to go through SLF4J. Also, VS Code will display the standard error output for debugging, so set the Logback output destination to 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 {  /* ... */ }

The server is created by implementing the LanguageServer interface. LanguageClientAware is an interface that indicates that the implementation class wants to send data to the client. We also want to send to the client in most cases, so we also implement LanguageClientAware.

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

var client = launcher.getRemoteProxy();

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

Start the server with the LSPLauncher.createServerLauncher () method. Since standard input / output is used for communication this time, System.in and System.out are used.

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

Of course, you can use other means such as sockets.

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

You can get an instance that represents the client with the getRemoteProxy () method. Call LanguageClientAware.connect (LanguageClient) to allow LanguageServer to call the client.

Finally, use startListening () to start the response. This method blocks. The server is terminated when the input stream reaches the end (ie ʻInputStream.read ()returns-1`).

Implementation of Language Server

By implementing each method of LanguageServer and LanguageClientAware interface, we will describe the callback when a specific process is requested from the client.

private LanguageClient client;

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

Keep the LanguageClient in the field. Let's implement Hello World for the time being.

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

initialized notification is a notification sent from the client to the server only once after startup. When we receive this, we will send the client a logMessage notification called hello, world.

Hopefully you should see the log in VS Code.

キャプチャ.PNG

Note that the ERROR is a warning that some services are not implemented, so don't worry.

Implementation of TextDocumentService

We will actually implement the CSV file completion function. But before that, you need to register Capability and tell the client that" this language server can handle input completion ".

@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 declares that the server needs to synchronize the entire file. It is possible to receive only the parts changed in ʻIncremental, but since implementation is troublesome, select Full` this time.

Now, let's implement 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() );
}

Synchronize on the server side when a file is opened or updated.

@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 ) {  //The last new line in the file is not in the list
            return Either.forLeft( List.of() );
        }
        var currentRow = src.get( currentLineIndex );
        var currentRowString = currentRow.stream().collect( joining( "," ) );
        var currentRowBeforeCursor = currentRowString
                //The source may not have been updated due to synchronization timing issues
                .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 );
    } );
}

When the input completion request completion is received, the completion process is performed. This time the implementation is not important, so it is appropriate. Please do not refer to it.

無題.png

Input completion worked properly.

Recommended Posts

Getting Started with Language Server Protocol with LSP4J
Getting Started with DBUnit
Getting Started with Ruby
Getting Started with Swift
Getting Started with Docker
Getting Started with Doma-Transactions
Getting Started with Doma-Annotation Processing
Getting Started with Java Collection
Getting Started with JSP & Servlet
Getting Started with Java Basics
Getting Started with Spring Boot
Getting Started with Ruby Modules
Getting Started with Java_Chapter 5_Practice Exercises 5_4
[Google Cloud] Getting Started with Docker
Getting started with Java lambda expressions
Getting Started with Docker with VS Code
Getting started with Swift / C bridges with porting Echo Server using libuv
Getting Started with Doma-Criteria API Cheat Sheet
Getting Started with Ruby for Java Engineers
Getting Started with Docker for Mac (Installation)
Getting Started with Parameterization Testing in JUnit
Getting Started with Java Starting from 0 Part 1
Getting Started with Ratpack (4)-Routing & Static Content
Getting Started with Creating Resource Bundles with ListResoueceBundle
Getting Started with Java_Chapter 8_About Instances and Classes
Links & memos for getting started with Java (for myself)
Getting Started with Doma-Using Projection with the Criteira API
Getting Started with Doma-Using Subqueries with the Criteria API
Getting Started with Java 1 Putting together similar things
Getting started with Kotlin to send to Java developers
Getting Started with Doma-Using Joins with the Criteira API
Getting Started with Doma-Introduction to the Criteria API
I tried Getting Started with Gradle on Heroku
Getting started with Java programs using Visual Studio Code
Getting Started with Legacy Java Engineers (Stream + Lambda Expression)
Get started with Gradle
Proceed with Rust official documentation on Docker container (1. Getting started)
Getting started with Java and creating an AsciiDoc editor with JavaFX
Getting Started with Doma-Dynamicly construct WHERE clauses with the Criteria API
Getting Started with Reactive Streams and the JDK 9 Flow API
Check communication from Android to node.js server with protocol buffers
Getting Started with GitHub Container Registry instead of Docker Hub