Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add get-type command #730

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ src/scopes
*.racertmp
target/
*.py[cod]
.vscode/**
.vscode/**
30 changes: 30 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(".");
Expand Down Expand Up @@ -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 <linenum> <charnum> <path>")
.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 <command> --help'")
}

Expand Down Expand Up @@ -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!()
}
}
Expand Down
51 changes: 51 additions & 0 deletions src/racer/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<P, C>(
filepath: P,
cursor: C,
session: &Session
) -> Option<Match> where P: AsRef<path::Path>, C: Into<Location>
{
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;
Expand Down
2 changes: 1 addition & 1 deletion src/racer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
12 changes: 12 additions & 0 deletions src/racer/scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
161 changes: 159 additions & 2 deletions tests/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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('~', ""))
}

Expand Down Expand Up @@ -190,7 +190,17 @@ fn get_definition(src: &str, dir: Option<TmpDir>) -> 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<TmpDir>) -> 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")
}


Expand Down Expand Up @@ -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<u16> = Some(5);
if val~ue.is_some() {
// do nothing
}
}
";

let got = get_type(src, None);
assert_eq!("Option", got.matchstr);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, can we not determine that it's an Option<usize>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't put generic types into the matchstr property today; they're in the generic_types array instead. I could try and do something to push that data into matchstr but I'm not sure which approach would be better for tooling authors.

}

#[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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be &str? name is a reference type (ie, a pointer).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

#[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!();
Expand Down