use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::process::Command;
use std::collections::HashSet;
use std::path::{PathBuf, Path};
use app_result::{AppResult, app_err_msg, app_err_missing_src};
use types::{Tags, TagsKind, SourceKind};
use path_ext::PathExt;
use config::Config;
use dirs::{
rusty_tags_cache_dir,
cargo_git_src_dir,
cargo_crates_io_src_dir,
glob_path
};
pub fn update_tags(config: &Config, source: &SourceKind) -> AppResult<Tags>
{
let src_tags = try!(cached_tags_file(&config.tags_kind, source));
let src_dir = try!(find_src_dir(source));
if src_tags.as_path().is_file() && ! config.force_recreate {
return Ok(Tags::new(&src_dir, &src_tags, true));
}
try!(create_tags(config, &src_dir, &src_tags));
Ok(Tags::new(&src_dir, &src_tags, false))
}
pub fn update_tags_and_check_for_reexports(config: &Config,
source: &SourceKind,
dependencies: &Vec<SourceKind>) -> AppResult<Tags>
{
let lib_tags = try!(update_tags(config, source));
if lib_tags.is_up_to_date(&config.tags_kind) && ! config.force_recreate {
return Ok(lib_tags);
}
let reexp_crates = try!(find_reexported_crates(&lib_tags.src_dir));
if reexp_crates.is_empty() {
return Ok(lib_tags);
}
println!("Found public reexports in '{}' of:", source.get_lib_name());
for rcrate in reexp_crates.iter() {
println!(" {}", rcrate);
}
println!("");
let mut crate_tags = Vec::<PathBuf>::new();
for rcrate in reexp_crates.iter() {
if let Some(crate_dep) = dependencies.iter().find(|d| d.get_lib_name() == *rcrate) {
crate_tags.push(try!(update_tags(config, crate_dep)).tags_file.clone());
}
}
if crate_tags.is_empty() {
return Ok(lib_tags);
}
crate_tags.push(lib_tags.tags_file.clone());
try!(merge_tags(config, &crate_tags, &lib_tags.tags_file));
Ok(lib_tags)
}
pub fn merge_tags(config: &Config, tag_files: &Vec<PathBuf>, into_tag_file: &Path) -> AppResult<()>
{
println!("Merging ...\n tags:");
for file in tag_files.iter() {
println!(" {}", file.display());
}
println!("\n into:\n {}\n", into_tag_file.display());
match config.tags_kind {
TagsKind::Vi => {
let mut file_contents: Vec<String> = Vec::new();
for file in tag_files.iter() {
let mut file = try!(File::open(file));
let mut contents = String::new();
try!(file.read_to_string(&mut contents));
file_contents.push(contents);
}
let mut merged_lines: Vec<&str> = Vec::with_capacity(100_000);
for content in file_contents.iter() {
for line in content.lines() {
if let Some(chr) = line.chars().nth(0) {
if chr != '!' {
merged_lines.push(line);
}
}
}
}
merged_lines.sort();
merged_lines.dedup();
let mut tag_file = try!(
OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(into_tag_file)
);
try!(tag_file.write_fmt(format_args!("{}\n", "!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/")));
try!(tag_file.write_fmt(format_args!("{}\n", "!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/")));
for line in merged_lines.iter() {
try!(tag_file.write_fmt(format_args!("{}\n", *line)));
}
},
TagsKind::Emacs => {
let mut tag_file = try!(
OpenOptions::new()
.create(true)
.append(true)
.read(true)
.write(true)
.open(into_tag_file)
);
for file in tag_files.iter() {
if file.as_path() != into_tag_file {
try!(tag_file.write_fmt(format_args!("{},include\n", file.display())));
}
}
}
}
Ok(())
}
pub fn create_tags(config: &Config, src_dir: &Path, tags_file: &Path) -> AppResult<()>
{
let mut cmd = Command::new("ctags");
config.tags_kind.ctags_option().map(|opt| { cmd.arg(opt); () });
cmd.arg("--recurse")
.arg("--languages=Rust")
.arg("--langdef=Rust")
.arg("--langmap=Rust:.rs")
.arg("--regex-Rust=/^[ \\t]*(#\\[[^\\]]\\][ \\t]*)*(pub[ \\t]+)?(extern[ \\t]+)?(\"[^\"]+\"[ \\t]+)?(unsafe[ \\t]+)?fn[ \\t]+([a-zA-Z0-9_]+)/\\6/f,functions,function definitions/")
.arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?type[ \\t]+([a-zA-Z0-9_]+)/\\2/T,types,type definitions/")
.arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?enum[ \\t]+([a-zA-Z0-9_]+)/\\2/g,enum,enumeration names/")
.arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?struct[ \\t]+([a-zA-Z0-9_]+)/\\2/s,structure names/")
.arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?mod[ \\t]+([a-zA-Z0-9_]+)/\\2/m,modules,module names/")
.arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?(static|const)[ \\t]+([a-zA-Z0-9_]+)/\\3/c,consts,static constants/")
.arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?trait[ \\t]+([a-zA-Z0-9_]+)/\\2/t,traits,traits/")
.arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?impl([ \\t\\n]*<[^>]*>)?[ \\t]+(([a-zA-Z0-9_:]+)[ \\t]*(<[^>]*>)?[ \\t]+(for)[ \\t]+)?([a-zA-Z0-9_]+)/\\4 \\6 \\7/i,impls,trait implementations/")
.arg("--regex-Rust=/^[ \\t]*macro_rules![ \\t]+([a-zA-Z0-9_]+)/\\1/d,macros,macro definitions/")
.arg("-o")
.arg(tags_file)
.arg(src_dir);
println!("Creating tags ...\n for source:\n {}\n\n cached at:\n {}\n",
src_dir.display(), tags_file.display());
try!(cmd.output());
Ok(())
}
fn find_src_dir(source: &SourceKind) -> AppResult<PathBuf>
{
match *source {
SourceKind::Git { ref lib_name, ref commit_hash } => {
let mut lib_src = lib_name.clone();
lib_src.push_str("-*");
let mut src_dir = try!(cargo_git_src_dir().map(Path::to_path_buf));
src_dir.push(&lib_src);
src_dir.push("master");
let src_paths = try!(glob_path(&format!("{}", src_dir.display())));
for src_path in src_paths {
if let Ok(path) = src_path {
let src_commit_hash = try!(get_commit_hash(&path));
if *commit_hash == src_commit_hash {
return Ok(path);
}
}
}
let mut src_dir = try!(cargo_git_src_dir().map(Path::to_path_buf));
src_dir.push("*");
src_dir.push("master");
let src_paths = try!(glob_path(&format!("{}", src_dir.display())));
for src_path in src_paths {
if let Ok(path) = src_path {
let src_commit_hash = try!(get_commit_hash(&path));
if *commit_hash == src_commit_hash {
return Ok(path);
}
}
}
Err(app_err_missing_src(source))
},
SourceKind::CratesIo { ref lib_name, ref version } => {
let mut lib_src = lib_name.clone();
lib_src.push('-');
lib_src.push_str(&**version);
let mut src_dir = try!(cargo_crates_io_src_dir().map(Path::to_path_buf));
src_dir.push(&lib_src);
if ! src_dir.is_dir() {
return Err(app_err_missing_src(source));
}
Ok(src_dir)
},
SourceKind::Path { ref path, .. } => {
if ! path.is_dir() {
return Err(app_err_missing_src(source));
}
Ok(path.clone())
}
}
}
fn cached_tags_file(tags_kind: &TagsKind, source: &SourceKind) -> AppResult<PathBuf>
{
match *source {
SourceKind::Git { .. } | SourceKind::CratesIo { .. } => {
let mut tags_file = try!(rusty_tags_cache_dir().map(Path::to_path_buf));
tags_file.push(&source.tags_file_name(tags_kind));
Ok(tags_file)
},
SourceKind::Path { ref path, .. } => {
let mut tags_file = path.clone();
tags_file.push(&source.tags_file_name(tags_kind));
Ok(tags_file)
}
}
}
type CrateName = String;
fn find_reexported_crates(src_dir: &Path) -> AppResult<Vec<CrateName>>
{
let mut lib_file = src_dir.to_path_buf();
lib_file.push("src");
lib_file.push("lib.rs");
if ! lib_file.is_file() {
return Ok(Vec::new());
}
let contents = {
let mut file = try!(File::open(&lib_file));
let mut contents = String::new();
try!(file.read_to_string(&mut contents));
contents
};
let lines = contents.lines();
type ModuleName = String;
let mut pub_uses = HashSet::<ModuleName>::new();
#[derive(Eq, PartialEq, Hash)]
struct ExternCrate<'a>
{
name: &'a str,
as_name: &'a str
}
let mut extern_crates = HashSet::<ExternCrate>::new();
for line in lines {
let items = line.trim_matches(';').split(' ').collect::<Vec<&str>>();
if items.len() < 3 {
continue;
}
if items[0] == "pub" && items[1] == "use" {
let mods = items[2].split("::").collect::<Vec<&str>>();
if mods.len() >= 1 {
pub_uses.insert(mods[0].to_string());
}
}
if items[0] == "extern" && items[1] == "crate" {
if items.len() == 3 {
extern_crates.insert(ExternCrate { name: items[2].trim_matches('"'), as_name: items[2] });
}
else if items.len() == 5 && items[3] == "as" {
extern_crates.insert(ExternCrate { name: items[2].trim_matches('"'), as_name: items[4] });
}
}
}
let mut reexp_crates = Vec::<CrateName>::new();
for extern_crate in extern_crates.iter() {
if pub_uses.contains(extern_crate.as_name) {
reexp_crates.push(extern_crate.name.to_string());
}
}
Ok(reexp_crates)
}
fn get_commit_hash(git_dir: &Path) -> AppResult<String>
{
let mut cmd = Command::new("git");
cmd.current_dir(git_dir)
.arg("rev-parse")
.arg("HEAD");
let out = try!(cmd.output());
String::from_utf8(out.stdout)
.map(|s| s.trim().to_string())
.map_err(|_| app_err_msg("Couldn't convert 'git rev-parse HEAD' output to utf8!".to_string()))
}