1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
use crate::search;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::{fmt, io, path::Path, process::Command};
/// Represents a vim variable to be extracted
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VimVar<Name: AsRef<str>> {
cmd: Cmd,
scope: Scope,
name: Name,
}
impl<Name: AsRef<str>> VimVar<Name> {
/// Creates a new vim variable definition that can be used later to
/// load the variable's contents
pub fn new(cmd: Cmd, scope: Scope, name: Name) -> Self {
Self { cmd, scope, name }
}
/// Returns [`Cmd`] tied to variable
pub fn cmd(&self) -> Cmd {
self.cmd
}
/// Returns [`Scope`] tied to variable
pub fn scope(&self) -> Scope {
self.scope
}
/// Returns name tied to variable
pub fn name(&self) -> &str {
self.name.as_ref()
}
}
impl<Name: AsRef<str>> VimVar<Name> {
/// Loads variable with [`Self::load`] and then attempts to convert it
/// to the specified type
///
/// ### Notes
///
/// * If `allow_zero` is true, then a value of 0 is considered the value of
/// the variable rather than vim's default of not being found
pub fn load_typed<T>(&self, allow_zero: bool) -> io::Result<Option<T>>
where
T: DeserializeOwned,
{
self.load(allow_zero)?
.map(|value| serde_json::from_value(value).map_err(Into::into))
.transpose()
}
/// Loads the variable's value using neovim's headless mode or vim's ex
/// mode using the default vimrc available in scope
///
/// ### Notes
///
/// * Will leverage [`search::find_vimrc`] to load in the appropriate vimrc
/// during ex mode
/// * If `allow_zero` is true, then a value of 0 is considered the value of
/// the variable rather than vim's default of not being found
pub fn load(&self, allow_zero: bool) -> io::Result<Option<Value>> {
let vimrc = search::find_vimrc()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "vimrc not found"))?;
self.load_with_config(vimrc, allow_zero)
}
/// Loads variable with [`Self::load_with_config`] and then attempts to
/// convert it to the specified type
///
/// ### Notes
///
/// * If `allow_zero` is true, then a value of 0 is considered the value of
/// the variable rather than vim's default of not being found
pub fn load_typed_with_config<P: AsRef<Path>, T>(
&self,
config: P,
allow_zero: bool,
) -> io::Result<Option<T>>
where
T: DeserializeOwned,
{
self.load_with_config(config, allow_zero)?
.map(|value| serde_json::from_value(value).map_err(Into::into))
.transpose()
}
/// Loads the variable's value using neovim's headless mode or vim's ex
/// mode
///
/// ### Notes
///
/// * Spawns a vim process whose goal is to print out the contents of a
/// variable as a JSON string
/// * Leverages batch & ex modes with redir to execute and capture output
/// * Relies on the variable being available upon loading vim configs
/// * If `allow_zero` is true, then a value of 0 is considered the value of
/// the variable rather than vim's default of not being found
pub fn load_with_config<P: AsRef<Path>>(
&self,
config: P,
allow_zero: bool,
) -> io::Result<Option<Value>> {
let cmd = self.cmd;
let scope = self.scope.as_str();
let var = self.name.as_ref();
let full_cmd = {
if config.as_ref().as_os_str().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"path to vimrc is required for neovim/vim",
));
}
// NOTE: We have a lot of settings being applied, so documenting
// them here
//
// 1. -Es is our silent, batch, ex mode
// 2. -i NONE removes shada/viminfo file reading and writing
// 3. -u "{}" loads our vimrc, which is required as -Es does
// not load vim scripts by default
// 4. +set nonumber is used to turn off line numbers, which
// are getting picked up by neovim/vim in vimrc configs
// and showing up in output
// 5. redir writes to a register our message (json) and then
// places it in our buffer
// 6. prints out the content in our buffer (current line)
format!(
r#"{} -Es -i NONE -u "{}" '+set nonumber' '+redir => m | echon json_encode(get({}, "{}")) | redir END | put=m' '+%p' '+qa!'"#,
cmd,
config.as_ref().to_string_lossy(),
scope,
var,
)
};
// TODO: Support windows here (won't have sh)
let output = Command::new("sh").arg("-c").arg(full_cmd).output()?;
// If our program failed, we want to report the failure
//
// NOTE: neovim/vim seems to return exit code 1; so, for now we'll
// ignore that specific exit code for now
if !output.status.success() && (output.status.code() != Some(1)) {
let code = output
.status
.code()
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| String::from("--"));
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"[Exit code {}]: {}",
code,
String::from_utf8_lossy(&output.stderr).trim()
)
.as_str(),
));
}
let output_string = String::from_utf8_lossy(&output.stdout);
// Report a better error than the serde one if the output was empty
if output_string.trim().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Result from {} was empty", self.cmd),
));
}
let value: Value = serde_json::from_str(output_string.trim()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse as JSON: \"{}\"", output_string.trim()),
)
})?;
if !allow_zero && value == serde_json::json!(0) {
Ok(None)
} else {
Ok(Some(value))
}
}
}
/// Represents type of vim instance being used
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Cmd {
Neovim,
Vim,
}
impl Cmd {
/// Converts to a str representing command
///
/// ### Examples
///
/// ```
/// use vimvar::Cmd;
///
/// assert_eq!(Cmd::Neovim.as_str(), "nvim");
/// assert_eq!(Cmd::Vim.as_str(), "vim");
/// ```
pub fn as_str(&self) -> &'static str {
match self {
Self::Vim => "vim",
Self::Neovim => "nvim",
}
}
}
impl fmt::Display for Cmd {
/// Writes cmd using the [`Self::as_str`] representation
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// Represents a vim variable scope
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Scope {
Nothing,
Buffer,
Window,
Tabpage,
Global,
Local,
Script,
FunctionArg,
Vim,
}
impl Default for Scope {
/// Returns global as default
fn default() -> Self {
Self::Global
}
}
impl Scope {
/// Converts to a str representing scope
///
/// ### Examples
///
/// ```
/// use vimvar::Scope;
///
/// assert_eq!(Scope::Nothing.as_str(), "");
/// assert_eq!(Scope::Buffer.as_str(), "b:");
/// assert_eq!(Scope::Window.as_str(), "w:");
/// assert_eq!(Scope::Tabpage.as_str(), "t:");
/// assert_eq!(Scope::Global.as_str(), "g:");
/// assert_eq!(Scope::Local.as_str(), "l:");
/// assert_eq!(Scope::Script.as_str(), "s:");
/// assert_eq!(Scope::FunctionArg.as_str(), "a:");
/// assert_eq!(Scope::Vim.as_str(), "v:");
/// ```
pub fn as_str(&self) -> &'static str {
match self {
Self::Nothing => "",
Self::Buffer => "b:",
Self::Window => "w:",
Self::Tabpage => "t:",
Self::Global => "g:",
Self::Local => "l:",
Self::Script => "s:",
Self::FunctionArg => "a:",
Self::Vim => "v:",
}
}
}
impl fmt::Display for Scope {
/// Writes scope using the [`Self::as_str`] representation
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}