├── .gitmodules ├── .gitignore ├── tests ├── regression │ ├── mod.rs │ └── buffering.rs ├── connecting │ ├── mod.rs │ ├── handshake.rs │ └── conns.rs ├── common │ └── mod.rs ├── basic.rs ├── notifications.rs └── nested_requests.rs ├── rustfmt.toml ├── pull_request_template.md ├── src ├── examples │ ├── scorched_earth_as.rs │ ├── mod.rs │ ├── quitting.rs │ ├── handler_drop.rs │ ├── scorched_earth.rs │ └── README.md ├── rpc │ ├── mod.rs │ ├── handler.rs │ ├── unpack.rs │ └── model.rs ├── exttypes │ ├── buffer.rs │ ├── mod.rs │ ├── window.rs │ └── tabpage.rs ├── bin │ ├── linebuffercrash.rs │ └── linebuffercrash_as.rs ├── lib.rs ├── create │ ├── mod.rs │ ├── async_std.rs │ └── tokio.rs ├── neovim_api_manual.rs ├── uioptions.rs ├── neovim.rs └── error.rs ├── CONTRIBUTING.md ├── TODO.md ├── examples ├── quitting.rs ├── bench_sync.rs ├── basic.rs ├── bench_async-std.rs ├── bench_tokio.rs ├── handler_drop.rs ├── scorched_earth_as.rs ├── scorched_earth.rs └── nested_requests.rs ├── LICENSE-MIT ├── bench_examples.vim ├── README.md ├── bindings ├── neovim_api.rs └── generate_bindings.py ├── benches └── rpc_tokio.rs ├── .github └── workflows │ └── ci.yml ├── Cargo.toml ├── CHANGELOG.md ├── LICENSE-LGPL └── LICENSE-APACHE /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | /doc 5 | neovim 6 | -------------------------------------------------------------------------------- /tests/regression/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "use_tokio")] 2 | pub mod buffering; 3 | #[cfg(feature = "use_async-std")] 4 | pub mod buffering; 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_try_shorthand = true 2 | edition = "2018" 3 | max_width = 80 4 | comment_width = 80 5 | wrap_comments = true 6 | tab_spaces = 2 7 | imports_granularity="Crate" 8 | -------------------------------------------------------------------------------- /tests/connecting/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "use_tokio")] 2 | pub mod conns; 3 | #[cfg(feature = "use_async-std")] 4 | pub mod conns; 5 | 6 | #[cfg(feature = "use_tokio")] 7 | pub mod handshake; 8 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Licensing**: The code contributed to nvim-rs is licensed under the MIT or 4 | Apache license as given in the project root directory. 5 | -------------------------------------------------------------------------------- /src/examples/scorched_earth_as.rs: -------------------------------------------------------------------------------- 1 | //! # Scorched earth with async-std 2 | //! 3 | //! A port of the [`scorched_earth`](crate::examples::scorched_earth) example to 4 | //! use [`async-std`](async-std). See [there](crate::examples::scorched_earth) 5 | //! for the full documentation, everything still applies with "scorched_earth" 6 | //! replaced by "scorched_earth_as". 7 | -------------------------------------------------------------------------------- /src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | //! RPC functionality for [`neovim`](crate::neovim::Neovim) 2 | //! 3 | //! For most plugins, the main implementation work will consist of defining and 4 | //! implementing the [`handler`](crate::rpc::handler::Handler). 5 | pub mod handler; 6 | pub mod model; 7 | pub mod unpack; 8 | 9 | pub use self::model::{IntoVal, RpcMessage}; 10 | pub use rmpv::Value; 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | I'd love contributions, comments, praise, criticism... You could open an 4 | [issue](https://github.com/KillTheMule/nvim-rs/issues) or a [pull 5 | request](https://github.com/KillTheMule/nvim-rs/pulls). I also read the 6 | subreddits for [rust](https://www.reddit.com/r/rust/) and 7 | [neovim](https://www.reddit.com/r/neovim/), if that suits you better. 8 | -------------------------------------------------------------------------------- /src/exttypes/buffer.rs: -------------------------------------------------------------------------------- 1 | use futures::io::AsyncWrite; 2 | use rmpv::Value; 3 | 4 | use crate::{impl_exttype_traits, rpc::model::IntoVal, Neovim}; 5 | /// A struct representing a neovim buffer. It is specific to a 6 | /// [`Neovim`](crate::neovim::Neovim) instance, and calling a method on it will 7 | /// always use this instance. 8 | pub struct Buffer 9 | where 10 | W: AsyncWrite + Send + Unpin + 'static, 11 | { 12 | pub(crate) code_data: Value, 13 | pub(crate) neovim: Neovim, 14 | } 15 | 16 | impl_exttype_traits!(Buffer); 17 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Check what we're doing with outgoing request parameters, there are 2 allocations going on in call_args! and rpc_args!, and we're just reading it in the end. 2 | 3 | * Can we use the non-generic `split` methods from tokio for unixstream, tcpstream? Supposedly better performance, but introduces lifetimes... 4 | 5 | * Propogate errors from `model::encode()` in `handler_loop()` 6 | 7 | * Don't return an error on channel close, because the the regular way to shut down a plugin 8 | --> maybe? For a plugin, sure, what about GUIs? 9 | 10 | * Don't build neovim ourselves, download a binary 11 | -------------------------------------------------------------------------------- /src/bin/linebuffercrash.rs: -------------------------------------------------------------------------------- 1 | 2 | use nvim_rs::{ 3 | create::tokio as create, 4 | rpc::handler::Dummy as DummyHandler 5 | }; 6 | 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let handler = DummyHandler::new(); 11 | let (nvim, _io_handler) = create::new_parent(handler).await.unwrap(); 12 | let curbuf = nvim.get_current_buf().await.unwrap(); 13 | 14 | // If our Stdout is linebuffered, this has a high chance of crashing neovim 15 | // Should probably befixed in neovim itself, but for now, let's just make 16 | // sure we're not using linebuffering, or at least don't crash neovim with 17 | // this. 18 | for i in 0..20 { 19 | curbuf.set_name(&format!("a{i}")).await.unwrap(); 20 | } 21 | 22 | let _ = nvim.command("quit!").await; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/bin/linebuffercrash_as.rs: -------------------------------------------------------------------------------- 1 | 2 | use nvim_rs::{ 3 | create::async_std as create, 4 | rpc::handler::Dummy as DummyHandler 5 | }; 6 | 7 | 8 | #[async_std::main] 9 | async fn main() { 10 | let handler = DummyHandler::new(); 11 | let (nvim, _io_handler) = create::new_parent(handler).await.unwrap(); 12 | let curbuf = nvim.get_current_buf().await.unwrap(); 13 | 14 | // If our Stdout is linebuffered, this has a high chance of crashing neovim 15 | // Should probably befixed in neovim itself, but for now, let's just make 16 | // sure we're not using linebuffering, or at least don't crash neovim with 17 | // this. 18 | for i in 0..20 { 19 | curbuf.set_name(&format!("a{i}")).await.unwrap(); 20 | } 21 | 22 | let _ = nvim.command("quit!").await; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | env, 4 | }; 5 | 6 | #[allow(dead_code)] 7 | pub const NVIM_BIN: &str = if cfg!(windows) { 8 | "nvim.exe" 9 | } else { 10 | "nvim" 11 | }; 12 | const NVIM_PATH: &str = if cfg!(windows) { 13 | "neovim/build/bin/nvim.exe" 14 | } else { 15 | "neovim/build/bin/nvim" 16 | }; 17 | 18 | pub fn nvim_path() -> PathBuf { 19 | let (path_str, have_env) = match env::var("NVIMRS_TEST_BIN") { 20 | Ok(path) => (path, true), 21 | Err(_) => (NVIM_PATH.into(), false), 22 | }; 23 | 24 | let path = PathBuf::from(&path_str); 25 | if !path.exists() { 26 | if have_env { 27 | panic!("nvim bin from $NVIMRS_TEST_BIN \"{}\" does not exist", path_str) 28 | } else { 29 | panic!( 30 | "\"{}\" not found, maybe you need to build it or set \ 31 | $NVIMRS_TEST_BIN?", 32 | NVIM_PATH 33 | ); 34 | } 35 | } 36 | path 37 | } 38 | -------------------------------------------------------------------------------- /tests/regression/buffering.rs: -------------------------------------------------------------------------------- 1 | #[path = "../common/mod.rs"] 2 | mod common; 3 | use common::*; 4 | 5 | use std::{path::PathBuf, process::Command}; 6 | 7 | fn viml_escape(in_str: &str) -> String { 8 | in_str.replace('\\', r"\\") 9 | } 10 | 11 | fn linebuffercrashbin() -> &'static str { 12 | #[cfg(feature = "use_tokio")] 13 | return "linebuffercrash"; 14 | #[cfg(feature = "use_async-std")] 15 | return "linebuffercrash_as"; 16 | } 17 | 18 | #[test] 19 | fn linebuffer_crash() { 20 | let c1 = format!( 21 | "let jobid = jobstart([\"{}\"], {{\"rpc\": v:true}})", 22 | viml_escape( 23 | PathBuf::from(env!("CARGO_MANIFEST_DIR")) 24 | .join("target") 25 | .join("debug") 26 | .join(linebuffercrashbin()) 27 | .to_str() 28 | .unwrap() 29 | ) 30 | ); 31 | 32 | let status = Command::new(nvim_path()) 33 | .args(&[ 34 | "-u", 35 | "NONE", 36 | "--headless", 37 | "-c", 38 | &c1, 39 | ]) 40 | .status() 41 | .unwrap(); 42 | 43 | assert!(status.success()); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /examples/quitting.rs: -------------------------------------------------------------------------------- 1 | //! Quitting. See src/examples/quitting.rs for documentation 2 | use nvim_rs::{create::tokio as create, rpc::handler::Dummy as DummyHandler}; 3 | 4 | use std::error::Error; 5 | 6 | use tokio::process::Command; 7 | 8 | const NVIMPATH: &str = "neovim/build/bin/nvim"; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let handler = DummyHandler::new(); 13 | 14 | let (nvim, _io_handle, _child) = create::new_child_cmd( 15 | Command::new(NVIMPATH) 16 | .args(&["-u", "NONE", "--embed", "--headless"]) 17 | .env("NVIM_LOG_FILE", "nvimlog"), 18 | handler, 19 | ) 20 | .await 21 | .unwrap(); 22 | 23 | let chan = nvim.get_api_info().await.unwrap()[0].as_i64().unwrap(); 24 | let close = format!("call chanclose({})", chan); 25 | 26 | if let Err(e) = nvim.command(&close).await { 27 | eprintln!("Error in last command: {}", e); 28 | eprintln!("Caused by : {:?}", e.as_ref().source()); 29 | 30 | if e.is_channel_closed() { 31 | eprintln!("Channel closed, quitting!"); 32 | } else { 33 | eprintln!("Channel was not closed, no idea what happened!"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Justin Charette 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/exttypes/mod.rs: -------------------------------------------------------------------------------- 1 | //! Buffers, windows, tabpages of neovim 2 | mod buffer; 3 | mod tabpage; 4 | mod window; 5 | 6 | pub use buffer::Buffer; 7 | pub use tabpage::Tabpage; 8 | pub use window::Window; 9 | 10 | /// A macro to implement trait for the [`exttypes`](crate::exttypes) 11 | #[macro_export] 12 | macro_rules! impl_exttype_traits { 13 | ($ext:ident) => { 14 | impl PartialEq for $ext 15 | where 16 | W: AsyncWrite + Send + Unpin + 'static, 17 | { 18 | fn eq(&self, other: &Self) -> bool { 19 | self.code_data == other.code_data && self.neovim == other.neovim 20 | } 21 | } 22 | impl Eq for $ext where W: AsyncWrite + Send + Unpin + 'static {} 23 | 24 | impl Clone for $ext 25 | where 26 | W: AsyncWrite + Send + Unpin + 'static, 27 | { 28 | fn clone(&self) -> Self { 29 | Self { 30 | code_data: self.code_data.clone(), 31 | neovim: self.neovim.clone(), 32 | } 33 | } 34 | } 35 | 36 | impl IntoVal for &$ext 37 | where 38 | W: AsyncWrite + Send + Unpin + 'static, 39 | { 40 | fn into_val(self) -> Value { 41 | self.code_data.clone() 42 | } 43 | } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/exttypes/window.rs: -------------------------------------------------------------------------------- 1 | use futures::io::AsyncWrite; 2 | use rmpv::Value; 3 | 4 | use super::{Buffer, Tabpage}; 5 | use crate::{ 6 | error::CallError, impl_exttype_traits, rpc::model::IntoVal, Neovim, 7 | }; 8 | 9 | /// A struct representing a neovim window. It is specific to a 10 | /// [`Neovim`](crate::neovim::Neovim) instance, and calling a method on it will 11 | /// always use this instance. 12 | pub struct Window 13 | where 14 | W: AsyncWrite + Send + Unpin + 'static, 15 | { 16 | pub(crate) code_data: Value, 17 | pub(crate) neovim: Neovim, 18 | } 19 | 20 | impl_exttype_traits!(Window); 21 | 22 | impl Window 23 | where 24 | W: AsyncWrite + Send + Unpin + 'static, 25 | { 26 | /// since: 1 27 | pub async fn get_buf(&self) -> Result, Box> { 28 | Ok( 29 | self 30 | .neovim 31 | .call("nvim_win_get_buf", call_args![self.code_data.clone()]) 32 | .await? 33 | .map(|val| Buffer::new(val, self.neovim.clone()))?, 34 | ) 35 | } 36 | /// since: 1 37 | pub async fn get_tabpage(&self) -> Result, Box> { 38 | Ok( 39 | self 40 | .neovim 41 | .call("nvim_win_get_tabpage", call_args![self.code_data.clone()]) 42 | .await? 43 | .map(|val| Tabpage::new(val, self.neovim.clone()))?, 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/bench_sync.rs: -------------------------------------------------------------------------------- 1 | use neovim_lib::{Neovim, NeovimApi, Session}; 2 | 3 | fn main() { 4 | let mut session = Session::new_parent().unwrap(); 5 | let receiver = session.start_event_loop_channel(); 6 | let mut nvim = Neovim::new(session); 7 | 8 | loop { 9 | match receiver.recv().unwrap().0.as_ref() { 10 | "file" => { 11 | let c = nvim.get_current_buf().unwrap(); 12 | for _ in 0..1_000_usize { 13 | let _x = c.get_lines(&mut nvim, 0, -1, false); 14 | } 15 | nvim 16 | .command("let g:finished_file = reltimestr(reltime(g:started_file))") 17 | .unwrap(); 18 | } 19 | "buffer" => { 20 | nvim.command("let g:started_buffer = reltime()").unwrap(); 21 | for _ in 0..10_000_usize { 22 | let _ = nvim.get_current_buf().unwrap(); 23 | } 24 | nvim 25 | .command( 26 | "let g:finished_buffer = reltimestr(reltime(g:started_buffer))", 27 | ) 28 | .unwrap(); 29 | } 30 | "api" => { 31 | nvim.command("let g:started_api = reltime()").unwrap(); 32 | for _ in 0..1_000_usize { 33 | let _ = nvim.get_api_info().unwrap(); 34 | } 35 | nvim 36 | .command("let g:finished_api = reltimestr(reltime(g:started_api))") 37 | .unwrap(); 38 | } 39 | _ => break, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/exttypes/tabpage.rs: -------------------------------------------------------------------------------- 1 | use futures::io::AsyncWrite; 2 | use rmpv::Value; 3 | 4 | use crate::{ 5 | error::CallError, exttypes::Window, impl_exttype_traits, rpc::model::IntoVal, 6 | Neovim, 7 | }; 8 | 9 | /// A struct representing a neovim tabpage. It is specific to a 10 | /// [`Neovim`](crate::neovim::Neovim) instance, and calling a method on it will 11 | /// always use this instance. 12 | pub struct Tabpage 13 | where 14 | W: AsyncWrite + Send + Unpin + 'static, 15 | { 16 | pub(crate) code_data: Value, 17 | pub(crate) neovim: Neovim, 18 | } 19 | 20 | impl_exttype_traits!(Tabpage); 21 | 22 | impl Tabpage 23 | where 24 | W: AsyncWrite + Send + Unpin + 'static, 25 | { 26 | /// since: 1 27 | pub async fn list_wins(&self) -> Result>, Box> { 28 | match self 29 | .neovim 30 | .call("nvim_tabpage_list_wins", call_args![self.code_data.clone()]) 31 | .await?? 32 | { 33 | Value::Array(arr) => Ok( 34 | arr 35 | .into_iter() 36 | .map(|v| Window::new(v, self.neovim.clone())) 37 | .collect(), 38 | ), 39 | val => Err(CallError::WrongValueType(val))?, 40 | } 41 | } 42 | /// since: 1 43 | pub async fn get_win(&self) -> Result, Box> { 44 | Ok( 45 | self 46 | .neovim 47 | .call("nvim_tabpage_get_win", call_args![self.code_data.clone()]) 48 | .await? 49 | .map(|val| Window::new(val, self.neovim.clone()))?, 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::*; 3 | 4 | use std::{fs, path::PathBuf, process::Command}; 5 | 6 | use tempfile::Builder; 7 | 8 | fn viml_escape(in_str: &str) -> String { 9 | in_str.replace('\\', r"\\") 10 | } 11 | 12 | #[test] 13 | fn basic() { 14 | let dir = Builder::new().prefix("nvim-rs.test").tempdir().unwrap(); 15 | let dir_path = dir.path(); 16 | let buf_path = dir_path.join("curbuf.txt"); 17 | let pong_path = dir_path.join("pong.txt"); 18 | 19 | let c1 = format!( 20 | "let jobid = jobstart([\"{}\", \"{}\"], {{\"rpc\": v:true}})", 21 | viml_escape( 22 | PathBuf::from(env!("EXAMPLES_PATH")) 23 | .join("basic") 24 | .to_str() 25 | .unwrap() 26 | ), 27 | viml_escape(buf_path.to_str().unwrap()) 28 | ); 29 | let c2 = r#"sleep 100m | let pong = rpcrequest(jobid, "ping")"#; 30 | let c3 = format!( 31 | "edit {}| put =pong", 32 | viml_escape(pong_path.to_str().unwrap()) 33 | ); 34 | let c4 = r#"wqa!"#; 35 | 36 | let status = Command::new(nvim_path()) 37 | .args(&[ 38 | "-u", 39 | "NONE", 40 | "--headless", 41 | "-c", 42 | &c1, 43 | "-c", 44 | c2, 45 | "-c", 46 | &c3, 47 | "-c", 48 | c4, 49 | ]) 50 | .status() 51 | .unwrap(); 52 | 53 | assert!(status.success()); 54 | 55 | let pong = fs::read_to_string(pong_path).unwrap(); 56 | let buf = fs::read_to_string(buf_path).unwrap(); 57 | 58 | assert_eq!("pong", pong.trim()); 59 | assert_eq!("Ext(0, [1])", buf.trim()); 60 | } 61 | -------------------------------------------------------------------------------- /bench_examples.vim: -------------------------------------------------------------------------------- 1 | let l = [] 2 | e Cargo.lock 3 | let id = jobstart('target/release/examples/bench_tokio', { 'rpc': v:true }) 4 | 5 | let start = reltime() 6 | call rpcrequest(id, 'file') 7 | let seconds = reltimestr(reltime(start)) 8 | call add(l, 'File Tokio: ' . seconds) 9 | 10 | let start = reltime() 11 | call rpcrequest(id, 'buffer') 12 | let seconds = reltimestr(reltime(start)) 13 | call add(l, 'Buffer Tokio: ' . seconds) 14 | 15 | let start = reltime() 16 | call rpcrequest(id, 'api') 17 | let seconds = reltimestr(reltime(start)) 18 | call add(l, 'API Tokio: ' . seconds) 19 | 20 | 21 | let id = jobstart('target/release/examples/bench_async-std', { 'rpc': v:true }) 22 | 23 | let start = reltime() 24 | call rpcrequest(id, 'file') 25 | let seconds = reltimestr(reltime(start)) 26 | call add(l, 'File Async-Std: ' . seconds) 27 | 28 | let start = reltime() 29 | call rpcrequest(id, 'buffer') 30 | let seconds = reltimestr(reltime(start)) 31 | call add(l, 'Buffer Async-Std: ' . seconds) 32 | 33 | let start = reltime() 34 | call rpcrequest(id, 'api') 35 | let seconds = reltimestr(reltime(start)) 36 | call add(l, 'API Async-Std: ' . seconds) 37 | 38 | 39 | let id = jobstart('target/release/examples/bench_sync', { 'rpc': v:true }) 40 | 41 | let g:started_file = reltime() 42 | call rpcnotify(id, 'file') 43 | 44 | call rpcnotify(id, 'buffer') 45 | 46 | call rpcnotify(id, 'api') 47 | 48 | sleep 20 49 | 50 | call add(l, 'File Neovim-Lib: ' . g:finished_file) 51 | call add(l, 'Buffer Neovim-Lib: ' . g:finished_buffer) 52 | call add(l, 'API Neovim-Lib: ' . g:finished_api) 53 | 54 | call nvim_buf_set_lines(0, 0, -1, v:false, l) 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Rust library for Neovim clients 2 | //! 3 | //! Implements support for rust plugins for 4 | //! [Neovim](https://github.com/neovim/neovim) through its msgpack-rpc API. 5 | //! 6 | //! ### Origins 7 | //! 8 | //! This library started as a fork of 9 | //! [neovim-lib](https://github.com/daa84/neovim-lib) with the goal to utilize 10 | //! Rust's `async/await` to allow requests/notification to/from neovim to be 11 | //! arbitrarily nested. After the fork, I started implementing more ideas I had 12 | //! for this library. 13 | //! 14 | //! ### Status 15 | //! 16 | //! As of the end of 2019, I'm somewhat confident to recommend starting to use 17 | //! this library. The overall handling should not change anymore. A breaking 18 | //! change I kind of expect is adding error variants to 19 | //! [`CallError`](crate::error::CallError) when I start working on the API 20 | //! (right now, it panics when messages don't have the right format, I'll want 21 | //! to return proper errors in that case). 22 | //! 23 | //! I've not yet worked through the details of what-to-export, but I'm quite 24 | //! willing to consider what people need or want. 25 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 26 | extern crate rmp; 27 | extern crate rmpv; 28 | #[macro_use] 29 | extern crate log; 30 | 31 | pub mod rpc; 32 | #[macro_use] 33 | pub mod neovim; 34 | pub mod error; 35 | pub mod examples; 36 | pub mod exttypes; 37 | pub mod neovim_api; 38 | pub mod neovim_api_manual; 39 | pub mod uioptions; 40 | 41 | pub mod create; 42 | 43 | pub use crate::{ 44 | exttypes::{Buffer, Tabpage, Window}, 45 | neovim::Neovim, 46 | rpc::handler::Handler, 47 | uioptions::{UiAttachOptions, UiOption}, 48 | }; 49 | 50 | #[cfg(feature = "use_tokio")] 51 | pub mod compat { 52 | //! A re-export of tokio-util's [`Compat`](tokio_util::compat::Compat) 53 | pub mod tokio { 54 | pub use tokio_util::compat::Compat; 55 | } 56 | } 57 | 58 | pub use rmpv::Value; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-rs ![CI](https://github.com/KillTheMule/nvim-rs/actions/workflows/ci.yml/badge.svg) [![(Docs.rs)](https://docs.rs/nvim-rs/badge.svg)](https://docs.rs/nvim-rs/) [![(Crates.io status)](https://img.shields.io/crates/v/nvim-rs.svg)](https://crates.io/crates/nvim-rs) 2 | Rust library for Neovim msgpack-rpc clients. Utilizes async to allow for arbitrary nesting of requests. 3 | 4 | ## Status 5 | 6 | Useable, see the `examples/` and `tests/` folders for examples. The `nvim_rs::examples` submodule contains documentation of the examples. 7 | 8 | The **API** is unstable, see the [Roadmap](https://github.com/KillTheMule/nvim-rs/issues/1) for things being planned. 9 | 10 | ## Contributing 11 | 12 | I'd love contributions, comments, praise, criticism... You could open an [issue](https://github.com/KillTheMule/nvim-rs/issues) or a [pull request](https://github.com/KillTheMule/nvim-rs/pulls). I also read the subreddits for [rust](https://www.reddit.com/r/rust/), if that suits you better. 13 | 14 | ## Running tests 15 | 16 | For some tests, neovim needs to be installed. Set the environment variable `NVIMRS_TEST_BIN` to 17 | the path of the binary before running the tests. 18 | 19 | Afterwards, you can simply run `cargo test --features="use_tokio"`. 20 | Also run `cargo build --examples --features="use_tokio"` as well as `cargo 21 | bench -- --test --features="use_tokio"` to make sure everything still compiles 22 | (replace `use_tokio` by `use_async-std` to do all the above with `async-std` 23 | instead of `tokio`). 24 | 25 | ## License 26 | 27 | As this is a fork of [neovim-lib](https://github.com/daa84/neovim-lib), it is licensed under the GNU Lesser General Public License v3.0. 28 | 29 | **IMPORTANT**: All commits to this project, including all PRs, are 30 | dual-licensed under the Apache or MIT license. This is to allow the possibility 31 | of relicensing this project later. 32 | 33 | ## CoC 34 | 35 | Wherever applicable, this project follows the [rust code of 36 | conduct](https://www.rust-lang.org/en-US/conduct.html). 37 | -------------------------------------------------------------------------------- /src/examples/mod.rs: -------------------------------------------------------------------------------- 1 | //! Examples on how to use [`nvim-rs`](crate). 2 | //! 3 | //! The code in question is in the `examples` directory of the project. The 4 | //! files in `src/examples/` contain the documentation. 5 | //! 6 | //! # Contents 7 | //! 8 | //! ### `handler_drop` 9 | //! 10 | //! An example showing how to implement cleanup-logic by implementing 11 | //! [`Drop`](std::ops::Drop) for the [`handler`](crate::rpc::handler::Handler). 12 | //! 13 | //! ### `quitting` 14 | //! 15 | //! An example showing how to handle quitting in a plugin by catching a [`closed 16 | //! channel`](crate::error::CallError::is_channel_closed). 17 | //! 18 | //! 19 | //! ## `scorched_earth` 20 | //! 21 | //! A port of a real existing plugin. 22 | //! 23 | //! ## `scorched_earth_as` 24 | //! 25 | //! A port of the `scorched_earth` example to `async-std`. 26 | //! 27 | //! ## `bench_*` 28 | //! 29 | //! Some crude benchmarks to measure performance. After running 30 | //! 31 | //! ```sh 32 | //! cargo build --examples --features use_tokio --release 33 | //! cargo build --examples --features use_async-std --release 34 | //! cargo build --examples --features use_neovim_lib --release 35 | //! ``` 36 | //! 37 | //! (the features aren't all compatible, so you need to run those separately 38 | //! indeed) you can run `nvim -u bench_examples.vim`, and after so and so long 39 | //! get a table in a modified buffer that tells you some numbers. 40 | //! 41 | //! The benchmarks of `tokio` and `async-std` should be pretty comparable, but 42 | //! note that tokio's runtime takes parameters that influence performance. 43 | //! Tweaking those, I found the runtimes don't differ by much. 44 | //! 45 | //! The benchmark of `neovim_lib` (called `bench_sync`) can't be designed the 46 | //! way the others are, since they use nested requests. I tried to get around 47 | //! that somewhat sneakily, but it's not 100% clear those benchmarks are 48 | //! equivalent (but, if anything, they should favor neovim-lib a tad). 49 | pub mod handler_drop; 50 | pub mod quitting; 51 | pub mod scorched_earth; 52 | pub mod scorched_earth_as; 53 | -------------------------------------------------------------------------------- /src/create/mod.rs: -------------------------------------------------------------------------------- 1 | //! Functions to spawn a [`neovim`](crate::neovim::Neovim) session. 2 | //! 3 | //! This implements various possibilities to connect to neovim, including 4 | //! spawning an own child process. Available capabilities might depend on your 5 | //! OS and choice of features. 6 | //! 7 | //! Supported features: `use_tokio` and `use_async-std`. 8 | //! 9 | //! **IMPORTANT**: Due to incompatibilities of the rust async ecosystem, you 10 | //! might not be able to use types from one lib with the runtime of another lib. 11 | //! E.g. when using the features `use_tokio`, you will need to run all the 12 | //! API functions from inside the tokio runtime. 13 | #[cfg(feature = "use_tokio")] 14 | pub mod tokio; 15 | 16 | #[cfg(feature = "use_async-std")] 17 | pub mod async_std; 18 | 19 | use core::future::Future; 20 | use std::{fs::File, io}; 21 | 22 | use crate::rpc::handler::Handler; 23 | 24 | /// A task to generalize spawning a future that returns `()`. 25 | /// 26 | /// If you use one of the features `use_tokio` or `use_async-std`, this will 27 | /// automatically be implemented on you 28 | /// [`Handler`](crate::rpc::handler::Handler) using the appropriate runtime. 29 | /// 30 | /// If you have a runtime that brings appropriate types, you can implement this 31 | /// on your [`Handler`](crate::rpc::handler::Handler) and use 32 | /// [`Neovim::new`](crate::neovim::Neovim::new) to connect to neovim. 33 | pub trait Spawner: Handler { 34 | type Handle; 35 | 36 | fn spawn(&self, future: Fut) -> Self::Handle 37 | where 38 | Fut: Future + Send + 'static; 39 | } 40 | 41 | /// Create a std::io::File for stdout, which is not line-buffered, as 42 | /// opposed to std::io::Stdout. 43 | #[cfg(unix)] 44 | pub fn unbuffered_stdout() -> io::Result { 45 | use std::{io::stdout, os::fd::AsFd}; 46 | 47 | let owned_sout_fd = stdout().as_fd().try_clone_to_owned()?; 48 | Ok(File::from(owned_sout_fd)) 49 | } 50 | #[cfg(windows)] 51 | pub fn unbuffered_stdout() -> io::Result { 52 | use std::{io::stdout, os::windows::io::AsHandle}; 53 | 54 | let owned_sout_handle = stdout().as_handle().try_clone_to_owned()?; 55 | Ok(File::from(owned_sout_handle)) 56 | } 57 | -------------------------------------------------------------------------------- /bindings/neovim_api.rs: -------------------------------------------------------------------------------- 1 | //! The auto generated API for [`neovim`](crate::neovim::Neovim) 2 | //! 3 | //! Auto generated {{date}} 4 | use futures::io::AsyncWrite; 5 | 6 | use crate::{ 7 | error::CallError, 8 | neovim::*, 9 | rpc::{unpack::TryUnpack, *}, 10 | Buffer, Tabpage, Window, 11 | }; 12 | 13 | {% for etype in exttypes %} 14 | 15 | impl {{ etype.name }} 16 | where W: AsyncWrite + Send + Unpin + 'static 17 | { 18 | #[must_use] 19 | pub fn new(code_data: Value, neovim: Neovim) -> {{ etype.name }} 20 | { 21 | {{ etype.name }} { 22 | code_data, 23 | neovim 24 | } 25 | } 26 | 27 | /// Internal value, that represent type 28 | #[must_use] 29 | pub fn get_value(&self) -> &Value { 30 | &self.code_data 31 | } 32 | 33 | {% for f in functions if f.ext and f.name.startswith(etype.prefix) %} 34 | /// since: {{f.since}} 35 | pub async fn {{f.name|replace(etype.prefix, '')}}(&self, {{f.argstring}}) -> Result<{{f.return_type.native_type_ret}}, Box> 36 | { 37 | self.neovim.call("{{f.name}}", 38 | call_args![self.code_data.clone() 39 | {% if f.parameters|count > 0 %} 40 | , {{ f.parameters|map(attribute = "name")|join(", ") }} 41 | {% endif %} 42 | ]) 43 | .await?? 44 | .try_unpack() 45 | .map_err(|v| Box::new(CallError::WrongValueType(v))) 46 | } 47 | {% endfor %} 48 | } 49 | 50 | {% endfor %} 51 | 52 | 53 | impl Neovim 54 | where 55 | W: AsyncWrite + Send + Unpin + 'static, 56 | { 57 | {% for f in functions if not f.ext %} 58 | pub async fn {{f.name|replace('nvim_', '')}}(&self, {{f.argstring}}) -> Result<{{f.return_type.native_type_ret}}, Box> { 59 | self.call("{{f.name}}", 60 | call_args![{{ f.parameters|map(attribute = "name")|join(", ") }}]) 61 | .await?? 62 | .try_unpack() 63 | .map_err(|v| Box::new(CallError::WrongValueType(v))) 64 | } 65 | 66 | {% endfor %} 67 | } 68 | -------------------------------------------------------------------------------- /src/examples/quitting.rs: -------------------------------------------------------------------------------- 1 | //! # Quitting 2 | //! 3 | //! A very small example showing how to handle quitting an application. 4 | //! 5 | //! * Running as an rpc plugin, just have neovim close the channel corresponding 6 | //! to our plugin 7 | //! * When embedding neovim, do the same via a command, as shown in this 8 | //! example. Note that this final request _will_ receive an error, since it will 9 | //! not get an answer from neovim. 10 | //! 11 | //! Also note that all other pending requests will receive an EOF error as well. 12 | //! 13 | //! ## Usage 14 | //! 15 | //! First, build the neovim included as a submodule: 16 | //! 17 | //! ```sh 18 | //! cd neovim 19 | //! make 20 | //! ``` 21 | //! 22 | //! See for build options. 23 | //! Nothing is really needed to run the example. 24 | //! 25 | //! After that, run the example via 26 | //! 27 | //! ```sh 28 | //! cargo run --example quitting 29 | //! ``` 30 | //! 31 | //! ## Description 32 | //! 33 | //! Some overview over the code: 34 | //! 35 | //! * Since we're not interested in handling anything, our `NeovimHandler` is a 36 | //! [`Dummy`](crate::rpc::handler::Dummy) that does nothing on requests and 37 | //! notifications. We need to pass something that implements 38 | //! [`Spawn`](futures::task::Spawn), which is represented by the `Spawner`. It 39 | //! doesn't carry any data here. We implement `Spawn` in the most trivial way 40 | //! possible, by calling [`tokio::spawn`](tokio::spawn) that in turn calls out 41 | //! to the surrounding runtime. 42 | //! 43 | //! * Any shutdown logic should be handled after the channel was closed. We 44 | //! don't actually need to inspect the error, since the application will shut 45 | //! down no matter what. If we need access to our handler for that, we should 46 | //! implement [`Drop`](std::ops::Drop) for it, see 47 | //! [`handler_drop`](crate::examples::handler_drop). 48 | //! 49 | //! * The last command (the one that instructs neovim to close the channel) will 50 | //! not receive an answer anymore, but an error. We just show the error and its 51 | //! source for demonstation purposes. We use the 52 | //! [`is_channel_closed`](crate::error::CallError::is_channel_closed) method 53 | //! to verify that the error originates from this. 54 | -------------------------------------------------------------------------------- /src/examples/handler_drop.rs: -------------------------------------------------------------------------------- 1 | //! # `handler_drop` 2 | //! 3 | //! An example of handling cleanup logic by implementing 4 | //! [`Drop`](std::ops::Drop) for the handler. The plugin attaches to the current 5 | //! buffer, then sets the first 2 lines, which get sent back to us with a 6 | //! `nvim_buf_lines_event` notification. We handle this notification by saving 7 | //! the lines in the handler. We then let nvim close the channel, and wait for 8 | //! the IO loop to finish. The handler gets dropped, and so our cleanup logic is 9 | //! executed. 10 | //! 11 | //! ## Usage 12 | //! 13 | //! First, build the neovim included as a submodule: 14 | //! 15 | //! ```sh 16 | //! cd neovim 17 | //! make 18 | //! ``` 19 | //! 20 | //! See for build options. 21 | //! Nothing is really needed to run the example. 22 | //! 23 | //! After that, run the example via 24 | //! 25 | //! ```sh 26 | //! cargo run --example handler_drop 27 | //! ``` 28 | //! 29 | //! You can verify it worked by looking at the file `handler_drop.txt` in the 30 | //! project directory which should contain 2 lines ("xyz" and "abc"). 31 | //! 32 | //! ## Description 33 | //! 34 | //! * The associated type for our [`Handler`](crate::rpc::handler::Handler) is 35 | //! the stdin of our child. But tokio's 36 | //! [`ChildStdin`](tokio::process::ChildStdin) does not implement 37 | //! [`futures::io::AsyncWrite`](futures::io::AsyncWrite), so it needs to be 38 | //! wrapped in the provided [`Compat`](crate::compat::tokio::Compat) type. 39 | //! 40 | //! * Implementing [`Drop`](std::ops::Drop) is straightforward, except that we 41 | //! cannot do so asynchronously. Since dropping the handler is one of the last 42 | //! things our plugin does, it's not problem to run even larger code bodies 43 | //! synchronously here. 44 | //! 45 | //! * The event handling code is not efficient, because we just read the 46 | //! arguments by reference and clone them. It's easy to take ownership directly 47 | //! by matching on the enum [`Value`](rmpv::Value) directly, though. 48 | //! 49 | //! * There's basically no error handling, other than `unwrap`ing all the 50 | //! `Result`s. 51 | //! 52 | //! * `await`ing the io future handle is probably not necessary, but feels like 53 | //! a nice thing to do. 54 | //! 55 | //! * As with the other examples, we implement [`Spawn`](futures::task::Spawn) 56 | //! for our `NeovimHandler` most trivially. 57 | -------------------------------------------------------------------------------- /tests/connecting/handshake.rs: -------------------------------------------------------------------------------- 1 | use nvim_rs::rpc::handler::Dummy as DummyHandler; 2 | 3 | #[cfg(feature = "use_tokio")] 4 | use nvim_rs::create::tokio as create; 5 | #[cfg(feature = "use_tokio")] 6 | use tokio::process::Command; 7 | #[cfg(feature = "use_tokio")] 8 | use tokio::test as atest; 9 | 10 | #[cfg(feature = "use_async-std")] 11 | use async_std::test as atest; 12 | #[cfg(feature = "use_async-std")] 13 | use nvim_rs::create::async_std as create; 14 | #[cfg(feature = "use_async-std")] 15 | use std::process::Command; 16 | 17 | #[path = "../common/mod.rs"] 18 | mod common; 19 | use common::*; 20 | 21 | use nvim_rs::error::HandshakeError; 22 | 23 | #[atest] 24 | async fn successful_handshake() { 25 | let handler = DummyHandler::new(); 26 | 27 | create::new_child_handshake_cmd( 28 | Command::new(nvim_path()).args(&["-u", "NONE", "--embed"]), 29 | handler, 30 | "handshake_message", 31 | ) 32 | .await 33 | .expect("Should launch correctly"); 34 | } 35 | 36 | #[cfg(unix)] 37 | #[atest] 38 | async fn successful_handshake_with_extra_output() { 39 | let handler = DummyHandler::new(); 40 | let nvim = nvim_path(); 41 | 42 | create::new_child_handshake_cmd( 43 | Command::new("/bin/sh").args(&[ 44 | "-c", 45 | &format!( 46 | "echo 'extra output';{} -u NONE --embed", 47 | nvim.to_string_lossy() 48 | ), 49 | ]), 50 | handler, 51 | "handshake_message", 52 | ) 53 | .await 54 | .expect("Should launch correctly"); 55 | } 56 | 57 | #[cfg(unix)] 58 | #[atest] 59 | async fn unsuccessful_handshake_with_wrong_output() { 60 | let handler = DummyHandler::new(); 61 | 62 | // NOTE: This has to match the exact length of the message sent 63 | let expected_request_len = 46; 64 | 65 | // Make sure that the command is alive for long enough by reading the request 66 | // message from stdin with dd 67 | let res = create::new_child_handshake_cmd( 68 | Command::new("/bin/sh").args(&[ 69 | "-c", 70 | &format!("echo 'wrong output'; 71 | timeout 5 dd bs=1 count={expected_request_len} > /dev/null 2>&1")]), 72 | handler, 73 | "handshake_message", 74 | ) 75 | .await; 76 | 77 | match res { 78 | Err(err) => match *err { 79 | HandshakeError::UnexpectedResponse(output) => { 80 | assert_eq!(output, "wrong output\n"); 81 | } 82 | _ => { 83 | panic!("Unexpected error returned {}", err); 84 | } 85 | }, 86 | _ => panic!("No error returned"), 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | //! A basic example. Mainly for use in a test, but also shows off some basic 2 | //! functionality. 3 | use std::{env, error::Error, fs}; 4 | 5 | 6 | use rmpv::Value; 7 | 8 | use tokio::fs::File as TokioFile; 9 | 10 | use nvim_rs::{ 11 | compat::tokio::Compat, create::tokio as create, rpc::IntoVal, Handler, Neovim, 12 | }; 13 | 14 | #[derive(Clone)] 15 | struct NeovimHandler {} 16 | 17 | impl Handler for NeovimHandler { 18 | type Writer = Compat; 19 | 20 | async fn handle_request( 21 | &self, 22 | name: String, 23 | _args: Vec, 24 | _neovim: Neovim>, 25 | ) -> Result { 26 | match name.as_ref() { 27 | "ping" => Ok(Value::from("pong")), 28 | _ => unimplemented!(), 29 | } 30 | } 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() { 35 | let handler: NeovimHandler = NeovimHandler {}; 36 | let (nvim, io_handler) = create::new_parent(handler).await.unwrap(); 37 | let curbuf = nvim.get_current_buf().await.unwrap(); 38 | 39 | let mut envargs = env::args(); 40 | let _ = envargs.next(); 41 | let testfile = envargs.next().unwrap(); 42 | 43 | fs::write(testfile, &format!("{:?}", curbuf.into_val())).unwrap(); 44 | 45 | // Any error should probably be logged, as stderr is not visible to users. 46 | match io_handler.await { 47 | Err(joinerr) => eprintln!("Error joining IO loop: '{}'", joinerr), 48 | Ok(Err(err)) => { 49 | if !err.is_reader_error() { 50 | // One last try, since there wasn't an error with writing to the 51 | // stream 52 | nvim 53 | .err_writeln(&format!("Error: '{}'", err)) 54 | .await 55 | .unwrap_or_else(|e| { 56 | // We could inspect this error to see what was happening, and 57 | // maybe retry, but at this point it's probably best 58 | // to assume the worst and print a friendly and 59 | // supportive message to our users 60 | eprintln!("Well, dang... '{}'", e); 61 | }); 62 | } 63 | 64 | if !err.is_channel_closed() { 65 | // Closed channel usually means neovim quit itself, or this plugin was 66 | // told to quit by closing the channel, so it's not always an error 67 | // condition. 68 | eprintln!("Error: '{}'", err); 69 | 70 | let mut source = err.source(); 71 | 72 | while let Some(e) = source { 73 | eprintln!("Caused by: '{}'", e); 74 | source = e.source(); 75 | } 76 | } 77 | } 78 | Ok(Ok(())) => {} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/bench_async-std.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use rmpv::Value; 4 | 5 | use async_std::{self, fs::File as ASFile}; 6 | 7 | use nvim_rs::{create::async_std as create, Handler, Neovim}; 8 | 9 | #[derive(Clone)] 10 | struct NeovimHandler {} 11 | 12 | impl Handler for NeovimHandler { 13 | type Writer = ASFile; 14 | 15 | async fn handle_request( 16 | &self, 17 | name: String, 18 | _args: Vec, 19 | neovim: Neovim, 20 | ) -> Result { 21 | match name.as_ref() { 22 | "file" => { 23 | let c = neovim.get_current_buf().await.unwrap(); 24 | for _ in 0..1_000_usize { 25 | let _x = c.get_lines(0, -1, false).await; 26 | } 27 | Ok(Value::Nil) 28 | } 29 | "buffer" => { 30 | for _ in 0..10_000_usize { 31 | let _ = neovim.get_current_buf().await.unwrap(); 32 | } 33 | Ok(Value::Nil) 34 | } 35 | "api" => { 36 | for _ in 0..1_000_usize { 37 | let _ = neovim.get_api_info().await.unwrap(); 38 | } 39 | Ok(Value::Nil) 40 | } 41 | _ => Ok(Value::Nil), 42 | } 43 | } 44 | } 45 | 46 | #[async_std::main] 47 | async fn main() { 48 | let handler: NeovimHandler = NeovimHandler {}; 49 | 50 | let (nvim, io_handler) = create::new_parent(handler).await.unwrap(); 51 | 52 | // Any error should probably be logged, as stderr is not visible to users. 53 | match io_handler.await { 54 | Err(err) => { 55 | if !err.is_reader_error() { 56 | // One last try, since there wasn't an error with writing to the 57 | // stream 58 | nvim 59 | .err_writeln(&format!("Error: '{}'", err)) 60 | .await 61 | .unwrap_or_else(|e| { 62 | // We could inspect this error to see what was happening, and 63 | // maybe retry, but at this point it's probably best 64 | // to assume the worst and print a friendly and 65 | // supportive message to our users 66 | eprintln!("Well, dang... '{}'", e); 67 | }); 68 | } 69 | 70 | if !err.is_channel_closed() { 71 | // Closed channel usually means neovim quit itself, or this plugin was 72 | // told to quit by closing the channel, so it's not always an error 73 | // condition. 74 | eprintln!("Error: '{}'", err); 75 | 76 | let mut source = err.source(); 77 | 78 | while let Some(e) = source { 79 | eprintln!("Caused by: '{}'", e); 80 | source = e.source(); 81 | } 82 | } 83 | } 84 | Ok(()) => {} 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /benches/rpc_tokio.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | 3 | use nvim_rs::{ 4 | call_args, 5 | create::tokio as create, 6 | rpc::{handler::Dummy, IntoVal}, 7 | }; 8 | 9 | use tokio::{process::Command, runtime::Builder}; 10 | 11 | #[path = "../tests/common/mod.rs"] 12 | mod common; 13 | use common::nvim_path; 14 | 15 | fn simple_requests(c: &mut Criterion) { 16 | let handler = Dummy::new(); 17 | 18 | let rt = Builder::new_current_thread() 19 | .enable_io() 20 | .build() 21 | .unwrap(); 22 | 23 | let (nvim, _io_handler, _child) = rt 24 | .block_on(create::new_child_cmd( 25 | Command::new(nvim_path()).args(&["-u", "NONE", "--embed", "--headless"]), 26 | handler, 27 | )) 28 | .unwrap(); 29 | 30 | let nvim1 = nvim.clone(); 31 | rt.block_on(async move { nvim1.command("set noswapfile").await }) 32 | .expect("0"); 33 | 34 | c.bench_function("simple_requests", move |b| { 35 | b.iter(|| { 36 | let nvim = nvim.clone(); 37 | let _curbuf = rt.block_on(async move { 38 | nvim.get_current_buf().await.expect("1"); 39 | }); 40 | }) 41 | }); 42 | } 43 | 44 | fn request_file(c: &mut Criterion) { 45 | let handler = Dummy::new(); 46 | 47 | let rt = Builder::new_current_thread() 48 | .enable_io() 49 | .build() 50 | .unwrap(); 51 | 52 | let (nvim, _io_handler, _child) = rt 53 | .block_on(create::new_child_cmd( 54 | Command::new(nvim_path()).args(&[ 55 | "-u", 56 | "NONE", 57 | "--embed", 58 | "--headless", 59 | "Cargo.lock", 60 | ]), 61 | handler, 62 | )) 63 | .unwrap(); 64 | 65 | let nvim1 = nvim.clone(); 66 | rt.block_on(async move { nvim1.command("set noswapfile").await }) 67 | .expect("0"); 68 | 69 | c.bench_function("request_file", move |b| { 70 | b.iter(|| { 71 | let nvim = nvim.clone(); 72 | let _lines = rt.block_on(async move { 73 | // Using `call` is not recommended. It returns a 74 | // Result> that needs to be massaged 75 | // in a proper Result at least. That's what the API 76 | // is for, but for now we don't want to deal with getting a buffer 77 | // from the API 78 | let _ = nvim 79 | .call("nvim_buf_get_lines", call_args![0i64, 0i64, -1i64, false]) 80 | .await 81 | .expect("1"); 82 | }); 83 | }) 84 | }); 85 | } 86 | 87 | criterion_group!(name = requests; config = Criterion::default().without_plots(); targets = simple_requests, request_file); 88 | criterion_main!(requests); 89 | -------------------------------------------------------------------------------- /src/rpc/handler.rs: -------------------------------------------------------------------------------- 1 | //! Handling notifications and request received from neovim 2 | //! 3 | //! The core of a plugin is defining and implementing the 4 | //! [`handler`](crate::rpc::handler::Handler). 5 | use std::{future::Future, marker::PhantomData, sync::Arc}; 6 | 7 | use futures::io::AsyncWrite; 8 | use rmpv::Value; 9 | 10 | use crate::Neovim; 11 | 12 | /// The central functionality of a plugin. The trait bounds asure that each 13 | /// asynchronous task can receive a copy of the handler, so some state can be 14 | /// shared. 15 | pub trait Handler: Send + Sync + Clone + 'static { 16 | /// The type where we write our responses to requests. Handling of incoming 17 | /// requests/notifications is done on the io loop, which passes the parsed 18 | /// messages to the handler. 19 | type Writer: AsyncWrite + Send + Unpin + 'static; 20 | 21 | /// Handling an rpc request. The ID's of requests are handled by the 22 | /// [`neovim`](crate::neovim::Neovim) instance. 23 | fn handle_request( 24 | &self, 25 | _name: String, 26 | _args: Vec, 27 | _neovim: Neovim, 28 | ) -> impl Future> + Send { 29 | async { Err(Value::from("Not implemented")) } 30 | } 31 | 32 | /// Handling an rpc notification. Notifications are handled one at a time in 33 | /// the order in which they were received, and will block new requests from 34 | /// being received until handle_notify returns. 35 | fn handle_notify( 36 | &self, 37 | _name: String, 38 | _args: Vec, 39 | _neovim: Neovim<::Writer>, 40 | ) -> impl Future + Send { 41 | async {} 42 | } 43 | } 44 | 45 | /// The dummy handler defaults to doing nothing with a notification, and 46 | /// returning a generic error for a request. It can be used if a plugin only 47 | /// wants to send requests to neovim and get responses, but not handle any 48 | /// notifications or requests. 49 | #[derive(Default)] 50 | pub struct Dummy 51 | where 52 | Q: AsyncWrite + Send + Sync + Unpin + 'static, 53 | { 54 | q: Arc>, 55 | } 56 | 57 | impl Clone for Dummy 58 | where 59 | Q: AsyncWrite + Send + Sync + Unpin + 'static, 60 | { 61 | fn clone(&self) -> Self { 62 | Dummy { q: self.q.clone() } 63 | } 64 | } 65 | 66 | impl Handler for Dummy 67 | where 68 | Q: AsyncWrite + Send + Sync + Unpin + 'static, 69 | { 70 | type Writer = Q; 71 | } 72 | 73 | impl Dummy 74 | where 75 | Q: AsyncWrite + Send + Sync + Unpin + 'static, 76 | { 77 | #[must_use] 78 | pub fn new() -> Dummy { 79 | Dummy { 80 | q: Arc::new(PhantomData), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/bench_tokio.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use rmpv::Value; 4 | 5 | use tokio::fs::File as TokioFile; 6 | 7 | use nvim_rs::{ 8 | compat::tokio::Compat, create::tokio as create, Handler, Neovim, 9 | }; 10 | 11 | #[derive(Clone)] 12 | struct NeovimHandler {} 13 | 14 | impl Handler for NeovimHandler { 15 | type Writer = Compat; 16 | 17 | async fn handle_request( 18 | &self, 19 | name: String, 20 | _args: Vec, 21 | neovim: Neovim>, 22 | ) -> Result { 23 | match name.as_ref() { 24 | "file" => { 25 | let c = neovim.get_current_buf().await.unwrap(); 26 | for _ in 0..1_000_usize { 27 | let _x = c.get_lines(0, -1, false).await; 28 | } 29 | Ok(Value::Nil) 30 | } 31 | "buffer" => { 32 | for _ in 0..10_000_usize { 33 | let _ = neovim.get_current_buf().await.unwrap(); 34 | } 35 | Ok(Value::Nil) 36 | } 37 | "api" => { 38 | for _ in 0..1_000_usize { 39 | let _ = neovim.get_api_info().await.unwrap(); 40 | } 41 | Ok(Value::Nil) 42 | } 43 | _ => Ok(Value::Nil), 44 | } 45 | } 46 | } 47 | 48 | #[tokio::main] 49 | async fn main() { 50 | let handler: NeovimHandler = NeovimHandler {}; 51 | 52 | let (nvim, io_handler) = create::new_parent(handler).await.unwrap(); 53 | 54 | // Any error should probably be logged, as stderr is not visible to users. 55 | match io_handler.await { 56 | Err(joinerr) => eprintln!("Error joining IO loop: '{}'", joinerr), 57 | Ok(Err(err)) => { 58 | if !err.is_reader_error() { 59 | // One last try, since there wasn't an error with writing to the 60 | // stream 61 | nvim 62 | .err_writeln(&format!("Error: '{}'", err)) 63 | .await 64 | .unwrap_or_else(|e| { 65 | // We could inspect this error to see what was happening, and 66 | // maybe retry, but at this point it's probably best 67 | // to assume the worst and print a friendly and 68 | // supportive message to our users 69 | eprintln!("Well, dang... '{}'", e); 70 | }); 71 | } 72 | 73 | if !err.is_channel_closed() { 74 | // Closed channel usually means neovim quit itself, or this plugin was 75 | // told to quit by closing the channel, so it's not always an error 76 | // condition. 77 | eprintln!("Error: '{}'", err); 78 | 79 | let mut source = err.source(); 80 | 81 | while let Some(e) = source { 82 | eprintln!("Caused by: '{}'", e); 83 | source = e.source(); 84 | } 85 | } 86 | } 87 | Ok(Ok(())) => {} 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/handler_drop.rs: -------------------------------------------------------------------------------- 1 | //! How to handle cleanup logic with access to the handler's data. See 2 | //! src/examples/handler_drop.rs for documentation. 3 | use nvim_rs::{ 4 | compat::tokio::Compat, create::tokio as create, Handler, Neovim, Value, 5 | }; 6 | 7 | use tokio::process::{ChildStdin, Command}; 8 | 9 | use std::{ 10 | fs::File, 11 | io::Write, 12 | ops::Drop, 13 | sync::{Arc, Mutex}, 14 | }; 15 | 16 | const OUTPUT_FILE: &str = "handler_drop.txt"; 17 | const NVIMPATH: &str = "neovim/build/bin/nvim"; 18 | 19 | #[derive(Clone)] 20 | struct NeovimHandler { 21 | buf: Arc>>, 22 | } 23 | 24 | impl Handler for NeovimHandler { 25 | type Writer = Compat; 26 | 27 | async fn handle_notify( 28 | &self, 29 | name: String, 30 | args: Vec, 31 | req: Neovim>, 32 | ) { 33 | match name.as_ref() { 34 | "nvim_buf_lines_event" => { 35 | // This can be made more efficient by taking ownership appropriately, 36 | // but we skip this in this example 37 | for s in args[4] 38 | .as_array() 39 | .unwrap() 40 | .iter() 41 | .map(|s| s.as_str().unwrap()) 42 | { 43 | self.buf.lock().unwrap().push(s.to_owned()); 44 | } 45 | // shut down after the first event 46 | let chan = req.get_api_info().await.unwrap()[0].as_i64().unwrap(); 47 | let close = format!("call chanclose({})", chan); 48 | // this will always return an EOF error, so let's just ignore that here 49 | let _ = req.command(&close).await; 50 | } 51 | _ => {} 52 | } 53 | } 54 | } 55 | 56 | impl Drop for NeovimHandler { 57 | fn drop(&mut self) { 58 | let mut file = File::create(OUTPUT_FILE).unwrap(); 59 | 60 | for line in self.buf.lock().unwrap().iter() { 61 | writeln!(file, "{}", line).unwrap(); 62 | } 63 | } 64 | } 65 | 66 | #[tokio::main] 67 | async fn main() { 68 | let handler = NeovimHandler { 69 | buf: Arc::new(Mutex::new(vec![])), 70 | }; 71 | 72 | let (nvim, io_handle, _child) = create::new_child_cmd( 73 | Command::new(NVIMPATH) 74 | .args(&["-u", "NONE", "--embed", "--headless"]) 75 | .env("NVIM_LOG_FILE", "nvimlog"), 76 | handler, 77 | ) 78 | .await 79 | .unwrap(); 80 | 81 | 82 | let curbuf = nvim.get_current_buf().await.unwrap(); 83 | if !curbuf.attach(false, vec![]).await.unwrap() { 84 | return; 85 | } 86 | curbuf 87 | .set_lines(0, 0, false, vec!["xyz".into(), "abc".into()]) 88 | .await 89 | .unwrap(); 90 | 91 | // The call will return an error because the channel is closed, so we 92 | // need to explicitely ignore it rather than unwrap it. 93 | let _ = io_handle.await; 94 | } 95 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_BACKTRACE: 1 12 | NVIMRS_LOG_FILE: nvim-rs.log 13 | NVIMRS_LOG_LEVEL: debug 14 | NVIMRS_STDERR: nvim-rs.stderr 15 | RUSTFLAGS: -C opt-level=0 16 | 17 | jobs: 18 | all: 19 | runs-on: ${{ matrix.os }} 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [windows-latest, macos-latest, ubuntu-latest] 25 | rust: [stable] 26 | include: 27 | - os: ubuntu-latest 28 | rust: beta 29 | - os: ubuntu-latest 30 | rust: nightly 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | 35 | - name: Install Rust ${{ matrix.rust }} 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: ${{ matrix.rust }} 39 | profile: minimal 40 | override: true 41 | 42 | - uses: Swatinem/rust-cache@v2 43 | 44 | - name: Check 45 | run: | 46 | cargo check && cargo check --examples --features use_tokio && cargo check --examples --features use_async-std 47 | 48 | - name: Download neovim binary on linux 49 | if: matrix.os == 'ubuntu-latest' 50 | run: | 51 | sudo apt install libfuse2 52 | curl -L https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.appimage -o nvim.appimage 53 | chmod a+x nvim.appimage 54 | echo "NVIMRS_TEST_BIN=$PWD/nvim.appimage" >> $GITHUB_ENV 55 | 56 | - name: Download neovim binary on macos 57 | if: matrix.os == 'macos-latest' 58 | run: | 59 | curl -L https://github.com/neovim/neovim/releases/download/nightly/nvim-macos-x86_64.tar.gz -o nvim-macos.tar.gz 60 | tar xfz nvim-macos.tar.gz 61 | ls 62 | echo "NVIMRS_TEST_BIN=$PWD/nvim-macos-x86_64/bin/nvim" >> $GITHUB_ENV 63 | 64 | - name: Download neovim binary on windows 65 | if: matrix.os == 'windows-latest' 66 | run: | 67 | curl -L https://github.com/neovim/neovim/releases/download/nightly/nvim-win64.zip -o nvim-win64.zip 68 | 7z x nvim-win64.zip 69 | $exe = Get-ChildItem -Path nvim-win64 -Filter nvim.exe -Recurse | %{$_.FullName} 70 | echo "NVIMRS_TEST_BIN=$exe" >> $env:GITHUB_ENV 71 | 72 | - name: Build basic example 73 | run: | 74 | cargo build --example basic --features use_tokio 75 | 76 | - name: Tests 77 | run: | 78 | cargo test -- --nocapture && cargo test --features use_tokio -- --nocapture && cargo test --features use_async-std -- --nocapture 79 | 80 | - name: Benchtests 81 | run: | 82 | cargo bench --features use_tokio -- --test && cargo bench --features use_async-std -- --test 83 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nvim-rs" 3 | version = "0.9.2" 4 | license = "LGPL-3.0" 5 | authors = ["KillTheMule Spawner for H 28 | where 29 | H: Handler, 30 | { 31 | type Handle = JoinHandle<()>; 32 | 33 | fn spawn(&self, future: Fut) -> Self::Handle 34 | where 35 | Fut: Future + Send + 'static, 36 | { 37 | spawn(future) 38 | } 39 | } 40 | 41 | /// Connect to a neovim instance via tcp 42 | #[deprecated( 43 | since = "0.9.2", 44 | note = "async-std is deprecated, use `smol` \ 45 | instead" 46 | )] 47 | pub async fn new_tcp( 48 | addr: A, 49 | handler: H, 50 | ) -> io::Result<( 51 | Neovim>, 52 | JoinHandle>>, 53 | )> 54 | where 55 | H: Handler>, 56 | A: ToSocketAddrs, 57 | { 58 | let stream = TcpStream::connect(addr).await?; 59 | let (reader, writer) = stream.split(); 60 | let (neovim, io) = 61 | Neovim::>::new(reader, writer, handler); 62 | let io_handle = spawn(io); 63 | 64 | Ok((neovim, io_handle)) 65 | } 66 | 67 | #[cfg(unix)] 68 | /// Connect to a neovim instance via unix socket by path. This is currently 69 | /// only available on Unix for async-std. 70 | #[deprecated( 71 | since = "0.9.2", 72 | note = "async-std is deprecated, use `smol` \ 73 | instead" 74 | )] 75 | pub async fn new_path + Clone>( 76 | path: P, 77 | handler: H, 78 | ) -> io::Result<( 79 | Neovim>, 80 | JoinHandle>>, 81 | )> 82 | where 83 | H: Handler> + Send + 'static, 84 | { 85 | let stream = UnixStream::connect(path).await?; 86 | let (reader, writer) = stream.split(); 87 | let (neovim, io) = 88 | Neovim::>::new(reader, writer, handler); 89 | let io_handle = spawn(io); 90 | 91 | Ok((neovim, io_handle)) 92 | } 93 | 94 | /// Connect to the neovim instance that spawned this process over stdin/stdout 95 | #[deprecated( 96 | since = "0.9.2", 97 | note = "async-std is deprecated, use `smol` \ 98 | instead" 99 | )] 100 | pub async fn new_parent( 101 | handler: H, 102 | ) -> io::Result<(Neovim, JoinHandle>>)> 103 | where 104 | H: Handler, 105 | { 106 | let sout: ASFile = unbuffered_stdout()?.into(); 107 | let (neovim, io) = Neovim::::new(stdin(), sout, handler); 108 | let io_handle = spawn(io); 109 | 110 | Ok((neovim, io_handle)) 111 | } 112 | -------------------------------------------------------------------------------- /src/neovim_api_manual.rs: -------------------------------------------------------------------------------- 1 | //! Some manually implemented API functions 2 | use futures::io::AsyncWrite; 3 | use rmpv::Value; 4 | 5 | use crate::{ 6 | error::CallError, neovim::Neovim, rpc::model::IntoVal, Buffer, Tabpage, 7 | Window, 8 | }; 9 | 10 | impl Neovim 11 | where 12 | W: AsyncWrite + Send + Unpin + 'static, 13 | { 14 | pub async fn list_bufs(&self) -> Result>, Box> { 15 | match self.call("nvim_list_bufs", call_args![]).await?? { 16 | Value::Array(arr) => Ok( 17 | arr 18 | .into_iter() 19 | .map(|v| Buffer::new(v, self.clone())) 20 | .collect(), 21 | ), 22 | val => Err(CallError::WrongValueType(val))?, 23 | } 24 | } 25 | 26 | pub async fn get_current_buf(&self) -> Result, Box> { 27 | Ok( 28 | self 29 | .call("nvim_get_current_buf", call_args![]) 30 | .await? 31 | .map(|val| Buffer::new(val, self.clone()))?, 32 | ) 33 | } 34 | 35 | pub async fn list_wins(&self) -> Result>, Box> { 36 | match self.call("nvim_list_wins", call_args![]).await?? { 37 | Value::Array(arr) => Ok( 38 | arr 39 | .into_iter() 40 | .map(|v| Window::new(v, self.clone())) 41 | .collect(), 42 | ), 43 | val => Err(CallError::WrongValueType(val))?, 44 | } 45 | } 46 | 47 | pub async fn get_current_win(&self) -> Result, Box> { 48 | Ok( 49 | self 50 | .call("nvim_get_current_win", call_args![]) 51 | .await? 52 | .map(|val| Window::new(val, self.clone()))?, 53 | ) 54 | } 55 | 56 | pub async fn create_buf( 57 | &self, 58 | listed: bool, 59 | scratch: bool, 60 | ) -> Result, Box> { 61 | Ok( 62 | self 63 | .call("nvim_create_buf", call_args![listed, scratch]) 64 | .await? 65 | .map(|val| Buffer::new(val, self.clone()))?, 66 | ) 67 | } 68 | 69 | pub async fn open_win( 70 | &self, 71 | buffer: &Buffer, 72 | enter: bool, 73 | config: Vec<(Value, Value)>, 74 | ) -> Result, Box> { 75 | Ok( 76 | self 77 | .call("nvim_open_win", call_args![buffer, enter, config]) 78 | .await? 79 | .map(|val| Window::new(val, self.clone()))?, 80 | ) 81 | } 82 | 83 | pub async fn list_tabpages(&self) -> Result>, Box> { 84 | match self.call("nvim_list_tabpages", call_args![]).await?? { 85 | Value::Array(arr) => Ok( 86 | arr 87 | .into_iter() 88 | .map(|v| Tabpage::new(v, self.clone())) 89 | .collect(), 90 | ), 91 | val => Err(CallError::WrongValueType(val))?, 92 | } 93 | } 94 | 95 | pub async fn get_current_tabpage( 96 | &self, 97 | ) -> Result, Box> { 98 | Ok( 99 | self 100 | .call("nvim_get_current_tabpage", call_args![]) 101 | .await? 102 | .map(|val| Tabpage::new(val, self.clone()))?, 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to Semantic Versioning. 7 | 8 | ## [Unreleased] 9 | 10 | ## 0.9.1 2025-03-23 11 | - Add support to connect to a child nvim with a handshake message, ignoring 12 | additional data in stdio (thanks @fredizzimo) 13 | 14 | ## 0.9.0 20250-03-01 15 | - Mistakenly change the version to 0.9.0 instead of 0.8.0 (sorry) 16 | - Remove dependency on `parity-tokio-ipc` (thanks @sdroege) 17 | - Update the neovim API 18 | 19 | ## 0.7.0 2024-02-03 20 | - Create our own Stdout for connecting to a parent neovim to avoid line buffering 21 | - Updated API 22 | - Updated dependencies 23 | 24 | ## 0.6.0 2023-09-16 25 | - Updated dependencies 26 | - Updated API 27 | - Extended UiOptions (thanks @fredizzimo) 28 | 29 | ## 0.5.0 2022-10-08 30 | 31 | - Updated dependencies 32 | - Updated API (some breakage from neovim's side here) 33 | - Improved some docs 34 | 35 | 36 | ## 0.4.0 - 2021-12-16 37 | 38 | ### Added 39 | - Added support for `ext_termcolors` (thanks @Lyude) 40 | 41 | ### Changed 42 | - Updated dependencies 43 | - Updated API (some breakage from neovim's side here) 44 | 45 | 46 | ## 0.3.0 - 2021-08-28 47 | 48 | - Updated tokio to 1.\* 49 | - Added UiOption::ExtMessages 50 | - Removed create::tokio::new_unix in favor of create::tokio::new_path, which also 51 | works on windows 52 | - Requests/notifications are now handled in order of arrival (which is mainly important 53 | for notifications) 54 | - Removed LoopError::SpawnError 55 | 56 | 57 | ## 0.2.0 - 2020-08-29 58 | 59 | ### Added 60 | - Connecting to neovim via tcp or a unix-socket (unix only) is now supported again 61 | 62 | - The API has been updated to reflect neovim HEAD as of commit 161cdba. 63 | 64 | ### Changed 65 | - The crate is now based on [`futures`](https://crates.io/crates/futures) 66 | rather than [`tokio`](https://crates.io/crates/tokio) to allow for different 67 | runtimes as far as possible. The features [`use_tokio`] or [`use_async-std`] 68 | can be used to get support for the 2 most popular rust runtimes, and give 69 | access to the `create::tokio` or `create::async_std` submodules that supply 70 | functionality to actually connect to neovim (depending on the features 71 | provided by the runtime library). 72 | 73 | - The `Handler` trait now depends on `Clone`. The library used to `Arc`-wrap 74 | the handler anyways, so now the user has the possibility of using types that 75 | are cheaper to clone. 76 | 77 | - `CallError` has a new variant `WrongType` to indicate that a message from 78 | neovim contained a value of the wrong type. Previously, the lib would panic 79 | in this case, now the user has the choice to handle it (or, more probably, 80 | log it properly and quit). 81 | 82 | - `LoopError` has an additional variant `IoSpawn` that indicates that spawning 83 | another task with the handler has failed. 84 | 85 | - The trait `FromVal` has been replaced by `TryUnpack`. 86 | 87 | - As a substitute for directly passing a runtime around, the `Handler` now 88 | needs to implement `nvim-rs::create::Spawner` 89 | 90 | - The function `new_parent` to connect to a parent neovim instance is now 91 | `async`. 92 | 93 | ## 0.1.0 - 2020-02-01 94 | - Initial release 95 | -------------------------------------------------------------------------------- /src/examples/scorched_earth.rs: -------------------------------------------------------------------------------- 1 | //! # Scorched earth 2 | //! 3 | //! A minimal port of 4 | //! [scorched earth by boxofrox](https://github.com/boxofrox/neovim-scorched-earth) 5 | //! to nvim-rs. Works the same, but foregoes any error handling, removes the 6 | //! customisation of color, and removes some abstractions that aren't helpfull 7 | //! in an example. 8 | //! 9 | //! Note that this example uses `tokio`, while `scorched_earth_as` uses 10 | //! async-std. 11 | //! 12 | //! ## Usage 13 | //! 14 | //! First, build this example via 15 | //! 16 | //! ```sh 17 | //! cargo build --example scorched_earth 18 | //! ``` 19 | //! 20 | //! Then follow the steps described in the [`README`](README.md) for the examples. 21 | //! 22 | //! ## Description 23 | //! 24 | //! Some overview over the code: 25 | //! 26 | //! * The associated type for our [`Handler`](crate::rpc::handler::Handler) is 27 | //! out stdout. But tokio's [`Stdout`](tokio::io::Stdout) does not implement 28 | //! [`futures::io::AsyncWrite`](futures::io::AsyncWrite), so it needs to be 29 | //! wrapped in the provided [`Compat`](crate::compat::tokio::Compat) type. 30 | //! 31 | //! * The handler struct `NeovimHandler` needs to contain some plugin state, 32 | //! namely two cursor positions `start` and `end`. It needs to be `Send` and 33 | //! `Sync`, and we need mutable access, so we wrap it in a `Arc>`. 34 | //! 35 | //! * Implementing the [`Handler`](crate::Handler) trait requires some magic 36 | //! because of the async functions, we we use the 37 | //! [`async_trait`](https://docs.rs/async-trait/0.1.21/async_trait/) macro. 38 | //! 39 | //! * We use `Stdout` as the type for the `Writer` because neovim acts as our 40 | //! parent, so it reads from our stdout. Note that this is the [async 41 | //! version](tokio::io::Stdout) from Tokio. 42 | //! 43 | //! * We only implement `handle_notify` since we don't want to serve requests. 44 | //! It gets a [`Neovim`](crate::Neovim) passed that we can use to send 45 | //! requests to neovim. All requests are async methods, so we need to `await` 46 | //! them. 47 | //! 48 | //! * The main function is denoted `#[tokio::main]` to use async notation, but 49 | //! it would be perfectly feasible to explicitely create a runtime and use that. 50 | //! 51 | //! * After creation of the handler, we connect to neovim via one of the 52 | //! [`new_*`](crate::create) functions. It gives back a 53 | //! [`Neovim`](crate::Neovim) instance which we could use for requests, and a 54 | //! handle for the io loop. 55 | //! 56 | //! * The plugin quits by ending the IO task when neovim closes the channel, so 57 | //! we don't need to do anything special. Any cleanup-logic can happen after 58 | //! the IO task has finished. Note that we're loosing access to our 59 | //! [`Handler`](crate::Handler), so we might need to implement 60 | //! [`Drop`](std::ops::Drop) for it, see the 61 | //! [example](crate::examples::handler_drop). 62 | //! 63 | //! * After the IO task has finished, we're inspecting the errors to see why it 64 | //! went. A join error simply gets printed, then we inspect potential errors 65 | //! from the io loop itself. First, if we did not see a general reader error, we 66 | //! try to send some last notification to the neovim user. Secondly, we quietly 67 | //! ignore the channel being closed, because this usually means that it was 68 | //! closed by neovim, which isn't always an error. 69 | //! 70 | //! *Note*: A closed channel could still mean an error, so the plugin has the 71 | //! option to react to this. 72 | //! 73 | //! * As with the other examples, we implement [`Spawn`](futures::task::Spawn) 74 | //! for our `NeovimHandler` most trivially. 75 | -------------------------------------------------------------------------------- /tests/notifications.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::Arc, 3 | time::Duration, 4 | }; 5 | 6 | use tokio::{ 7 | sync::{ 8 | mpsc::{UnboundedSender, unbounded_channel}, 9 | Notify, 10 | }, 11 | process::{ChildStdin, Command}, 12 | task::yield_now, 13 | }; 14 | 15 | use futures::FutureExt; 16 | 17 | use nvim_rs::{ 18 | self, 19 | create::tokio as create, 20 | compat::tokio::Compat, 21 | neovim::Neovim, 22 | Value, 23 | }; 24 | 25 | mod common; 26 | use common::*; 27 | 28 | const ITERS: usize = 25; 29 | const TIMEOUT: Duration = Duration::from_secs(60); 30 | 31 | macro_rules! timeout { 32 | ($x:expr) => { 33 | tokio::time::timeout(TIMEOUT, $x).map(|res| { 34 | res.expect(&format!("Timed out waiting for {}", stringify!($x))) 35 | }) 36 | } 37 | } 38 | 39 | #[derive(Clone)] 40 | struct Handler { 41 | result_sender: UnboundedSender, 42 | notifiers: Arc>, 43 | } 44 | 45 | impl nvim_rs::Handler for Handler { 46 | type Writer = Compat; 47 | 48 | async fn handle_notify( 49 | &self, 50 | name: String, 51 | args: Vec, 52 | _: Neovim, 53 | ) { 54 | assert_eq!(name, "idx"); 55 | let idx = args[0].as_i64().unwrap(); 56 | 57 | /* Wait until we've received a message from the test, then send back the 58 | * number we received. 59 | */ 60 | self.notifiers[idx as usize].notified().await; 61 | self.result_sender.send(idx as u8).unwrap(); 62 | } 63 | } 64 | 65 | #[tokio::test] 66 | async fn sequential_notifications() { 67 | // Create a tokio notifier for each nvim notification that we'll be handling 68 | let mut notifiers = Vec::::with_capacity(ITERS); 69 | for _ in 0..ITERS { 70 | notifiers.push(Notify::new()); 71 | } 72 | let notifiers = Arc::new(notifiers); 73 | 74 | /* Create a channel the notification handler will use each time for writing 75 | * back the number provided by each notification 76 | */ 77 | let (result_sender, mut result_receiver) = unbounded_channel(); 78 | let handler = Handler { 79 | result_sender, 80 | notifiers: notifiers.clone(), 81 | }; 82 | 83 | // Startup nvim 84 | let (nvim, _io_handler, _child) = create::new_child_cmd( 85 | Command::new(nvim_path()).args(&[ 86 | "-u", 87 | "NONE", 88 | "--embed", 89 | "--headless", 90 | ]), 91 | handler 92 | ) 93 | .await 94 | .unwrap(); 95 | 96 | // Generate the commands to send the notifications 97 | let mut nvim_cmds = Vec::::with_capacity(ITERS); 98 | for i in 0..ITERS { 99 | nvim_cmds.push(format!("call rpcnotify(1, 'idx', {})", i)); 100 | } 101 | 102 | /* Start sending the notifications. We use nvim.command() instead of passing 103 | * the commands via -c on the command line so that we block the test until the 104 | * notifications are actually pending. 105 | */ 106 | timeout!(nvim.command(nvim_cmds.join("|").as_str())).await.unwrap(); 107 | 108 | /* Unblock notifications in the reverse order that they were sent, if 109 | * notifications are being handled sequentially then they should still be 110 | * processed in their original order. 111 | */ 112 | for notifier in notifiers.iter().rev() { 113 | notifier.notify_one(); 114 | /* Yield once, so that we guarantee the notification handler for this 115 | * notification executes pre-maturely if it was handled out of order. 116 | */ 117 | yield_now().await; 118 | } 119 | 120 | // Check the results 121 | for i in 0..ITERS { 122 | assert_eq!(i as u8, timeout!(result_receiver.recv()).await.unwrap()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/rpc/unpack.rs: -------------------------------------------------------------------------------- 1 | //! Tools to unpack a [`Value`](rmpv::Value) to something we can use. 2 | //! 3 | //! Conversion is fallible, so [`try_unpack`](self::TryUnpack::try_unpack) 4 | //! returns the [`Value`](rmpv::Value) if it is not of the correct type. 5 | //! 6 | //! ### Usage 7 | //! 8 | //! ``` 9 | //! use rmpv::Value; 10 | //! use nvim_rs::rpc::unpack::TryUnpack; 11 | //! 12 | //! let v = Value::from("hoodle"); 13 | //! let s:String = v.try_unpack().unwrap(); 14 | //! 15 | //! assert_eq!(String::from("hoodle"), s); 16 | //! ``` 17 | use rmpv::Value; 18 | 19 | /// Trait to allow seamless conversion from a [`Value`](rmpv::Value) to the type 20 | /// it contains. In particular, this should never panic. 21 | /// 22 | /// This is basically a specialized variant of `TryInto for Value`. 23 | pub trait TryUnpack { 24 | /// Returns the value contained in `self`. 25 | /// 26 | /// # Errors 27 | /// 28 | /// Returns `Err(self)` if `self` does not contain a value of type `V`. 29 | fn try_unpack(self) -> Result; 30 | } 31 | 32 | /// This is needed because the blanket impl `TryFrom for Value` uses 33 | /// `Error = !`. 34 | impl TryUnpack for Value { 35 | fn try_unpack(self) -> Result { 36 | Ok(self) 37 | } 38 | } 39 | 40 | impl TryUnpack<()> for Value { 41 | fn try_unpack(self) -> Result<(), Value> { 42 | if self.is_nil() { 43 | Ok(()) 44 | } else { 45 | Err(self) 46 | } 47 | } 48 | } 49 | 50 | // TODO: Replace this when rmpv implements `TryFrom for String`. 51 | impl TryUnpack for Value { 52 | fn try_unpack(self) -> Result { 53 | match self { 54 | Value::String(s) if s.is_str() => { 55 | Ok(s.into_str().expect("This was valid UTF8")) 56 | } 57 | val => Err(val), 58 | } 59 | } 60 | } 61 | 62 | impl TryUnpack<(i64, i64)> for Value { 63 | fn try_unpack(self) -> Result<(i64, i64), Value> { 64 | if let Value::Array(ref v) = self { 65 | if v.len() == 2 { 66 | let mut vi = v.iter().map(Value::as_i64); 67 | if let (Some(Some(i)), Some(Some(j))) = (vi.next(), vi.next()) { 68 | return Ok((i, j)); 69 | } 70 | } 71 | } 72 | Err(self) 73 | } 74 | } 75 | 76 | /// The bound `Value: From` is necessary so we can recover the values if one 77 | /// of the elements could not be unpacked. In practice, though, we only 78 | /// implement `TryUnpack` in those cases anyways. 79 | impl TryUnpack> for Value 80 | where 81 | Value: TryUnpack + From, 82 | { 83 | fn try_unpack(self) -> Result, Value> { 84 | match self { 85 | Value::Array(v) => { 86 | let mut newvec = vec![]; 87 | let mut vi = v.into_iter(); 88 | 89 | while let Some(ele) = vi.next() { 90 | match ele.try_unpack() { 91 | Ok(t) => newvec.push(t), 92 | Err(ele) => { 93 | let mut restorevec: Vec = 94 | newvec.into_iter().map(Value::from).collect(); 95 | restorevec.push(ele); 96 | restorevec.extend(vi); 97 | return Err(Value::Array(restorevec)); 98 | } 99 | } 100 | } 101 | Ok(newvec) 102 | } 103 | val => Err(val), 104 | } 105 | } 106 | } 107 | 108 | macro_rules! impl_try_unpack_tryfrom { 109 | ($t: ty) => { 110 | impl TryUnpack<$t> for Value { 111 | fn try_unpack(self) -> Result<$t, Value> { 112 | use std::convert::TryInto; 113 | self.try_into() 114 | } 115 | } 116 | }; 117 | } 118 | 119 | impl_try_unpack_tryfrom!(i64); 120 | impl_try_unpack_tryfrom!(bool); 121 | impl_try_unpack_tryfrom!(Vec<(Value, Value)>); 122 | -------------------------------------------------------------------------------- /tests/connecting/conns.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use nvim_rs::rpc::handler::Dummy as DummyHandler; 3 | 4 | #[cfg(feature = "use_tokio")] 5 | use nvim_rs::create::tokio as create; 6 | #[cfg(feature = "use_tokio")] 7 | use tokio::test as atest; 8 | 9 | #[cfg(feature = "use_async-std")] 10 | use async_std::test as atest; 11 | #[cfg(feature = "use_async-std")] 12 | use nvim_rs::create::async_std as create; 13 | 14 | use std::{ 15 | path::Path, 16 | process::Command, 17 | thread::sleep, 18 | time::{Duration, Instant}, 19 | }; 20 | 21 | #[cfg(unix)] 22 | use tempfile::{Builder, TempDir}; 23 | 24 | #[path = "../common/mod.rs"] 25 | mod common; 26 | use common::*; 27 | 28 | const HOST: &str = "127.0.0.1"; 29 | const PORT: u16 = 6666; 30 | 31 | #[atest] 32 | async fn can_connect_via_tcp() { 33 | let listen = HOST.to_string() + ":" + &PORT.to_string(); 34 | 35 | let mut child = Command::new(nvim_path()) 36 | .args(&["-u", "NONE", "--headless", "--listen", &listen]) 37 | .spawn() 38 | .expect("Cannot start neovim"); 39 | 40 | // wait at most 1 second for neovim to start and create the tcp socket 41 | let start = Instant::now(); 42 | 43 | let (nvim, _io_handle) = loop { 44 | sleep(Duration::from_millis(100)); 45 | 46 | let handler = DummyHandler::new(); 47 | if let Ok(r) = create::new_tcp(&listen, handler).await { 48 | break r; 49 | } else { 50 | if Duration::from_secs(1) <= start.elapsed() { 51 | panic!("Unable to connect to neovim via tcp at {}", listen); 52 | } 53 | } 54 | }; 55 | 56 | let servername = nvim 57 | .get_vvar("servername") 58 | .await 59 | .expect("Error retrieving servername from neovim"); 60 | 61 | child.kill().expect("Could not kill neovim"); 62 | 63 | assert_eq!(&listen, servername.as_str().unwrap()); 64 | } 65 | 66 | #[cfg(unix)] 67 | fn get_socket_path() -> (std::path::PathBuf, TempDir) { 68 | let dir = Builder::new() 69 | .prefix("neovim-lib.test") 70 | .tempdir() 71 | .expect("Cannot create temporary directory for test."); 72 | 73 | (dir.path().join("unix_socket"), dir) 74 | } 75 | 76 | #[cfg(windows)] 77 | fn get_socket_path() -> (std::path::PathBuf, ()) { 78 | let rand = fastrand::u32(..); 79 | let name = format!(r"\\.\pipe\nvim-rs-test-{}", rand); 80 | (name.into(), ()) 81 | } 82 | 83 | #[cfg(not(all(feature = "use_async-std", windows)))] 84 | #[atest] 85 | async fn can_connect_via_path() { 86 | let (socket_path, _guard) = get_socket_path(); 87 | 88 | let mut child = Command::new(nvim_path()) 89 | .args(&["-u", "NONE", "--headless"]) 90 | .env("NVIM_LISTEN_ADDRESS", &socket_path) 91 | .spawn() 92 | .expect("Cannot start neovim"); 93 | 94 | // wait at most 1 second for neovim to start and create the socket 95 | { 96 | let start = Instant::now(); 97 | let one_second = Duration::from_secs(1); 98 | loop { 99 | sleep(Duration::from_millis(100)); 100 | 101 | if let Ok(_) = std::fs::metadata(&socket_path) { 102 | break; 103 | } 104 | 105 | if one_second <= start.elapsed() { 106 | panic!("neovim socket not found at '{:?}'", &socket_path); 107 | } 108 | } 109 | } 110 | 111 | let handler = DummyHandler::new(); 112 | 113 | let (nvim, _io_handle) = create::new_path(&socket_path, handler) 114 | .await 115 | .expect(&format!( 116 | "Unable to connect to neovim's unix socket at {:?}", 117 | &socket_path 118 | )); 119 | 120 | let servername = nvim 121 | .get_vvar("servername") 122 | .await 123 | .expect("Error retrieving servername from neovim") 124 | .as_str() 125 | .unwrap() 126 | .to_string(); 127 | 128 | child.kill().expect("Could not kill neovim"); 129 | 130 | assert_eq!(socket_path, Path::new(&servername)); 131 | } 132 | -------------------------------------------------------------------------------- /src/examples/README.md: -------------------------------------------------------------------------------- 1 | Several examples on how to use `nvim-rs` to implement RPC plugins for neovim. 2 | Documentation can be found on 3 | [`docs.rs`](https://docs.rs/nvim-rs/latest/nvim_rs/examples/index.html). 4 | 5 | 6 | ### Integration 7 | 8 | To integrate an PRC plugin (i.e. a child process of neovim, as opposed to e.g. 9 | a GUI that embeds neovim as a child process) some scripting on the neovim side 10 | is necessary. The following will use the `scorched_earth` binary from the 11 | corresponding example, but can be used as a template for other plugins. To 12 | understand the usage of `$rtp`, please read the [neovim 13 | docs](https://neovim.io/doc/user/options.html#'runtimepath'). 14 | 15 | First, put the following into `$rtp/autoload/scorched_earth.vim`: 16 | 17 | ```vim 18 | if ! exists('s:jobid') 19 | let s:jobid = 0 20 | endif 21 | 22 | let s:scriptdir = resolve(expand(':p:h') . '/..') 23 | 24 | if ! exists('g:scorched_earth_program') 25 | let g:scorched_earth_program = s:scriptdir . '/target/release/neovim-scorched-earth' 26 | endif 27 | 28 | function! scorchedEarth#init() 29 | call scorchedEarth#connect() 30 | endfunction 31 | 32 | function! scorchedEarth#connect() 33 | let result = s:StartJob() 34 | 35 | if 0 == result 36 | echoerr "scortched earth: cannot start rpc process" 37 | elseif -1 == result 38 | echoerr "scortched earth: rpc process is not executable" 39 | else 40 | let s:jobid = result 41 | call s:ConfigureJob(result) 42 | endif 43 | endfunction 44 | 45 | function! scorchedEarth#reset() 46 | let s:jobid = 0 47 | endfunction 48 | 49 | function! s:ConfigureJob(jobid) 50 | augroup scortchedEarth 51 | " clear all previous autocommands 52 | autocmd! 53 | 54 | autocmd VimLeavePre * :call s:StopJob() 55 | 56 | autocmd InsertChange * :call s:NotifyInsertChange() 57 | autocmd InsertEnter * :call s:NotifyInsertEnter() 58 | autocmd InsertLeave * :call s:NotifyInsertLeave() 59 | 60 | autocmd CursorMovedI * :call s:NotifyCursorMovedI() 61 | augroup END 62 | endfunction 63 | 64 | function! s:NotifyCursorMovedI() 65 | let [ bufnum, lnum, column, off ] = getpos('.') 66 | call rpcnotify(s:jobid, 'cursor-moved-i', lnum, column) 67 | endfunction 68 | 69 | function! s:NotifyInsertChange() 70 | let [ bufnum, lnum, column, off ] = getpos('.') 71 | call rpcnotify(s:jobid, 'insert-change', v:insertmode, lnum, column) 72 | endfunction 73 | 74 | function! s:NotifyInsertEnter() 75 | let [ bufnum, lnum, column, off ] = getpos('.') 76 | call rpcnotify(s:jobid, 'insert-enter', v:insertmode, lnum, column) 77 | endfunction 78 | 79 | function! s:NotifyInsertLeave() 80 | call rpcnotify(s:jobid, 'insert-leave') 81 | endfunction 82 | 83 | function! s:OnStderr(id, data, event) dict 84 | echom 'scorched earth: stderr: ' . join(a:data, "\n") 85 | endfunction 86 | 87 | function! s:StartJob() 88 | if 0 == s:jobid 89 | let id = jobstart([g:scorched_earth_program], { 'rpc': v:true, 'on_stderr': function('s:OnStderr') }) 90 | return id 91 | else 92 | return 0 93 | endif 94 | endfunction 95 | 96 | function! s:StopJob() 97 | if 0 < s:jobid 98 | augroup scortchedEarth 99 | autocmd! " clear all previous autocommands 100 | augroup END 101 | 102 | call rpcnotify(s:jobid, 'quit') 103 | let result = jobwait(s:jobid, 500) 104 | 105 | if -1 == result 106 | " kill the job 107 | call jobstop(s:jobid) 108 | endif 109 | 110 | " reset job id back to zero 111 | let s:jobid = 0 112 | endif 113 | endfunction 114 | 115 | call color#highlight('default ScorchedEarth', 'dddddd', '550000', 'bold', '', '') 116 | ``` 117 | 118 | This sets up the commands to actually run the plugin, register some 119 | `autocommand`s, and stopping the plugin when shutting down neovim. The variable 120 | `g:scorched_earth_program` needs to point to the binary of the plugin. 121 | 122 | Secondly, to provide a command from neovim to run the plugin, put the following 123 | into `$rtp/plugin/scorched_earth.vim`: 124 | 125 | ```vim 126 | command! -nargs=0 ScorchedEarthConnect call scorchedEarth#connect() 127 | ``` 128 | 129 | Now you can start the plugin from neovim by running `:ScorchedEarthConnect`. 130 | -------------------------------------------------------------------------------- /examples/scorched_earth_as.rs: -------------------------------------------------------------------------------- 1 | //! Scorched earth. See src/examples/scorched_earth.rs for documentation 2 | use std::{error::Error, sync::Arc}; 3 | 4 | use rmpv::Value; 5 | 6 | use futures::lock::Mutex; 7 | 8 | use nvim_rs::{create::async_std as create, Handler, Neovim}; 9 | 10 | use async_std::{self, fs::File as ASFile}; 11 | 12 | struct Posis { 13 | cursor_start: Option<(u64, u64)>, 14 | cursor_end: Option<(u64, u64)>, 15 | } 16 | 17 | fn the_larger( 18 | first: Option<(u64, u64)>, 19 | second: (u64, u64), 20 | ) -> Option<(u64, u64)> { 21 | if first.map(|t| t >= second) == Some(true) { 22 | first 23 | } else { 24 | Some(second) 25 | } 26 | } 27 | 28 | fn the_smaller( 29 | first: Option<(u64, u64)>, 30 | second: (u64, u64), 31 | ) -> Option<(u64, u64)> { 32 | if first.map(|t| t <= second) == Some(true) { 33 | first 34 | } else { 35 | Some(second) 36 | } 37 | } 38 | 39 | #[derive(Clone)] 40 | struct NeovimHandler(Arc>); 41 | 42 | impl Handler for NeovimHandler { 43 | type Writer = ASFile; 44 | 45 | async fn handle_notify( 46 | &self, 47 | name: String, 48 | args: Vec, 49 | neovim: Neovim, 50 | ) { 51 | match name.as_ref() { 52 | "cursor-moved-i" => { 53 | let line = args[0].as_u64().unwrap(); 54 | let column = args[1].as_u64().unwrap(); 55 | 56 | let posis = &mut *(self.0).lock().await; 57 | 58 | posis.cursor_start = the_smaller(posis.cursor_start, (line, column)); 59 | posis.cursor_end = the_larger(posis.cursor_end, (line, column)); 60 | 61 | let cmd = format!( 62 | "syntax region ScorchedEarth start=/\\%{}l\\%{}c/ end=/\\%{}l\\%{}c/", 63 | posis.cursor_start.unwrap().0, 64 | posis.cursor_start.unwrap().1, 65 | posis.cursor_end.unwrap().0, 66 | posis.cursor_end.unwrap().1 67 | ); 68 | 69 | neovim.command(&cmd).await.unwrap(); 70 | } 71 | "insert-enter" => { 72 | let _mode = args[0].as_str().unwrap(); 73 | let line = args[1].as_u64().unwrap(); 74 | let column = args[2].as_u64().unwrap(); 75 | 76 | let posis = &mut *(self.0).lock().await; 77 | 78 | posis.cursor_start = Some((line, column)); 79 | posis.cursor_end = Some((line, column)); 80 | } 81 | "insert-leave" => { 82 | let posis = &mut *(self.0).lock().await; 83 | 84 | posis.cursor_start = None; 85 | posis.cursor_end = None; 86 | neovim.command("syntax clear ScorchedEarth").await.unwrap(); 87 | } 88 | _ => {} 89 | } 90 | } 91 | } 92 | 93 | #[async_std::main] 94 | async fn main() { 95 | let p = Posis { 96 | cursor_start: None, 97 | cursor_end: None, 98 | }; 99 | let handler: NeovimHandler = NeovimHandler(Arc::new(Mutex::new(p))); 100 | 101 | let (nvim, io_handler) = create::new_parent(handler).await.unwrap(); 102 | 103 | // Any error should probably be logged, as stderr is not visible to users. 104 | match io_handler.await { 105 | Err(err) => { 106 | if !err.is_reader_error() { 107 | // One last try, since there wasn't an error with writing to the 108 | // stream 109 | nvim 110 | .err_writeln(&format!("Error: '{}'", err)) 111 | .await 112 | .unwrap_or_else(|e| { 113 | // We could inspect this error to see what was happening, and 114 | // maybe retry, but at this point it's probably best 115 | // to assume the worst and print a friendly and 116 | // supportive message to our users 117 | eprintln!("Well, dang... '{}'", e); 118 | }); 119 | } 120 | 121 | if !err.is_channel_closed() { 122 | // Closed channel usually means neovim quit itself, or this plugin was 123 | // told to quit by closing the channel, so it's not always an error 124 | // condition. 125 | eprintln!("Error: '{}'", err); 126 | 127 | let mut source = err.source(); 128 | 129 | while let Some(e) = source { 130 | eprintln!("Caused by: '{}'", e); 131 | source = e.source(); 132 | } 133 | } 134 | } 135 | Ok(()) => {} 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/nested_requests.rs: -------------------------------------------------------------------------------- 1 | use nvim_rs::{ 2 | compat::tokio::Compat, create::tokio as create, neovim::Neovim, Handler, 3 | }; 4 | 5 | use rmpv::Value; 6 | 7 | use std::sync::Arc; 8 | 9 | use tokio::{ 10 | self, 11 | process::{ChildStdin, Command}, 12 | spawn, 13 | }; 14 | 15 | use futures::lock::Mutex; 16 | 17 | mod common; 18 | use common::*; 19 | 20 | #[derive(Clone)] 21 | struct NeovimHandler { 22 | froodle: Arc>, 23 | } 24 | 25 | impl Handler for NeovimHandler { 26 | type Writer = Compat; 27 | 28 | async fn handle_request( 29 | &self, 30 | name: String, 31 | args: Vec, 32 | neovim: Neovim>, 33 | ) -> Result { 34 | match name.as_ref() { 35 | "dummy" => Ok(Value::from("o")), 36 | "req" => { 37 | let v = args[0].as_str().unwrap(); 38 | 39 | let neovim = neovim.clone(); 40 | match v { 41 | "y" => { 42 | let mut x: String = neovim 43 | .get_vvar("progname") 44 | .await 45 | .unwrap() 46 | .as_str() 47 | .unwrap() 48 | .into(); 49 | x.push_str(" - "); 50 | x.push_str( 51 | neovim.get_var("oogle").await.unwrap().as_str().unwrap(), 52 | ); 53 | x.push_str(" - "); 54 | x.push_str( 55 | neovim 56 | .eval("rpcrequest(1,'dummy')") 57 | .await 58 | .unwrap() 59 | .as_str() 60 | .unwrap(), 61 | ); 62 | x.push_str(" - "); 63 | x.push_str( 64 | neovim 65 | .eval("rpcrequest(1,'req', 'z')") 66 | .await 67 | .unwrap() 68 | .as_str() 69 | .unwrap(), 70 | ); 71 | Ok(Value::from(x)) 72 | } 73 | "z" => { 74 | let x: String = neovim 75 | .get_vvar("progname") 76 | .await 77 | .unwrap() 78 | .as_str() 79 | .unwrap() 80 | .into(); 81 | Ok(Value::from(x)) 82 | } 83 | &_ => Err(Value::from("wrong argument to req")), 84 | } 85 | } 86 | &_ => Err(Value::from("wrong method name for request")), 87 | } 88 | } 89 | 90 | async fn handle_notify( 91 | &self, 92 | name: String, 93 | args: Vec, 94 | _neovim: Neovim>, 95 | ) { 96 | match name.as_ref() { 97 | "set_froodle" => { 98 | *self.froodle.lock().await = args[0].as_str().unwrap().to_string() 99 | } 100 | _ => {} 101 | }; 102 | } 103 | } 104 | 105 | #[tokio::test(flavor = "current_thread")] 106 | async fn nested_requests() { 107 | let rs = r#"exe ":fun M(timer) 108 | call rpcnotify(1, 'set_froodle', rpcrequest(1, 'req', 'y')) 109 | endfun""#; 110 | let rs2 = r#"exe ":fun N(timer) 111 | call chanclose(1) 112 | endfun""#; 113 | 114 | let froodle = Arc::new(Mutex::new(String::new())); 115 | let handler = NeovimHandler { 116 | froodle: froodle.clone(), 117 | }; 118 | 119 | let (nvim, io_handler, _child) = create::new_child_cmd( 120 | Command::new(nvim_path()).args(&[ 121 | "-u", 122 | "NONE", 123 | "--embed", 124 | "--headless", 125 | "-c", 126 | rs, 127 | "-c", 128 | ":let timer = timer_start(500, 'M')", 129 | "-c", 130 | rs2, 131 | "-c", 132 | ":let timer = timer_start(1500, 'N')", 133 | ]), 134 | handler, 135 | ) 136 | .await 137 | .unwrap(); 138 | 139 | let nv = nvim.clone(); 140 | spawn(async move { nv.set_var("oogle", Value::from("doodle")).await }); 141 | 142 | // The 2nd timer closes the channel, which will be returned as an error from 143 | // the io handler. We only fail the test if we got another error 144 | if let Err(err) = io_handler.await.unwrap() { 145 | if !err.is_channel_closed() { 146 | panic!("{}", err); 147 | } 148 | } 149 | 150 | assert_eq!( 151 | format!("{nvim} - doodle - o - {nvim}", nvim = NVIM_BIN), 152 | *froodle.lock().await 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /examples/scorched_earth.rs: -------------------------------------------------------------------------------- 1 | //! Scorched earth. See src/examples/scorched_earth.rs for documentation 2 | use std::{error::Error, sync::Arc}; 3 | 4 | use rmpv::Value; 5 | 6 | use futures::lock::Mutex; 7 | use tokio::fs::File as TokioFile; 8 | 9 | use nvim_rs::{ 10 | compat::tokio::Compat, create::tokio as create, Handler, Neovim, 11 | }; 12 | 13 | struct Posis { 14 | cursor_start: Option<(u64, u64)>, 15 | cursor_end: Option<(u64, u64)>, 16 | } 17 | 18 | fn the_larger( 19 | first: Option<(u64, u64)>, 20 | second: (u64, u64), 21 | ) -> Option<(u64, u64)> { 22 | if first.map(|t| t >= second) == Some(true) { 23 | first 24 | } else { 25 | Some(second) 26 | } 27 | } 28 | 29 | fn the_smaller( 30 | first: Option<(u64, u64)>, 31 | second: (u64, u64), 32 | ) -> Option<(u64, u64)> { 33 | if first.map(|t| t <= second) == Some(true) { 34 | first 35 | } else { 36 | Some(second) 37 | } 38 | } 39 | 40 | #[derive(Clone)] 41 | struct NeovimHandler(Arc>); 42 | 43 | impl Handler for NeovimHandler { 44 | type Writer = Compat; 45 | 46 | async fn handle_notify( 47 | &self, 48 | name: String, 49 | args: Vec, 50 | neovim: Neovim>, 51 | ) { 52 | match name.as_ref() { 53 | "cursor-moved-i" => { 54 | let line = args[0].as_u64().unwrap(); 55 | let column = args[1].as_u64().unwrap(); 56 | 57 | let posis = &mut *(self.0).lock().await; 58 | 59 | posis.cursor_start = the_smaller(posis.cursor_start, (line, column)); 60 | posis.cursor_end = the_larger(posis.cursor_end, (line, column)); 61 | 62 | let cmd = format!( 63 | "syntax region ScorchedEarth start=/\\%{}l\\%{}c/ end=/\\%{}l\\%{}c/", 64 | posis.cursor_start.unwrap().0, 65 | posis.cursor_start.unwrap().1, 66 | posis.cursor_end.unwrap().0, 67 | posis.cursor_end.unwrap().1 68 | ); 69 | 70 | neovim.command(&cmd).await.unwrap(); 71 | } 72 | "insert-enter" => { 73 | let _mode = args[0].as_str().unwrap(); 74 | let line = args[1].as_u64().unwrap(); 75 | let column = args[2].as_u64().unwrap(); 76 | 77 | let posis = &mut *(self.0).lock().await; 78 | 79 | posis.cursor_start = Some((line, column)); 80 | posis.cursor_end = Some((line, column)); 81 | } 82 | "insert-leave" => { 83 | let posis = &mut *(self.0).lock().await; 84 | 85 | posis.cursor_start = None; 86 | posis.cursor_end = None; 87 | neovim.command("syntax clear ScorchedEarth").await.unwrap(); 88 | } 89 | _ => {} 90 | } 91 | } 92 | } 93 | 94 | #[tokio::main] 95 | async fn main() { 96 | let p = Posis { 97 | cursor_start: None, 98 | cursor_end: None, 99 | }; 100 | let handler: NeovimHandler = NeovimHandler(Arc::new(Mutex::new(p))); 101 | 102 | let (nvim, io_handler) = create::new_parent(handler).await.unwrap(); 103 | 104 | // Any error should probably be logged, as stderr is not visible to users. 105 | match io_handler.await { 106 | Err(joinerr) => eprintln!("Error joining IO loop: '{}'", joinerr), 107 | Ok(Err(err)) => { 108 | if !err.is_reader_error() { 109 | // One last try, since there wasn't an error with writing to the 110 | // stream 111 | nvim 112 | .err_writeln(&format!("Error: '{}'", err)) 113 | .await 114 | .unwrap_or_else(|e| { 115 | // We could inspect this error to see what was happening, and 116 | // maybe retry, but at this point it's probably best 117 | // to assume the worst and print a friendly and 118 | // supportive message to our users 119 | eprintln!("Well, dang... '{}'", e); 120 | }); 121 | } 122 | 123 | if !err.is_channel_closed() { 124 | // Closed channel usually means neovim quit itself, or this plugin was 125 | // told to quit by closing the channel, so it's not always an error 126 | // condition. 127 | eprintln!("Error: '{}'", err); 128 | 129 | let mut source = err.source(); 130 | 131 | while let Some(e) = source { 132 | eprintln!("Caused by: '{}'", e); 133 | source = e.source(); 134 | } 135 | } 136 | } 137 | Ok(Ok(())) => {} 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/uioptions.rs: -------------------------------------------------------------------------------- 1 | //! Options for UI implementations 2 | //! 3 | //! This should be used with the manually implemented 4 | //! [`ui_attach`](crate::neovim::Neovim::ui_attach) 5 | use rmpv::Value; 6 | 7 | pub enum UiOption { 8 | Rgb(bool), 9 | Override(bool), 10 | ExtCmdline(bool), 11 | ExtHlstate(bool), 12 | ExtLinegrid(bool), 13 | ExtMessages(bool), 14 | ExtMultigrid(bool), 15 | ExtPopupmenu(bool), 16 | ExtTabline(bool), 17 | ExtTermcolors(bool), 18 | TermName(String), 19 | TermColors(u64), 20 | TermBackground(String), 21 | StdinFd(u64), 22 | StdinTty(bool), 23 | StdoutTty(bool), 24 | ExtWildmenu(bool), 25 | } 26 | 27 | impl UiOption { 28 | fn to_value(&self) -> (Value, Value) { 29 | let name_value = self.to_name_value(); 30 | (name_value.0.into(), name_value.1) 31 | } 32 | 33 | fn to_name_value(&self) -> (&'static str, Value) { 34 | match self { 35 | Self::Rgb(val) => ("rgb", (*val).into()), 36 | Self::Override(val) => ("override", (*val).into()), 37 | Self::ExtCmdline(val) => ("ext_cmdline", (*val).into()), 38 | Self::ExtHlstate(val) => ("ext_hlstate", (*val).into()), 39 | Self::ExtLinegrid(val) => ("ext_linegrid", (*val).into()), 40 | Self::ExtMessages(val) => ("ext_messages", (*val).into()), 41 | Self::ExtMultigrid(val) => ("ext_multigrid", (*val).into()), 42 | Self::ExtPopupmenu(val) => ("ext_popupmenu", (*val).into()), 43 | Self::ExtTabline(val) => ("ext_tabline", (*val).into()), 44 | Self::ExtTermcolors(val) => ("ext_termcolors", (*val).into()), 45 | Self::TermName(val) => ("term_name", val.as_str().into()), 46 | Self::TermColors(val) => ("term_colors", (*val).into()), 47 | Self::TermBackground(val) => ("term_background", val.as_str().into()), 48 | Self::StdinFd(val) => ("stdin_fd", (*val).into()), 49 | Self::StdinTty(val) => ("stdin_tty", (*val).into()), 50 | Self::StdoutTty(val) => ("stdout_tty", (*val).into()), 51 | Self::ExtWildmenu(val) => ("ext_wildmenu", (*val).into()), 52 | } 53 | } 54 | } 55 | 56 | #[derive(Default)] 57 | pub struct UiAttachOptions { 58 | options: Vec<(&'static str, UiOption)>, 59 | } 60 | 61 | macro_rules! ui_opt_setters { 62 | ($( $opt:ident as $set:ident($type:ty) );+ ;) => { 63 | impl UiAttachOptions { 64 | $( 65 | pub fn $set(&mut self, val: $type) -> &mut Self { 66 | self.set_option(UiOption::$opt(val.into())); 67 | self 68 | } 69 | )+ 70 | } 71 | } 72 | } 73 | 74 | ui_opt_setters! ( 75 | 76 | Rgb as set_rgb(bool); 77 | Override as set_override(bool); 78 | ExtCmdline as set_cmdline_external(bool); 79 | ExtHlstate as set_hlstate_external(bool); 80 | ExtLinegrid as set_linegrid_external(bool); 81 | ExtMessages as set_messages_externa(bool); 82 | ExtMultigrid as set_multigrid_external(bool); 83 | ExtPopupmenu as set_popupmenu_external(bool); 84 | ExtTabline as set_tabline_external(bool); 85 | ExtTermcolors as set_termcolors_external(bool); 86 | TermName as set_term_name(&str); 87 | TermColors as set_term_colors(u64); 88 | TermBackground as set_term_background(&str); 89 | StdinFd as set_stdin_fd(u64); 90 | StdinTty as set_stdin_tty(bool); 91 | StdoutTty as set_stdout_tty(bool); 92 | ExtWildmenu as set_wildmenu_external(bool); 93 | ); 94 | 95 | impl UiAttachOptions { 96 | #[must_use] 97 | pub fn new() -> UiAttachOptions { 98 | UiAttachOptions { 99 | options: Vec::new(), 100 | } 101 | } 102 | 103 | fn set_option(&mut self, option: UiOption) { 104 | let name = option.to_name_value(); 105 | let position = self.options.iter().position(|o| o.0 == name.0); 106 | 107 | if let Some(position) = position { 108 | self.options[position].1 = option; 109 | } else { 110 | self.options.push((name.0, option)); 111 | } 112 | } 113 | 114 | #[must_use] 115 | pub fn to_value_map(&self) -> Value { 116 | let map = self.options.iter().map(|o| o.1.to_value()).collect(); 117 | Value::Map(map) 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | 125 | #[test] 126 | fn test_ui_options() { 127 | let value_map = UiAttachOptions::new() 128 | .set_rgb(true) 129 | .set_rgb(false) 130 | .set_popupmenu_external(true) 131 | .to_value_map(); 132 | 133 | assert_eq!( 134 | Value::Map(vec![ 135 | ("rgb".into(), false.into()), 136 | ("ext_popupmenu".into(), true.into()), 137 | ]), 138 | value_map 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/nested_requests.rs: -------------------------------------------------------------------------------- 1 | use nvim_rs::{ 2 | compat::tokio::Compat, 3 | create::tokio as create, 4 | neovim::Neovim, 5 | Handler, 6 | }; 7 | 8 | use rmpv::Value; 9 | 10 | use std::{ 11 | sync::Arc, 12 | path::Path, 13 | }; 14 | 15 | use tokio::{ 16 | self, 17 | process::{ChildStdin, Command}, 18 | sync::Mutex, 19 | spawn 20 | }; 21 | 22 | const NVIM_BIN: &str = if cfg!(windows) { 23 | "nvim.exe" 24 | } else { 25 | "nvim" 26 | }; 27 | const NVIM_PATH: &str = if cfg!(windows) { 28 | "neovim/build/bin/nvim.exe" 29 | } else { 30 | "neovim/build/bin/nvim" 31 | }; 32 | 33 | #[derive(Clone)] 34 | struct NeovimHandler { 35 | froodle: Arc>, 36 | } 37 | 38 | impl Handler for NeovimHandler { 39 | type Writer = Compat; 40 | 41 | async fn handle_request( 42 | &self, 43 | name: String, 44 | args: Vec, 45 | neovim: Neovim>, 46 | ) -> Result { 47 | match name.as_ref() { 48 | "dummy" => Ok(Value::from("o")), 49 | "req" => { 50 | let v = args[0].as_str().unwrap(); 51 | 52 | let neovim = neovim.clone(); 53 | match v { 54 | "y" => { 55 | let mut x: String = neovim 56 | .get_vvar("progname") 57 | .await 58 | .unwrap() 59 | .as_str() 60 | .unwrap() 61 | .into(); 62 | x.push_str(" - "); 63 | x.push_str( 64 | neovim.get_var("oogle").await.unwrap().as_str().unwrap(), 65 | ); 66 | x.push_str(" - "); 67 | x.push_str( 68 | neovim 69 | .eval("rpcrequest(1,'dummy')") 70 | .await 71 | .unwrap() 72 | .as_str() 73 | .unwrap(), 74 | ); 75 | x.push_str(" - "); 76 | x.push_str( 77 | neovim 78 | .eval("rpcrequest(1,'req', 'z')") 79 | .await 80 | .unwrap() 81 | .as_str() 82 | .unwrap(), 83 | ); 84 | Ok(Value::from(x)) 85 | } 86 | "z" => { 87 | let x: String = neovim 88 | .get_vvar("progname") 89 | .await 90 | .unwrap() 91 | .as_str() 92 | .unwrap() 93 | .into(); 94 | Ok(Value::from(x)) 95 | } 96 | &_ => Err(Value::from("wrong argument to req")), 97 | } 98 | } 99 | &_ => Err(Value::from("wrong method name for request")), 100 | } 101 | } 102 | 103 | async fn handle_notify( 104 | &self, 105 | name: String, 106 | args: Vec, 107 | _neovim: Neovim>, 108 | ) { 109 | match name.as_ref() { 110 | "set_froodle" => { 111 | *self.froodle.lock().await = args[0].as_str().unwrap().to_string() 112 | } 113 | _ => {} 114 | }; 115 | } 116 | } 117 | 118 | #[tokio::main(flavor = "current_thread")] 119 | async fn main() { 120 | let rs = r#"exe ":fun M(timer) 121 | call rpcnotify(1, 'set_froodle', rpcrequest(1, 'req', 'y')) 122 | endfun""#; 123 | let rs2 = r#"exe ":fun N(timer) 124 | call chanclose(1) 125 | endfun""#; 126 | 127 | let froodle = Arc::new(Mutex::new(String::new())); 128 | let handler = NeovimHandler { 129 | froodle: froodle.clone(), 130 | }; 131 | 132 | let path = if Path::new(NVIM_PATH).exists() { 133 | NVIM_PATH 134 | } else { 135 | NVIM_BIN 136 | }; 137 | let (nvim, io, _child) = create::new_child_cmd( 138 | Command::new(path).args(&[ 139 | "-u", 140 | "NONE", 141 | "--embed", 142 | "--headless", 143 | "-c", 144 | rs, 145 | "-c", 146 | ":let timer = timer_start(500, 'M')", 147 | "-c", 148 | rs2, 149 | "-c", 150 | ":let timer = timer_start(1500, 'N')", 151 | ]), 152 | handler, 153 | ) 154 | .await 155 | .unwrap(); 156 | 157 | let nv = nvim.clone(); 158 | spawn(async move { nv.set_var("oogle", Value::from("doodle")).await }); 159 | 160 | // The 2nd timer closes the channel, which will be returned as an error from 161 | // the io handler. We only fail the test if we got another error 162 | if let Err(err) = io.await.unwrap() { 163 | if !err.is_channel_closed() { 164 | panic!("Error in io: '{:?}'", err); 165 | } 166 | } 167 | 168 | assert_eq!( 169 | format!("{nvim} - doodle - o - {nvim}", nvim = NVIM_BIN), 170 | *froodle.lock().await 171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /src/create/tokio.rs: -------------------------------------------------------------------------------- 1 | //! Functions to spawn a [`neovim`](crate::neovim::Neovim) session using 2 | //! [`tokio`](tokio) 3 | use std::{ 4 | future::Future, 5 | io::{self, Error, ErrorKind}, 6 | path::Path, 7 | process::Stdio, 8 | }; 9 | 10 | use tokio::{ 11 | fs::File as TokioFile, 12 | io::{split, stdin, WriteHalf}, 13 | net::{TcpStream, ToSocketAddrs}, 14 | process::{Child, ChildStdin, Command}, 15 | spawn, 16 | task::JoinHandle, 17 | }; 18 | 19 | #[cfg(unix)] 20 | type Connection = tokio::net::UnixStream; 21 | #[cfg(windows)] 22 | type Connection = tokio::net::windows::named_pipe::NamedPipeClient; 23 | 24 | use tokio_util::compat::{ 25 | Compat, TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt, 26 | }; 27 | 28 | use crate::{ 29 | create::{unbuffered_stdout, Spawner}, 30 | error::{HandshakeError, LoopError}, 31 | neovim::Neovim, 32 | Handler, 33 | }; 34 | 35 | impl Spawner for H 36 | where 37 | H: Handler, 38 | { 39 | type Handle = JoinHandle<()>; 40 | 41 | fn spawn(&self, future: Fut) -> Self::Handle 42 | where 43 | Fut: Future + Send + 'static, 44 | { 45 | spawn(future) 46 | } 47 | } 48 | 49 | /// Connect to a neovim instance via tcp 50 | pub async fn new_tcp( 51 | addr: A, 52 | handler: H, 53 | ) -> io::Result<( 54 | Neovim>>, 55 | JoinHandle>>, 56 | )> 57 | where 58 | H: Handler>>, 59 | A: ToSocketAddrs, 60 | { 61 | let stream = TcpStream::connect(addr).await?; 62 | let (reader, writer) = split(stream); 63 | let (neovim, io) = Neovim::>>::new( 64 | reader.compat(), 65 | writer.compat_write(), 66 | handler, 67 | ); 68 | let io_handle = spawn(io); 69 | 70 | Ok((neovim, io_handle)) 71 | } 72 | 73 | /// Connect to a neovim instance via unix socket (Unix) or named pipe (Windows) 74 | pub async fn new_path + Clone>( 75 | path: P, 76 | handler: H, 77 | ) -> io::Result<( 78 | Neovim>>, 79 | JoinHandle>>, 80 | )> 81 | where 82 | H: Handler>> + Send + 'static, 83 | { 84 | let stream = { 85 | #[cfg(unix)] 86 | { 87 | use tokio::net::UnixStream; 88 | 89 | UnixStream::connect(path).await? 90 | } 91 | #[cfg(windows)] 92 | { 93 | use std::time::Duration; 94 | use tokio::net::windows::named_pipe::ClientOptions; 95 | use tokio::time; 96 | 97 | // From windows-sys so we don't have to depend on that for just this constant 98 | // https://docs.rs/windows-sys/latest/windows_sys/Win32/Foundation/constant.ERROR_PIPE_BUSY.html 99 | pub const ERROR_PIPE_BUSY: i32 = 231i32; 100 | 101 | // Based on the example in the tokio docs, see explanation there 102 | // https://docs.rs/tokio/latest/tokio/net/windows/named_pipe/struct.NamedPipeClient.html 103 | let client = loop { 104 | match ClientOptions::new().open(path.as_ref()) { 105 | Ok(client) => break client, 106 | Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY) => (), 107 | Err(e) => return Err(e), 108 | } 109 | 110 | time::sleep(Duration::from_millis(50)).await; 111 | }; 112 | 113 | client 114 | } 115 | }; 116 | let (reader, writer) = split(stream); 117 | let (neovim, io) = Neovim::>>::new( 118 | reader.compat(), 119 | writer.compat_write(), 120 | handler, 121 | ); 122 | let io_handle = spawn(io); 123 | 124 | Ok((neovim, io_handle)) 125 | } 126 | 127 | /// Connect to a neovim instance by spawning a new one 128 | pub async fn new_child( 129 | handler: H, 130 | ) -> io::Result<( 131 | Neovim>, 132 | JoinHandle>>, 133 | Child, 134 | )> 135 | where 136 | H: Handler> + Send + 'static, 137 | { 138 | if cfg!(target_os = "windows") { 139 | new_child_path("nvim.exe", handler).await 140 | } else { 141 | new_child_path("nvim", handler).await 142 | } 143 | } 144 | 145 | /// Connect to a neovim instance by spawning a new one 146 | pub async fn new_child_path>( 147 | program: S, 148 | handler: H, 149 | ) -> io::Result<( 150 | Neovim>, 151 | JoinHandle>>, 152 | Child, 153 | )> 154 | where 155 | H: Handler> + Send + 'static, 156 | { 157 | new_child_cmd(Command::new(program.as_ref()).arg("--embed"), handler).await 158 | } 159 | 160 | /// Connect to a neovim instance by spawning a new one 161 | /// 162 | /// stdin/stdout will be rewritten to `Stdio::piped()` 163 | pub async fn new_child_cmd( 164 | cmd: &mut Command, 165 | handler: H, 166 | ) -> io::Result<( 167 | Neovim>, 168 | JoinHandle>>, 169 | Child, 170 | )> 171 | where 172 | H: Handler> + Send + 'static, 173 | { 174 | let mut child = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?; 175 | let stdout = child 176 | .stdout 177 | .take() 178 | .ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdout"))? 179 | .compat(); 180 | let stdin = child 181 | .stdin 182 | .take() 183 | .ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdin"))? 184 | .compat_write(); 185 | 186 | let (neovim, io) = Neovim::>::new(stdout, stdin, handler); 187 | let io_handle = spawn(io); 188 | 189 | Ok((neovim, io_handle, child)) 190 | } 191 | 192 | /// Connect to the neovim instance that spawned this process over stdin/stdout 193 | pub async fn new_parent( 194 | handler: H, 195 | ) -> Result< 196 | ( 197 | Neovim>, 198 | JoinHandle>>, 199 | ), 200 | Error, 201 | > 202 | where 203 | H: Handler>, 204 | { 205 | let sout = TokioFile::from_std(unbuffered_stdout()?); 206 | 207 | let (neovim, io) = Neovim::>::new( 208 | stdin().compat(), 209 | sout.compat(), 210 | handler, 211 | ); 212 | let io_handle = spawn(io); 213 | 214 | Ok((neovim, io_handle)) 215 | } 216 | 217 | /// Connect to a neovim instance by spawning a new one and send a handshake 218 | /// message. Unlike `new_child_cmd`, this function is tolerant to extra 219 | /// data in the reader before the handshake response is received. 220 | /// 221 | /// `message` should be a unique string that is normally not found in the 222 | /// stdout. Due to the way Neovim packs strings, the length has to be either 223 | /// less than 20 characters or more than 31 characters long. 224 | /// See https://github.com/neovim/neovim/issues/32784 for more information. 225 | pub async fn new_child_handshake_cmd( 226 | cmd: &mut Command, 227 | handler: H, 228 | message: &str, 229 | ) -> Result< 230 | ( 231 | Neovim>, 232 | JoinHandle>>, 233 | Child, 234 | ), 235 | Box, 236 | > 237 | where 238 | H: Handler> + Send + 'static, 239 | { 240 | let mut child = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?; 241 | let stdout = child 242 | .stdout 243 | .take() 244 | .ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdout"))? 245 | .compat(); 246 | let stdin = child 247 | .stdin 248 | .take() 249 | .ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdin"))? 250 | .compat_write(); 251 | 252 | let (neovim, io) = 253 | Neovim::>::handshake(stdout, stdin, handler, message) 254 | .await?; 255 | let io_handle = spawn(io); 256 | 257 | Ok((neovim, io_handle, child)) 258 | } 259 | -------------------------------------------------------------------------------- /LICENSE-LGPL: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /bindings/generate_bindings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Rust code generator, based on neovim-qt generator 4 | """ 5 | 6 | import msgpack 7 | import sys, subprocess, os 8 | import re 9 | import jinja2 10 | import datetime 11 | 12 | INPUT = 'bindings' 13 | 14 | MANUALLY_IMPLEMENTED = [ 15 | "nvim_ui_attach", 16 | "nvim_tabpage_list_wins", 17 | "nvim_tabpage_get_win", 18 | "nvim_win_get_buf", 19 | "nvim_win_get_tabpage", 20 | "nvim_list_bufs", 21 | "nvim_get_current_buf", 22 | "nvim_list_wins", 23 | "nvim_get_current_win", 24 | "nvim_create_buf", 25 | "nvim_open_win", 26 | "nvim_list_tabpages", 27 | "nvim_get_current_tabpage", 28 | ] 29 | 30 | def decutf8(inp): 31 | """ 32 | Recursively decode bytes as utf8 into unicode 33 | """ 34 | if isinstance(inp, bytes): 35 | return inp.decode('utf8') 36 | elif isinstance(inp, list): 37 | return [decutf8(x) for x in inp] 38 | elif isinstance(inp, dict): 39 | return {decutf8(key):decutf8(val) for key,val in inp.items()} 40 | else: 41 | return inp 42 | 43 | def get_api_info(nvim): 44 | """ 45 | Call the neovim binary to get the api info 46 | """ 47 | args = [nvim, '--api-info'] 48 | info = subprocess.check_output(args) 49 | return decutf8(msgpack.unpackb(info)) 50 | 51 | def generate_file(name, outpath, **kw): 52 | from jinja2 import Environment, FileSystemLoader 53 | env=Environment(loader=FileSystemLoader('bindings'), trim_blocks=True) 54 | template = env.get_template(name) 55 | with open(os.path.join(outpath, name), 'w') as fp: 56 | fp.write(template.render(kw)) 57 | 58 | subprocess.call(["rustfmt", os.path.join(outpath, name)]) 59 | # os.remove(os.path.join(outpath, name + ".bk")) 60 | 61 | class UnsupportedType(Exception): pass 62 | class NeovimTypeVal: 63 | """ 64 | Representation for Neovim Parameter/Return 65 | """ 66 | # msgpack simple types types 67 | SIMPLETYPES_REF = { 68 | 'Array': 'Vec', 69 | 'ArrayOf(Integer, 2)': '(i64, i64)', 70 | 'void': '()', 71 | 'Integer': 'i64', 72 | 'Float': 'f64', 73 | 'Boolean': 'bool', 74 | 'String': '&str', 75 | 'Object': 'Value', 76 | 'Dict': 'Vec<(Value, Value)>', 77 | } 78 | 79 | SIMPLETYPES_VAL = { 80 | 'Array': 'Vec', 81 | 'ArrayOf(Integer, 2)': '(i64, i64)', 82 | 'void': '()', 83 | 'Integer': 'i64', 84 | 'Float': 'f64', 85 | 'Boolean': 'bool', 86 | 'String': 'String', 87 | 'Object': 'Value', 88 | 'Dict': 'Vec<(Value, Value)>', 89 | } 90 | # msgpack extension types 91 | EXTTYPES = { 92 | 'Window': 'Window', 93 | 'Buffer': 'Buffer', 94 | 'Tabpage': 'Tabpage', 95 | } 96 | # Unbound Array types 97 | UNBOUND_ARRAY = re.compile(r'ArrayOf\(\s*(\w+)\s*\)') 98 | 99 | def __init__(self, typename, name=''): 100 | self.name = name 101 | self.neovim_type = typename 102 | self.ext = False 103 | self.native_type_arg = NeovimTypeVal.nativeTypeRef(typename) 104 | self.native_type_ret = NeovimTypeVal.nativeTypeVal(typename) 105 | 106 | if typename in self.EXTTYPES: 107 | self.ext = True 108 | 109 | def __getitem__(self, key): 110 | if key == "native_type_arg": 111 | return self.native_type_arg 112 | if key == "name": 113 | return self._convert_arg_name(self.name) 114 | return None 115 | 116 | def _convert_arg_name(self, key): 117 | """Rust keyword must not be used as function arguments""" 118 | if key == "fn": 119 | return "fname" 120 | if key == "type": 121 | return "typ" 122 | return key 123 | 124 | @classmethod 125 | def nativeTypeVal(cls, typename): 126 | """Return the native type for this Neovim type.""" 127 | if typename in cls.SIMPLETYPES_VAL: 128 | return cls.SIMPLETYPES_VAL[typename] 129 | elif typename in cls.EXTTYPES: 130 | return cls.EXTTYPES[typename] 131 | elif cls.UNBOUND_ARRAY.match(typename): 132 | m = cls.UNBOUND_ARRAY.match(typename) 133 | return 'Vec<%s>' % cls.nativeTypeVal(m.groups()[0]) 134 | raise UnsupportedType(typename) 135 | 136 | 137 | @classmethod 138 | def nativeTypeRef(cls, typename): 139 | """Return the native type for this Neovim type.""" 140 | if typename in cls.SIMPLETYPES_REF: 141 | return cls.SIMPLETYPES_REF[typename] 142 | elif typename in cls.EXTTYPES: 143 | return "&%s" % cls.EXTTYPES[typename] 144 | elif cls.UNBOUND_ARRAY.match(typename): 145 | m = cls.UNBOUND_ARRAY.match(typename) 146 | return 'Vec<%s>' % cls.nativeTypeVal(m.groups()[0]) 147 | raise UnsupportedType(typename) 148 | 149 | class Function: 150 | """ 151 | Representation for a Neovim API Function 152 | """ 153 | def __init__(self, nvim_fun, all_ext_prefixes): 154 | self.valid = False 155 | self.fun = nvim_fun 156 | self.parameters = [] 157 | self.name = self.fun['name'] 158 | self.since = self.fun['since'] 159 | 160 | self.ext = self._is_ext(all_ext_prefixes) 161 | 162 | try: 163 | self.return_type = NeovimTypeVal(self.fun['return_type']) 164 | if self.ext: 165 | for param in self.fun['parameters'][1:]: 166 | self.parameters.append(NeovimTypeVal(*param)) 167 | else: 168 | for param in self.fun['parameters']: 169 | self.parameters.append(NeovimTypeVal(*param)) 170 | except UnsupportedType as ex: 171 | print('Found unsupported type(%s) when adding function %s(), skipping' % (ex,self.name)) 172 | return 173 | 174 | # Build the argument string - makes it easier for the templates 175 | self.argstring = ', '.join(['%s: %s' % (tv["name"], tv.native_type_arg) for tv in self.parameters]) 176 | 177 | # Build the call string - even easier for the templates ;) 178 | self.callstring = ', '.join(['%s' % tv["name"] for tv in self.parameters]) 179 | # filter function, use only nvim one 180 | # nvim_ui_attach implemented manually 181 | self.valid = self.name.startswith('nvim')\ 182 | and self.name not in MANUALLY_IMPLEMENTED 183 | 184 | def _is_ext(self, all_ext_prefixes): 185 | for prefix in all_ext_prefixes: 186 | if self.name.startswith(prefix): 187 | return True 188 | return False 189 | 190 | class ExtType: 191 | 192 | """Ext type, Buffer, Window, Tab""" 193 | 194 | def __init__(self, typename, info): 195 | self.name = typename 196 | self.id = info['id'] 197 | self.prefix = info['prefix'] 198 | 199 | 200 | def print_api(api): 201 | print(api.keys()); 202 | for key in api.keys(): 203 | if key == 'functions': 204 | print('Functions') 205 | for f in api[key]: 206 | if f['name'].startswith('nvim'): 207 | print(f) 208 | print('') 209 | elif key == 'types': 210 | print('Data Types') 211 | for typ in api[key]: 212 | print('\t%s' % typ) 213 | print('') 214 | elif key == 'error_types': 215 | print('Error Types') 216 | for err,desc in api[key].items(): 217 | print('\t%s:%d' % (err,desc['id'])) 218 | print('') 219 | elif key == 'version': 220 | print('Version') 221 | print(api[key]) 222 | print('') 223 | else: 224 | print('Unknown API info attribute: %s' % key) 225 | 226 | if __name__ == '__main__': 227 | 228 | if len(sys.argv) < 2 or len(sys.argv) > 3 : 229 | print('Usage:') 230 | print('\tgenerate_bindings ') 231 | print('\tgenerate_bindings [path]') 232 | sys.exit(-1) 233 | 234 | nvim = sys.argv[1] 235 | outpath = None if len(sys.argv) < 3 else sys.argv[2] 236 | 237 | try: 238 | api = get_api_info(sys.argv[1]) 239 | except subprocess.CalledProcessError as ex: 240 | print(ex) 241 | sys.exit(-1) 242 | 243 | if outpath: 244 | print('Writing auto generated bindings to %s' % outpath) 245 | if not os.path.exists(outpath): 246 | os.makedirs(outpath) 247 | for name in os.listdir(INPUT): 248 | if name.startswith('.'): 249 | continue 250 | if name.endswith('.rs'): 251 | env = {} 252 | env['date'] = datetime.datetime.now() 253 | 254 | exttypes = [ ExtType(typename, info) for typename,info in api['types'].items() ] 255 | all_ext_prefixes = { exttype.prefix for exttype in exttypes } 256 | functions = [Function(f, all_ext_prefixes) for f in api['functions']] 257 | env['functions'] = [f for f in functions if f.valid] 258 | env['exttypes'] = exttypes 259 | generate_file(name, outpath, **env) 260 | 261 | else: 262 | print('Neovim api info:') 263 | print_api(api) 264 | -------------------------------------------------------------------------------- /src/rpc/model.rs: -------------------------------------------------------------------------------- 1 | //! Decoding and encoding msgpack rpc messages from/to neovim. 2 | use std::{ 3 | self, 4 | convert::TryInto, 5 | io::{self, Cursor, ErrorKind, Read, Write}, 6 | sync::Arc, 7 | }; 8 | 9 | use futures::{ 10 | io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, 11 | lock::Mutex, 12 | }; 13 | use rmpv::{decode::read_value, encode::write_value, Value}; 14 | 15 | use crate::error::{DecodeError, EncodeError}; 16 | 17 | /// A msgpack-rpc message, see 18 | /// 19 | #[derive(Debug, PartialEq, Clone)] 20 | pub enum RpcMessage { 21 | RpcRequest { 22 | msgid: u64, 23 | method: String, 24 | params: Vec, 25 | }, // 0 26 | RpcResponse { 27 | msgid: u64, 28 | error: Value, 29 | result: Value, 30 | }, // 1 31 | RpcNotification { 32 | method: String, 33 | params: Vec, 34 | }, // 2 35 | } 36 | 37 | macro_rules! rpc_args { 38 | ($($e:expr), *) => {{ 39 | let vec = vec![ 40 | $(Value::from($e),)* 41 | ]; 42 | Value::from(vec) 43 | }} 44 | } 45 | 46 | /// Continously reads from reader, pushing onto `rest`. Then tries to decode the 47 | /// contents of `rest`. If it succeeds, returns the message, and leaves any 48 | /// non-decoded bytes in `rest`. If we did not read enough for a full message, 49 | /// read more. Return on all other errors. 50 | // 51 | // TODO: This might be inefficient. Can't we read into `rest` directly? 52 | pub async fn decode( 53 | reader: &mut R, 54 | rest: &mut Vec, 55 | ) -> std::result::Result> { 56 | let mut buf = Box::new([0_u8; 80 * 1024]); 57 | let mut bytes_read; 58 | 59 | loop { 60 | let mut c = Cursor::new(&rest); 61 | 62 | match decode_buffer(&mut c).map_err(|b| *b) { 63 | Ok(msg) => { 64 | let pos = c.position(); 65 | *rest = rest.split_off(pos as usize); // TODO: more efficiency 66 | return Ok(msg); 67 | } 68 | Err(DecodeError::BufferError(e)) 69 | if e.kind() == ErrorKind::UnexpectedEof => 70 | { 71 | debug!("Not enough data, reading more!"); 72 | bytes_read = reader.read(&mut *buf).await; 73 | } 74 | Err(err) => return Err(err.into()), 75 | } 76 | 77 | match bytes_read { 78 | Ok(n) if n == 0 => { 79 | return Err(io::Error::new(ErrorKind::UnexpectedEof, "EOF").into()); 80 | } 81 | Ok(n) => { 82 | rest.extend_from_slice(&buf[..n]); 83 | } 84 | Err(err) => return Err(err.into()), 85 | } 86 | } 87 | } 88 | 89 | /// Syncronously decode the content of a reader into an rpc message. Tries to 90 | /// give detailed errors if something went wrong. 91 | fn decode_buffer( 92 | reader: &mut R, 93 | ) -> std::result::Result> { 94 | use crate::error::InvalidMessage::*; 95 | 96 | let arr: Vec = read_value(reader)?.try_into().map_err(NotAnArray)?; 97 | 98 | let mut arr = arr.into_iter(); 99 | 100 | let msgtyp: u64 = arr 101 | .next() 102 | .ok_or(WrongArrayLength(3..=4, 0))? 103 | .try_into() 104 | .map_err(InvalidType)?; 105 | 106 | match msgtyp { 107 | 0 => { 108 | let msgid: u64 = arr 109 | .next() 110 | .ok_or(WrongArrayLength(4..=4, 1))? 111 | .try_into() 112 | .map_err(InvalidMsgid)?; 113 | let method = match arr.next() { 114 | Some(Value::String(s)) if s.is_str() => { 115 | s.into_str().expect("Can remove using #230 of rmpv") 116 | } 117 | Some(val) => return Err(InvalidRequestName(msgid, val).into()), 118 | None => return Err(WrongArrayLength(4..=4, 2).into()), 119 | }; 120 | let params: Vec = arr 121 | .next() 122 | .ok_or(WrongArrayLength(4..=4, 3))? 123 | .try_into() 124 | .map_err(|val| InvalidParams(val, method.clone()))?; 125 | 126 | Ok(RpcMessage::RpcRequest { 127 | msgid, 128 | method, 129 | params, 130 | }) 131 | } 132 | 1 => { 133 | let msgid: u64 = arr 134 | .next() 135 | .ok_or(WrongArrayLength(4..=4, 1))? 136 | .try_into() 137 | .map_err(InvalidMsgid)?; 138 | let error = arr.next().ok_or(WrongArrayLength(4..=4, 2))?; 139 | let result = arr.next().ok_or(WrongArrayLength(4..=4, 3))?; 140 | Ok(RpcMessage::RpcResponse { 141 | msgid, 142 | error, 143 | result, 144 | }) 145 | } 146 | 2 => { 147 | let method = match arr.next() { 148 | Some(Value::String(s)) if s.is_str() => { 149 | s.into_str().expect("Can remove using #230 of rmpv") 150 | } 151 | Some(val) => return Err(InvalidNotificationName(val).into()), 152 | None => return Err(WrongArrayLength(3..=3, 1).into()), 153 | }; 154 | let params: Vec = arr 155 | .next() 156 | .ok_or(WrongArrayLength(3..=3, 2))? 157 | .try_into() 158 | .map_err(|val| InvalidParams(val, method.clone()))?; 159 | Ok(RpcMessage::RpcNotification { method, params }) 160 | } 161 | t => Err(UnknownMessageType(t).into()), 162 | } 163 | } 164 | 165 | /// Encode the given message into the `writer`. 166 | pub fn encode_sync( 167 | writer: &mut W, 168 | msg: RpcMessage, 169 | ) -> std::result::Result<(), Box> { 170 | match msg { 171 | RpcMessage::RpcRequest { 172 | msgid, 173 | method, 174 | params, 175 | } => { 176 | let val = rpc_args!(0, msgid, method, params); 177 | write_value(writer, &val)?; 178 | } 179 | RpcMessage::RpcResponse { 180 | msgid, 181 | error, 182 | result, 183 | } => { 184 | let val = rpc_args!(1, msgid, error, result); 185 | write_value(writer, &val)?; 186 | } 187 | RpcMessage::RpcNotification { method, params } => { 188 | let val = rpc_args!(2, method, params); 189 | write_value(writer, &val)?; 190 | } 191 | }; 192 | 193 | Ok(()) 194 | } 195 | 196 | /// Encode the given message into the `BufWriter`. Flushes the writer when 197 | /// finished. 198 | pub async fn encode( 199 | writer: Arc>, 200 | msg: RpcMessage, 201 | ) -> std::result::Result<(), Box> { 202 | let mut v: Vec = vec![]; 203 | encode_sync(&mut v, msg)?; 204 | 205 | let mut writer = writer.lock().await; 206 | writer.write_all(&v).await?; 207 | writer.flush().await?; 208 | 209 | Ok(()) 210 | } 211 | 212 | pub trait IntoVal { 213 | fn into_val(self) -> T; 214 | } 215 | 216 | impl<'a> IntoVal for &'a str { 217 | fn into_val(self) -> Value { 218 | Value::from(self) 219 | } 220 | } 221 | 222 | impl IntoVal for Vec { 223 | fn into_val(self) -> Value { 224 | let vec: Vec = self.into_iter().map(Value::from).collect(); 225 | Value::from(vec) 226 | } 227 | } 228 | 229 | impl IntoVal for Vec { 230 | fn into_val(self) -> Value { 231 | Value::from(self) 232 | } 233 | } 234 | 235 | impl IntoVal for (i64, i64) { 236 | fn into_val(self) -> Value { 237 | Value::from(vec![Value::from(self.0), Value::from(self.1)]) 238 | } 239 | } 240 | 241 | impl IntoVal for bool { 242 | fn into_val(self) -> Value { 243 | Value::from(self) 244 | } 245 | } 246 | 247 | impl IntoVal for i64 { 248 | fn into_val(self) -> Value { 249 | Value::from(self) 250 | } 251 | } 252 | 253 | impl IntoVal for f64 { 254 | fn into_val(self) -> Value { 255 | Value::from(self) 256 | } 257 | } 258 | 259 | impl IntoVal for String { 260 | fn into_val(self) -> Value { 261 | Value::from(self) 262 | } 263 | } 264 | 265 | impl IntoVal for Value { 266 | fn into_val(self) -> Value { 267 | self 268 | } 269 | } 270 | 271 | impl IntoVal for Vec<(Value, Value)> { 272 | fn into_val(self) -> Value { 273 | Value::from(self) 274 | } 275 | } 276 | 277 | #[cfg(all(test, feature = "use_tokio"))] 278 | mod test { 279 | use super::*; 280 | use futures::{io::BufWriter, lock::Mutex}; 281 | use std::{io::Cursor, sync::Arc}; 282 | 283 | use tokio; 284 | 285 | #[tokio::test] 286 | async fn request_test() { 287 | let msg = RpcMessage::RpcRequest { 288 | msgid: 1, 289 | method: "test_method".to_owned(), 290 | params: vec![], 291 | }; 292 | 293 | let buff: Vec = vec![]; 294 | let tmp = Arc::new(Mutex::new(BufWriter::new(buff))); 295 | let tmp2 = tmp.clone(); 296 | let msg2 = msg.clone(); 297 | 298 | encode(tmp2, msg2).await.unwrap(); 299 | 300 | let msg_dest = { 301 | let v = &mut *tmp.lock().await; 302 | let x = v.get_mut(); 303 | decode_buffer(&mut x.as_slice()).unwrap() 304 | }; 305 | 306 | assert_eq!(msg, msg_dest); 307 | } 308 | 309 | #[tokio::test] 310 | async fn request_test_twice() { 311 | let msg_1 = RpcMessage::RpcRequest { 312 | msgid: 1, 313 | method: "test_method".to_owned(), 314 | params: vec![], 315 | }; 316 | 317 | let msg_2 = RpcMessage::RpcRequest { 318 | msgid: 2, 319 | method: "test_method_2".to_owned(), 320 | params: vec![], 321 | }; 322 | 323 | let buff: Vec = vec![]; 324 | let tmp = Arc::new(Mutex::new(BufWriter::new(buff))); 325 | let msg_1_c = msg_1.clone(); 326 | let msg_2_c = msg_2.clone(); 327 | 328 | let tmp_c = tmp.clone(); 329 | encode(tmp_c, msg_1_c).await.unwrap(); 330 | let tmp_c = tmp.clone(); 331 | encode(tmp_c, msg_2_c).await.unwrap(); 332 | let len = (*tmp).lock().await.get_ref().len(); 333 | assert_eq!(34, len); // Note: msg2 is 2 longer than msg 334 | 335 | let v = &mut *tmp.lock().await; 336 | let x = v.get_mut(); 337 | let mut cursor = Cursor::new(x.as_slice()); 338 | let msg_dest_1 = decode_buffer(&mut cursor).unwrap(); 339 | 340 | assert_eq!(msg_1, msg_dest_1); 341 | assert_eq!(16, cursor.position()); 342 | 343 | let msg_dest_2 = decode_buffer(&mut cursor).unwrap(); 344 | assert_eq!(msg_2, msg_dest_2); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/neovim.rs: -------------------------------------------------------------------------------- 1 | //! An active neovim session. 2 | use std::{ 3 | future::Future, 4 | sync::{ 5 | atomic::{AtomicU64, Ordering}, 6 | Arc, 7 | }, 8 | }; 9 | 10 | use futures::{ 11 | channel::{ 12 | mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, 13 | oneshot, 14 | }, 15 | future, 16 | io::{AsyncRead, AsyncReadExt, AsyncWrite}, 17 | lock::Mutex, 18 | sink::SinkExt, 19 | stream::StreamExt, 20 | TryFutureExt, 21 | }; 22 | 23 | use crate::{ 24 | create::Spawner, 25 | error::{CallError, DecodeError, EncodeError, HandshakeError, LoopError}, 26 | rpc::{ 27 | handler::Handler, 28 | model, 29 | model::{IntoVal, RpcMessage}, 30 | }, 31 | uioptions::UiAttachOptions, 32 | }; 33 | use rmpv::Value; 34 | 35 | /// Pack the given arguments into a `Vec`, suitable for using it for a 36 | /// [`call`](crate::neovim::Neovim::call) to neovim. 37 | #[macro_export] 38 | macro_rules! call_args { 39 | () => (Vec::new()); 40 | ($($e:expr), +,) => (call_args![$($e),*]); 41 | ($($e:expr), +) => {{ 42 | let vec = vec![ 43 | $($e.into_val(),)* 44 | ]; 45 | vec 46 | }}; 47 | } 48 | 49 | type ResponseResult = Result, Arc>; 50 | 51 | type Queue = Arc)>>>; 52 | 53 | /// An active Neovim session. 54 | pub struct Neovim 55 | where 56 | W: AsyncWrite + Send + Unpin + 'static, 57 | { 58 | pub(crate) writer: Arc>, 59 | pub(crate) queue: Queue, 60 | pub(crate) msgid_counter: Arc, 61 | } 62 | 63 | impl Clone for Neovim 64 | where 65 | W: AsyncWrite + Send + Unpin + 'static, 66 | { 67 | fn clone(&self) -> Self { 68 | Neovim { 69 | writer: self.writer.clone(), 70 | queue: self.queue.clone(), 71 | msgid_counter: self.msgid_counter.clone(), 72 | } 73 | } 74 | } 75 | 76 | impl PartialEq for Neovim 77 | where 78 | W: AsyncWrite + Send + Unpin + 'static, 79 | { 80 | fn eq(&self, other: &Self) -> bool { 81 | Arc::ptr_eq(&self.writer, &other.writer) 82 | } 83 | } 84 | impl Eq for Neovim where W: AsyncWrite + Send + Unpin + 'static {} 85 | 86 | impl Neovim 87 | where 88 | W: AsyncWrite + Send + Unpin + 'static, 89 | { 90 | #[allow(clippy::new_ret_no_self)] 91 | pub fn new( 92 | reader: R, 93 | writer: W, 94 | handler: H, 95 | ) -> ( 96 | Neovim<::Writer>, 97 | impl Future>>, 98 | ) 99 | where 100 | R: AsyncRead + Send + Unpin + 'static, 101 | H: Handler + Spawner, 102 | { 103 | let req = Neovim { 104 | writer: Arc::new(Mutex::new(writer)), 105 | msgid_counter: Arc::new(AtomicU64::new(0)), 106 | queue: Arc::new(Mutex::new(Vec::new())), 107 | }; 108 | 109 | let (sender, receiver) = unbounded(); 110 | let fut = future::try_join( 111 | req.clone().io_loop(reader, sender), 112 | req.clone().handler_loop(handler, receiver), 113 | ) 114 | .map_ok(|_| ()); 115 | 116 | (req, fut) 117 | } 118 | 119 | /// Create a new instance, immediately send a handshake message and 120 | /// wait for the response. Unlike `new`, this function is tolerant to extra 121 | /// data in the reader before the handshake response is received. 122 | /// 123 | /// `message` should be a unique string that is normally not found in the 124 | /// stdout. Due to the way Neovim packs strings, the length has to be either 125 | /// less than 20 characters or more than 31 characters long. 126 | /// See https://github.com/neovim/neovim/issues/32784 for more information. 127 | pub async fn handshake( 128 | mut reader: R, 129 | writer: W, 130 | handler: H, 131 | message: &str, 132 | ) -> Result< 133 | ( 134 | Neovim<::Writer>, 135 | impl Future>>, 136 | ), 137 | Box, 138 | > 139 | where 140 | R: AsyncRead + Send + Unpin + 'static, 141 | H: Handler + Spawner, 142 | { 143 | let instance = Neovim { 144 | writer: Arc::new(Mutex::new(writer)), 145 | msgid_counter: Arc::new(AtomicU64::new(0)), 146 | queue: Arc::new(Mutex::new(Vec::new())), 147 | }; 148 | 149 | let msgid = instance.msgid_counter.fetch_add(1, Ordering::SeqCst); 150 | // Nvim encodes fixed size strings with a length of 20-31 bytes wrong, so 151 | // avoid that 152 | let msg_len = message.len(); 153 | assert!( 154 | !(20..=31).contains(&msg_len), 155 | "The message should be less than 20 characters or more than 31 characters 156 | long, but the length is {msg_len}." 157 | ); 158 | 159 | let req = RpcMessage::RpcRequest { 160 | msgid, 161 | method: "nvim_exec_lua".to_owned(), 162 | params: call_args![format!("return '{message}'"), Vec::::new()], 163 | }; 164 | model::encode(instance.writer.clone(), req).await?; 165 | 166 | let expected_resp = RpcMessage::RpcResponse { 167 | msgid, 168 | error: rmpv::Value::Nil, 169 | result: rmpv::Value::String(message.into()), 170 | }; 171 | let mut expected_data = Vec::new(); 172 | model::encode_sync(&mut expected_data, expected_resp) 173 | .expect("Encoding static data can't fail"); 174 | let mut actual_data = Vec::new(); 175 | let mut start = 0; 176 | let mut end = 0; 177 | while end - start != expected_data.len() { 178 | actual_data.resize(start + expected_data.len(), 0); 179 | 180 | let bytes_read = 181 | reader 182 | .read(&mut actual_data[start..]) 183 | .await 184 | .map_err(|err| { 185 | ( 186 | err, 187 | String::from_utf8_lossy(&actual_data[..end]).to_string(), 188 | ) 189 | })?; 190 | if bytes_read == 0 { 191 | // The end of the stream has been reached when the reader returns Ok(0). 192 | // Since we haven't detected a suitable response yet, return an error. 193 | return Err(Box::new(HandshakeError::UnexpectedResponse( 194 | String::from_utf8_lossy(&actual_data[..end]).to_string(), 195 | ))); 196 | } 197 | end += bytes_read; 198 | while end - start > 0 { 199 | if actual_data[start..end] == expected_data[..end - start] { 200 | break; 201 | } 202 | start += 1; 203 | } 204 | } 205 | 206 | let (sender, receiver) = unbounded(); 207 | let fut = future::try_join( 208 | instance.clone().io_loop(reader, sender), 209 | instance.clone().handler_loop(handler, receiver), 210 | ) 211 | .map_ok(|_| ()); 212 | 213 | Ok((instance, fut)) 214 | } 215 | 216 | async fn send_msg( 217 | &self, 218 | method: &str, 219 | args: Vec, 220 | ) -> Result, Box> { 221 | let msgid = self.msgid_counter.fetch_add(1, Ordering::SeqCst); 222 | 223 | let req = RpcMessage::RpcRequest { 224 | msgid, 225 | method: method.to_owned(), 226 | params: args, 227 | }; 228 | 229 | let (sender, receiver) = oneshot::channel(); 230 | 231 | self.queue.lock().await.push((msgid, sender)); 232 | 233 | let writer = self.writer.clone(); 234 | model::encode(writer, req).await?; 235 | 236 | Ok(receiver) 237 | } 238 | 239 | pub async fn call( 240 | &self, 241 | method: &str, 242 | args: Vec, 243 | ) -> Result, Box> { 244 | let receiver = self 245 | .send_msg(method, args) 246 | .await 247 | .map_err(|e| CallError::SendError(*e, method.to_string()))?; 248 | 249 | match receiver.await { 250 | // Result, Arc>, RecvError> 251 | Ok(Ok(r)) => Ok(r), // r is Result, i.e. we got an answer 252 | Ok(Err(err)) => { 253 | // err is a Decode Error, i.e. the answer wasn't decodable 254 | Err(Box::new(CallError::DecodeError(err, method.to_string()))) 255 | } 256 | Err(err) => { 257 | // err is RecvError 258 | Err(Box::new(CallError::InternalReceiveError( 259 | err, 260 | method.to_string(), 261 | ))) 262 | } 263 | } 264 | } 265 | 266 | async fn send_error_to_callers( 267 | &self, 268 | queue: &Queue, 269 | err: DecodeError, 270 | ) -> Result, Box> { 271 | let err = Arc::new(err); 272 | let mut v: Vec = vec![]; 273 | 274 | let mut queue = queue.lock().await; 275 | queue.drain(0..).for_each(|sender| { 276 | let msgid = sender.0; 277 | sender 278 | .1 279 | .send(Err(err.clone())) 280 | .unwrap_or_else(|_| v.push(msgid)); 281 | }); 282 | 283 | if v.is_empty() { 284 | Ok(err) 285 | } else { 286 | Err((err, v).into()) 287 | } 288 | } 289 | 290 | async fn handler_loop( 291 | self, 292 | handler: H, 293 | mut receiver: UnboundedReceiver, 294 | ) -> Result<(), Box> 295 | where 296 | H: Handler + Spawner, 297 | { 298 | loop { 299 | let msg = match receiver.next().await { 300 | Some(msg) => msg, 301 | /* If our receiver closes, that just means that io_handler started 302 | * shutting down. This is normal, so shut down along with it and don't 303 | * report an error 304 | */ 305 | None => break Ok(()), 306 | }; 307 | 308 | match msg { 309 | RpcMessage::RpcRequest { 310 | msgid, 311 | method, 312 | params, 313 | } => { 314 | let handler_c = handler.clone(); 315 | let neovim = self.clone(); 316 | let writer = self.writer.clone(); 317 | 318 | handler.spawn(async move { 319 | let response = match handler_c 320 | .handle_request(method, params, neovim) 321 | .await 322 | { 323 | Ok(result) => RpcMessage::RpcResponse { 324 | msgid, 325 | result, 326 | error: Value::Nil, 327 | }, 328 | Err(error) => RpcMessage::RpcResponse { 329 | msgid, 330 | result: Value::Nil, 331 | error, 332 | }, 333 | }; 334 | 335 | model::encode(writer, response) 336 | .await 337 | .unwrap_or_else(|e| { 338 | error!("Error sending response to request {}: '{}'", msgid, e); 339 | }); 340 | }); 341 | }, 342 | RpcMessage::RpcNotification { 343 | method, 344 | params 345 | } => handler.handle_notify(method, params, self.clone()).await, 346 | RpcMessage::RpcResponse { .. } => unreachable!(), 347 | } 348 | } 349 | } 350 | 351 | async fn io_loop( 352 | self, 353 | mut reader: R, 354 | mut sender: UnboundedSender, 355 | ) -> Result<(), Box> 356 | where 357 | R: AsyncRead + Send + Unpin + 'static, 358 | { 359 | let mut rest: Vec = vec![]; 360 | 361 | loop { 362 | let msg = match model::decode(&mut reader, &mut rest).await { 363 | Ok(msg) => msg, 364 | Err(err) => { 365 | let e = self.send_error_to_callers(&self.queue, *err).await?; 366 | return Err(Box::new(LoopError::DecodeError(e, None))); 367 | } 368 | }; 369 | 370 | debug!("Get message {:?}", msg); 371 | if let RpcMessage::RpcResponse { msgid, result, error, } = msg { 372 | let sender = find_sender(&self.queue, msgid).await?; 373 | if error == Value::Nil { 374 | sender 375 | .send(Ok(Ok(result))) 376 | .map_err(|r| (msgid, r.expect("This was an OK(_)")))?; 377 | } else { 378 | sender 379 | .send(Ok(Err(error))) 380 | .map_err(|r| (msgid, r.expect("This was an OK(_)")))?; 381 | } 382 | } else { 383 | // Send message to handler_loop() 384 | sender.send(msg).await.unwrap(); 385 | } 386 | } 387 | } 388 | 389 | /// Register as a remote UI. 390 | /// 391 | /// After this method is called, the client will receive redraw notifications. 392 | pub async fn ui_attach( 393 | &self, 394 | width: i64, 395 | height: i64, 396 | opts: &UiAttachOptions, 397 | ) -> Result<(), Box> { 398 | self 399 | .call( 400 | "nvim_ui_attach", 401 | call_args!(width, height, opts.to_value_map()), 402 | ) 403 | .await? 404 | .map(|_| Ok(()))? 405 | } 406 | 407 | /// Send a quit command to Nvim. 408 | /// The quit command is 'qa!' which will make Nvim quit without 409 | /// saving anything. 410 | pub async fn quit_no_save(&self) -> Result<(), Box> { 411 | self.command("qa!").await 412 | } 413 | } 414 | 415 | /* The idea to use Vec here instead of HashMap 416 | * is that Vec is faster on small queue sizes 417 | * in most cases Vec.len = 1 so we just take first item in iteration. 418 | */ 419 | async fn find_sender( 420 | queue: &Queue, 421 | msgid: u64, 422 | ) -> Result, Box> { 423 | let mut queue = queue.lock().await; 424 | 425 | let pos = match queue.iter().position(|req| req.0 == msgid) { 426 | Some(p) => p, 427 | None => return Err(msgid.into()), 428 | }; 429 | Ok(queue.remove(pos).1) 430 | } 431 | 432 | #[cfg(all(test, feature = "use_tokio"))] 433 | mod tests { 434 | use super::*; 435 | 436 | #[tokio::test] 437 | async fn test_find_sender() { 438 | let queue = Arc::new(Mutex::new(Vec::new())); 439 | 440 | { 441 | let (sender, _receiver) = oneshot::channel(); 442 | queue.lock().await.push((1, sender)); 443 | } 444 | { 445 | let (sender, _receiver) = oneshot::channel(); 446 | queue.lock().await.push((2, sender)); 447 | } 448 | { 449 | let (sender, _receiver) = oneshot::channel(); 450 | queue.lock().await.push((3, sender)); 451 | } 452 | 453 | find_sender(&queue, 1).await.unwrap(); 454 | assert_eq!(2, queue.lock().await.len()); 455 | find_sender(&queue, 2).await.unwrap(); 456 | assert_eq!(1, queue.lock().await.len()); 457 | find_sender(&queue, 3).await.unwrap(); 458 | assert!(queue.lock().await.is_empty()); 459 | 460 | if let LoopError::MsgidNotFound(17) = 461 | *find_sender(&queue, 17).await.unwrap_err() 462 | { 463 | } else { 464 | panic!() 465 | } 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! # Errors of nvim-rs. 2 | //! 3 | //! Nvim-rs reports very detailed errors, to facilitate debugging by logging 4 | //! even in rare edge cases, and to enable clients to handle errors according to 5 | //! their needs. Errors are boxed to not overly burden the size of the 6 | //! `Result`s. 7 | //! 8 | //! ### Overview 9 | //! 10 | //! Errors can originate in three ways: 11 | //! 12 | //! 1. Failure of a request to neovim is communicated by a 13 | //! [`CallError`](crate::error::CallError). 14 | //! 2. A failure in the io loop is communicated by a 15 | //! [`LoopError`](crate::error::LoopError). 16 | //! 3. A failure to connect to neovim when starting up via one of the 17 | //! [`new_*`](crate::create) functions is communicated by an 18 | //! [`io::Error`](std::io::Error). 19 | //! 20 | //! Most errors should probably be treated as fatal, and the application should 21 | //! just exit. 22 | //! 23 | //! 24 | //! ### Special errors 25 | //! 26 | //! Use [`is_reader_error`](crate::error::LoopError::is_reader_error) 27 | //! to check if it might sense to try to show an error message to the neovim 28 | //! user (see [this example](crate::examples::scorched_earth)). 29 | //! 30 | //! Use 31 | //! [`CallError::is_channel_closed`](crate::error::CallError::is_channel_closed) 32 | //! or 33 | //! [`LoopError::is_channel_closed`](crate::error::LoopError::is_channel_closed) 34 | //! to determine if the error originates from a closed channel. This means 35 | //! either neovim closed the channel actively, or neovim was closed. Often, this 36 | //! is not seen as a real error, but the signal for the plugin to quit. Again, 37 | //! see the [example](crate::examples::scorched_earth). 38 | use std::{ 39 | error::Error, fmt, fmt::Display, io, io::ErrorKind, ops::RangeInclusive, 40 | sync::Arc, 41 | }; 42 | 43 | use futures::channel::oneshot; 44 | use rmpv::{ 45 | decode::Error as RmpvDecodeError, encode::Error as RmpvEncodeError, Value, 46 | }; 47 | 48 | /// A message from neovim had an invalid format 49 | /// 50 | /// This should be very basically non-existent, since it would indicate a bug in 51 | /// neovim. 52 | #[derive(Debug, PartialEq, Clone)] 53 | pub enum InvalidMessage { 54 | /// The value read was not an array 55 | NotAnArray(Value), 56 | /// WrongArrayLength(should, is) means that the array should have length in 57 | /// the range `should`, but has length `is` 58 | WrongArrayLength(RangeInclusive, u64), 59 | /// The first array element (=the message type) was not decodable into a u64 60 | InvalidType(Value), 61 | /// The first array element (=the message type) was decodable into a u64 62 | /// larger than 2 63 | UnknownMessageType(u64), 64 | /// The params of a request or notification weren't an array 65 | InvalidParams(Value, String), 66 | /// The method name of a notification was not decodable into a String 67 | InvalidNotificationName(Value), 68 | /// The method name of a request was not decodable into a String 69 | InvalidRequestName(u64, Value), 70 | /// The msgid of a request or response was not decodable into a u64 71 | InvalidMsgid(Value), 72 | } 73 | 74 | impl Error for InvalidMessage {} 75 | 76 | impl Display for InvalidMessage { 77 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 78 | use InvalidMessage::*; 79 | 80 | match self { 81 | NotAnArray(val) => write!(fmt, "Value not an Array: '{val}'"), 82 | WrongArrayLength(should, is) => write!( 83 | fmt, 84 | "Array should have length {:?}, has length {}", 85 | should, is 86 | ), 87 | InvalidType(val) => { 88 | write!(fmt, "Message type not decodable into u64: {val}") 89 | } 90 | UnknownMessageType(m) => { 91 | write!(fmt, "Message type {m} is not 0, 1 or 2") 92 | } 93 | InvalidParams(val, s) => { 94 | write!(fmt, "Params of method '{s}' not an Array: '{val}'") 95 | } 96 | InvalidNotificationName(val) => write!( 97 | fmt, 98 | "Notification name not a 99 | string: '{}'", 100 | val 101 | ), 102 | InvalidRequestName(id, val) => { 103 | write!(fmt, "Request id {id}: name not valid String: '{val}'") 104 | } 105 | InvalidMsgid(val) => { 106 | write!(fmt, "Msgid of message not decodable into u64: '{val}'") 107 | } 108 | } 109 | } 110 | } 111 | 112 | /// Receiving a message from neovim failed 113 | #[derive(Debug)] 114 | pub enum DecodeError { 115 | /// Reading from the internal buffer failed. 116 | BufferError(RmpvDecodeError), 117 | /// Reading from the stream failed. This is probably unrecoverable from, but 118 | /// might also mean that neovim closed the stream and wants the plugin to 119 | /// finish. See examples/quitting.rs on how this might be caught. 120 | ReaderError(io::Error), 121 | /// Neovim sent a message that's not valid. 122 | InvalidMessage(InvalidMessage), 123 | } 124 | 125 | impl Error for DecodeError { 126 | fn source(&self) -> Option<&(dyn Error + 'static)> { 127 | match *self { 128 | DecodeError::BufferError(ref e) => Some(e), 129 | DecodeError::InvalidMessage(ref e) => Some(e), 130 | DecodeError::ReaderError(ref e) => Some(e), 131 | } 132 | } 133 | } 134 | 135 | impl Display for DecodeError { 136 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 137 | let s = match *self { 138 | DecodeError::BufferError(_) => "Error while reading from buffer", 139 | DecodeError::InvalidMessage(_) => "Error while decoding", 140 | DecodeError::ReaderError(_) => "Error while reading from Reader", 141 | }; 142 | 143 | fmt.write_str(s) 144 | } 145 | } 146 | 147 | impl From for Box { 148 | fn from(err: RmpvDecodeError) -> Box { 149 | Box::new(DecodeError::BufferError(err)) 150 | } 151 | } 152 | 153 | impl From for Box { 154 | fn from(err: InvalidMessage) -> Box { 155 | Box::new(DecodeError::InvalidMessage(err)) 156 | } 157 | } 158 | 159 | impl From for Box { 160 | fn from(err: io::Error) -> Box { 161 | Box::new(DecodeError::ReaderError(err)) 162 | } 163 | } 164 | 165 | /// Sending a message to neovim failed 166 | #[derive(Debug)] 167 | pub enum EncodeError { 168 | /// Encoding the message into the internal buffer has failed. 169 | BufferError(RmpvEncodeError), 170 | /// Writing the encoded message to the stream failed. 171 | WriterError(io::Error), 172 | } 173 | 174 | impl Error for EncodeError { 175 | fn source(&self) -> Option<&(dyn Error + 'static)> { 176 | match *self { 177 | EncodeError::BufferError(ref e) => Some(e), 178 | EncodeError::WriterError(ref e) => Some(e), 179 | } 180 | } 181 | } 182 | 183 | impl Display for EncodeError { 184 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 185 | let s = match *self { 186 | Self::BufferError(_) => "Error writing to buffer", 187 | Self::WriterError(_) => "Error writing to the Writer", 188 | }; 189 | 190 | fmt.write_str(s) 191 | } 192 | } 193 | 194 | impl From for Box { 195 | fn from(err: RmpvEncodeError) -> Box { 196 | Box::new(EncodeError::BufferError(err)) 197 | } 198 | } 199 | 200 | impl From for Box { 201 | fn from(err: io::Error) -> Box { 202 | Box::new(EncodeError::WriterError(err)) 203 | } 204 | } 205 | 206 | /// A [`call`](crate::neovim::Neovim::call) to neovim failed 207 | /// 208 | /// The API functions return this, as they are just 209 | /// proxies for [`call`](crate::neovim::Neovim::call). 210 | #[derive(Debug)] 211 | pub enum CallError { 212 | /// Sending the request to neovim has failed. 213 | /// 214 | /// Fields: 215 | /// 216 | /// 0. The underlying error 217 | /// 1. The name of the called method 218 | SendError(EncodeError, String), 219 | /// The internal channel to send the response to the right task was closed. 220 | /// This really should not happen, unless someone manages to kill individual 221 | /// tasks. 222 | /// 223 | /// Fields: 224 | /// 225 | /// 0. The underlying error 226 | /// 1. The name of the called method 227 | InternalReceiveError(oneshot::Canceled, String), 228 | /// Decoding neovim's response failed. 229 | /// 230 | /// Fields: 231 | /// 232 | /// 0. The underlying error 233 | /// 1. The name of the called method 234 | /// 235 | /// *Note*: DecodeError can't be Clone, so we Arc-wrap it 236 | DecodeError(Arc, String), 237 | /// Neovim encountered an error while executing the reqest. 238 | /// 239 | /// Fields: 240 | /// 241 | /// 0. Neovim's error type (see `:h api`) 242 | /// 1. Neovim's error message 243 | NeovimError(Option, String), 244 | /// The response from neovim contained a [`Value`](rmpv::Value) of the wrong 245 | /// type 246 | WrongValueType(Value), 247 | } 248 | 249 | impl Error for CallError { 250 | fn source(&self) -> Option<&(dyn Error + 'static)> { 251 | match *self { 252 | CallError::SendError(ref e, _) => Some(e), 253 | CallError::InternalReceiveError(ref e, _) => Some(e), 254 | CallError::DecodeError(ref e, _) => Some(e.as_ref()), 255 | CallError::NeovimError(_, _) | CallError::WrongValueType(_) => None, 256 | } 257 | } 258 | } 259 | 260 | impl CallError { 261 | /// Determine if the error originated from a closed channel. This is generally 262 | /// used to close a plugin from neovim's side, and so most of the time should 263 | /// not be treated as a real error, but a signal to finish the program. 264 | #[must_use] 265 | pub fn is_channel_closed(&self) -> bool { 266 | match *self { 267 | CallError::SendError(EncodeError::WriterError(ref e), _) 268 | if e.kind() == ErrorKind::UnexpectedEof => 269 | { 270 | return true 271 | } 272 | CallError::DecodeError(ref err, _) => { 273 | if let DecodeError::ReaderError(ref e) = err.as_ref() { 274 | if e.kind() == ErrorKind::UnexpectedEof { 275 | return true; 276 | } 277 | } 278 | } 279 | _ => {} 280 | } 281 | 282 | false 283 | } 284 | } 285 | 286 | impl Display for CallError { 287 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 288 | match *self { 289 | Self::SendError(_, ref s) => write!(fmt, "Error sending request '{s}'"), 290 | Self::InternalReceiveError(_, ref s) => { 291 | write!(fmt, "Error receiving response for '{s}'") 292 | } 293 | Self::DecodeError(_, ref s) => { 294 | write!(fmt, "Error decoding response to request '{s}'") 295 | } 296 | Self::NeovimError(ref i, ref s) => match i { 297 | Some(i) => write!(fmt, "Error processing request: {i} - '{s}')"), 298 | None => write!( 299 | fmt, 300 | "Error processing request, unknown error format: '{s}'" 301 | ), 302 | }, 303 | CallError::WrongValueType(ref val) => { 304 | write!(fmt, "Wrong value type: '{val}'") 305 | } 306 | } 307 | } 308 | } 309 | 310 | impl From for Box { 311 | fn from(val: Value) -> Box { 312 | match val { 313 | Value::Array(mut arr) 314 | if arr.len() == 2 && arr[0].is_i64() && arr[1].is_str() => 315 | { 316 | let s = arr 317 | .pop() 318 | .expect("This was checked") 319 | .as_str() 320 | .expect("This was checked") 321 | .into(); 322 | let i = arr.pop().expect("This was checked").as_i64(); 323 | Box::new(CallError::NeovimError(i, s)) 324 | } 325 | val => Box::new(CallError::NeovimError(None, format!("{val:?}"))), 326 | } 327 | } 328 | } 329 | 330 | /// A failure in the io loop 331 | #[derive(Debug)] 332 | pub enum LoopError { 333 | /// A Msgid could not be found in the request queue 334 | MsgidNotFound(u64), 335 | /// Decoding a message failed. 336 | /// 337 | /// Fields: 338 | /// 339 | /// 0. The underlying error 340 | /// 1. The msgids of the requests we could not send the error to. 341 | /// 342 | /// Note: DecodeError can't be clone, so we Arc-wrap it. 343 | DecodeError(Arc, Option>), 344 | /// Failed to send a Response (from neovim) through the sender from the 345 | /// request queue 346 | /// 347 | /// Fields: 348 | /// 349 | /// 0. The msgid of the request the response was sent for 350 | /// 1. The response from neovim 351 | InternalSendResponseError(u64, Result), 352 | } 353 | 354 | impl Error for LoopError { 355 | fn source(&self) -> Option<&(dyn Error + 'static)> { 356 | match *self { 357 | LoopError::MsgidNotFound(_) 358 | | LoopError::InternalSendResponseError(_, _) => None, 359 | LoopError::DecodeError(ref e, _) => Some(e.as_ref()), 360 | } 361 | } 362 | } 363 | 364 | impl LoopError { 365 | #[must_use] 366 | pub fn is_channel_closed(&self) -> bool { 367 | if let LoopError::DecodeError(ref err, _) = *self { 368 | if let DecodeError::ReaderError(ref e) = err.as_ref() { 369 | if e.kind() == ErrorKind::UnexpectedEof { 370 | return true; 371 | } 372 | } 373 | } 374 | false 375 | } 376 | 377 | #[must_use] 378 | pub fn is_reader_error(&self) -> bool { 379 | if let LoopError::DecodeError(ref err, _) = *self { 380 | if let DecodeError::ReaderError(_) = err.as_ref() { 381 | return true; 382 | } 383 | } 384 | false 385 | } 386 | } 387 | 388 | impl Display for LoopError { 389 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 390 | match *self { 391 | Self::MsgidNotFound(i) => { 392 | write!(fmt, "Could not find Msgid '{i}' in the Queue") 393 | } 394 | Self::DecodeError(_, ref o) => match o { 395 | None => write!(fmt, "Error reading message"), 396 | Some(v) => write!( 397 | fmt, 398 | "Error reading message, could not forward \ 399 | error to the following requests: '{:?}'", 400 | v 401 | ), 402 | }, 403 | Self::InternalSendResponseError(i, ref res) => write!( 404 | fmt, 405 | "Request {i}: Could not send response, which was {:?}", 406 | res 407 | ), 408 | } 409 | } 410 | } 411 | 412 | impl From<(u64, Result)> for Box { 413 | fn from(res: (u64, Result)) -> Box { 414 | Box::new(LoopError::InternalSendResponseError(res.0, res.1)) 415 | } 416 | } 417 | 418 | impl From<(Arc, Vec)> for Box { 419 | fn from(v: (Arc, Vec)) -> Box { 420 | Box::new(LoopError::DecodeError(v.0, Some(v.1))) 421 | } 422 | } 423 | 424 | impl From for Box { 425 | fn from(i: u64) -> Box { 426 | Box::new(LoopError::MsgidNotFound(i)) 427 | } 428 | } 429 | 430 | #[derive(Debug)] 431 | pub enum HandshakeError { 432 | /// Sending the request to neovim has failed. 433 | /// 434 | /// Fields: 435 | /// 436 | /// 0. The underlying error 437 | SendError(EncodeError), 438 | /// Sending the request to neovim has failed. 439 | /// 440 | /// Fields: 441 | /// 442 | /// 0. The underlying error 443 | /// 1. The data read so far 444 | RecvError(io::Error, String), 445 | /// Unexpected response received 446 | /// 447 | /// Fields: 448 | /// 449 | /// 0. The data read so far 450 | UnexpectedResponse(String), 451 | /// The launch of Neovim failed 452 | /// 453 | /// Fields: 454 | /// 455 | /// 0. The underlying error 456 | LaunchError(io::Error), 457 | } 458 | 459 | impl From> for Box { 460 | fn from(v: Box) -> Box { 461 | Box::new(HandshakeError::SendError(*v)) 462 | } 463 | } 464 | 465 | impl From<(io::Error, String)> for Box { 466 | fn from(v: (io::Error, String)) -> Box { 467 | Box::new(HandshakeError::RecvError(v.0, v.1)) 468 | } 469 | } 470 | 471 | impl From for Box { 472 | fn from(v: io::Error) -> Box { 473 | Box::new(HandshakeError::LaunchError(v)) 474 | } 475 | } 476 | 477 | impl Error for HandshakeError { 478 | fn source(&self) -> Option<&(dyn Error + 'static)> { 479 | match *self { 480 | Self::SendError(ref s) => Some(s), 481 | Self::RecvError(ref s, _) => Some(s), 482 | Self::LaunchError(ref s) => Some(s), 483 | Self::UnexpectedResponse(_) => None, 484 | } 485 | } 486 | } 487 | 488 | impl Display for HandshakeError { 489 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 490 | match *self { 491 | Self::SendError(ref s) => write!(fmt, "Error sending handshake '{s}'"), 492 | Self::RecvError(ref s, ref output) => { 493 | write!( 494 | fmt, 495 | "Error receiving handshake response '{s}'\n\ 496 | Unexpected output:\n{output}" 497 | ) 498 | } 499 | Self::LaunchError(ref s) => write!(fmt, "Error launching nvim '{s}'"), 500 | Self::UnexpectedResponse(ref output) => write!( 501 | fmt, 502 | "Error receiving handshake response, unexpected output:\n{output}" 503 | ), 504 | } 505 | } 506 | } 507 | --------------------------------------------------------------------------------