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 throwing 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 exception and send a response message with the attached error object.
For example:
@Override
public CompletableFuture<Object> shutdown() {
if (!isInitialized()) {
ResponseError error = new ResponseError(ResponseErrorCode.serverNotInitialized, "Server was not initialized", null);
throw new ResponseErrorException(error);
}
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();
}
LSP4J provides a bundle that helps generate classes that are suitable for use with LSP4J's JSON-RPC.
These files can be written in Eclipse xtend and use xtend's active annotation feature to provide compilation participants.
The annotation to use is @JsonRpcData
which adds getters, setters, equals, toString and other functionality automatically to simply defined classes.
For example, in an xtend file a simple class can be defined such as:
@JsonRpcData
class HelloParam {
@NonNull String helloMessage
int repeatCount
}
which will generate a fully functional Java class with all the extra parts suitable for integrating with LSP4J and the rest of your Java application:
@SuppressWarnings("all")
public class HelloParam {
@NonNull
private String helloMessage;
private int repeatCount;
@NonNull
public String getHelloMessage() {
return this.helloMessage;
}
public void setHelloMessage(@NonNull final String helloMessage) {
this.helloMessage = Preconditions.checkNotNull(helloMessage, "helloMessage");
}
public int getRepeatCount() {
return this.repeatCount;
}
public void setRepeatCount(final int repeatCount) {
this.repeatCount = repeatCount;
}
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this);
b.add("helloMessage", this.helloMessage);
b.add("repeatCount", this.repeatCount);
return b.toString();
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
// rest of the method elided for brevity in the documentation
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.helloMessage== null) ? 0 : this.helloMessage.hashCode());
return prime * result + this.repeatCount;
}
}
The generation may generate dependencies on some additional classes. Refer to the following sub-sections for details.
When using the generator the generated code may refer to ToStringBuilder
, Preconditions
and other classes in the org.eclipse.lsp4j.jsonrpc
bundle.
Ensure that there is a runtime dependency on the org.eclipse.lsp4j.jsonrpc
in your project.