Channels

The introduction of asynchronous I/O support in Vim 8.0, with channels and jobs created many new possibilities for extending Vim with plug-ins. For example, interfacing to a third party code quality server.

It also introduced new Vim script types that (currently) have no Python equivalents. Also, it is advantageous to be able to use callback functions to receive data in order to keep your plug-in responsive. This makes direct use from Python non-trivial so VPE provides extensions to make things easier.

Status

VPE’s support for channels is not yet particularly mature. However is is functional and seems stable, and you should not feel discouraged from using the current API.

There are a number of areas that zero or minimal support.

Vim function/feature

Status

Jobs

No support. For quite a few use cases it is arguably better to use Python subprocess module.

ch_readblob

Currently there is no way to read pure binary data. Even you use a vpe.channels.RawChannel Vim will replace NUL bytes with NL (char 10) bytes.

ch_getbufnr

VPE does not really attempt to support associating buffers with channels.

ch_logfile

Not supported, although ch_log is. This should really be fixed.

ch_readraw

Not implemented, but it is arguably better to use on_message to handle input. But I must admit, I have not really given this much thought.

I am happy to receive suggestions about the best way to support the missing features at https://github.com/paul-ollis/vim-vpe/issues.

The Channel class

Introduction

VPE provides the vpe.channels.Channel cnd related classes as the basis for socket and pipe I/O. A vpe.channels.Channel provides a Pythonic, object oriented, interface to the various Vim ch_... functions. vpe.channels.Channel is a base class from which four API classes are ultimately derived.

vpe.channels.RawChannel

Has an underlying Vim channel with mode == ‘raw’.

vpe.channels.NLChannel

Has an underlying Vim channel with mode == ‘nl’.

vpe.channels.JSChannel

Has an underlying Vim channel with mode == ‘js’.

vpe.channels.JsonChannel

Has an underlying Vim channel with mode == ‘json’.

Python code should use one of the above four classes.

Here is an approximate mapping from vim’s functions to vpe.channels.Channel methods.

Vim function

vpe.channels.Channel method

ch_open()

This is invoked when a vpe.channels.Channel is created. A channel also automatically keeps trying to connect until successful.

ch_close_in()

vpe.Channel.close_in`

ch_close()

vpe.Channel.close`

ch_read()

vpe.Channel.read`

ch_readraw()

Not (yet) implemented, but the data delivered by vpe.Channel.on_message` is the same as ch_readraw provides.

ch_readblob()

Not (yet) implemented.

ch_sendraw()

vpe.Channel.send`

ch_evalexpr()

vpe.SyncChannel.evalexpr`. Only available for vpe.channels.JSChannel and vpe.channels.JsonChannel.

ch_sendexpr()

vpe.SyncChannel.sendexpr`. Only available for vpe.channels.JSChannel and vpe.channels.JsonChannel.

ch_evalraw()

Not (yet) supported.

ch_getbufnr()

vpe.Channel.getbufnr`

ch_getjob()

Not (yet) supported

ch_info()

vpe.Channel.info`. Note that the id, port and sock_timeout values are integers; not strings.

ch_logfile()

Not (yet) implemented.

ch_log()

vpe.Channel.log`

ch_setoptions()

The socket timeout can be set using vpe.Channel.settimeout`. The mode cannot be changed and the callback cannot be explicitly set.

ch_status()

vpe.Channel.status`

Channel paradigm

The channel classes are intended to be used by inheritence. Below is some code showing the basic pattern.

from vpe import channels

class ServerChannel(channels.JsonChannel):
    """Interface to the server program."""

    def on_connect(self):
        """Handle a new outgoing connection."""

    def on_message(self, message: Any) -> None:
        """Handle a new incoming message.

        :message: The incoming, JSON-encoded message.
        """

ch = ServerChannel('localhost:6789')

When the ServerChannel is created, a connection attempt is immediately made (internally ch_open() is invoked). If the connection attempt succeeds then the vpe.Channel.on_connect` method is invoked in the very near future (via vpe.call_soon`). Normally you use your vpe.Channel.on_connect` method to perform any operations that need to happen immediately upon a successful connection.

The vpe.Channel.on_message` method is invoked whenever Vim’s channel’s input buffer contains a complete message or, for a vpe.channels.RawChannel, whenever any data is received.

Messages are typically sent using the vpe.SyncChannel.sendexpr` method. Unformatted data can be sent using vpe.Channel.send`, but this will more typically be used with vpe.channels.RawChannel instances.

If the initial connection attempt does time out, the attempt can be retried using the vpe.Channel.connect` method. Sometimes using a timer to keep retrying is a good approach.

class ServerChannel(channels.JsonChannel):
    """Interface to the server program."""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.conn_timer = vpe.Timer(
            ms=1000, func=self.connect, repeat=-1, pass_timer=False)

    def on_connect(self):
        """Handle a new outgoing connection."""
        self.conn_timer.stop()

    ...

In the above example, the timer is set to fire every second forever. The vpe.Channel.connect` method can be invoked directly by the timer - vpe.Channel.connect` does nothing if the connection is already active. The vpe.Channel.on_connect` method is a good place to stop the timer running.

A vpe.channels.Channel always sets a the channel-callback and close_cb options on channel. So incoming messages are handled asynchronously, invoking the vpe.Channel.on_message` method.

The close_cb option is used by the vpe.channels.Channel to properly clean things up. As part of this cleanup the methods vpe.Channel.on_close` and vpe.Channel.close` are invoked in that order. You can override vpe.Channel.on_close` to perform any additional clean up; the base class implementation does nothing.

Channel functions

The vpe.channels module also provides a set of very thin wrapper around most of the Vim ‘ch_…’ functions. All those listed in the mapping table above are provided, except for ch_canread(), ch_readraw(), ch_readblob(), ch_open() and ch_logfile(). These wrappers are provided mainly for use by the vpe.channels.Channel class, but you can use them in your own code.

from vpe import channels


class ServerChannel(channels.JsonChannel):
    ...

ch = ServerChannel('localhost:6789')
info = channels.ch_info(ch.vch)

The functions takes the same arguments as the built-in Vim functions except that a Channel’s vpe.Channel.vch` attribute must be used in cases where the Vim function expects a handle.

If you find it necessary to use any of these functions, please raise an issue at https://github.com/paul-ollis/vim-vpe/issues, explaining why you could not achieve you aim using only vpe.channels.Channel class methods.