diff --git a/docs/config.md b/docs/config.md index b20374bd9..d68643594 100644 --- a/docs/config.md +++ b/docs/config.md @@ -85,7 +85,8 @@ For GUI editors you possibly need to use a `-w` or `--wait`. Some examples: ui.editor = "bbedit -w" # BBEdit ui.editor = "subl -n -w" # Sublime Text ui.editor = "mate -w" # TextMate - ui.editor = "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin" # Notepad++ + ui.editor = ["C:/Program Files/Notepad++/notepad++.exe", + "-multiInst", "-notabbar", "-nosession", "-noPlugin"] # Notepad++ ui.editor = "idea --temp-project --wait" #IntelliJ Obviously, you would only set one line, don't copy them all in! diff --git a/src/config.rs b/src/config.rs index 60005b661..894d49156 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; use std::path::PathBuf; use std::process::Command; use std::{env, fmt}; @@ -135,17 +136,18 @@ pub fn read_config() -> Result { #[serde(untagged)] pub enum FullCommandArgs { String(String), - // TODO: Vec + Vec(NonEmptyCommandArgsVec), } impl FullCommandArgs { /// Returns arguments including the command name. /// /// The list is not empty, but each element may be an empty string. - pub fn args(&self) -> Vec { + pub fn args(&self) -> Cow<[String]> { match self { // Handle things like `EDITOR=emacs -nw` (TODO: parse shell escapes) FullCommandArgs::String(s) => s.split(' ').map(|s| s.to_owned()).collect(), + FullCommandArgs::Vec(a) => Cow::Borrowed(&a.0), } } @@ -168,6 +170,65 @@ impl fmt::Display for FullCommandArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FullCommandArgs::String(s) => write!(f, "{s}"), + // TODO: format with shell escapes + FullCommandArgs::Vec(a) => write!(f, "{}", a.0.join(" ")), } } } + +/// Wrapper to reject an array without command name. +// Based on https://github.com/serde-rs/serde/issues/939 +#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)] +#[serde(try_from = "Vec")] +pub struct NonEmptyCommandArgsVec(Vec); + +impl TryFrom> for NonEmptyCommandArgsVec { + type Error = &'static str; + + fn try_from(args: Vec) -> Result { + if args.is_empty() { + Err("command arguments should not be empty") + } else { + Ok(NonEmptyCommandArgsVec(args)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_command_args() { + let config = config::Config::builder() + .set_override("empty_array", Vec::::new()) + .unwrap() + .set_override("empty_string", "") + .unwrap() + .set_override("array", vec!["emacs", "-nw"]) + .unwrap() + .set_override("string", "emacs -nw") + .unwrap() + .build() + .unwrap(); + + assert!(config.get::("empty_array").is_err()); + + let args: FullCommandArgs = config.get("empty_string").unwrap(); + assert_eq!(args, FullCommandArgs::String("".to_owned())); + assert_eq!(args.args(), [""].as_ref()); + + let args: FullCommandArgs = config.get("array").unwrap(); + assert_eq!( + args, + FullCommandArgs::Vec(NonEmptyCommandArgsVec( + ["emacs", "-nw",].map(|s| s.to_owned()).to_vec() + )) + ); + assert_eq!(args.args(), ["emacs", "-nw"].as_ref()); + + let args: FullCommandArgs = config.get("string").unwrap(); + assert_eq!(args, FullCommandArgs::String("emacs -nw".to_owned())); + assert_eq!(args.args(), ["emacs", "-nw"].as_ref()); + } +}