use crate::cmd_ctags::CmdCtags;
use crate::cmd_git::CmdGit;
use dirs;
use failure::{Error, ResultExt};
use serde_derive::{Deserialize, Serialize};
use std::fs;
use std::io::BufRead;
use std::io::{stdout, BufWriter, Read, Write};
use std::path::PathBuf;
use std::process::Output;
use std::str;
use structopt::{clap, StructOpt};
use structopt_toml::StructOptToml;
use time::{Duration, PreciseTime};
use toml;
#[derive(Debug, Deserialize, Serialize, StructOpt, StructOptToml)]
#[serde(default)]
#[structopt(name = "ptags")]
#[structopt(raw(
long_version = "option_env!(\"LONG_VERSION\").unwrap_or(env!(\"CARGO_PKG_VERSION\"))"
))]
#[structopt(raw(setting = "clap::AppSettings::AllowLeadingHyphen"))]
#[structopt(raw(setting = "clap::AppSettings::ColoredHelp"))]
pub struct Opt {
#[structopt(short = "t", long = "thread", default_value = "8")]
pub thread: usize,
#[structopt(short = "f", long = "file", default_value = "tags", parse(from_os_str))]
pub output: PathBuf,
#[structopt(name = "DIR", default_value = ".", parse(from_os_str))]
pub dir: PathBuf,
#[structopt(short = "s", long = "stat")]
pub stat: bool,
#[structopt(short = "L", long = "list")]
pub list: Option<String>,
#[structopt(long = "bin-ctags", default_value = "ctags", parse(from_os_str))]
pub bin_ctags: PathBuf,
#[structopt(long = "bin-git", default_value = "git", parse(from_os_str))]
pub bin_git: PathBuf,
#[structopt(short = "c", long = "opt-ctags", raw(number_of_values = "1"))]
pub opt_ctags: Vec<String>,
#[structopt(short = "g", long = "opt-git", raw(number_of_values = "1"))]
pub opt_git: Vec<String>,
#[structopt(long = "opt-git-lfs", raw(number_of_values = "1"))]
pub opt_git_lfs: Vec<String>,
#[structopt(short = "v", long = "verbose")]
pub verbose: bool,
#[structopt(long = "exclude-lfs")]
pub exclude_lfs: bool,
#[structopt(long = "include-untracked")]
pub include_untracked: bool,
#[structopt(long = "include-ignored")]
pub include_ignored: bool,
#[structopt(long = "include-submodule")]
pub include_submodule: bool,
#[structopt(long = "validate-utf8")]
pub validate_utf8: bool,
#[structopt(long = "unsorted")]
pub unsorted: bool,
#[structopt(short = "e", long = "exclude", raw(number_of_values = "1"))]
pub exclude: Vec<String>,
#[structopt(
long = "completion",
raw(possible_values = "&[\"bash\", \"fish\", \"zsh\", \"powershell\"]")
)]
pub completion: Option<String>,
#[structopt(long = "config")]
pub config: bool,
}
macro_rules! watch_time (
( $func:block ) => (
{
let beg = PreciseTime::now();
$func;
beg.to(PreciseTime::now())
}
);
);
pub fn git_files(opt: &Opt) -> Result<Vec<String>, Error> {
let list = CmdGit::get_files(&opt)?;
let mut files = vec![String::from(""); opt.thread];
for (i, f) in list.iter().enumerate() {
files[i % opt.thread].push_str(f);
files[i % opt.thread].push_str("\n");
}
Ok(files)
}
pub fn input_files(file: &String, opt: &Opt) -> Result<Vec<String>, Error> {
let mut list = Vec::new();
if file == &String::from("-") {
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
list.push(String::from(line?));
}
} else {
for line in fs::read_to_string(file)?.lines() {
list.push(String::from(line));
}
}
let mut files = vec![String::from(""); opt.thread];
for (i, f) in list.iter().enumerate() {
files[i % opt.thread].push_str(f);
files[i % opt.thread].push_str("\n");
}
Ok(files)
}
fn call_ctags(opt: &Opt, files: &[String]) -> Result<Vec<Output>, Error> {
Ok(CmdCtags::call(&opt, &files)?)
}
fn get_tags_header(opt: &Opt) -> Result<String, Error> {
Ok(CmdCtags::get_tags_header(&opt).context("failed to get ctags header")?)
}
fn write_tags(opt: &Opt, outputs: &[Output]) -> Result<(), Error> {
let mut iters = Vec::new();
let mut lines = Vec::new();
for o in outputs {
let mut iter = if opt.validate_utf8 {
str::from_utf8(&o.stdout)?.lines()
} else {
unsafe { str::from_utf8_unchecked(&o.stdout).lines() }
};
lines.push(iter.next());
iters.push(iter);
}
let mut f = if opt.output.to_str().unwrap_or("") == "-" {
BufWriter::new(Box::new(stdout()) as Box<dyn Write>)
} else {
let f = fs::File::create(&opt.output)?;
BufWriter::new(Box::new(f) as Box<dyn Write>)
};
f.write(get_tags_header(&opt)?.as_bytes())?;
while lines.iter().any(|x| x.is_some()) {
let mut min = 0;
for i in 1..lines.len() {
if opt.unsorted {
if !lines[i].is_none() && lines[min].is_none() {
min = i;
}
} else {
if !lines[i].is_none()
&& (lines[min].is_none() || lines[i].unwrap() < lines[min].unwrap())
{
min = i;
}
}
}
f.write(lines[min].unwrap().as_bytes())?;
f.write("\n".as_bytes())?;
lines[min] = iters[min].next();
}
Ok(())
}
pub fn run_opt(opt: &Opt) -> Result<(), Error> {
if opt.config {
let toml = toml::to_string(&opt)?;
println!("{}", toml);
return Ok(());
}
match opt.completion {
Some(ref x) => {
let shell = match x.as_str() {
"bash" => clap::Shell::Bash,
"fish" => clap::Shell::Fish,
"zsh" => clap::Shell::Zsh,
"powershell" => clap::Shell::PowerShell,
_ => clap::Shell::Bash,
};
Opt::clap().gen_completions("ptags", shell, "./");
return Ok(());
}
None => {}
}
let files;
let time_git_files;
if let Some(ref list) = opt.list {
files = input_files(list, &opt).context("failed to get file list")?;
time_git_files = Duration::seconds(0);
} else {
time_git_files = watch_time!({
files = git_files(&opt).context("failed to get file list")?;
});
}
let outputs;
let time_call_ctags = watch_time!({
outputs = call_ctags(&opt, &files).context("failed to call ctags")?;
});
let time_write_tags = watch_time!({
let _ = write_tags(&opt, &outputs)
.context(format!("failed to write file ({:?})", &opt.output))?;
});
if opt.stat {
let sum: usize = files.iter().map(|x| x.lines().count()).sum();
eprintln!("\nStatistics");
eprintln!("- Options");
eprintln!(" thread : {}\n", opt.thread);
eprintln!("- Searched files");
eprintln!(" total : {}\n", sum);
eprintln!("- Elapsed time[ms]");
eprintln!(" git_files : {}", time_git_files.num_milliseconds());
eprintln!(" call_ctags: {}", time_call_ctags.num_milliseconds());
eprintln!(" write_tags: {}", time_write_tags.num_milliseconds());
}
Ok(())
}
#[cfg_attr(tarpaulin, skip)]
pub fn run() -> Result<(), Error> {
let cfg_path = match dirs::home_dir() {
Some(mut path) => {
path.push(".ptags.toml");
if path.exists() {
Some(path)
} else {
None
}
}
None => None,
};
let opt = match cfg_path {
Some(path) => {
let mut f =
fs::File::open(&path).context(format!("failed to open file ({:?})", path))?;
let mut s = String::new();
let _ = f.read_to_string(&mut s);
Opt::from_args_with_toml(&s).context(format!("failed to parse toml ({:?})", path))?
}
None => Opt::from_args(),
};
run_opt(&opt)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_run() {
let args = vec!["ptags"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert!(ret.is_ok());
}
#[test]
fn test_run_opt() {
let args = vec!["ptags", "-s", "-v", "--validate-utf8", "--unsorted"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert!(ret.is_ok());
}
#[test]
fn test_run_fail() {
let args = vec!["ptags", "--bin-git", "aaa"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert_eq!(
&format!("{:?}", ret)[0..42],
"Err(Os { code: 2, kind: NotFound, message:"
);
}
#[test]
fn test_run_completion() {
let args = vec!["ptags", "--completion", "bash"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert!(ret.is_ok());
let args = vec!["ptags", "--completion", "fish"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert!(ret.is_ok());
let args = vec!["ptags", "--completion", "zsh"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert!(ret.is_ok());
let args = vec!["ptags", "--completion", "powershell"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert!(ret.is_ok());
assert!(Path::new("ptags.bash").exists());
assert!(Path::new("ptags.fish").exists());
assert!(Path::new("_ptags").exists());
assert!(Path::new("_ptags.ps1").exists());
let _ = fs::remove_file("ptags.bash");
let _ = fs::remove_file("ptags.fish");
let _ = fs::remove_file("_ptags");
let _ = fs::remove_file("_ptags.ps1");
}
#[test]
fn test_run_config() {
let args = vec!["ptags", "--config"];
let opt = Opt::from_iter(args.iter());
let ret = run_opt(&opt);
assert!(ret.is_ok());
}
}