diff --git a/.gitignore b/.gitignore index 4abdcb60..adfc99f9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ src/scopes *.racertmp target/ *.py[cod] -.vscode/** \ No newline at end of file +.vscode/** diff --git a/src/bin/main.rs b/src/bin/main.rs index c6e03467..3e9f11a0 100755 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -59,6 +59,10 @@ fn complete_by_line_coords(cfg: Config, interface.emit(Message::End); } +fn get_type(cfg: Config) { + run_get_type(&cfg); +} + #[derive(Debug)] enum CompletePrinter { Normal, @@ -121,6 +125,18 @@ fn run_the_complete_fn(cfg: &Config, print_type: CompletePrinter) { } } +fn run_get_type(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); + + load_query_file(&fn_path, &substitute_file, &session); + racer::get_type(&fn_path, cfg.coords(), &session).map(|m| match_fn(m, cfg.interface)); + cfg.interface.emit(Message::End); +} + /// Completes a fully qualified name specified on command line fn external_complete(cfg: Config, print_type: CompletePrinter) { let cwd = Path::new("."); @@ -380,6 +396,19 @@ fn build_cli<'a, 'b>() -> App<'a, 'b> { .help("An optional substitute file")) .arg(Arg::with_name("linenum") .help("The line number at which to find the match"))) + .subcommand(SubCommand::with_name("get-type") + .about("finds the type of the specified value") + .usage("racer get-type ") + .setting(AppSettings::ArgRequiredElseHelp) + .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))) .after_help("For more information about a specific command try 'racer --help'") } @@ -409,6 +438,7 @@ fn run(m: ArgMatches, interface: Interface) { "complete" => complete(cfg, Normal), "complete-with-snippet" => complete(cfg, WithSnippets), "find-definition" => find_definition(cfg), + "get-type" => get_type(cfg), _ => unreachable!() } } diff --git a/src/racer/core.rs b/src/racer/core.rs index 6d0edf33..1818bc59 100644 --- a/src/racer/core.rs +++ b/src/racer/core.rs @@ -1111,6 +1111,57 @@ pub fn find_definition_(filepath: &path::Path, cursor: Location, session: &Sessi } } +/// Gets the type of a variable or field identified by position in the input file. +/// +/// When the cursor is placed anywhere inside a fully-typed identifier, this function +/// will attempt to determine the concrete type of that value. +pub fn get_type( + filepath: P, + cursor: C, + session: &Session +) -> Option where P: AsRef, C: Into +{ + let fpath = filepath.as_ref(); + let cursor = cursor.into(); + let src = session.load_file_and_mask_comments(fpath); + let src = &src.as_src()[..]; + + // TODO return result + let pos = match cursor.to_point(&session.load_file(fpath)) { + Some(pos) => pos, + None => { + debug!("Failed to convert cursor to point"); + return None; + } + }; + + /// Backtrack to find the start of the expression, like completion does. This should + /// enable get-type to work with field accesses as well as local variables. + let start = scopes::get_start_of_search_expr(src, pos); + + /// Unlike `complete`, we are doing an exact match, so we also need to search forward + /// to find the end of the identifier. We deliberately don't look for the end of the + /// expression, as we aren't trying to find the whole expression's type. + let end = scopes::get_end_of_ident(src, pos); + + + let expr = &src[start..end]; + + ast::get_type_of(expr.to_owned(), fpath, pos, session).and_then(|ty| { + if let Ty::Match(mut m) = ty { + if m.coords.is_none() { + let point = m.point; + let src = session.load_file(m.filepath.as_path()); + m.coords = src.point_to_coords(point); + } + + Some(m) + } else { + None + } + }) +} + #[cfg(test)] mod tests { use std::path::Path; diff --git a/src/racer/lib.rs b/src/racer/lib.rs index 1a5ddfee..9eb03d28 100644 --- a/src/racer/lib.rs +++ b/src/racer/lib.rs @@ -28,7 +28,7 @@ mod matchers; mod snippets; mod cargo; -pub use core::{find_definition, complete_from_file, complete_fully_qualified_name}; +pub use core::{find_definition, complete_from_file, complete_fully_qualified_name, get_type}; pub use snippets::snippet_for_match; pub use core::{Match, MatchType, PathSearch}; pub use core::{FileCache, Session, Coordinate, Location, FileLoader}; diff --git a/src/racer/scopes.rs b/src/racer/scopes.rs index 0ff72211..2dcfffc3 100644 --- a/src/racer/scopes.rs +++ b/src/racer/scopes.rs @@ -207,6 +207,18 @@ pub fn get_line(src: &str, point: usize) -> usize { 0 } +/// Search forward to the end of the current ident. +/// Used by `get-type` to ensure that it is searching for the full ident rather than a prefix match. +pub fn get_end_of_ident(src: &str, point: usize) -> usize { + for (i, _) in src.as_bytes()[point..].iter().enumerate() { + if !util::is_ident_char(char_at(src, point + i)) { + return point + i; + } + } + + return point + src.as_bytes()[point..].len(); +} + /// search in reverse for the start of the current expression /// allow . and :: to be surrounded by white chars to enable multi line call chains pub fn get_start_of_search_expr(src: &str, point: usize) -> usize { diff --git a/tests/system.rs b/tests/system.rs index fbb07002..d509e498 100644 --- a/tests/system.rs +++ b/tests/system.rs @@ -150,7 +150,7 @@ impl Drop for TmpDir { } fn get_pos_and_source(src: &str) -> (usize, String) { - let point = src.find('~').unwrap(); + let point = src.find('~').expect("Test input must contain a `~` character to represent cursor location"); (point, src.replace('~', "")) } @@ -190,7 +190,17 @@ fn get_definition(src: &str, dir: Option) -> Match { let cache = racer::FileCache::default(); let session = racer::Session::new(&cache); - find_definition(&path, completion_point, &session).unwrap() + find_definition(&path, completion_point, &session).expect("find-definition must produce a definition") +} + +fn get_type(src: &str, dir: Option) -> Match { + let dir = dir.unwrap_or_else(|| TmpDir::new()); + let (search_point, clean_src) = get_pos_and_source(src); + let path = dir.write_file("src.rs", &clean_src); + let cache = racer::FileCache::default(); + let session = racer::Session::new(&cache); + + racer::get_type(&path, search_point, &session).expect("get-type must produce a type") } @@ -3242,6 +3252,153 @@ fn closure_bracket_scope_nested_match_outside() { assert_eq!("| x: i32 |", got.contextstr); } +#[test] +fn get_type_finds_explicit_local_var() { + let _lock = sync!(); + let src = " + fn main() { + let value: Option = Some(5); + if val~ue.is_some() { + // do nothing + } + } + "; + + let got = get_type(src, None); + assert_eq!("Option", got.matchstr); +} + +#[test] +fn get_type_finds_fn_arg() { + let _lock = sync!(); + let src = r#" + fn say_hello(name: &str) { + if nam~e != "teddriggs" { + // do nothing + } + } + "#; + + let got = get_type(src, None); + assert_eq!("str", got.matchstr); +} + +#[test] +fn get_type_finds_tuple_field() { + let _lock = sync!(); + let src = " + struct Bar; + struct Foo(Bar); + + fn do_things(foo: Foo) -> Bar { + foo.0~ + } + "; + + let got = get_type(src, None); + assert_eq!("Bar", got.matchstr); +} + +#[test] +fn get_type_finds_struct_field() { + let _lock = sync!(); + let src = " + struct Bar; + struct Foo { value: Bar } + + fn do_things(foo: Foo) -> Bar { + foo.va~lue + } + "; + + let got = get_type(src, None); + assert_eq!("Bar", got.matchstr); +} + +#[test] +fn get_type_finds_destructured() { + let _lock = sync!(); + let src = " + struct Bar; + struct Foo { value: Bar } + + fn do_things(foo: Foo) -> Bar { + let Foo { value: value } = foo; + valu~e + } + "; + + let got = get_type(src, None); + assert_eq!("Bar", got.matchstr); +} + +#[test] +fn get_type_finds_single_character_var_at_end() { + let _lock = sync!(); + let src = " + struct Bar; + fn do_things(y: Bar) -> Bar { + y~ + } + "; + + let got = get_type(src, None); + assert_eq!("Bar", got.matchstr); +} + +#[test] +fn get_type_finds_single_character_var_at_start() { + let _lock = sync!(); + let src = " + struct Bar; + fn do_things(y: Bar) -> Bar { + ~y + } + "; + + let got = get_type(src, None); + assert_eq!("Bar", got.matchstr); +} + +#[test] +fn get_type_finds_function_return() { + let _lock = sync!(); + let src = r#" + struct Bar; + fn do_things(y: Bar) -> Bar { + y + } + + fn main() { + let z = do_things(); + println!("{:?}", ~z); + } + "#; + + let got = get_type(src, None); + assert_eq!("Bar", got.matchstr); +} + +#[test] +fn get_type_finds_function() { + let _lock = sync!(); + let src = r#" + struct Bar; + + fn do_things(y: Bar) -> Bar { + y + } + + fn main() { + let z = do_th~ings(); + println!("{:?}", ~z); + } + "#; + + let got = get_type(src, None); + assert_eq!("do_things", got.matchstr); +} + #[test] fn literal_string_method() { let _lock = sync!();