use anyhow::Result; use helix_core::Position; use helix_view::tree::Layout; use std::path::{Path, PathBuf}; #[derive(Default)] pub struct Args { pub display_help: bool, pub display_version: bool, pub health: bool, pub health_arg: Option, pub load_tutor: bool, pub fetch_grammars: bool, pub build_grammars: bool, pub split: Option, pub verbosity: u64, pub log_file: Option, pub config_file: Option, pub files: Vec<(PathBuf, Position)>, pub open_cwd: bool, pub working_directory: Option, } impl Args { pub fn parse_args() -> Result { let mut args = Args::default(); let mut argv = std::env::args().peekable(); let mut line_number = 0; argv.next(); // skip the program, we don't care about that while let Some(arg) = argv.next() { match arg.as_str() { "--" => break, // stop parsing at this point treat the remaining as files "--version" => args.display_version = true, "--help" => args.display_help = true, "--tutor" => args.load_tutor = true, "--vsplit" => match args.split { Some(_) => anyhow::bail!("can only set a split once of a specific type"), None => args.split = Some(Layout::Vertical), }, "--hsplit" => match args.split { Some(_) => anyhow::bail!("can only set a split once of a specific type"), None => args.split = Some(Layout::Horizontal), }, "--health" => { args.health = true; args.health_arg = argv.next_if(|opt| !opt.starts_with('-')); } "-g" | "--grammar" => match argv.next().as_deref() { Some("fetch") => args.fetch_grammars = true, Some("build") => args.build_grammars = true, _ => { anyhow::bail!("--grammar must be followed by either 'fetch' or 'build'") } }, "-c" | "--config" => match argv.next().as_deref() { Some(path) => args.config_file = Some(path.into()), None => anyhow::bail!("--config must specify a path to read"), }, "--log" => match argv.next().as_deref() { Some(path) => args.log_file = Some(path.into()), None => anyhow::bail!("--log must specify a path to write"), }, "-w" | "--working-dir" => match argv.next().as_deref() { Some(path) => { args.working_directory = if Path::new(path).is_dir() { Some(PathBuf::from(path)) } else { anyhow::bail!( "--working-dir specified does not exist or is not a directory" ) } } None => { anyhow::bail!("--working-dir must specify an initial working directory") } }, arg if arg.starts_with("--") => { anyhow::bail!("unexpected double dash argument: {}", arg) } arg if arg.starts_with('-') => { let arg = arg.get(1..).unwrap().chars(); for chr in arg { match chr { 'v' => args.verbosity += 1, 'V' => args.display_version = true, 'h' => args.display_help = true, _ => anyhow::bail!("unexpected short arg {}", chr), } } } arg if arg.starts_with('+') => { let arg = &arg[1..]; line_number = match arg.parse::() { Ok(n) => n.saturating_sub(1), _ => anyhow::bail!("bad line number after +"), }; } arg => args.files.push(parse_file(arg)), } } // push the remaining args, if any to the files for arg in argv { args.files.push(parse_file(&arg)); } if let Some(file) = args.files.first_mut() { if line_number != 0 { file.1.row = line_number; } } Ok(args) } } /// Parse arg into [`PathBuf`] and position. pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) { let def = || (PathBuf::from(s), Position::default()); if Path::new(s).exists() { return def(); } split_path_row_col(s) .or_else(|| split_path_row(s)) .unwrap_or_else(def) } /// Split file.rs:10:2 into [`PathBuf`], row and col. /// /// Does not validate if file.rs is a file or directory. fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> { let mut s = s.rsplitn(3, ':'); let col: usize = s.next()?.parse().ok()?; let row: usize = s.next()?.parse().ok()?; let path = s.next()?.into(); let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); Some((path, pos)) } /// Split file.rs:10 into [`PathBuf`] and row. /// /// Does not validate if file.rs is a file or directory. fn split_path_row(s: &str) -> Option<(PathBuf, Position)> { let (path, row) = s.rsplit_once(':')?; let row: usize = row.parse().ok()?; let path = path.into(); let pos = Position::new(row.saturating_sub(1), 0); Some((path, pos)) }