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 {
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)
);
}
}