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.
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.
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.
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 below
contributes.languages, declare that the
* .csv` file is recognized as a CSV file.
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.)
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`).
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.
Note that the ERROR is a warning that some services are not implemented, so don't worry.
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.
Input completion worked properly.
Recommended Posts