#[macro_use]
extern crate log;
use env_logger;
#[macro_use]
extern crate clap;
use humantime;
use racer;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use racer::{BytePos, Coordinate, FileCache, Match, MatchType, Session};
use std::fs::File;
use std::io::{self, BufRead, Read};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
fn point(cfg: &Config) {
let cache = FileCache::default();
let session = Session::new(&cache, None);
cfg.interface.emit(Message::Coords(cfg.coords()));
if let Some(point) = racer::to_point(cfg.coords(), cfg.expect_file(), &session) {
cfg.interface.emit(Message::Point(point));
}
cfg.interface.emit(Message::End);
}
fn coord(cfg: &Config) {
let cache = FileCache::default();
let session = Session::new(&cache, None);
cfg.interface.emit(Message::Point(cfg.point));
if let Some(coords) = racer::to_coords(cfg.point, cfg.expect_file(), &session) {
cfg.interface.emit(Message::Coords(coords));
}
cfg.interface.emit(Message::End);
}
fn match_with_snippet_fn(m: Match, session: &Session<'_>, interface: Interface) {
let cd = m
.coords
.expect("[match_with_snipper_fn] failed to get coordinate");
if m.matchstr == "" {
panic!("MATCHSTR is empty - waddup?");
}
let snippet = racer::snippet_for_match(&m, session);
interface.emit(Message::MatchWithSnippet(
m.matchstr,
snippet,
cd,
m.filepath.as_path(),
m.mtype,
m.contextstr,
m.docs,
));
}
fn match_fn(m: Match, interface: Interface) {
if let Some(coords) = m.coords {
interface.emit(Message::Match(
m.matchstr,
coords,
m.filepath.as_path(),
m.mtype,
m.contextstr,
));
} else {
error!("Could not resolve file coords for match {:?}", m);
}
}
fn complete(cfg: Config, print_type: CompletePrinter) {
if cfg.fqn.is_some() {
return external_complete(&cfg, print_type);
}
complete_by_line_coords(cfg, print_type);
}
fn complete_by_line_coords(cfg: Config, print_type: CompletePrinter) {
let tb = std::thread::Builder::new().name("searcher".to_owned());
let interface = cfg.interface;
let res = tb
.spawn(move || {
run_the_complete_fn(&cfg, print_type);
})
.unwrap();
if let Err(e) = res.join() {
error!("Search thread panicked: {:?}", e);
}
interface.emit(Message::End);
}
#[derive(Debug, Clone, Copy)]
enum CompletePrinter {
Normal,
WithSnippets,
}
fn read_file_from_stdin() -> String {
let mut rawbytes = Vec::new();
let stdin = io::stdin();
stdin
.lock()
.read_until(0x04, &mut rawbytes)
.expect("read until EOT");
String::from_utf8(rawbytes).expect("utf8 from stdin")
}
fn read_file<P>(path: P) -> io::Result<String>
where
P: AsRef<Path>,
{
let mut res = String::new();
let mut f = File::open(path)?;
f.read_to_string(&mut res)?;
Ok(res)
}
fn load_query_file<P, S>(path: P, sub: S, session: &Session<'_>)
where
P: Into<PathBuf>,
S: AsRef<Path>,
{
let path = path.into();
let sub = sub.as_ref();
if sub.to_str() == Some("-") {
let contents = read_file_from_stdin();
session.cache_file_contents(path, contents);
} else if sub != path {
let contents = read_file(sub).unwrap();
session.cache_file_contents(path, contents);
}
}
fn run_the_complete_fn(cfg: &Config, print_type: CompletePrinter) {
let fn_path = cfg.fn_name.as_ref().unwrap();
let substitute_file = cfg.substitute_file.as_ref().unwrap_or(fn_path);
let cache = FileCache::default();
let session = Session::new(&cache, Some(fn_path));
load_query_file(fn_path, &substitute_file, &session);
if let Some(expanded) = racer::expand_ident(&fn_path, cfg.coords(), &session) {
cfg.interface.emit(Message::Prefix(
expanded.start(),
expanded.pos(),
expanded.ident(),
));
for m in racer::complete_from_file(&fn_path, cfg.coords(), &session) {
match print_type {
CompletePrinter::Normal => match_fn(m, cfg.interface),
CompletePrinter::WithSnippets => match_with_snippet_fn(m, &session, cfg.interface),
};
}
}
}
fn external_complete(cfg: &Config, print_type: CompletePrinter) {
let cwd = Path::new(".");
let cache = FileCache::default();
let session = Session::new(&cache, Some(cwd));
for m in racer::complete_fully_qualified_name(cfg.fqn.as_ref().unwrap(), &cwd, &session) {
match print_type {
CompletePrinter::Normal => match_fn(m, cfg.interface),
CompletePrinter::WithSnippets => match_with_snippet_fn(m, &session, cfg.interface),
}
}
}
fn prefix(cfg: &Config) {
let fn_path = cfg.fn_name.as_ref().unwrap();
let substitute_file = cfg.substitute_file.as_ref().unwrap_or(fn_path);
let cache = FileCache::default();
let session = Session::new(&cache, Some(fn_path));
load_query_file(fn_path, &substitute_file, &session);
let expanded = racer::expand_ident(fn_path, cfg.coords(), &session).unwrap();
cfg.interface.emit(Message::Prefix(
expanded.start(),
expanded.pos(),
expanded.ident(),
));
}
fn find_definition(cfg: &Config) {
let fn_path = cfg.fn_name.as_ref().unwrap();
let substitute_file = cfg.substitute_file.as_ref().unwrap_or(fn_path);
let cache = FileCache::default();
let session = Session::new(&cache, Some(fn_path));
load_query_file(fn_path, &substitute_file, &session);
if let Some(m) = racer::find_definition(fn_path, cfg.coords(), &session) {
match_fn(m, cfg.interface);
}
cfg.interface.emit(Message::End);
}
fn validate_rust_src_path_env_var() {
match racer::get_rust_src_path() {
Ok(_) => (),
Err(err) => {
println!("{}", err);
std::process::exit(1);
}
}
}
fn daemon(cfg: &Config) {
let mut input = String::new();
while let Ok(n) = io::stdin().read_line(&mut input) {
if n == 1 {
break;
}
let cli = build_cli().setting(AppSettings::NoBinaryName);
let matches = match cfg.interface {
Interface::Text => cli.get_matches_from(input.trim_end().split_whitespace()),
Interface::TabText => cli.get_matches_from(input.trim_end().split('\t')),
};
run(&matches, cfg.interface);
input.clear();
}
}
enum Message<'a> {
End,
Prefix(BytePos, BytePos, &'a str),
Match(String, Coordinate, &'a Path, MatchType, String),
MatchWithSnippet(
String,
String,
Coordinate,
&'a Path,
MatchType,
String,
String,
),
Point(BytePos),
Coords(Coordinate),
}
#[derive(Copy, Clone)]
enum Interface {
Text, TabText, }
impl Default for Interface {
fn default() -> Self {
Interface::Text
}
}
impl Interface {
fn leading_space(&self) -> &str {
match *self {
Interface::Text => " ",
Interface::TabText => "\t",
}
}
fn field_separator(&self) -> &str {
match *self {
Interface::Text => ",",
Interface::TabText => "\t",
}
}
fn emit(&self, message: Message<'_>) {
match message {
Message::End => println!("END"),
Message::Prefix(start, pos, text) => match *self {
Interface::Text => println!("PREFIX {},{},{}", start, pos, text),
Interface::TabText => println!("PREFIX\t{}\t{}\t{}", start, pos, text),
},
Message::Point(point) => println!("POINT{}{}", self.leading_space(), point),
Message::Coords(coord) => {
println!(
"COORD{lead}{}{field}{}",
coord.row.0,
coord.col.0,
lead = self.leading_space(),
field = self.field_separator()
);
}
Message::Match(mstr, cd, path, mtype, context) => match *self {
Interface::Text => {
let context = context.split_whitespace().collect::<Vec<&str>>().join(" ");
println!(
"MATCH {},{},{},{},{},{}",
mstr,
cd.row.0,
cd.col.0,
path.display(),
mtype,
context
);
}
Interface::TabText => {
let context = context.split_whitespace().collect::<Vec<&str>>().join(" ");
println!(
"MATCH\t{}\t{}\t{}\t{}\t{}\t{}",
mstr,
cd.row.0,
cd.col.0,
path.display(),
mtype,
context
);
}
},
Message::MatchWithSnippet(mstr, snippet, cd, path, mtype, context, docs) => match *self
{
Interface::Text => {
let context = context
.replace(";", "\\;")
.split_whitespace()
.collect::<Vec<&str>>()
.join(" ");
let docs = format!("{:?}", docs).replace(";", "\\;");
println!(
"MATCH {};{};{};{};{};{};{};{}",
mstr,
snippet,
cd.row.0,
cd.col.0,
path.display(),
mtype,
context,
docs
);
}
Interface::TabText => {
let context = context
.replace("\t", "\\t")
.split_whitespace()
.collect::<Vec<&str>>()
.join(" ");
println!(
"MATCH\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:?}",
mstr,
snippet,
cd.row.0,
cd.col.0,
path.display(),
mtype,
context,
docs
);
}
},
}
}
}
#[derive(Default)]
struct Config {
fqn: Option<String>,
linenum: usize,
charnum: usize,
fn_name: Option<PathBuf>,
substitute_file: Option<PathBuf>,
interface: Interface,
point: BytePos,
}
impl Config {
fn coords(&self) -> Coordinate {
Coordinate::new(self.linenum as u32, self.charnum as u32)
}
fn expect_file(&self) -> &PathBuf {
self.fn_name.as_ref().expect("File path required")
}
}
impl<'a> From<&'a ArgMatches<'a>> for Config {
fn from(m: &'a ArgMatches<'_>) -> Self {
if m.is_present("point") && m.is_present("path") {
return Config {
point: value_t_or_exit!(m.value_of("point"), usize).into(),
fn_name: m.value_of("path").map(PathBuf::from),
..Default::default()
};
}
if m.is_present("charnum") {
let cfg = Config {
charnum: value_t_or_exit!(m.value_of("charnum"), usize),
fn_name: m.value_of("path").map(PathBuf::from),
substitute_file: m.value_of("substitute_file").map(PathBuf::from),
..Default::default()
};
if !m.is_present("linenum") {
return Config {
linenum: value_t_or_exit!(m.value_of("fqn"), usize),
..cfg
};
}
return Config {
linenum: value_t_or_exit!(m.value_of("linenum"), usize),
..cfg
};
}
Config {
fqn: m.value_of("fqn").map(ToOwned::to_owned),
..Default::default()
}
}
}
fn build_cli<'a, 'b>() -> App<'a, 'b> {
App::new("racer")
.version(env!("CARGO_PKG_VERSION"))
.author("Phil Dawes")
.about("A Rust code completion utility")
.settings(&[
AppSettings::GlobalVersion,
AppSettings::SubcommandRequiredElseHelp,
])
.arg(
Arg::with_name("interface")
.long("interface")
.short("i")
.takes_value(true)
.possible_value("text")
.possible_value("tab-text")
.value_name("mode")
.help("Interface mode"),
)
.subcommand(
SubCommand::with_name("complete")
.about("performs completion and returns matches")
.usage(
"racer complete <fqn>\n \
racer complete <linenum> <charnum> <path> [substitute_file]",
)
.setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::with_name("fqn")
.help("complete with a fully-qualified-name (e.g. std::io::)"),
)
.arg(
Arg::with_name("charnum")
.help("The char number to search for matches")
.requires("path"),
)
.arg(Arg::with_name("path").help("The path to search for name to match"))
.arg(Arg::with_name("substitute_file").help("An optional substitute file"))
.arg(Arg::with_name("linenum").help("The line number at which to find the match")),
)
.subcommand(
SubCommand::with_name("daemon")
.about("start a process that receives the above commands via stdin"),
)
.subcommand(
SubCommand::with_name("find-definition")
.about("finds the definition of a function")
.arg(
Arg::with_name("linenum")
.help("The line number at which to find the match")
.required(true),
)
.arg(
Arg::with_name("charnum")
.help("The char number at which to find the match")
.required(true),
)
.arg(
Arg::with_name("path")
.help("The path to search for name to match")
.required(true),
)
.arg(Arg::with_name("substitute_file").help("An optional substitute file")),
)
.subcommand(
SubCommand::with_name("prefix")
.arg(
Arg::with_name("linenum")
.help("The line number at which to find the match")
.required(true),
)
.arg(
Arg::with_name("charnum")
.help("The char number at which to find the match")
.required(true),
)
.arg(
Arg::with_name("path")
.help("The path to search for the match to prefix")
.required(true),
),
)
.subcommand(
SubCommand::with_name("complete-with-snippet")
.about("performs completion and returns more detailed matches")
.usage(
"racer complete-with-snippet <fqn>\n \
racer complete-with-snippet <linenum> <charnum> <path> [substitute_file]",
)
.setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::with_name("fqn")
.help("complete with a fully-qualified-name (e.g. std::io::)"),
)
.arg(
Arg::with_name("charnum")
.help("The char number to search for matches")
.requires("path"),
)
.arg(Arg::with_name("path").help("The path to search for name to match"))
.arg(Arg::with_name("substitute_file").help("An optional substitute file"))
.arg(Arg::with_name("linenum").help("The line number at which to find the match")),
)
.subcommand(
SubCommand::with_name("point")
.about("converts linenum and charnum in a file to a point")
.setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::with_name("linenum")
.help("The line number at which to convert to point")
.required(true),
)
.arg(
Arg::with_name("charnum")
.help("The char number at which to convert to point")
.required(true),
)
.arg(
Arg::with_name("path")
.help("The path where the line and char occur")
.required(true),
),
)
.subcommand(
SubCommand::with_name("coord")
.about("converts a racer point to line and character numbers")
.setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::with_name("point")
.help("The point to convert to line and character coordinates")
.required(true),
)
.arg(
Arg::with_name("path")
.help("The path where the line and char occur")
.required(true),
),
)
.after_help("For more information about a specific command try 'racer <command> --help'")
}
fn main() {
use std::io::Write;
env_logger::Builder::from_default_env()
.format(|f, record| {
writeln!(
f,
"{:>5} {}: {}: {}",
record.level(),
humantime::format_rfc3339_nanos(SystemTime::now()),
record.module_path().unwrap_or("-"),
record.args()
)
})
.init();
let matches = build_cli().get_matches();
let interface = match matches.value_of("interface") {
Some("tab-text") => Interface::TabText,
Some("text") | _ => Interface::Text,
};
validate_rust_src_path_env_var();
run(&matches, interface);
}
fn run(m: &ArgMatches<'_>, interface: Interface) {
use crate::CompletePrinter::{Normal, WithSnippets};
if let (name, Some(sub_m)) = m.subcommand() {
let mut cfg = Config::from(sub_m);
cfg.interface = interface;
match name {
"daemon" => daemon(&cfg),
"prefix" => prefix(&cfg),
"complete" => complete(cfg, Normal),
"complete-with-snippet" => complete(cfg, WithSnippets),
"find-definition" => find_definition(&cfg),
"point" => point(&cfg),
"coord" => coord(&cfg),
_ => unreachable!(),
}
}
}