From b5021411a84822cb3f1e3aeffad9550dd15bdeb6 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 16 Sep 2018 12:54:24 +0300 Subject: rename all things --- crates/ra_editor/Cargo.toml | 15 + crates/ra_editor/src/code_actions.rs | 218 ++++++++++++++ crates/ra_editor/src/completion.rs | 480 +++++++++++++++++++++++++++++++ crates/ra_editor/src/edit.rs | 84 ++++++ crates/ra_editor/src/extend_selection.rs | 167 +++++++++++ crates/ra_editor/src/lib.rs | 228 +++++++++++++++ crates/ra_editor/src/line_index.rs | 62 ++++ crates/ra_editor/src/scope/fn_scope.rs | 329 +++++++++++++++++++++ crates/ra_editor/src/scope/mod.rs | 8 + crates/ra_editor/src/scope/mod_scope.rs | 115 ++++++++ crates/ra_editor/src/symbols.rs | 167 +++++++++++ crates/ra_editor/src/test_utils.rs | 37 +++ crates/ra_editor/src/typing.rs | 348 ++++++++++++++++++++++ 13 files changed, 2258 insertions(+) create mode 100644 crates/ra_editor/Cargo.toml create mode 100644 crates/ra_editor/src/code_actions.rs create mode 100644 crates/ra_editor/src/completion.rs create mode 100644 crates/ra_editor/src/edit.rs create mode 100644 crates/ra_editor/src/extend_selection.rs create mode 100644 crates/ra_editor/src/lib.rs create mode 100644 crates/ra_editor/src/line_index.rs create mode 100644 crates/ra_editor/src/scope/fn_scope.rs create mode 100644 crates/ra_editor/src/scope/mod.rs create mode 100644 crates/ra_editor/src/scope/mod_scope.rs create mode 100644 crates/ra_editor/src/symbols.rs create mode 100644 crates/ra_editor/src/test_utils.rs create mode 100644 crates/ra_editor/src/typing.rs (limited to 'crates/ra_editor') diff --git a/crates/ra_editor/Cargo.toml b/crates/ra_editor/Cargo.toml new file mode 100644 index 000000000..40e3254ff --- /dev/null +++ b/crates/ra_editor/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ra_editor" +version = "0.1.0" +authors = ["Aleksey Kladov "] +publish = false + +[dependencies] +itertools = "0.7.8" +superslice = "0.1.0" +join_to_string = "0.1.1" + +ra_syntax = { path = "../ra_syntax" } + +[dev-dependencies] +test_utils = { path = "../test_utils" } diff --git a/crates/ra_editor/src/code_actions.rs b/crates/ra_editor/src/code_actions.rs new file mode 100644 index 000000000..83f7956d2 --- /dev/null +++ b/crates/ra_editor/src/code_actions.rs @@ -0,0 +1,218 @@ +use join_to_string::join; + +use ra_syntax::{ + File, TextUnit, TextRange, + ast::{self, AstNode, AttrsOwner, TypeParamsOwner, NameOwner}, + SyntaxKind::{COMMA, WHITESPACE}, + SyntaxNodeRef, + algo::{ + Direction, siblings, + find_leaf_at_offset, + find_covering_node, + ancestors, + }, +}; + +use {EditBuilder, Edit, find_node_at_offset}; + +#[derive(Debug)] +pub struct LocalEdit { + pub edit: Edit, + pub cursor_position: Option, +} + +pub fn flip_comma<'a>(file: &'a File, offset: TextUnit) -> Option LocalEdit + 'a> { + let syntax = file.syntax(); + + let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?; + let left = non_trivia_sibling(comma, Direction::Backward)?; + let right = non_trivia_sibling(comma, Direction::Forward)?; + Some(move || { + let mut edit = EditBuilder::new(); + edit.replace(left.range(), right.text().to_string()); + edit.replace(right.range(), left.text().to_string()); + LocalEdit { + edit: edit.finish(), + cursor_position: None, + } + }) +} + +pub fn add_derive<'a>(file: &'a File, offset: TextUnit) -> Option LocalEdit + 'a> { + let nominal = find_node_at_offset::(file.syntax(), offset)?; + Some(move || { + let derive_attr = nominal + .attrs() + .filter_map(|x| x.as_call()) + .filter(|(name, _arg)| name == "derive") + .map(|(_name, arg)| arg) + .next(); + let mut edit = EditBuilder::new(); + let offset = match derive_attr { + None => { + let node_start = nominal.syntax().range().start(); + edit.insert(node_start, "#[derive()]\n".to_string()); + node_start + TextUnit::of_str("#[derive(") + } + Some(tt) => { + tt.syntax().range().end() - TextUnit::of_char(')') + } + }; + LocalEdit { + edit: edit.finish(), + cursor_position: Some(offset), + } + }) +} + +pub fn add_impl<'a>(file: &'a File, offset: TextUnit) -> Option LocalEdit + 'a> { + let nominal = find_node_at_offset::(file.syntax(), offset)?; + let name = nominal.name()?; + + Some(move || { + let type_params = nominal.type_param_list(); + let mut edit = EditBuilder::new(); + let start_offset = nominal.syntax().range().end(); + let mut buf = String::new(); + buf.push_str("\n\nimpl"); + if let Some(type_params) = type_params { + type_params.syntax().text() + .push_to(&mut buf); + } + buf.push_str(" "); + buf.push_str(name.text().as_str()); + if let Some(type_params) = type_params { + let lifetime_params = type_params.lifetime_params().filter_map(|it| it.lifetime()).map(|it| it.text()); + let type_params = type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()); + join(lifetime_params.chain(type_params)) + .surround_with("<", ">") + .to_buf(&mut buf); + } + buf.push_str(" {\n"); + let offset = start_offset + TextUnit::of_str(&buf); + buf.push_str("\n}"); + edit.insert(start_offset, buf); + LocalEdit { + edit: edit.finish(), + cursor_position: Some(offset), + } + }) +} + +pub fn introduce_variable<'a>(file: &'a File, range: TextRange) -> Option LocalEdit + 'a> { + let node = find_covering_node(file.syntax(), range); + let expr = ancestors(node).filter_map(ast::Expr::cast).next()?; + let anchor_stmt = ancestors(expr.syntax()).filter_map(ast::Stmt::cast).next()?; + let indent = anchor_stmt.syntax().prev_sibling()?; + if indent.kind() != WHITESPACE { + return None; + } + Some(move || { + let mut buf = String::new(); + let mut edit = EditBuilder::new(); + + buf.push_str("let var_name = "); + expr.syntax().text().push_to(&mut buf); + if expr.syntax().range().start() == anchor_stmt.syntax().range().start() { + edit.replace(expr.syntax().range(), buf); + } else { + buf.push_str(";"); + indent.text().push_to(&mut buf); + edit.replace(expr.syntax().range(), "var_name".to_string()); + edit.insert(anchor_stmt.syntax().range().start(), buf); + } + let cursor_position = anchor_stmt.syntax().range().start() + TextUnit::of_str("let "); + LocalEdit { + edit: edit.finish(), + cursor_position: Some(cursor_position), + } + }) +} + +fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option { + siblings(node, direction) + .skip(1) + .find(|node| !node.kind().is_trivia()) +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::{check_action, check_action_range}; + + #[test] + fn test_swap_comma() { + check_action( + "fn foo(x: i32,<|> y: Result<(), ()>) {}", + "fn foo(y: Result<(), ()>,<|> x: i32) {}", + |file, off| flip_comma(file, off).map(|f| f()), + ) + } + + #[test] + fn test_add_derive() { + check_action( + "struct Foo { a: i32, <|>}", + "#[derive(<|>)]\nstruct Foo { a: i32, }", + |file, off| add_derive(file, off).map(|f| f()), + ); + check_action( + "struct Foo { <|> a: i32, }", + "#[derive(<|>)]\nstruct Foo { a: i32, }", + |file, off| add_derive(file, off).map(|f| f()), + ); + check_action( + "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", + "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", + |file, off| add_derive(file, off).map(|f| f()), + ); + } + + #[test] + fn test_add_impl() { + check_action( + "struct Foo {<|>}\n", + "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", + |file, off| add_impl(file, off).map(|f| f()), + ); + check_action( + "struct Foo {<|>}", + "struct Foo {}\n\nimpl Foo {\n<|>\n}", + |file, off| add_impl(file, off).map(|f| f()), + ); + check_action( + "struct Foo<'a, T: Foo<'a>> {<|>}", + "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", + |file, off| add_impl(file, off).map(|f| f()), + ); + } + + #[test] + fn test_intrdoduce_var_simple() { + check_action_range( + " +fn foo() { + foo(<|>1 + 1<|>); +}", " +fn foo() { + let <|>var_name = 1 + 1; + foo(var_name); +}", + |file, range| introduce_variable(file, range).map(|f| f()), + ); + } + #[test] + fn test_intrdoduce_var_expr_stmt() { +check_action_range( + " +fn foo() { + <|>1 + 1<|>; +}", " +fn foo() { + let <|>var_name = 1 + 1; +}", + |file, range| introduce_variable(file, range).map(|f| f()), + ); + } + +} diff --git a/crates/ra_editor/src/completion.rs b/crates/ra_editor/src/completion.rs new file mode 100644 index 000000000..5000b32a0 --- /dev/null +++ b/crates/ra_editor/src/completion.rs @@ -0,0 +1,480 @@ +use std::collections::{HashSet, HashMap}; + +use ra_syntax::{ + File, TextUnit, AstNode, SyntaxNodeRef, SyntaxKind::*, + ast::{self, LoopBodyOwner, ModuleItemOwner}, + algo::{ + ancestors, + visit::{visitor, Visitor, visitor_ctx, VisitorCtx}, + }, + text_utils::is_subrange, +}; + +use { + AtomEdit, find_node_at_offset, + scope::{FnScopes, ModuleScope}, +}; + +#[derive(Debug)] +pub struct CompletionItem { + /// What user sees in pop-up + pub label: String, + /// What string is used for filtering, defaults to label + pub lookup: Option, + /// What is inserted, defaults to label + pub snippet: Option +} + +pub fn scope_completion(file: &File, offset: TextUnit) -> Option> { + // Insert a fake ident to get a valid parse tree + let file = { + let edit = AtomEdit::insert(offset, "intellijRulezz".to_string()); + file.reparse(&edit) + }; + let mut has_completions = false; + let mut res = Vec::new(); + if let Some(name_ref) = find_node_at_offset::(file.syntax(), offset) { + has_completions = true; + complete_name_ref(&file, name_ref, &mut res); + // special case, `trait T { fn foo(i_am_a_name_ref) {} }` + if is_node::(name_ref.syntax()) { + param_completions(name_ref.syntax(), &mut res); + } + } + if let Some(name) = find_node_at_offset::(file.syntax(), offset) { + if is_node::(name.syntax()) { + has_completions = true; + param_completions(name.syntax(), &mut res); + } + } + if has_completions { + Some(res) + } else { + None + } +} + +fn complete_name_ref(file: &File, name_ref: ast::NameRef, acc: &mut Vec) { + if !is_node::(name_ref.syntax()) { + return; + } + let mut visited_fn = false; + for node in ancestors(name_ref.syntax()) { + if let Some(items) = visitor() + .visit::(|it| Some(it.items())) + .visit::(|it| Some(it.item_list()?.items())) + .accept(node) { + if let Some(items) = items { + let scope = ModuleScope::new(items); + acc.extend( + scope.entries().iter() + .filter(|entry| entry.syntax() != name_ref.syntax()) + .map(|entry| CompletionItem { + label: entry.name().to_string(), + lookup: None, + snippet: None, + }) + ); + } + break; + + } else if !visited_fn { + if let Some(fn_def) = ast::FnDef::cast(node) { + visited_fn = true; + complete_expr_keywords(&file, fn_def, name_ref, acc); + let scopes = FnScopes::new(fn_def); + complete_fn(name_ref, &scopes, acc); + } + } + } +} + +fn param_completions(ctx: SyntaxNodeRef, acc: &mut Vec) { + let mut params = HashMap::new(); + for node in ancestors(ctx) { + let _ = visitor_ctx(&mut params) + .visit::(process) + .visit::(process) + .accept(node); + } + params.into_iter() + .filter_map(|(label, (count, param))| { + let lookup = param.pat()?.syntax().text().to_string(); + if count < 2 { None } else { Some((label, lookup)) } + }) + .for_each(|(label, lookup)| { + acc.push(CompletionItem { + label, lookup: Some(lookup), snippet: None + }) + }); + + fn process<'a, N: ast::FnDefOwner<'a>>(node: N, params: &mut HashMap)>) { + node.functions() + .filter_map(|it| it.param_list()) + .flat_map(|it| it.params()) + .for_each(|param| { + let text = param.syntax().text().to_string(); + params.entry(text) + .or_insert((0, param)) + .0 += 1; + }) + } +} + +fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool { + match ancestors(node).filter_map(N::cast).next() { + None => false, + Some(n) => n.syntax().range() == node.range(), + } +} + + +fn complete_expr_keywords(file: &File, fn_def: ast::FnDef, name_ref: ast::NameRef, acc: &mut Vec) { + acc.push(keyword("if", "if $0 {}")); + acc.push(keyword("match", "match $0 {}")); + acc.push(keyword("while", "while $0 {}")); + acc.push(keyword("loop", "loop {$0}")); + + if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { + if let Some(if_expr) = find_node_at_offset::(file.syntax(), off) { + if if_expr.syntax().range().end() < name_ref.syntax().range().start() { + acc.push(keyword("else", "else {$0}")); + acc.push(keyword("else if", "else if $0 {}")); + } + } + } + if is_in_loop_body(name_ref) { + acc.push(keyword("continue", "continue")); + acc.push(keyword("break", "break")); + } + acc.extend(complete_return(fn_def, name_ref)); +} + +fn is_in_loop_body(name_ref: ast::NameRef) -> bool { + for node in ancestors(name_ref.syntax()) { + if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR { + break; + } + let loop_body = visitor() + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .accept(node); + if let Some(Some(body)) = loop_body { + if is_subrange(body.syntax().range(), name_ref.syntax().range()) { + return true; + } + } + } + false +} + +fn complete_return(fn_def: ast::FnDef, name_ref: ast::NameRef) -> Option { + // let is_last_in_block = ancestors(name_ref.syntax()).filter_map(ast::Expr::cast) + // .next() + // .and_then(|it| it.syntax().parent()) + // .and_then(ast::Block::cast) + // .is_some(); + + // if is_last_in_block { + // return None; + // } + + let is_stmt = match ancestors(name_ref.syntax()).filter_map(ast::ExprStmt::cast).next() { + None => false, + Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range() + }; + let snip = match (is_stmt, fn_def.ret_type().is_some()) { + (true, true) => "return $0;", + (true, false) => "return;", + (false, true) => "return $0", + (false, false) => "return", + }; + Some(keyword("return", snip)) +} + +fn keyword(kw: &str, snip: &str) -> CompletionItem { + CompletionItem { + label: kw.to_string(), + lookup: None, + snippet: Some(snip.to_string()), + } +} + +fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec) { + let mut shadowed = HashSet::new(); + acc.extend( + scopes.scope_chain(name_ref.syntax()) + .flat_map(|scope| scopes.entries(scope).iter()) + .filter(|entry| shadowed.insert(entry.name())) + .map(|entry| CompletionItem { + label: entry.name().to_string(), + lookup: None, + snippet: None, + }) + ); + if scopes.self_param.is_some() { + acc.push(CompletionItem { + label: "self".to_string(), + lookup: None, + snippet: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::{assert_eq_dbg, extract_offset}; + + fn check_scope_completion(code: &str, expected_completions: &str) { + let (off, code) = extract_offset(&code); + let file = File::parse(&code); + let completions = scope_completion(&file, off) + .unwrap() + .into_iter() + .filter(|c| c.snippet.is_none()) + .collect::>(); + assert_eq_dbg(expected_completions, &completions); + } + + fn check_snippet_completion(code: &str, expected_completions: &str) { + let (off, code) = extract_offset(&code); + let file = File::parse(&code); + let completions = scope_completion(&file, off) + .unwrap() + .into_iter() + .filter(|c| c.snippet.is_some()) + .collect::>(); + assert_eq_dbg(expected_completions, &completions); + } + + #[test] + fn test_completion_let_scope() { + check_scope_completion(r" + fn quux(x: i32) { + let y = 92; + 1 + <|>; + let z = (); + } + ", r#"[CompletionItem { label: "y", lookup: None, snippet: None }, + CompletionItem { label: "x", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#); + } + + #[test] + fn test_completion_if_let_scope() { + check_scope_completion(r" + fn quux() { + if let Some(x) = foo() { + let y = 92; + }; + if let Some(a) = bar() { + let b = 62; + 1 + <|> + } + } + ", r#"[CompletionItem { label: "b", lookup: None, snippet: None }, + CompletionItem { label: "a", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#); + } + + #[test] + fn test_completion_for_scope() { + check_scope_completion(r" + fn quux() { + for x in &[1, 2, 3] { + <|> + } + } + ", r#"[CompletionItem { label: "x", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#); + } + + #[test] + fn test_completion_mod_scope() { + check_scope_completion(r" + struct Foo; + enum Baz {} + fn quux() { + <|> + } + ", r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, + CompletionItem { label: "Baz", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#); + } + + #[test] + fn test_completion_mod_scope_no_self_use() { + check_scope_completion(r" + use foo<|>; + ", r#"[]"#); + } + + #[test] + fn test_completion_mod_scope_nested() { + check_scope_completion(r" + struct Foo; + mod m { + struct Bar; + fn quux() { <|> } + } + ", r#"[CompletionItem { label: "Bar", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#); + } + + #[test] + fn test_complete_type() { + check_scope_completion(r" + struct Foo; + fn x() -> <|> + ", r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, + CompletionItem { label: "x", lookup: None, snippet: None }]"#) + } + + #[test] + fn test_complete_shadowing() { + check_scope_completion(r" + fn foo() -> { + let bar = 92; + { + let bar = 62; + <|> + } + } + ", r#"[CompletionItem { label: "bar", lookup: None, snippet: None }, + CompletionItem { label: "foo", lookup: None, snippet: None }]"#) + } + + #[test] + fn test_complete_self() { + check_scope_completion(r" + impl S { fn foo(&self) { <|> } } + ", r#"[CompletionItem { label: "self", lookup: None, snippet: None }]"#) + } + + #[test] + fn test_completion_kewords() { + check_snippet_completion(r" + fn quux() { + <|> + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return") }]"#); + } + + #[test] + fn test_completion_else() { + check_snippet_completion(r" + fn quux() { + if true { + () + } <|> + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "else", lookup: None, snippet: Some("else {$0}") }, + CompletionItem { label: "else if", lookup: None, snippet: Some("else if $0 {}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return") }]"#); + } + + #[test] + fn test_completion_return_value() { + check_snippet_completion(r" + fn quux() -> i32 { + <|> + 92 + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0;") }]"#); + check_snippet_completion(r" + fn quux() { + <|> + 92 + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return;") }]"#); + } + + #[test] + fn test_completion_return_no_stmt() { + check_snippet_completion(r" + fn quux() -> i32 { + match () { + () => <|> + } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }]"#); + } + + #[test] + fn test_continue_break_completion() { + check_snippet_completion(r" + fn quux() -> i32 { + loop { <|> } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "continue", lookup: None, snippet: Some("continue") }, + CompletionItem { label: "break", lookup: None, snippet: Some("break") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }]"#); + check_snippet_completion(r" + fn quux() -> i32 { + loop { || { <|> } } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }]"#); + } + + #[test] + fn test_param_completion_last_param() { + check_scope_completion(r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>) {} + ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_param_completion_nth_param() { + check_scope_completion(r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>, x: i32) {} + ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_param_completion_trait_param() { + check_scope_completion(r" + pub(crate) trait SourceRoot { + pub fn contains(&self, file_id: FileId) -> bool; + pub fn module_map(&self) -> &ModuleMap; + pub fn lines(&self, file_id: FileId) -> &LineIndex; + pub fn syntax(&self, file<|>) + } + ", r#"[CompletionItem { label: "self", lookup: None, snippet: None }, + CompletionItem { label: "SourceRoot", lookup: None, snippet: None }, + CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } +} diff --git a/crates/ra_editor/src/edit.rs b/crates/ra_editor/src/edit.rs new file mode 100644 index 000000000..2839ac20a --- /dev/null +++ b/crates/ra_editor/src/edit.rs @@ -0,0 +1,84 @@ +use {TextRange, TextUnit}; +use ra_syntax::{ + AtomEdit, + text_utils::contains_offset_nonstrict, +}; + +#[derive(Debug, Clone)] +pub struct Edit { + atoms: Vec, +} + +#[derive(Debug)] +pub struct EditBuilder { + atoms: Vec +} + +impl EditBuilder { + pub fn new() -> EditBuilder { + EditBuilder { atoms: Vec::new() } + } + pub fn replace(&mut self, range: TextRange, replace_with: String) { + self.atoms.push(AtomEdit::replace(range, replace_with)) + } + pub fn delete(&mut self, range: TextRange) { + self.atoms.push(AtomEdit::delete(range)) + } + pub fn insert(&mut self, offset: TextUnit, text: String) { + self.atoms.push(AtomEdit::insert(offset, text)) + } + pub fn finish(self) -> Edit { + let mut atoms = self.atoms; + atoms.sort_by_key(|a| a.delete.start()); + for (a1, a2) in atoms.iter().zip(atoms.iter().skip(1)) { + assert!(a1.delete.end() <= a2.delete.start()) + } + Edit { atoms } + } + pub fn invalidates_offset(&self, offset: TextUnit) -> bool { + self.atoms.iter().any(|atom| contains_offset_nonstrict(atom.delete, offset)) + } +} + +impl Edit { + pub fn into_atoms(self) -> Vec { + self.atoms + } + + pub fn apply(&self, text: &str) -> String { + let mut total_len = text.len(); + for atom in self.atoms.iter() { + total_len += atom.insert.len(); + total_len -= u32::from(atom.delete.end() - atom.delete.start()) as usize; + } + let mut buf = String::with_capacity(total_len); + let mut prev = 0; + for atom in self.atoms.iter() { + let start = u32::from(atom.delete.start()) as usize; + let end = u32::from(atom.delete.end()) as usize; + if start > prev { + buf.push_str(&text[prev..start]); + } + buf.push_str(&atom.insert); + prev = end; + } + buf.push_str(&text[prev..text.len()]); + assert_eq!(buf.len(), total_len); + buf + } + + pub fn apply_to_offset(&self, offset: TextUnit) -> Option { + let mut res = offset; + for atom in self.atoms.iter() { + if atom.delete.start() >= offset { + break; + } + if offset < atom.delete.end() { + return None + } + res += TextUnit::of_str(&atom.insert); + res -= atom.delete.len(); + } + Some(res) + } +} diff --git a/crates/ra_editor/src/extend_selection.rs b/crates/ra_editor/src/extend_selection.rs new file mode 100644 index 000000000..5fd1ca4fc --- /dev/null +++ b/crates/ra_editor/src/extend_selection.rs @@ -0,0 +1,167 @@ +use ra_syntax::{ + File, TextRange, SyntaxNodeRef, TextUnit, + SyntaxKind::*, + algo::{find_leaf_at_offset, LeafAtOffset, find_covering_node, ancestors, Direction, siblings}, +}; + +pub fn extend_selection(file: &File, range: TextRange) -> Option { + let syntax = file.syntax(); + extend(syntax.borrowed(), range) +} + +pub(crate) fn extend(root: SyntaxNodeRef, range: TextRange) -> Option { + if range.is_empty() { + let offset = range.start(); + let mut leaves = find_leaf_at_offset(root, offset); + if leaves.clone().all(|it| it.kind() == WHITESPACE) { + return Some(extend_ws(root, leaves.next()?, offset)); + } + let leaf = match leaves { + LeafAtOffset::None => return None, + LeafAtOffset::Single(l) => l, + LeafAtOffset::Between(l, r) => pick_best(l, r), + }; + return Some(leaf.range()); + }; + let node = find_covering_node(root, range); + if node.kind() == COMMENT && range == node.range() { + if let Some(range) = extend_comments(node) { + return Some(range); + } + } + + match ancestors(node).skip_while(|n| n.range() == range).next() { + None => None, + Some(parent) => Some(parent.range()), + } +} + +fn extend_ws(root: SyntaxNodeRef, ws: SyntaxNodeRef, offset: TextUnit) -> TextRange { + let ws_text = ws.leaf_text().unwrap(); + let suffix = TextRange::from_to(offset, ws.range().end()) - ws.range().start(); + let prefix = TextRange::from_to(ws.range().start(), offset) - ws.range().start(); + let ws_suffix = &ws_text.as_str()[suffix]; + let ws_prefix = &ws_text.as_str()[prefix]; + if ws_text.contains("\n") && !ws_suffix.contains("\n") { + if let Some(node) = ws.next_sibling() { + let start = match ws_prefix.rfind('\n') { + Some(idx) => ws.range().start() + TextUnit::from((idx + 1) as u32), + None => node.range().start() + }; + let end = if root.text().char_at(node.range().end()) == Some('\n') { + node.range().end() + TextUnit::of_char('\n') + } else { + node.range().end() + }; + return TextRange::from_to(start, end); + } + } + ws.range() +} + +fn pick_best<'a>(l: SyntaxNodeRef<'a>, r: SyntaxNodeRef<'a>) -> SyntaxNodeRef<'a> { + return if priority(r) > priority(l) { r } else { l }; + fn priority(n: SyntaxNodeRef) -> usize { + match n.kind() { + WHITESPACE => 0, + IDENT | SELF_KW | SUPER_KW | CRATE_KW => 2, + _ => 1, + } + } +} + +fn extend_comments(node: SyntaxNodeRef) -> Option { + let left = adj_comments(node, Direction::Backward); + let right = adj_comments(node, Direction::Forward); + if left != right { + Some(TextRange::from_to( + left.range().start(), + right.range().end(), + )) + } else { + None + } +} + +fn adj_comments(node: SyntaxNodeRef, dir: Direction) -> SyntaxNodeRef { + let mut res = node; + for node in siblings(node, dir) { + match node.kind() { + COMMENT => res = node, + WHITESPACE if !node.leaf_text().unwrap().as_str().contains("\n\n") => (), + _ => break + } + } + res +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::extract_offset; + + fn do_check(before: &str, afters: &[&str]) { + let (cursor, before) = extract_offset(before); + let file = File::parse(&before); + let mut range = TextRange::offset_len(cursor, 0.into()); + for &after in afters { + range = extend_selection(&file, range) + .unwrap(); + let actual = &before[range]; + assert_eq!(after, actual); + } + } + + #[test] + fn test_extend_selection_arith() { + do_check( + r#"fn foo() { <|>1 + 1 }"#, + &["1", "1 + 1", "{ 1 + 1 }"], + ); + } + + #[test] + fn test_extend_selection_start_of_the_lind() { + do_check( + r#" +impl S { +<|> fn foo() { + + } +}"#, + &[" fn foo() {\n\n }\n"] + ); + } + + #[test] + fn test_extend_selection_comments() { + do_check( + r#" +fn bar(){} + +// fn foo() { +// 1 + <|>1 +// } + +// fn foo(){} + "#, + &["// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"] + ); + } + + #[test] + fn test_extend_selection_prefer_idents() { + do_check( + r#" +fn main() { foo<|>+bar;} + "#, + &["foo", "foo+bar"] + ); + do_check( + r#" +fn main() { foo+<|>bar;} + "#, + &["bar", "foo+bar"] + ); + } +} diff --git a/crates/ra_editor/src/lib.rs b/crates/ra_editor/src/lib.rs new file mode 100644 index 000000000..78ed34c7c --- /dev/null +++ b/crates/ra_editor/src/lib.rs @@ -0,0 +1,228 @@ +extern crate ra_syntax; +extern crate superslice; +extern crate itertools; +extern crate join_to_string; +#[cfg(test)] +#[macro_use] +extern crate test_utils as _test_utils; + +mod extend_selection; +mod symbols; +mod line_index; +mod edit; +mod code_actions; +mod typing; +mod completion; +mod scope; +#[cfg(test)] +mod test_utils; + +use ra_syntax::{ + File, TextUnit, TextRange, SyntaxNodeRef, + ast::{self, AstNode, NameOwner}, + algo::{walk, find_leaf_at_offset, ancestors}, + SyntaxKind::{self, *}, +}; +pub use ra_syntax::AtomEdit; +pub use self::{ + line_index::{LineIndex, LineCol}, + extend_selection::extend_selection, + symbols::{StructureNode, file_structure, FileSymbol, file_symbols}, + edit::{EditBuilder, Edit}, + code_actions::{ + LocalEdit, + flip_comma, add_derive, add_impl, + introduce_variable, + }, + typing::{join_lines, on_eq_typed}, + completion::{scope_completion, CompletionItem}, +}; + +#[derive(Debug)] +pub struct HighlightedRange { + pub range: TextRange, + pub tag: &'static str, +} + +#[derive(Debug)] +pub struct Diagnostic { + pub range: TextRange, + pub msg: String, +} + +#[derive(Debug)] +pub struct Runnable { + pub range: TextRange, + pub kind: RunnableKind, +} + +#[derive(Debug)] +pub enum RunnableKind { + Test { name: String }, + Bin, +} + +pub fn matching_brace(file: &File, offset: TextUnit) -> Option { + const BRACES: &[SyntaxKind] = &[ + L_CURLY, R_CURLY, + L_BRACK, R_BRACK, + L_PAREN, R_PAREN, + L_ANGLE, R_ANGLE, + ]; + let (brace_node, brace_idx) = find_leaf_at_offset(file.syntax(), offset) + .filter_map(|node| { + let idx = BRACES.iter().position(|&brace| brace == node.kind())?; + Some((node, idx)) + }) + .next()?; + let parent = brace_node.parent()?; + let matching_kind = BRACES[brace_idx ^ 1]; + let matching_node = parent.children() + .find(|node| node.kind() == matching_kind)?; + Some(matching_node.range().start()) +} + +pub fn highlight(file: &File) -> Vec { + let mut res = Vec::new(); + for node in walk::preorder(file.syntax()) { + let tag = match node.kind() { + ERROR => "error", + COMMENT | DOC_COMMENT => "comment", + STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", + ATTR => "attribute", + NAME_REF => "text", + NAME => "function", + INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", + LIFETIME => "parameter", + k if k.is_keyword() => "keyword", + _ => continue, + }; + res.push(HighlightedRange { + range: node.range(), + tag, + }) + } + res +} + +pub fn diagnostics(file: &File) -> Vec { + let mut res = Vec::new(); + + for node in walk::preorder(file.syntax()) { + if node.kind() == ERROR { + res.push(Diagnostic { + range: node.range(), + msg: "Syntax Error".to_string(), + }); + } + } + res.extend(file.errors().into_iter().map(|err| Diagnostic { + range: TextRange::offset_len(err.offset, 1.into()), + msg: err.msg, + })); + res +} + +pub fn syntax_tree(file: &File) -> String { + ::ra_syntax::utils::dump_tree(file.syntax()) +} + +pub fn runnables(file: &File) -> Vec { + walk::preorder(file.syntax()) + .filter_map(ast::FnDef::cast) + .filter_map(|f| { + let name = f.name()?.text(); + let kind = if name == "main" { + RunnableKind::Bin + } else if f.has_atom_attr("test") { + RunnableKind::Test { + name: name.to_string() + } + } else { + return None; + }; + Some(Runnable { + range: f.syntax().range(), + kind, + }) + }) + .collect() +} + +pub fn find_node_at_offset<'a, N: AstNode<'a>>( + syntax: SyntaxNodeRef<'a>, + offset: TextUnit, +) -> Option { + let leaves = find_leaf_at_offset(syntax, offset); + let leaf = leaves.clone() + .find(|leaf| !leaf.kind().is_trivia()) + .or_else(|| leaves.right_biased())?; + ancestors(leaf) + .filter_map(N::cast) + .next() +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::{assert_eq_dbg, extract_offset, add_cursor}; + + #[test] + fn test_highlighting() { + let file = File::parse(r#" +// comment +fn main() {} + println!("Hello, {}!", 92); +"#); + let hls = highlight(&file); + assert_eq_dbg( + r#"[HighlightedRange { range: [1; 11), tag: "comment" }, + HighlightedRange { range: [12; 14), tag: "keyword" }, + HighlightedRange { range: [15; 19), tag: "function" }, + HighlightedRange { range: [29; 36), tag: "text" }, + HighlightedRange { range: [38; 50), tag: "string" }, + HighlightedRange { range: [52; 54), tag: "literal" }]"#, + &hls, + ); + } + + #[test] + fn test_runnables() { + let file = File::parse(r#" +fn main() {} + +#[test] +fn test_foo() {} + +#[test] +#[ignore] +fn test_foo() {} +"#); + let runnables = runnables(&file); + assert_eq_dbg( + r#"[Runnable { range: [1; 13), kind: Bin }, + Runnable { range: [15; 39), kind: Test { name: "test_foo" } }, + Runnable { range: [41; 75), kind: Test { name: "test_foo" } }]"#, + &runnables, + ) + } + + #[test] + fn test_matching_brace() { + fn do_check(before: &str, after: &str) { + let (pos, before) = extract_offset(before); + let file = File::parse(&before); + let new_pos = match matching_brace(&file, pos) { + None => pos, + Some(pos) => pos, + }; + let actual = add_cursor(&before, new_pos); + assert_eq_text!(after, &actual); + } + + do_check( + "struct Foo { a: i32, }<|>", + "struct Foo <|>{ a: i32, }", + ); + } +} diff --git a/crates/ra_editor/src/line_index.rs b/crates/ra_editor/src/line_index.rs new file mode 100644 index 000000000..9cd8da3a8 --- /dev/null +++ b/crates/ra_editor/src/line_index.rs @@ -0,0 +1,62 @@ +use superslice::Ext; +use ::TextUnit; + +#[derive(Clone, Debug, Hash)] +pub struct LineIndex { + newlines: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct LineCol { + pub line: u32, + pub col: TextUnit, +} + +impl LineIndex { + pub fn new(text: &str) -> LineIndex { + let mut newlines = vec![0.into()]; + let mut curr = 0.into(); + for c in text.chars() { + curr += TextUnit::of_char(c); + if c == '\n' { + newlines.push(curr); + } + } + LineIndex { newlines } + } + + pub fn line_col(&self, offset: TextUnit) -> LineCol { + let line = self.newlines.upper_bound(&offset) - 1; + let line_start_offset = self.newlines[line]; + let col = offset - line_start_offset; + return LineCol { line: line as u32, col }; + } + + pub fn offset(&self, line_col: LineCol) -> TextUnit { + //TODO: return Result + self.newlines[line_col.line as usize] + line_col.col + } +} + +#[test] +fn test_line_index() { + let text = "hello\nworld"; + let index = LineIndex::new(text); + assert_eq!(index.line_col(0.into()), LineCol { line: 0, col: 0.into() }); + assert_eq!(index.line_col(1.into()), LineCol { line: 0, col: 1.into() }); + assert_eq!(index.line_col(5.into()), LineCol { line: 0, col: 5.into() }); + assert_eq!(index.line_col(6.into()), LineCol { line: 1, col: 0.into() }); + assert_eq!(index.line_col(7.into()), LineCol { line: 1, col: 1.into() }); + assert_eq!(index.line_col(8.into()), LineCol { line: 1, col: 2.into() }); + assert_eq!(index.line_col(10.into()), LineCol { line: 1, col: 4.into() }); + assert_eq!(index.line_col(11.into()), LineCol { line: 1, col: 5.into() }); + assert_eq!(index.line_col(12.into()), LineCol { line: 1, col: 6.into() }); + + let text = "\nhello\nworld"; + let index = LineIndex::new(text); + assert_eq!(index.line_col(0.into()), LineCol { line: 0, col: 0.into() }); + assert_eq!(index.line_col(1.into()), LineCol { line: 1, col: 0.into() }); + assert_eq!(index.line_col(2.into()), LineCol { line: 1, col: 1.into() }); + assert_eq!(index.line_col(6.into()), LineCol { line: 1, col: 5.into() }); + assert_eq!(index.line_col(7.into()), LineCol { line: 2, col: 0.into() }); +} diff --git a/crates/ra_editor/src/scope/fn_scope.rs b/crates/ra_editor/src/scope/fn_scope.rs new file mode 100644 index 000000000..3ae5276a2 --- /dev/null +++ b/crates/ra_editor/src/scope/fn_scope.rs @@ -0,0 +1,329 @@ +use std::{ + fmt, + collections::HashMap, +}; + +use ra_syntax::{ + SyntaxNodeRef, SyntaxNode, SmolStr, AstNode, + ast::{self, NameOwner, LoopBodyOwner, ArgListOwner}, + algo::{ancestors, generate, walk::preorder} +}; + +type ScopeId = usize; + +#[derive(Debug)] +pub struct FnScopes { + pub self_param: Option, + scopes: Vec, + scope_for: HashMap, +} + +impl FnScopes { + pub fn new(fn_def: ast::FnDef) -> FnScopes { + let mut scopes = FnScopes { + self_param: fn_def.param_list() + .and_then(|it| it.self_param()) + .map(|it| it.syntax().owned()), + scopes: Vec::new(), + scope_for: HashMap::new() + }; + let root = scopes.root_scope(); + scopes.add_params_bindings(root, fn_def.param_list()); + if let Some(body) = fn_def.body() { + compute_block_scopes(body, &mut scopes, root) + } + scopes + } + pub fn entries(&self, scope: ScopeId) -> &[ScopeEntry] { + &self.scopes[scope].entries + } + pub fn scope_chain<'a>(&'a self, node: SyntaxNodeRef) -> impl Iterator + 'a { + generate(self.scope_for(node), move |&scope| self.scopes[scope].parent) + } + fn root_scope(&mut self) -> ScopeId { + let res = self.scopes.len(); + self.scopes.push(ScopeData { parent: None, entries: vec![] }); + res + } + fn new_scope(&mut self, parent: ScopeId) -> ScopeId { + let res = self.scopes.len(); + self.scopes.push(ScopeData { parent: Some(parent), entries: vec![] }); + res + } + fn add_bindings(&mut self, scope: ScopeId, pat: ast::Pat) { + let entries = preorder(pat.syntax()) + .filter_map(ast::BindPat::cast) + .filter_map(ScopeEntry::new); + self.scopes[scope].entries.extend(entries); + } + fn add_params_bindings(&mut self, scope: ScopeId, params: Option) { + params.into_iter() + .flat_map(|it| it.params()) + .filter_map(|it| it.pat()) + .for_each(|it| self.add_bindings(scope, it)); + } + fn set_scope(&mut self, node: SyntaxNodeRef, scope: ScopeId) { + self.scope_for.insert(node.owned(), scope); + } + fn scope_for(&self, node: SyntaxNodeRef) -> Option { + ancestors(node) + .filter_map(|it| self.scope_for.get(&it.owned()).map(|&scope| scope)) + .next() + } +} + +pub struct ScopeEntry { + syntax: SyntaxNode +} + +impl ScopeEntry { + fn new(pat: ast::BindPat) -> Option { + if pat.name().is_some() { + Some(ScopeEntry { syntax: pat.syntax().owned() }) + } else { + None + } + } + pub fn name(&self) -> SmolStr { + self.ast().name() + .unwrap() + .text() + } + fn ast(&self) -> ast::BindPat { + ast::BindPat::cast(self.syntax.borrowed()) + .unwrap() + } +} + +impl fmt::Debug for ScopeEntry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("ScopeEntry") + .field("name", &self.name()) + .field("syntax", &self.syntax) + .finish() + } +} + +fn compute_block_scopes(block: ast::Block, scopes: &mut FnScopes, mut scope: ScopeId) { + for stmt in block.statements() { + match stmt { + ast::Stmt::LetStmt(stmt) => { + if let Some(expr) = stmt.initializer() { + scopes.set_scope(expr.syntax(), scope); + compute_expr_scopes(expr, scopes, scope); + } + scope = scopes.new_scope(scope); + if let Some(pat) = stmt.pat() { + scopes.add_bindings(scope, pat); + } + } + ast::Stmt::ExprStmt(expr_stmt) => { + if let Some(expr) = expr_stmt.expr() { + scopes.set_scope(expr.syntax(), scope); + compute_expr_scopes(expr, scopes, scope); + } + } + } + } + if let Some(expr) = block.expr() { + scopes.set_scope(expr.syntax(), scope); + compute_expr_scopes(expr, scopes, scope); + } +} + +fn compute_expr_scopes(expr: ast::Expr, scopes: &mut FnScopes, scope: ScopeId) { + match expr { + ast::Expr::IfExpr(e) => { + let cond_scope = e.condition().and_then(|cond| { + compute_cond_scopes(cond, scopes, scope) + }); + if let Some(block) = e.then_branch() { + compute_block_scopes(block, scopes, cond_scope.unwrap_or(scope)); + } + if let Some(block) = e.else_branch() { + compute_block_scopes(block, scopes, scope); + } + }, + ast::Expr::BlockExpr(e) => { + if let Some(block) = e.block() { + compute_block_scopes(block, scopes, scope); + } + } + ast::Expr::LoopExpr(e) => { + if let Some(block) = e.loop_body() { + compute_block_scopes(block, scopes, scope); + } + } + ast::Expr::WhileExpr(e) => { + let cond_scope = e.condition().and_then(|cond| { + compute_cond_scopes(cond, scopes, scope) + }); + if let Some(block) = e.loop_body() { + compute_block_scopes(block, scopes, cond_scope.unwrap_or(scope)); + } + } + ast::Expr::ForExpr(e) => { + if let Some(expr) = e.iterable() { + compute_expr_scopes(expr, scopes, scope); + } + let mut scope = scope; + if let Some(pat) = e.pat() { + scope = scopes.new_scope(scope); + scopes.add_bindings(scope, pat); + } + if let Some(block) = e.loop_body() { + compute_block_scopes(block, scopes, scope); + } + } + ast::Expr::LambdaExpr(e) => { + let mut scope = scopes.new_scope(scope); + scopes.add_params_bindings(scope, e.param_list()); + if let Some(body) = e.body() { + scopes.set_scope(body.syntax(), scope); + compute_expr_scopes(body, scopes, scope); + } + } + ast::Expr::CallExpr(e) => { + compute_call_scopes(e.expr(), e.arg_list(), scopes, scope); + } + ast::Expr::MethodCallExpr(e) => { + compute_call_scopes(e.expr(), e.arg_list(), scopes, scope); + } + ast::Expr::MatchExpr(e) => { + if let Some(expr) = e.expr() { + compute_expr_scopes(expr, scopes, scope); + } + for arm in e.match_arm_list().into_iter().flat_map(|it| it.arms()) { + let scope = scopes.new_scope(scope); + for pat in arm.pats() { + scopes.add_bindings(scope, pat); + } + if let Some(expr) = arm.expr() { + compute_expr_scopes(expr, scopes, scope); + } + } + } + _ => { + expr.syntax().children() + .filter_map(ast::Expr::cast) + .for_each(|expr| compute_expr_scopes(expr, scopes, scope)) + } + }; + + fn compute_call_scopes( + receiver: Option, + arg_list: Option, + scopes: &mut FnScopes, scope: ScopeId, + ) { + arg_list.into_iter() + .flat_map(|it| it.args()) + .chain(receiver) + .for_each(|expr| compute_expr_scopes(expr, scopes, scope)); + } + + fn compute_cond_scopes(cond: ast::Condition, scopes: &mut FnScopes, scope: ScopeId) -> Option { + if let Some(expr) = cond.expr() { + compute_expr_scopes(expr, scopes, scope); + } + if let Some(pat) = cond.pat() { + let s = scopes.new_scope(scope); + scopes.add_bindings(s, pat); + Some(s) + } else { + None + } + } +} + +#[derive(Debug)] +struct ScopeData { + parent: Option, + entries: Vec +} + +#[cfg(test)] +mod tests { + use super::*; + use ra_syntax::File; + use {find_node_at_offset, test_utils::extract_offset}; + + fn do_check(code: &str, expected: &[&str]) { + let (off, code) = extract_offset(code); + let code = { + let mut buf = String::new(); + let off = u32::from(off) as usize; + buf.push_str(&code[..off]); + buf.push_str("marker"); + buf.push_str(&code[off..]); + buf + }; + let file = File::parse(&code); + let marker: ast::PathExpr = find_node_at_offset(file.syntax(), off).unwrap(); + let fn_def: ast::FnDef = find_node_at_offset(file.syntax(), off).unwrap(); + let scopes = FnScopes::new(fn_def); + let actual = scopes.scope_chain(marker.syntax()) + .flat_map(|scope| scopes.entries(scope)) + .map(|it| it.name()) + .collect::>(); + assert_eq!(expected, actual.as_slice()); + } + + #[test] + fn test_lambda_scope() { + do_check(r" + fn quux(foo: i32) { + let f = |bar, baz: i32| { + <|> + }; + }", + &["bar", "baz", "foo"], + ); + } + + #[test] + fn test_call_scope() { + do_check(r" + fn quux() { + f(|x| <|> ); + }", + &["x"], + ); + } + + #[test] + fn test_metod_call_scope() { + do_check(r" + fn quux() { + z.f(|x| <|> ); + }", + &["x"], + ); + } + + #[test] + fn test_loop_scope() { + do_check(r" + fn quux() { + loop { + let x = (); + <|> + }; + }", + &["x"], + ); + } + + #[test] + fn test_match() { + do_check(r" + fn quux() { + match () { + Some(x) => { + <|> + } + }; + }", + &["x"], + ); + } +} diff --git a/crates/ra_editor/src/scope/mod.rs b/crates/ra_editor/src/scope/mod.rs new file mode 100644 index 000000000..2f25230f8 --- /dev/null +++ b/crates/ra_editor/src/scope/mod.rs @@ -0,0 +1,8 @@ +mod fn_scope; +mod mod_scope; + +pub use self::{ + fn_scope::FnScopes, + mod_scope::ModuleScope, +}; + diff --git a/crates/ra_editor/src/scope/mod_scope.rs b/crates/ra_editor/src/scope/mod_scope.rs new file mode 100644 index 000000000..d2a3e7c58 --- /dev/null +++ b/crates/ra_editor/src/scope/mod_scope.rs @@ -0,0 +1,115 @@ +use ra_syntax::{ + AstNode, SyntaxNode, SyntaxNodeRef, SmolStr, + ast::{self, AstChildren}, +}; + +pub struct ModuleScope { + entries: Vec, +} + +pub struct Entry { + node: SyntaxNode, + kind: EntryKind, +} + +enum EntryKind { + Item, Import, +} + +impl ModuleScope { + pub fn new(items: AstChildren) -> ModuleScope { + let mut entries = Vec::new(); + for item in items { + let entry = match item { + ast::ModuleItem::StructDef(item) => Entry::new(item), + ast::ModuleItem::EnumDef(item) => Entry::new(item), + ast::ModuleItem::FnDef(item) => Entry::new(item), + ast::ModuleItem::ConstDef(item) => Entry::new(item), + ast::ModuleItem::StaticDef(item) => Entry::new(item), + ast::ModuleItem::TraitDef(item) => Entry::new(item), + ast::ModuleItem::TypeDef(item) => Entry::new(item), + ast::ModuleItem::Module(item) => Entry::new(item), + ast::ModuleItem::UseItem(item) => { + if let Some(tree) = item.use_tree() { + collect_imports(tree, &mut entries); + } + continue; + }, + ast::ModuleItem::ExternCrateItem(_) | + ast::ModuleItem::ImplItem(_) => continue, + }; + entries.extend(entry) + } + + ModuleScope { entries } + } + + pub fn entries(&self) -> &[Entry] { + self.entries.as_slice() + } +} + +impl Entry { + fn new<'a>(item: impl ast::NameOwner<'a>) -> Option { + let name = item.name()?; + Some(Entry { node: name.syntax().owned(), kind: EntryKind::Item }) + } + fn new_import(path: ast::Path) -> Option { + let name_ref = path.segment()?.name_ref()?; + Some(Entry { node: name_ref.syntax().owned(), kind: EntryKind::Import }) + } + pub fn name(&self) -> SmolStr { + match self.kind { + EntryKind::Item => + ast::Name::cast(self.node.borrowed()).unwrap() + .text(), + EntryKind::Import => + ast::NameRef::cast(self.node.borrowed()).unwrap() + .text(), + } + } + pub fn syntax(&self) -> SyntaxNodeRef { + self.node.borrowed() + } +} + +fn collect_imports(tree: ast::UseTree, acc: &mut Vec) { + if let Some(use_tree_list) = tree.use_tree_list() { + return use_tree_list.use_trees().for_each(|it| collect_imports(it, acc)); + } + if let Some(path) = tree.path() { + acc.extend(Entry::new_import(path)); + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use ra_syntax::{File, ast::ModuleItemOwner}; + + fn do_check(code: &str, expected: &[&str]) { + let file = File::parse(&code); + let scope = ModuleScope::new(file.ast().items()); + let actual = scope.entries + .iter() + .map(|it| it.name()) + .collect::>(); + assert_eq!(expected, actual.as_slice()); + } + + #[test] + fn test_module_scope() { + do_check(" + struct Foo; + enum Bar {} + mod baz {} + fn quux() {} + use x::{ + y::z, + t, + }; + type T = (); + ", &["Foo", "Bar", "baz", "quux", "z", "t", "T"]) + } +} diff --git a/crates/ra_editor/src/symbols.rs b/crates/ra_editor/src/symbols.rs new file mode 100644 index 000000000..917984177 --- /dev/null +++ b/crates/ra_editor/src/symbols.rs @@ -0,0 +1,167 @@ +use ra_syntax::{ + SyntaxKind, SyntaxNodeRef, AstNode, File, SmolStr, + ast::{self, NameOwner}, + algo::{ + visit::{visitor, Visitor}, + walk::{walk, WalkEvent, preorder}, + }, +}; +use TextRange; + +#[derive(Debug, Clone)] +pub struct StructureNode { + pub parent: Option, + pub label: String, + pub navigation_range: TextRange, + pub node_range: TextRange, + pub kind: SyntaxKind, +} + +#[derive(Debug, Clone, Hash)] +pub struct FileSymbol { + pub name: SmolStr, + pub node_range: TextRange, + pub kind: SyntaxKind, +} + +pub fn file_symbols(file: &File) -> Vec { + preorder(file.syntax()) + .filter_map(to_symbol) + .collect() +} + +fn to_symbol(node: SyntaxNodeRef) -> Option { + fn decl<'a, N: NameOwner<'a>>(node: N) -> Option { + let name = node.name()?; + Some(FileSymbol { + name: name.text(), + node_range: node.syntax().range(), + kind: node.syntax().kind(), + }) + } + visitor() + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .accept(node)? +} + + +pub fn file_structure(file: &File) -> Vec { + let mut res = Vec::new(); + let mut stack = Vec::new(); + + for event in walk(file.syntax()) { + match event { + WalkEvent::Enter(node) => { + match structure_node(node) { + Some(mut symbol) => { + symbol.parent = stack.last().map(|&n| n); + stack.push(res.len()); + res.push(symbol); + } + None => (), + } + } + WalkEvent::Exit(node) => { + if structure_node(node).is_some() { + stack.pop().unwrap(); + } + } + } + } + res +} + +fn structure_node(node: SyntaxNodeRef) -> Option { + fn decl<'a, N: NameOwner<'a>>(node: N) -> Option { + let name = node.name()?; + Some(StructureNode { + parent: None, + label: name.text().to_string(), + navigation_range: name.syntax().range(), + node_range: node.syntax().range(), + kind: node.syntax().kind(), + }) + } + + visitor() + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(|im: ast::ImplItem| { + let target_type = im.target_type()?; + let target_trait = im.target_trait(); + let label = match target_trait { + None => format!("impl {}", target_type.syntax().text()), + Some(t) => format!( + "impl {} for {}", + t.syntax().text(), + target_type.syntax().text(), + ), + }; + + let node = StructureNode { + parent: None, + label, + navigation_range: target_type.syntax().range(), + node_range: im.syntax().range(), + kind: im.syntax().kind(), + }; + Some(node) + }) + .accept(node)? +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::assert_eq_dbg; + + #[test] + fn test_file_structure() { + let file = File::parse(r#" +struct Foo { + x: i32 +} + +mod m { + fn bar() {} +} + +enum E { X, Y(i32) } +type T = (); +static S: i32 = 92; +const C: i32 = 92; + +impl E {} + +impl fmt::Debug for E {} +"#); + let symbols = file_structure(&file); + assert_eq_dbg( + r#"[StructureNode { parent: None, label: "Foo", navigation_range: [8; 11), node_range: [1; 26), kind: STRUCT_DEF }, + StructureNode { parent: Some(0), label: "x", navigation_range: [18; 19), node_range: [18; 24), kind: NAMED_FIELD_DEF }, + StructureNode { parent: None, label: "m", navigation_range: [32; 33), node_range: [28; 53), kind: MODULE }, + StructureNode { parent: Some(2), label: "bar", navigation_range: [43; 46), node_range: [40; 51), kind: FN_DEF }, + StructureNode { parent: None, label: "E", navigation_range: [60; 61), node_range: [55; 75), kind: ENUM_DEF }, + StructureNode { parent: None, label: "T", navigation_range: [81; 82), node_range: [76; 88), kind: TYPE_DEF }, + StructureNode { parent: None, label: "S", navigation_range: [96; 97), node_range: [89; 108), kind: STATIC_DEF }, + StructureNode { parent: None, label: "C", navigation_range: [115; 116), node_range: [109; 127), kind: CONST_DEF }, + StructureNode { parent: None, label: "impl E", navigation_range: [134; 135), node_range: [129; 138), kind: IMPL_ITEM }, + StructureNode { parent: None, label: "impl fmt::Debug for E", navigation_range: [160; 161), node_range: [140; 164), kind: IMPL_ITEM }]"#, + &symbols, + ) + } +} diff --git a/crates/ra_editor/src/test_utils.rs b/crates/ra_editor/src/test_utils.rs new file mode 100644 index 000000000..c4ea4db6c --- /dev/null +++ b/crates/ra_editor/src/test_utils.rs @@ -0,0 +1,37 @@ +use ra_syntax::{File, TextUnit, TextRange}; +pub use _test_utils::*; +use LocalEdit; + +pub fn check_action Option> ( + before: &str, + after: &str, + f: F, +) { + let (before_cursor_pos, before) = extract_offset(before); + let file = File::parse(&before); + let result = f(&file, before_cursor_pos).expect("code action is not applicable"); + let actual = result.edit.apply(&before); + let actual_cursor_pos = match result.cursor_position { + None => result.edit.apply_to_offset(before_cursor_pos).unwrap(), + Some(off) => off, + }; + let actual = add_cursor(&actual, actual_cursor_pos); + assert_eq_text!(after, &actual); +} + +pub fn check_action_range Option> ( + before: &str, + after: &str, + f: F, +) { + let (range, before) = extract_range(before); + let file = File::parse(&before); + let result = f(&file, range).expect("code action is not applicable"); + let actual = result.edit.apply(&before); + let actual_cursor_pos = match result.cursor_position { + None => result.edit.apply_to_offset(range.start()).unwrap(), + Some(off) => off, + }; + let actual = add_cursor(&actual, actual_cursor_pos); + assert_eq_text!(after, &actual); +} diff --git a/crates/ra_editor/src/typing.rs b/crates/ra_editor/src/typing.rs new file mode 100644 index 000000000..0f4e7e0d0 --- /dev/null +++ b/crates/ra_editor/src/typing.rs @@ -0,0 +1,348 @@ +use std::mem; + +use ra_syntax::{ + TextUnit, TextRange, SyntaxNodeRef, File, AstNode, SyntaxKind, + ast, + algo::{ + walk::preorder, + find_covering_node, + }, + text_utils::{intersect, contains_offset_nonstrict}, + SyntaxKind::*, +}; + +use {LocalEdit, EditBuilder, find_node_at_offset}; + +pub fn join_lines(file: &File, range: TextRange) -> LocalEdit { + let range = if range.is_empty() { + let syntax = file.syntax(); + let text = syntax.text().slice(range.start()..); + let pos = match text.find('\n') { + None => return LocalEdit { + edit: EditBuilder::new().finish(), + cursor_position: None + }, + Some(pos) => pos + }; + TextRange::offset_len( + range.start() + pos, + TextUnit::of_char('\n'), + ) + } else { + range + }; + let node = find_covering_node(file.syntax(), range); + let mut edit = EditBuilder::new(); + for node in preorder(node) { + let text = match node.leaf_text() { + Some(text) => text, + None => continue, + }; + let range = match intersect(range, node.range()) { + Some(range) => range, + None => continue, + } - node.range().start(); + for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { + let pos: TextUnit = (pos as u32).into(); + let off = node.range().start() + range.start() + pos; + if !edit.invalidates_offset(off) { + remove_newline(&mut edit, node, text.as_str(), off); + } + } + } + + LocalEdit { + edit: edit.finish(), + cursor_position: None, + } +} + +pub fn on_eq_typed(file: &File, offset: TextUnit) -> Option { + let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; + if let_stmt.has_semi() { + return None; + } + if let Some(expr) = let_stmt.initializer() { + let expr_range = expr.syntax().range(); + if contains_offset_nonstrict(expr_range, offset) && offset != expr_range.start() { + return None; + } + if file.syntax().text().slice(offset..expr_range.start()).contains('\n') { + return None; + } + } else { + return None; + } + let offset = let_stmt.syntax().range().end(); + let mut edit = EditBuilder::new(); + edit.insert(offset, ";".to_string()); + Some(LocalEdit { + edit: edit.finish(), + cursor_position: None, + }) +} + +fn remove_newline( + edit: &mut EditBuilder, + node: SyntaxNodeRef, + node_text: &str, + offset: TextUnit, +) { + if node.kind() == WHITESPACE && node_text.bytes().filter(|&b| b == b'\n').count() == 1 { + if join_single_expr_block(edit, node).is_some() { + return + } + match (node.prev_sibling(), node.next_sibling()) { + (Some(prev), Some(next)) => { + let range = TextRange::from_to(prev.range().start(), node.range().end()); + if is_trailing_comma(prev.kind(), next.kind()) { + edit.delete(range); + } else if no_space_required(prev.kind(), next.kind()) { + edit.delete(node.range()); + } else if prev.kind() == COMMA && next.kind() == R_CURLY { + edit.replace(range, " ".to_string()); + } else { + edit.replace( + node.range(), + compute_ws(prev, next).to_string(), + ); + } + return; + } + _ => (), + } + } + + let suff = &node_text[TextRange::from_to( + offset - node.range().start() + TextUnit::of_char('\n'), + TextUnit::of_str(node_text), + )]; + let spaces = suff.bytes().take_while(|&b| b == b' ').count(); + + edit.replace( + TextRange::offset_len(offset, ((spaces + 1) as u32).into()), + " ".to_string(), + ); +} + +fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { + match (left, right) { + (COMMA, R_PAREN) | (COMMA, R_BRACK) => true, + _ => false + } +} + +fn no_space_required(left: SyntaxKind, right: SyntaxKind) -> bool { + match (left, right) { + (_, DOT) => true, + _ => false + } +} + +fn join_single_expr_block( + edit: &mut EditBuilder, + node: SyntaxNodeRef, +) -> Option<()> { + let block = ast::Block::cast(node.parent()?)?; + let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?; + let expr = single_expr(block)?; + edit.replace( + block_expr.syntax().range(), + expr.syntax().text().to_string(), + ); + Some(()) +} + +fn single_expr(block: ast::Block) -> Option { + let mut res = None; + for child in block.syntax().children() { + if let Some(expr) = ast::Expr::cast(child) { + if expr.syntax().text().contains('\n') { + return None; + } + if mem::replace(&mut res, Some(expr)).is_some() { + return None; + } + } else { + match child.kind() { + WHITESPACE | L_CURLY | R_CURLY => (), + _ => return None, + } + } + } + res +} + +fn compute_ws(left: SyntaxNodeRef, right: SyntaxNodeRef) -> &'static str { + match left.kind() { + L_PAREN | L_BRACK => return "", + _ => (), + } + match right.kind() { + R_PAREN | R_BRACK => return "", + _ => (), + } + " " +} + +#[cfg(test)] +mod tests { + use super::*; + use test_utils::{check_action, extract_range, extract_offset}; + + fn check_join_lines(before: &str, after: &str) { + check_action(before, after, |file, offset| { + let range = TextRange::offset_len(offset, 0.into()); + let res = join_lines(file, range); + Some(res) + }) + } + + #[test] + fn test_join_lines_comma() { + check_join_lines(r" +fn foo() { + <|>foo(1, + ) +} +", r" +fn foo() { + <|>foo(1) +} +"); + } + + #[test] + fn test_join_lines_lambda_block() { + check_join_lines(r" +pub fn reparse(&self, edit: &AtomEdit) -> File { + <|>self.incremental_reparse(edit).unwrap_or_else(|| { + self.full_reparse(edit) + }) +} +", r" +pub fn reparse(&self, edit: &AtomEdit) -> File { + <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit)) +} +"); + } + + #[test] + fn test_join_lines_block() { + check_join_lines(r" +fn foo() { + foo(<|>{ + 92 + }) +}", r" +fn foo() { + foo(<|>92) +}"); + } + + fn check_join_lines_sel(before: &str, after: &str) { + let (sel, before) = extract_range(before); + let file = File::parse(&before); + let result = join_lines(&file, sel); + let actual = result.edit.apply(&before); + assert_eq_text!(after, &actual); + } + + #[test] + fn test_join_lines_selection_fn_args() { + check_join_lines_sel(r" +fn foo() { + <|>foo(1, + 2, + 3, + <|>) +} + ", r" +fn foo() { + foo(1, 2, 3) +} + "); + } + + #[test] + fn test_join_lines_selection_struct() { + check_join_lines_sel(r" +struct Foo <|>{ + f: u32, +}<|> + ", r" +struct Foo { f: u32 } + "); + } + + #[test] + fn test_join_lines_selection_dot_chain() { + check_join_lines_sel(r" +fn foo() { + join(<|>type_params.type_params() + .filter_map(|it| it.name()) + .map(|it| it.text())<|>) +}", r" +fn foo() { + join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text())) +}"); + } + + #[test] + fn test_join_lines_selection_lambda_block_body() { + check_join_lines_sel(r" +pub fn handle_find_matching_brace() { + params.offsets + .map(|offset| <|>{ + world.analysis().matching_brace(&file, offset).unwrap_or(offset) + }<|>) + .collect(); +}", r" +pub fn handle_find_matching_brace() { + params.offsets + .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset)) + .collect(); +}"); + } + + #[test] + fn test_on_eq_typed() { + fn do_check(before: &str, after: &str) { + let (offset, before) = extract_offset(before); + let file = File::parse(&before); + let result = on_eq_typed(&file, offset).unwrap(); + let actual = result.edit.apply(&before); + assert_eq_text!(after, &actual); + } + + // do_check(r" + // fn foo() { + // let foo =<|> + // } + // ", r" + // fn foo() { + // let foo =; + // } + // "); + do_check(r" +fn foo() { + let foo =<|> 1 + 1 +} +", r" +fn foo() { + let foo = 1 + 1; +} +"); + // do_check(r" + // fn foo() { + // let foo =<|> + // let bar = 1; + // } + // ", r" + // fn foo() { + // let foo =; + // let bar = 1; + // } + // "); + } +} -- cgit v1.2.3