esensar/neovim-java

Documentation for a minimum working example?

Closed this issue · 13 comments

Hello,

I'm trying to get this to work, but I cannot figure out how to put the parts together. I want to try a minimal example first where I can send a request from Neovim to a Java process and receive a response. But it appears that the reply is never returned. Here is my main class:

public class Tooling {
	public static void main(String[] args) {
		var rpcConnection = new StdIoConnection();
		var streamer = RPCClient.getDefaultAsyncInstance();
		streamer.addRequestCallback(new RequestCallback(streamer));
		streamer.attach(rpcConnection);
	}
}

It uses a custom RPC connection which has System.out and System.in as its streams (using stdin and stdout for RPC in Neovim is the simplest option). The request callback is a custom class which always returns the same response:

public class RequestCallback implements RPCListener.RequestCallback {
	private RPCStreamer streamer;

	public RequestCallback(RPCStreamer streamer) {
		this.streamer = streamer;
	}

	@Override
	public void requestReceived(RequestMessage message) {
		var id = message.getId();
		try {
			streamer.send(new ResponseMessage(id, null, "hello from Java"));
		} catch (IOException e){
			e.printStackTrace();
		}
	}
}

As far as I understand this should keep returning hello from Java for each request, yet when I try it no request is ever returned, Neovim keeps blocking forever. I tried it outside of Vim in order to be sure that the problem is indeed on the Java side:

echo '[0, 1, "say-hello", [2, 3]]' | msgpack-tools/json2msgpack | ./build/install/gradle.nvim/bin/gradle.nvim | msgpack-tools/msgpack2json

I am using msgpack-tools in order to turn a JSON object into a MessagePack object, then pipe it into the standard input of the Java process, and pipe its standard output (which should contain the response) into messagepack-tools to print the JSON representation of the reply.

The behaviour is the same as in Neovim, nothing ever gets printed, the program gets stuck listening forever. The request does arrive inside the requestReceived method, that much I know, but beyond that nothing seems to happen. What I am I doing wrong here?

Hello,

Everything seems correct in your code and if requestReceived was called, it means everything was parsed correctly and your response should have been written to stdout. The only possible issue I see is that your process will never complete, since attaching a streamer to connection results in holding connection forever until close is called or process is killed. I think you should just call System.exit(0) after sending the message to get desired results.

Adding a System.exit gets the output to show, but I am getting the wrong output: [0,1,"nvim_command",["echom 'Hello from Java'"]]. This is a request to Neovim to echo a string message, but what I should be getting back instead is [1, 1, null, "Hello from Java"], a successful response object carrying the return value (a string in this case).

In my Neovim-side code I might have something like this:

let greeting = rpcrequest(123, 'say-hello')

The rpcrequest function sends a request to the channel and blocks until a response arrives. Your code never sends any response however.

The only possible issue I see is that your process will never complete, since attaching a streamer to connection results in holding connection forever until close is called or process is killed.

That should not matter, standard output gets written to as the program is running. Do you flush the output stream after each message? That is important, otherwise nothing will get written until enough data has accumulated.

I have just tested this with UnixDomainSocketRPCConnection and code properly sends response to Neovim process without changes. I will need to investigate further using StdIoConnection since other connections don't seem to have this issue.

public class Test {
    public static final void main(String[] args) {
        var rpcConnection = new UnixDomainSocketRPCConnection(new File("/var/folders/02/ql5k3_6d70z1p7lyzjvdl34h0000gp/T/nvimFMZYNV/0"));
        var streamer = RPCClient.getDefaultAsyncInstance();
        streamer.addRequestCallback(new RPCListener.RequestCallback() {
            @Override
            public void requestReceived(RequestMessage message) {
                var id = message.getId();
                try {
                    streamer.send(new ResponseMessage(id, null, "hello from Java"));
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        });
        streamer.attach(rpcConnection);
        try {
            streamer.send(new RequestMessage.Builder(NeovimApi.LIST_CHANNELS), new RPCListener.ResponseCallback() {
                @Override
                public void responseReceived(int forId, ResponseMessage responseMessage) {
                    System.out.println(responseMessage);
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Here is the example I used for this. I sent list channels request to see which channel I should be sending requests to.

Path for UnixDomainSocketRPCConnection is taken from $NVIM_LISTEN_ADDRESS and request is made from nvim:

let result = rpcrequest(9, 'test')

After this result has value of hello from Java.
This did not block my nvim process or my java process and I was able to repeat this many times in one session.

@HiPhish
Did this work for you?

I am not sure if you are using java process as a remote plugin, but there is now a simple working example of plugin using this library in https://github.com/esensar/neovim-java/tree/master/rplugin-example

Thank you for the remote plugin example, that's exactly what I was looking for! It works in the master, but it uses classes which are not yet in the 0.1.16 release. I'm looking forward to the next release

Which one exactly?
Only StdIoRpcConnection is not available in 0.1.16. NeovimHandlerManager is from handler-annotations module, but is not required to communicate with neovim, just provides a nicer interface. I am not sure when 0.2.0 will be released.

You may also use SNAPSHOT version while you wait.

Which one exactly?

The handler-annotations classes, without those the example is not of much use. I'm probably going to be the only person using my plugin for a while, so I wouldn't mind using the snapshot version until the next proper release. How do I need to set up my gradle.build file then? I currently have

dependencies {
    // Neovim API client library
    implementation 'com.ensarsarajcic.neovim.java:neovim-api:0.1.16'
}

Simply replacing 0.1.16 with SNAPSHOT throws a build error Could not find com.ensarsarajcic.neovim.java:neovim-api:SNAPSHOT. Sorry if this question is stupid, I'm still learning Gradle.

Oh, I guess I should update my README to better represent what is packaged with this. There are multiple different dependencies you can add to access different parts of this library. You can see these as different directories in the root of the project.
To add handler-annotations to your project, you don't need SNAPSHOT version, you can just add them to your dependencies (all modules have same versions):

dependencies {
    // Neovim API client library
    implementation 'com.ensarsarajcic.neovim.java:neovim-api:0.1.16'
    // Handler annotations support
    implementation 'com.ensarsarajcic.neovim.java:handler-annotations:0.1.16'
}

Some other modules are:

  • core-rpc - Low level interface for communicating with neovim API (no specific methods for functions provided by API)
  • handler-annotations - Allows using annotations to listen for requests and notifications
     * neovim-api - High level neovim API
     * neovim-notifications - Provides neovim API notifications as models and allows listening to notifications through Java 9 Flows API
     * neovim-rx-api - RxJava2 wrapper for neovim-api, if you prefer it
  • reactive-core-rpc - Reactive version of core-rpc (neovim-api actually uses it)
  • unix-socket-connection - Adds unix domain socket rpc connection type

Check out these directories, they all have README files with more info on how to use them.

If you still wish to use snapshot, the correct version is 0.2.0-SNAPSHOT and for that you also need to add Sonatype snapshots repository to your list of repositories, e.g.:

repositories {
    jcenter()
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
}

OK, I just got the minimum example working thanks to your instructions. The only difference now is that RPCError.exception does not exist in 0.1.16. Is this the correct substitute?

var id = requestMessage.getId();
rpcStreamer.send(new ResponseMessage(id, new RPCError(id, "Too many calls!"), null));

I.e. I have to reuse the same ID two times?

Yes, that is the correct substitute.
You have to use same id, since you are responding to a message by using the same id as that message.