├── .travis.yml ├── .gitignore ├── example ├── Cargo.toml └── src │ └── main.rs ├── Cargo.toml ├── README.md ├── LICENSE └── src ├── lib.rs ├── metadata.rs └── session.rs /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | script: 4 | - cargo build --verbose 5 | - cargo test --verbose 6 | 7 | # TODO: add support for building documentation 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | Cargo.lock 13 | 14 | # From Vim. 15 | **/.*.swp 16 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.0.1" 4 | authors = ["Damien Radtke "] 5 | 6 | [dependencies] 7 | mpack = "0.1.2" 8 | 9 | [dependencies.neovim] 10 | path = ".." 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "neovim" 3 | version = "0.1.0" 4 | authors = ["Damien Radtke "] 5 | 6 | description = "Support for writing Neovim plugins in Rust." 7 | repository = "https://github.com/dradtke/neovim-rs.git" 8 | # TODO: verify this path and publish documentation 9 | # documentation = "http://www.rust-ci.org/dradtke/neovim/doc/neovim/" 10 | keywords = ["neovim", "plugin"] 11 | license = "MIT" 12 | 13 | [dependencies] 14 | mpack = "0.1.2" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neovim 2 | Support for writing Neovim plugins in Rust. 3 | 4 | This crate doesn't actually do anything yet except open up a session that can communicate over TCP, Unix socket, stdio, or a child process, but in the near future it will provide full support for communicating with Neovim. 5 | 6 | There's a lot that needs to stabilize before this will become possible, including Neovim's plugin API, Rust itself, and the `msgpack` crate which will be used for serialization. 7 | 8 | Development will attempt to mimick that of the [Python client](https://github.com/neovim/python-client). 9 | -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Example program that spawns a child Neovim instance and uses the Msgpack-RPC 2 | //! protocol directly to request the current version number. 3 | 4 | extern crate mpack; 5 | extern crate neovim; 6 | 7 | fn main() { 8 | // Open up a Neovim session by spawning a new instance of it. 9 | let mut s = neovim::Session::new_child(&[]).unwrap(); 10 | 11 | let vim_version = s.call_sync(String::from("vim_get_vvar"), vec![mpack::Value::String(String::from("version"))]).unwrap(); 12 | match vim_version { 13 | Ok(x) => println!("Neovim Version: {}", x.uint().unwrap()), 14 | Err(err) => println!("Unexpected error: {:?}", err), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Damien Radtke 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 | 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_variables)] 2 | //! `neovim` is a crate that enables building Neovim plugins with Rust. 3 | //! 4 | //! Neovim is a fork of Vim that enables fully asynchronous communication 5 | //! with plugins that run as external programs, communicating via Msgpack-RPC. 6 | //! This crate provides facilities for connecting to a Neovim instance 7 | //! and making API calls to take actions within the editor. 8 | 9 | extern crate mpack; 10 | 11 | use std::env; 12 | use std::fmt; 13 | use std::io; 14 | use std::process::Command; 15 | 16 | pub use self::metadata::Metadata; 17 | pub use self::session::Session; 18 | 19 | mod metadata; 20 | mod session; 21 | 22 | /// A function as parsed from `get_api_info()`. 23 | pub struct Function { 24 | pub name: String, 25 | pub parameters: Vec<(String, String)>, 26 | pub return_type: String, 27 | pub async: bool, 28 | pub can_fail: bool, 29 | } 30 | 31 | impl fmt::Display for Function { 32 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 33 | write!(fmt, "{name}({params}) -> {return_type} {can_fail}{async}", 34 | return_type=self.return_type, 35 | async=if self.async { "async" } else { "" }, 36 | name=self.name, 37 | params=self.parameters.iter().map(|p| format!("{} {}", p.0, p.1)).collect::>().join(", "), 38 | can_fail=if self.can_fail { "[can fail]" } else { "" }, 39 | ) 40 | } 41 | } 42 | 43 | /// The result of `get_api_info()`. 44 | pub struct ApiInfo { 45 | pub functions: Vec, 46 | } 47 | 48 | /// Get API information from Vim by running `nvim --api-info` and parsing the output. 49 | pub fn get_api_info() -> Result { 50 | let cmd = env::var("NVIM_BIN").unwrap_or(String::from("nvim")); 51 | let output = try!(Command::new(cmd).arg("--api-info").output()); 52 | if !output.status.success() { 53 | return Err(mpack::ReadError::Io(match output.status.code() { 54 | Some(code) => io::Error::from_raw_os_error(code), 55 | None => io::Error::new(io::ErrorKind::Other, "killed by signal"), 56 | })) 57 | } 58 | 59 | let mut r = mpack::Reader::new(&output.stdout[..]); 60 | let dict = try!(r.read_value()).map().unwrap(); 61 | let dict_functions = dict.get_array("functions").unwrap(); 62 | 63 | let mut functions = Vec::with_capacity(dict_functions.len()); 64 | 65 | for f in dict_functions { 66 | let f = f.map().unwrap(); 67 | let name = f.get_string("name").unwrap(); 68 | let parameters = f.get_array("parameters").unwrap().into_iter().map(|p| { 69 | let p = p.array().unwrap(); 70 | (p[0].clone().string().unwrap(), p[1].clone().string().unwrap()) 71 | }).collect(); 72 | let return_type = f.get_string("return_type").unwrap(); 73 | let async = f.get_bool("async").unwrap(); 74 | let can_fail = f.get_bool("can_fail").unwrap_or(false); 75 | functions.push(Function{ 76 | name: name, 77 | parameters: parameters, 78 | return_type: return_type, 79 | async: async, 80 | can_fail: can_fail, 81 | }); 82 | } 83 | 84 | Ok(ApiInfo{ 85 | functions: functions, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/metadata.rs: -------------------------------------------------------------------------------- 1 | use mpack::{self, Value, ValueMap}; 2 | use std::convert; 3 | use std::error; 4 | use std::fmt; 5 | 6 | /// Type identifiers for certain types, determined at runtime. 7 | pub struct Metadata { 8 | pub buffer_id: i64, 9 | pub window_id: i64, 10 | pub tabpage_id: i64, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub enum GetMetadataError { 15 | /// Attempted to retrieve metadata from a non-map value. 16 | NotAMap, 17 | /// The map contains no `types` field. 18 | NoTypeInformation, 19 | /// A requested `id` value could not be found. 20 | Missing(String), 21 | /// A requested `id` value was found, but couldn't be parsed as an int. 22 | Invalid(String), 23 | /// Generic read error. 24 | ReadError(mpack::ReadError), 25 | } 26 | 27 | impl fmt::Display for GetMetadataError { 28 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 29 | write!(fmt, "{}", self) 30 | } 31 | } 32 | 33 | impl error::Error for GetMetadataError { 34 | fn description(&self) -> &str { 35 | match *self { 36 | GetMetadataError::NotAMap => "not a map", 37 | GetMetadataError::NoTypeInformation => "no type information", 38 | GetMetadataError::Invalid(_) => "invalid id", 39 | GetMetadataError::Missing(_) => "missing id", 40 | GetMetadataError::ReadError(_) => "read error", 41 | } 42 | } 43 | 44 | fn cause(&self) -> Option<&error::Error> { 45 | match *self { 46 | GetMetadataError::ReadError(ref e) => Some(e as &error::Error), 47 | _ => None, 48 | } 49 | } 50 | } 51 | 52 | impl convert::From for GetMetadataError { 53 | fn from(err: mpack::ReadError) -> GetMetadataError { 54 | GetMetadataError::ReadError(err) 55 | } 56 | } 57 | 58 | impl Metadata { 59 | /// Attempt to read metadata information from the provided value. 60 | /// 61 | /// This method expects the value to represent this type of data structure: 62 | /// 63 | /// ```json 64 | /// { 65 | /// "types": { 66 | /// "Buffer": { "id": }, 67 | /// "Window": { "id": }, 68 | /// "Tabpage": { "id": } 69 | /// } 70 | /// } 71 | /// ``` 72 | /// 73 | /// It then pulls out the id values and stores them in the returned `Metadata` struct 74 | /// so that buffer, window, and tabpage values received from Neovim can be parsed 75 | /// appropriately. 76 | pub fn new(metadata: Value) -> Result { 77 | let metadata = match metadata.map() { 78 | Ok(m) => m, 79 | Err(_) => return Err(GetMetadataError::NotAMap), 80 | }; 81 | 82 | let types = match metadata.get("types") { 83 | Some(t) => t.clone().map().unwrap(), 84 | None => return Err(GetMetadataError::NoTypeInformation), 85 | }; 86 | 87 | fn get_id(types: &ValueMap, name: &'static str) -> Result { 88 | let ob = match types.get(name) { 89 | Some(v) => match v.clone().map() { 90 | Ok(ob) => ob, 91 | Err(_) => return Err(GetMetadataError::Missing(format!("{}.id", name))), 92 | }, 93 | None => return Err(GetMetadataError::Missing(format!("{}.id", name))), 94 | }; 95 | 96 | match ob.get("id") { 97 | Some(id) => Ok(id.clone().int().unwrap()), 98 | None => return Err(GetMetadataError::Invalid(format!("{}.id", name))), 99 | } 100 | } 101 | 102 | Ok(Metadata { 103 | buffer_id: try!(get_id(&types, "Buffer")), 104 | window_id: try!(get_id(&types, "Window")), 105 | tabpage_id: try!(get_id(&types, "Tabpage")), 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | use mpack::{Value, WriteError}; 2 | use mpack::rpc::{Client, RpcResult}; 3 | 4 | use std::env; 5 | use std::error::Error; 6 | use std::io::{self, Read, Stdin, Stdout, Write}; 7 | use std::net::{TcpStream, ToSocketAddrs, SocketAddr}; 8 | use std::process::{Command, Child, ChildStdin, ChildStdout, Stdio}; 9 | use std::sync::mpsc::Receiver; 10 | use super::metadata::Metadata; 11 | 12 | /// An active Neovim session. 13 | pub struct Session { 14 | pub metadata: Metadata, 15 | conn: ClientConn, 16 | } 17 | 18 | impl Session { 19 | /// Connect to a Neovim instance over TCP. 20 | pub fn new_tcp(addr: A) -> io::Result { 21 | let reader = try!(TcpStream::connect(&addr)); 22 | let writer = try!(reader.try_clone()); 23 | let addr = reader.peer_addr().unwrap(); 24 | let mut client = Client::new(reader, writer); 25 | Ok(Session{ 26 | metadata: try!(Session::get_vim_api_info(&mut client)), 27 | conn: ClientConn::Tcp(client, addr), 28 | }) 29 | } 30 | 31 | /// Connect to a Neovim instance using this process' standard input 32 | /// and output. Useful if Neovim started this process. 33 | pub fn new_stdio() -> Session { 34 | let mut client = Client::new(io::stdin(), io::stdout()); 35 | Session{ 36 | metadata: Session::get_vim_api_info(&mut client).unwrap(), 37 | conn: ClientConn::Stdio(client), 38 | } 39 | } 40 | 41 | /// Connect to a Neovim instance by spawning a new one. Automatically passes `--embed` 42 | /// as a command-line parameter. 43 | /// 44 | /// Uses `nvim` as the default command for launching Neovim, but this can be overridden 45 | /// with the `NVIM_BIN` environment variable. 46 | pub fn new_child(args: &[String]) -> io::Result { 47 | let cmd = env::var("NVIM_BIN").unwrap_or(String::from("nvim")); 48 | let mut child = try!(Command::new(cmd).args(args).arg("--embed").stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()); 49 | let mut client = Client::new(child.stdout.take().unwrap(), child.stdin.take().unwrap()); 50 | Ok(Session{ 51 | metadata: try!(Session::get_vim_api_info(&mut client)), 52 | conn: ClientConn::Child(client, child), 53 | }) 54 | } 55 | 56 | /// Connect to a Neovim instance over a Unix socket. Currently unimplemented. 57 | pub fn new_socket() { 58 | unimplemented!() 59 | } 60 | 61 | /// Call a method over RPC. 62 | pub fn call(&mut self, method: String, params: Vec) -> Result, WriteError> { 63 | match self.conn { 64 | ClientConn::Tcp(ref mut client, _) => client.call(method, params), 65 | ClientConn::Stdio(ref mut client) => client.call(method, params), 66 | ClientConn::Child(ref mut client, _) => client.call(method, params), 67 | } 68 | } 69 | 70 | /// Call a method over RPC, synchronously. 71 | pub fn call_sync(&mut self, method: String, params: Vec) -> Result { 72 | match self.conn { 73 | ClientConn::Tcp(ref mut client, _) => client.call_sync(method, params), 74 | ClientConn::Stdio(ref mut client) => client.call_sync(method, params), 75 | ClientConn::Child(ref mut client, _) => client.call_sync(method, params), 76 | } 77 | } 78 | 79 | /// Returns a reference to the TCP socket address used by this session. 80 | /// 81 | /// If the connection isn't over TCP, this method returns None. 82 | pub fn socket_addr(&self) -> Option<&SocketAddr> { 83 | match self.conn { 84 | ClientConn::Tcp(_, ref addr) => Some(addr), 85 | ClientConn::Stdio(..) | ClientConn::Child(..) => None, 86 | } 87 | } 88 | 89 | fn get_vim_api_info(client: &mut Client) -> io::Result { 90 | let api_info = match client.call_sync(String::from("vim_get_api_info"), vec![]) { 91 | Ok(result) => match result { 92 | Ok(api_info) => api_info, 93 | Err(e) => return Err(io::Error::new(io::ErrorKind::Other, "call to vim_get_api_info failed")), 94 | }, 95 | Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e.description())), 96 | }; 97 | Ok(Metadata::new(api_info.array().unwrap().get(1).unwrap().clone()).unwrap()) 98 | } 99 | } 100 | 101 | enum ClientConn { 102 | Tcp(Client, SocketAddr), 103 | Stdio(Client), 104 | Child(Client, Child), 105 | } 106 | --------------------------------------------------------------------------------