The LSP is based on an extended version of JSON-RPC v2.0, for which LSP4J provides a Java implementation. There are basically three levels of interaction:
On the lowest level, JSON-RPC just sends messages from a client to a server. Those messages can be notifications, requests, or responses. The relation between an incoming request and a sent response is done through a request id
. As a user, you usually don't want to do the wiring yourself, but want to work at least with an Endpoint
.
LSP4J provides the notion of an Endpoint that takes care of the connecting a request messages with responses. The interface defines two methods:
/**
* An endpoint is a generic interface that accepts jsonrpc requests and notifications.
*/
public interface Endpoint {
CompletableFuture<?> request(String method, Object parameter);
void notify(String method, Object parameter);
}
You always work with two Endpoints
. Usually one of the endpoints, a RemoteEndpoint
, sits on some remote communication channel, like a socket and receives and sends json messages. A local Endpoint
implementation is connected bidirectionally such that it can receive and send messages. For instance, when a notification messages comes in the RemoteEndpoint
simply translates it to a call on your local Endpoint
implementation. This simple approach works nicely in both directions.
For requests, the story is slightly more complicated. When a request message comes in, the RemoteEndpoint
tracks the request id
and invokes request
on the local endpoint. In addition, it adds completion stage to the returned CompletableFuture
, that translates the result into a JSON-RPC response message.
For the other direction, if the implementation calls request on the RemoteEndpoint, the message is sent and tracked locally. The returned CompletableFuture
will complete once a corresponding result message is received.
The receiver of a request always needs to return a response message to conform to the JSON-RPC specification. In case the result value cannot be provided in a response because of an error, the error
property of the ResponseMessage
must be set to a ResponseError
describing the failure.
This can be done by returning a CompletableFuture
completed exceptionally with a ResponseErrorException
from the request message handler in a local endpoint. The exception carries a ResponseError
to attach to the response. The RemoteEndpoint
will handle the exceptionally completed future and send a response message with the attached error object.
For example:
@Override
public CompletableFuture<Object> shutdown() {
if (!isInitialized()) {
CompletableFuture<Object> exceptionalResult = new CompletableFuture<>();
ResponseError error = new ResponseError(ResponseErrorCode.ServerNotInitialized, "Server was not initialized", null);
exceptionalResult.completeExceptionally(new ResponseErrorException(error));
return exceptionalResult;
}
return doShutdown();
}
The LSP defines an extension to the JSON-RPC, that allows to cancel requests. It is done through a special notification message, which contains the request id
that should be cancelled. If you want to cancel a pending request in LSP4J, you can simply call cancel(true)
on the returned CompletableFuture
. The RemoteEndpoint
will send the cancellation notification. If you are implementing a request message, you should return a CompletableFuture
created through CompletableFutures.computeAsync
. It accepts a lambda that is provided with a CancelChecker
, which you need to ask checkCanceled
and which will throw a CancellationException
in case the request got canceled.
@JsonRequest
public CompletableFuture<CompletionList> completion(TextDocumentPositionParams position) {
return CompletableFutures.computeAsync(cancelToken -> {
// the actual implementation should check for
// cancellation like this
cancelToken.checkCanceled();
// more code... and more cancel checking
return completionList;
});
}
So far with Endpoint
and Object
as parameter and result the API is quite generic. In order to leverage Java's type system and tool support, the JSON-RPC module supports the notion of service objects.
A service object provides methods that are annotated with either @JsonNotification
or @JsonRequest
. A GenericEndpoint
is a reflective implementation of an Endpoint that simply delegates any calls to request
or notify
to the corresponding method in the service object. Here is a simple example:
public class MyService {
@JsonNotification public void sayHello(HelloParam param) {
// do stuff
}
}
// turn it into an Endpoint
MyService service = new MyService();
Endpoint serviceAsEndpoint = ServiceEndpoints.toEndpoint(service);
If in turn you want to talk to an Endpoint in a more statically typed fashion, the EndpointProxy
comes in handy. It is a dynamic proxy for a given service interface with annotated @JsonRequest
and @JsonNotification
methods. You can create one like this:
public interface MyService {
@JsonNotification public void sayHello(HelloParam param);
}
Endpoint endpoint = ...
MyService proxy = ServiceEndpoints.toProxy(endpoint, MyService.class);
Of course you can use the same interface, as is done with the interfaces defining the messages of the LSP.
When annotated with @JsonRequest
or @JsonNotification
LSP4J will use the name of the annotated method to create the JSON-RPC method name. This naming can be customized by using segments and providing explicit names in the annotations. Here are some examples of method naming options:
@JsonSegment("mysegment")
public interface NamingExample {
// The JSON-RPC method name will be "mysegment/myrequest"
@JsonRequest
CompletableFuture<?> myrequest();
// The JSON-RPC method name will be "myotherrequest"
@JsonRequest(useSegment = false)
CompletableFuture<?> myotherrequest();
// The JSON-RPC method name will be "mysegment/somethirdrequest"
@JsonRequest(value="somethirdrequest")
CompletableFuture<?> notthesamenameasvalue();
// The JSON-RPC method name will be "call/it/what/you/want"
@JsonRequest(value="call/it/what/you/want", useSegment = false)
CompletableFuture<?> yetanothername();
}