navy-nvim-rs 0.0.27

A library for writing neovim rpc clients
use std::{
    io::{self, Error},
    marker::PhantomData,
    sync::atomic::{AtomicU32, Ordering},
};

use parking_lot::Mutex as SyncMutex;
use rmpv::ValueRef;
use tokio::{
    io::AsyncWrite,
    sync::{Mutex, oneshot},
};

use crate::{
    error::{MessageIdNotFound, NeovimError, ResponseReceiveError},
    rpc::{
        decode::Decode,
        encode::{Encode, EncodeArgs, Encoder},
        payload::ResponsePayload,
    },
};

type RequestCallback = oneshot::Sender<ResponsePayload>;

#[derive(Default)]
struct PendingRequests {
    // `Vec` is okay here because its size is 1 in most cases.
    callbacks: SyncMutex<Vec<(u32, RequestCallback)>>,
}

impl PendingRequests {
    #[inline]
    fn clear(&self) {
        self.callbacks.lock().clear();
    }

    #[inline]
    fn add(&self, id: u32, callback: RequestCallback) {
        self.callbacks.lock().push((id, callback));
    }

    #[inline]
    fn take(&self, msgid: u32) -> io::Result<RequestCallback> {
        let mut callbacks = self.callbacks.lock();
        if let Some(pos) = callbacks.iter().position(|(id, _)| *id == msgid) {
            Ok(callbacks.remove(pos).1)
        } else {
            Err(Error::other(MessageIdNotFound::new(msgid)))
        }
    }
}

pub(crate) struct ApiCaller<W> {
    encoder: Mutex<Encoder<W>>,
    pending: PendingRequests,
    next_msgid: AtomicU32,
}

impl<W: AsyncWrite + Unpin> ApiCaller<W> {
    #[inline]
    pub(crate) fn new(writer: W) -> Self {
        Self {
            encoder: Mutex::new(Encoder::new(writer)),
            pending: PendingRequests::default(),
            next_msgid: AtomicU32::new(0),
        }
    }

    pub(crate) async fn request<A: EncodeArgs>(
        &self,
        method: &'static str,
        args: A,
    ) -> io::Result<ResponsePayload> {
        let msgid = self.next_msgid.fetch_add(1, Ordering::Relaxed);
        let (sender, receiver) = oneshot::channel();

        self.pending.add(msgid, sender);
        self.encoder
            .lock()
            .await
            .encode_request(msgid, method, args)
            .await?;

        match receiver.await {
            Ok(response) => Ok(response),
            Err(err) => Err(Error::other(ResponseReceiveError::new(method, err))),
        }
    }

    #[inline]
    pub(crate) async fn notify<A: EncodeArgs>(
        &self,
        method: &'static str,
        args: A,
    ) -> io::Result<()> {
        self.encoder.lock().await.encode_notify(method, args).await
    }

    #[inline]
    pub(crate) fn call<'a, A>(&'a self, method: &'static str, args: A) -> ApiCall<'a, W, A>
    where
        A: EncodeArgs,
    {
        ApiCall {
            method,
            caller: self,
            args,
        }
    }

    #[inline]
    pub(crate) fn call_and_decode<'a, R, A>(
        &'a self,
        method: &'static str,
        args: A,
    ) -> ApiCallAndDecode<'a, W, R, A>
    where
        A: EncodeArgs,
        R: for<'de> Decode<'de>,
    {
        ApiCallAndDecode {
            method,
            caller: self,
            args,
            _return_type: PhantomData,
        }
    }

    #[inline]
    pub(crate) fn callback(&self, message_id: u32) -> io::Result<RequestCallback> {
        self.pending.take(message_id)
    }

    #[inline]
    pub(crate) async fn respond(
        &self,
        message_id: u32,
        response: Result<impl Encode, impl Encode>,
    ) -> io::Result<()> {
        let mut encoder = self.encoder.lock().await;
        match response {
            Ok(result) => encoder.encode_result_response(message_id, &result).await,
            Err(error) => encoder.encode_error_response(message_id, &error).await,
        }
    }

    #[inline]
    pub(crate) fn clear(&self) {
        self.pending.clear();
    }
}

pub struct ApiCall<'a, W, A> {
    method: &'static str,
    caller: &'a ApiCaller<W>,
    args: A,
}

impl<'a, W, A> ApiCall<'a, W, A>
where
    W: AsyncWrite + Unpin,
    A: EncodeArgs,
{
    #[inline]
    pub async fn request(self) -> io::Result<ResponsePayload> {
        self.caller.request(self.method, self.args).await
    }

    #[inline]
    pub async fn notify(self) -> io::Result<()> {
        self.caller.notify(self.method, self.args).await
    }
}

pub struct ApiCallAndDecode<'a, W, R, A> {
    method: &'static str,
    caller: &'a ApiCaller<W>,
    args: A,
    _return_type: PhantomData<fn() -> R>,
}

impl<'a, W, R, A> ApiCallAndDecode<'a, W, R, A>
where
    W: AsyncWrite + Unpin,
    A: EncodeArgs,
    R: for<'de> Decode<'de>,
{
    #[inline]
    pub async fn request(self) -> io::Result<R> {
        self.caller
            .request(self.method, self.args)
            .await?
            .decode()?
            .map_err(|error| Error::other(NeovimError::new(self.method, error)))
    }

    #[inline]
    pub async fn request_raw(self) -> io::Result<ResponsePayload> {
        self.caller.request(self.method, self.args).await
    }

    #[inline]
    pub async fn notify(self) -> io::Result<()> {
        self.caller.notify(self.method, self.args).await
    }
}

#[derive(Default)]
pub struct UiAttachOptions<'a>(Vec<(&'a str, ValueRef<'a>)>);

impl<'a> UiAttachOptions<'a> {
    pub fn set_rgb(&mut self, val: bool) {
        self.0.push(("rgb", ValueRef::Boolean(val)));
    }

    pub fn set_override(&mut self, val: bool) {
        self.0.push(("override", ValueRef::Boolean(val)));
    }

    pub fn set_cmdline_external(&mut self, val: bool) {
        self.0.push(("ext_cmdline", ValueRef::Boolean(val)));
    }

    pub fn set_hlstate_external(&mut self, val: bool) {
        self.0.push(("ext_hlstate", ValueRef::Boolean(val)));
    }

    pub fn set_linegrid_external(&mut self, val: bool) {
        self.0.push(("ext_linegrid", ValueRef::Boolean(val)));
    }

    pub fn set_messages_external(&mut self, val: bool) {
        self.0.push(("ext_messages", ValueRef::Boolean(val)));
    }

    pub fn set_multigrid_external(&mut self, val: bool) {
        self.0.push(("ext_multigrid", ValueRef::Boolean(val)));
    }

    pub fn set_popupmenu_external(&mut self, val: bool) {
        self.0.push(("ext_popupmenu", ValueRef::Boolean(val)));
    }

    pub fn set_tabline_external(&mut self, val: bool) {
        self.0.push(("ext_tabline", ValueRef::Boolean(val)));
    }

    pub fn set_termcolors_external(&mut self, val: bool) {
        self.0.push(("ext_termcolors", ValueRef::Boolean(val)));
    }

    pub fn set_term_name(&mut self, val: &'a str) {
        self.0.push(("term_name", val.into()));
    }

    pub fn set_term_colors(&mut self, val: u64) {
        self.0.push(("term_colors", val.into()));
    }

    pub fn set_stdin_fd(&mut self, val: u64) {
        self.0.push(("stdin_fd", val.into()));
    }

    pub fn set_stdin_tty(&mut self, val: bool) {
        self.0.push(("stdin_tty", ValueRef::Boolean(val)));
    }

    pub fn set_stdout_tty(&mut self, val: bool) {
        self.0.push(("stdout_tty", ValueRef::Boolean(val)));
    }

    #[deprecated = "ext_wildmenu option has been deprecated since Neovim 0.9"]
    pub fn set_wildmenu_external(&mut self, val: bool) {
        self.0.push(("ext_wildmenu", ValueRef::Boolean(val)));
    }
}

impl Encode for UiAttachOptions<'_> {
    fn encode(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
        let map = self.0.as_slice();
        debug_assert_eq!(
            map.iter()
                .find(|(key, _)| self.0.iter().filter(|(k, _)| key == k).count() != 1),
            None,
            "duplicate entry in UI options: {map:?}",
        );
        map.encode(buf)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rmpv::{Value, decode::read_value};

    #[test]
    fn pending_requests_takes_callback_by_message_id() {
        let pending = PendingRequests::default();
        let (callback1, _receiver1) = oneshot::channel();
        let (callback2, _receiver2) = oneshot::channel();
        let (callback3, _receiver3) = oneshot::channel();
        pending.add(1, callback1);
        pending.add(2, callback2);
        pending.add(3, callback3);

        pending.take(2).unwrap();
        let ids = pending
            .callbacks
            .lock()
            .iter()
            .map(|(id, _)| *id)
            .collect::<Vec<_>>();
        assert_eq!(ids, [1, 3]);
    }

    #[test]
    fn pending_requests_reports_unknown_message_id() {
        let err = PendingRequests::default().take(17).unwrap_err();
        let err = err
            .get_ref()
            .and_then(|err| err.downcast_ref::<MessageIdNotFound>())
            .expect("expected MessageIdNotFound");
        assert_eq!(err.msgid, 17);
    }

    #[tokio::test]
    async fn pending_requests_clear_closes_all_callbacks() {
        let pending = PendingRequests::default();
        let (callback1, receiver1) = oneshot::channel();
        let (callback2, receiver2) = oneshot::channel();
        pending.add(1, callback1);
        pending.add(2, callback2);

        pending.clear();

        assert!(receiver1.await.is_err());
        assert!(receiver2.await.is_err());
        assert!(pending.callbacks.lock().is_empty());
    }

    fn options_value(options: &UiAttachOptions<'_>) -> Value {
        let mut bytes = Vec::new();
        options.encode(&mut bytes).unwrap();
        read_value(&mut bytes.as_slice()).unwrap()
    }

    #[test]
    fn default_options_encode_empty_map() {
        let options = UiAttachOptions::default();
        assert_eq!(Value::Map(vec![]), options_value(&options));
    }

    #[test]
    fn setters_encode_all_ui_attach_options() {
        let mut options = UiAttachOptions::default();
        options.set_rgb(true);
        options.set_override(false);
        options.set_cmdline_external(true);
        options.set_hlstate_external(false);
        options.set_linegrid_external(true);
        options.set_messages_external(false);
        options.set_multigrid_external(true);
        options.set_popupmenu_external(true);
        options.set_tabline_external(false);
        options.set_termcolors_external(true);
        options.set_term_name("xterm-256color");
        options.set_term_colors(256);
        options.set_stdin_fd(3);
        options.set_stdin_tty(true);
        options.set_stdout_tty(false);
        #[allow(deprecated)]
        options.set_wildmenu_external(true);

        assert_eq!(
            Value::Map(vec![
                ("rgb".into(), Value::Boolean(true)),
                ("override".into(), Value::Boolean(false)),
                ("ext_cmdline".into(), Value::Boolean(true)),
                ("ext_hlstate".into(), Value::Boolean(false)),
                ("ext_linegrid".into(), Value::Boolean(true)),
                ("ext_messages".into(), Value::Boolean(false)),
                ("ext_multigrid".into(), Value::Boolean(true)),
                ("ext_popupmenu".into(), Value::Boolean(true)),
                ("ext_tabline".into(), Value::Boolean(false)),
                ("ext_termcolors".into(), Value::Boolean(true)),
                ("term_name".into(), "xterm-256color".into()),
                ("term_colors".into(), 256.into()),
                ("stdin_fd".into(), 3.into()),
                ("stdin_tty".into(), Value::Boolean(true)),
                ("stdout_tty".into(), Value::Boolean(false)),
                ("ext_wildmenu".into(), Value::Boolean(true)),
            ]),
            options_value(&options)
        );
    }
}