From a5935687cbc62e893753ec81a00655281b03feae Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 3 Jan 2019 15:08:32 +0300 Subject: split assists over several files --- crates/ra_editor/src/assists.rs | 421 +-------------------- crates/ra_editor/src/assists/add_derive.rs | 97 +++++ crates/ra_editor/src/assists/add_impl.rs | 78 ++++ crates/ra_editor/src/assists/change_visibility.rs | 90 +++++ crates/ra_editor/src/assists/flip_comma.rs | 45 +++ crates/ra_editor/src/assists/introduce_variable.rs | 156 ++++++++ crates/ra_editor/src/lib.rs | 2 +- 7 files changed, 487 insertions(+), 402 deletions(-) create mode 100644 crates/ra_editor/src/assists/add_derive.rs create mode 100644 crates/ra_editor/src/assists/add_impl.rs create mode 100644 crates/ra_editor/src/assists/change_visibility.rs create mode 100644 crates/ra_editor/src/assists/flip_comma.rs create mode 100644 crates/ra_editor/src/assists/introduce_variable.rs (limited to 'crates/ra_editor') diff --git a/crates/ra_editor/src/assists.rs b/crates/ra_editor/src/assists.rs index 7615f37a6..b6e6dd628 100644 --- a/crates/ra_editor/src/assists.rs +++ b/crates/ra_editor/src/assists.rs @@ -1,15 +1,25 @@ -use join_to_string::join; - -use ra_syntax::{ - algo::{find_covering_node, find_leaf_at_offset}, - ast::{self, AstNode, AttrsOwner, NameOwner, TypeParamsOwner}, - Direction, SourceFileNode, - SyntaxKind::{COMMA, WHITESPACE, COMMENT, VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF}, - SyntaxNodeRef, TextRange, TextUnit, +//! This modules contains various "assits": suggestions for source code edits +//! which are likely to occur at a given cursor positon. For example, if the +//! cursor is on the `,`, a possible assist is swapping the elments around the +//! comma. + +mod flip_comma; +mod add_derive; +mod add_impl; +mod introduce_variable; +mod change_visibility; + +use ra_text_edit::TextEdit; +use ra_syntax::{Direction, SyntaxNodeRef, TextUnit}; + +pub use self::{ + flip_comma::flip_comma, + add_derive::add_derive, + add_impl::add_impl, + introduce_variable::introduce_variable, + change_visibility::change_visibility, }; -use crate::{find_node_at_offset, TextEdit, TextEditBuilder}; - #[derive(Debug)] pub struct LocalEdit { pub label: String, @@ -17,399 +27,8 @@ pub struct LocalEdit { pub cursor_position: Option, } -pub fn flip_comma<'a>( - file: &'a SourceFileNode, - offset: TextUnit, -) -> Option LocalEdit + 'a> { - let syntax = file.syntax(); - - let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?; - let prev = non_trivia_sibling(comma, Direction::Prev)?; - let next = non_trivia_sibling(comma, Direction::Next)?; - Some(move || { - let mut edit = TextEditBuilder::new(); - edit.replace(prev.range(), next.text().to_string()); - edit.replace(next.range(), prev.text().to_string()); - LocalEdit { - label: "flip comma".to_string(), - edit: edit.finish(), - cursor_position: None, - } - }) -} - -pub fn add_derive<'a>( - file: &'a SourceFileNode, - offset: TextUnit, -) -> Option LocalEdit + 'a> { - let nominal = find_node_at_offset::(file.syntax(), offset)?; - let node_start = derive_insertion_offset(nominal)?; - return 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 = TextEditBuilder::new(); - let offset = match derive_attr { - None => { - edit.insert(node_start, "#[derive()]\n".to_string()); - node_start + TextUnit::of_str("#[derive(") - } - Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), - }; - LocalEdit { - label: "add `#[derive]`".to_string(), - edit: edit.finish(), - cursor_position: Some(offset), - } - }); - - // Insert `derive` after doc comments. - fn derive_insertion_offset(nominal: ast::NominalDef) -> Option { - let non_ws_child = nominal - .syntax() - .children() - .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; - Some(non_ws_child.range().start()) - } -} - -pub fn add_impl<'a>( - file: &'a SourceFileNode, - 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 = TextEditBuilder::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 { - label: "add impl".to_string(), - edit: edit.finish(), - cursor_position: Some(offset), - } - }) -} - -pub fn introduce_variable<'a>( - file: &'a SourceFileNode, - range: TextRange, -) -> Option LocalEdit + 'a> { - let node = find_covering_node(file.syntax(), range); - let expr = node.ancestors().filter_map(ast::Expr::cast).next()?; - - let anchor_stmt = anchor_stmt(expr)?; - let indent = anchor_stmt.prev_sibling()?; - if indent.kind() != WHITESPACE { - return None; - } - return Some(move || { - let mut buf = String::new(); - let mut edit = TextEditBuilder::new(); - - buf.push_str("let var_name = "); - expr.syntax().text().push_to(&mut buf); - let is_full_stmt = if let Some(expr_stmt) = ast::ExprStmt::cast(anchor_stmt) { - Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) - } else { - false - }; - if is_full_stmt { - 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.range().start(), buf); - } - let cursor_position = anchor_stmt.range().start() + TextUnit::of_str("let "); - LocalEdit { - label: "introduce variable".to_string(), - edit: edit.finish(), - cursor_position: Some(cursor_position), - } - }); - - /// Statement or last in the block expression, which will follow - /// the freshly introduced var. - fn anchor_stmt(expr: ast::Expr) -> Option { - expr.syntax().ancestors().find(|&node| { - if ast::Stmt::cast(node).is_some() { - return true; - } - if let Some(expr) = node - .parent() - .and_then(ast::Block::cast) - .and_then(|it| it.expr()) - { - if expr.syntax() == node { - return true; - } - } - false - }) - } -} - -pub fn make_pub_crate<'a>( - file: &'a SourceFileNode, - offset: TextUnit, -) -> Option LocalEdit + 'a> { - let syntax = file.syntax(); - - let keyword = find_leaf_at_offset(syntax, offset).find(|leaf| match leaf.kind() { - FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, - _ => false, - })?; - let parent = keyword.parent()?; - let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; - let node_start = parent.range().start(); - Some(move || { - let mut edit = TextEditBuilder::new(); - - if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) - || parent.children().any(|child| child.kind() == VISIBILITY) - { - return LocalEdit { - label: "make pub crate".to_string(), - edit: edit.finish(), - cursor_position: Some(offset), - }; - } - - edit.insert(node_start, "pub(crate) ".to_string()); - LocalEdit { - label: "make pub crate".to_string(), - edit: edit.finish(), - cursor_position: Some(node_start), - } - }) -} - fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option { node.siblings(direction) .skip(1) .find(|node| !node.kind().is_trivia()) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::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 add_derive_new() { - 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()), - ); - } - - #[test] - fn add_derive_existing() { - 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 add_derive_new_with_doc_comment() { - check_action( - " -/// `Foo` is a pretty important struct. -/// It does stuff. -struct Foo { a: i32<|>, } - ", - " -/// `Foo` is a pretty important struct. -/// It does stuff. -#[derive(<|>)] -struct 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_introduce_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_introduce_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()), - ); - } - - #[test] - fn test_introduce_var_part_of_expr_stmt() { - check_action_range( - " -fn foo() { - <|>1<|> + 1; -}", - " -fn foo() { - let <|>var_name = 1; - var_name + 1; -}", - |file, range| introduce_variable(file, range).map(|f| f()), - ); - } - - #[test] - fn test_introduce_var_last_expr() { - check_action_range( - " -fn foo() { - bar(<|>1 + 1<|>) -}", - " -fn foo() { - let <|>var_name = 1 + 1; - bar(var_name) -}", - |file, range| introduce_variable(file, range).map(|f| f()), - ); - } - - #[test] - fn test_introduce_var_last_full_expr() { - check_action_range( - " -fn foo() { - <|>bar(1 + 1)<|> -}", - " -fn foo() { - let <|>var_name = bar(1 + 1); - var_name -}", - |file, range| introduce_variable(file, range).map(|f| f()), - ); - } - - #[test] - fn test_make_pub_crate() { - check_action( - "<|>fn foo() {}", - "<|>pub(crate) fn foo() {}", - |file, off| make_pub_crate(file, off).map(|f| f()), - ); - check_action( - "f<|>n foo() {}", - "<|>pub(crate) fn foo() {}", - |file, off| make_pub_crate(file, off).map(|f| f()), - ); - check_action( - "<|>struct Foo {}", - "<|>pub(crate) struct Foo {}", - |file, off| make_pub_crate(file, off).map(|f| f()), - ); - check_action("<|>mod foo {}", "<|>pub(crate) mod foo {}", |file, off| { - make_pub_crate(file, off).map(|f| f()) - }); - check_action( - "<|>trait Foo {}", - "<|>pub(crate) trait Foo {}", - |file, off| make_pub_crate(file, off).map(|f| f()), - ); - check_action("m<|>od {}", "<|>pub(crate) mod {}", |file, off| { - make_pub_crate(file, off).map(|f| f()) - }); - check_action( - "pub(crate) f<|>n foo() {}", - "pub(crate) f<|>n foo() {}", - |file, off| make_pub_crate(file, off).map(|f| f()), - ); - check_action( - "unsafe f<|>n foo() {}", - "<|>pub(crate) unsafe fn foo() {}", - |file, off| make_pub_crate(file, off).map(|f| f()), - ); - } -} diff --git a/crates/ra_editor/src/assists/add_derive.rs b/crates/ra_editor/src/assists/add_derive.rs new file mode 100644 index 000000000..33d9d2c31 --- /dev/null +++ b/crates/ra_editor/src/assists/add_derive.rs @@ -0,0 +1,97 @@ +use ra_text_edit::TextEditBuilder; +use ra_syntax::{ + ast::{self, AstNode, AttrsOwner}, + SourceFileNode, + SyntaxKind::{WHITESPACE, COMMENT}, + TextUnit, +}; + +use crate::{ + find_node_at_offset, + assists::LocalEdit, +}; + +pub fn add_derive<'a>( + file: &'a SourceFileNode, + offset: TextUnit, +) -> Option LocalEdit + 'a> { + let nominal = find_node_at_offset::(file.syntax(), offset)?; + let node_start = derive_insertion_offset(nominal)?; + return 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 = TextEditBuilder::new(); + let offset = match derive_attr { + None => { + edit.insert(node_start, "#[derive()]\n".to_string()); + node_start + TextUnit::of_str("#[derive(") + } + Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), + }; + LocalEdit { + label: "add `#[derive]`".to_string(), + edit: edit.finish(), + cursor_position: Some(offset), + } + }); + + // Insert `derive` after doc comments. + fn derive_insertion_offset(nominal: ast::NominalDef) -> Option { + let non_ws_child = nominal + .syntax() + .children() + .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; + Some(non_ws_child.range().start()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::check_action; + + #[test] + fn add_derive_new() { + 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()), + ); + } + + #[test] + fn add_derive_existing() { + 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 add_derive_new_with_doc_comment() { + check_action( + " +/// `Foo` is a pretty important struct. +/// It does stuff. +struct Foo { a: i32<|>, } + ", + " +/// `Foo` is a pretty important struct. +/// It does stuff. +#[derive(<|>)] +struct Foo { a: i32, } + ", + |file, off| add_derive(file, off).map(|f| f()), + ); + } +} diff --git a/crates/ra_editor/src/assists/add_impl.rs b/crates/ra_editor/src/assists/add_impl.rs new file mode 100644 index 000000000..50e00688e --- /dev/null +++ b/crates/ra_editor/src/assists/add_impl.rs @@ -0,0 +1,78 @@ +use join_to_string::join; +use ra_text_edit::TextEditBuilder; +use ra_syntax::{ + ast::{self, AstNode, NameOwner, TypeParamsOwner}, + SourceFileNode, + TextUnit, +}; + +use crate::{find_node_at_offset, assists::LocalEdit}; + +pub fn add_impl<'a>( + file: &'a SourceFileNode, + 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 = TextEditBuilder::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 { + label: "add impl".to_string(), + edit: edit.finish(), + cursor_position: Some(offset), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::check_action; + + #[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()), + ); + } + +} diff --git a/crates/ra_editor/src/assists/change_visibility.rs b/crates/ra_editor/src/assists/change_visibility.rs new file mode 100644 index 000000000..98c218f32 --- /dev/null +++ b/crates/ra_editor/src/assists/change_visibility.rs @@ -0,0 +1,90 @@ +use ra_text_edit::TextEditBuilder; +use ra_syntax::{ + SourceFileNode, + algo::find_leaf_at_offset, + SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF}, + TextUnit, +}; + +use crate::assists::LocalEdit; + +pub fn change_visibility<'a>( + file: &'a SourceFileNode, + offset: TextUnit, +) -> Option LocalEdit + 'a> { + let syntax = file.syntax(); + + let keyword = find_leaf_at_offset(syntax, offset).find(|leaf| match leaf.kind() { + FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, + _ => false, + })?; + let parent = keyword.parent()?; + let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; + let node_start = parent.range().start(); + Some(move || { + let mut edit = TextEditBuilder::new(); + + if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) + || parent.children().any(|child| child.kind() == VISIBILITY) + { + return LocalEdit { + label: "make pub crate".to_string(), + edit: edit.finish(), + cursor_position: Some(offset), + }; + } + + edit.insert(node_start, "pub(crate) ".to_string()); + LocalEdit { + label: "make pub crate".to_string(), + edit: edit.finish(), + cursor_position: Some(node_start), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::check_action; + + #[test] + fn test_change_visibility() { + check_action( + "<|>fn foo() {}", + "<|>pub(crate) fn foo() {}", + |file, off| change_visibility(file, off).map(|f| f()), + ); + check_action( + "f<|>n foo() {}", + "<|>pub(crate) fn foo() {}", + |file, off| change_visibility(file, off).map(|f| f()), + ); + check_action( + "<|>struct Foo {}", + "<|>pub(crate) struct Foo {}", + |file, off| change_visibility(file, off).map(|f| f()), + ); + check_action("<|>mod foo {}", "<|>pub(crate) mod foo {}", |file, off| { + change_visibility(file, off).map(|f| f()) + }); + check_action( + "<|>trait Foo {}", + "<|>pub(crate) trait Foo {}", + |file, off| change_visibility(file, off).map(|f| f()), + ); + check_action("m<|>od {}", "<|>pub(crate) mod {}", |file, off| { + change_visibility(file, off).map(|f| f()) + }); + check_action( + "pub(crate) f<|>n foo() {}", + "pub(crate) f<|>n foo() {}", + |file, off| change_visibility(file, off).map(|f| f()), + ); + check_action( + "unsafe f<|>n foo() {}", + "<|>pub(crate) unsafe fn foo() {}", + |file, off| change_visibility(file, off).map(|f| f()), + ); + } +} diff --git a/crates/ra_editor/src/assists/flip_comma.rs b/crates/ra_editor/src/assists/flip_comma.rs new file mode 100644 index 000000000..d8727db0d --- /dev/null +++ b/crates/ra_editor/src/assists/flip_comma.rs @@ -0,0 +1,45 @@ +use ra_text_edit::TextEditBuilder; +use ra_syntax::{ + algo::find_leaf_at_offset, + Direction, SourceFileNode, + SyntaxKind::COMMA, + TextUnit, +}; + +use crate::assists::{LocalEdit, non_trivia_sibling}; + +pub fn flip_comma<'a>( + file: &'a SourceFileNode, + offset: TextUnit, +) -> Option LocalEdit + 'a> { + let syntax = file.syntax(); + + let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?; + let prev = non_trivia_sibling(comma, Direction::Prev)?; + let next = non_trivia_sibling(comma, Direction::Next)?; + Some(move || { + let mut edit = TextEditBuilder::new(); + edit.replace(prev.range(), next.text().to_string()); + edit.replace(next.range(), prev.text().to_string()); + LocalEdit { + label: "flip comma".to_string(), + edit: edit.finish(), + cursor_position: None, + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::check_action; + + #[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()), + ) + } +} diff --git a/crates/ra_editor/src/assists/introduce_variable.rs b/crates/ra_editor/src/assists/introduce_variable.rs new file mode 100644 index 000000000..17ab521fa --- /dev/null +++ b/crates/ra_editor/src/assists/introduce_variable.rs @@ -0,0 +1,156 @@ +use ra_text_edit::TextEditBuilder; +use ra_syntax::{ + algo::{find_covering_node}, + ast::{self, AstNode}, + SourceFileNode, + SyntaxKind::{WHITESPACE}, + SyntaxNodeRef, TextRange, TextUnit, +}; + +use crate::assists::LocalEdit; + +pub fn introduce_variable<'a>( + file: &'a SourceFileNode, + range: TextRange, +) -> Option LocalEdit + 'a> { + let node = find_covering_node(file.syntax(), range); + let expr = node.ancestors().filter_map(ast::Expr::cast).next()?; + + let anchor_stmt = anchor_stmt(expr)?; + let indent = anchor_stmt.prev_sibling()?; + if indent.kind() != WHITESPACE { + return None; + } + return Some(move || { + let mut buf = String::new(); + let mut edit = TextEditBuilder::new(); + + buf.push_str("let var_name = "); + expr.syntax().text().push_to(&mut buf); + let is_full_stmt = if let Some(expr_stmt) = ast::ExprStmt::cast(anchor_stmt) { + Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) + } else { + false + }; + if is_full_stmt { + 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.range().start(), buf); + } + let cursor_position = anchor_stmt.range().start() + TextUnit::of_str("let "); + LocalEdit { + label: "introduce variable".to_string(), + edit: edit.finish(), + cursor_position: Some(cursor_position), + } + }); + + /// Statement or last in the block expression, which will follow + /// the freshly introduced var. + fn anchor_stmt(expr: ast::Expr) -> Option { + expr.syntax().ancestors().find(|&node| { + if ast::Stmt::cast(node).is_some() { + return true; + } + if let Some(expr) = node + .parent() + .and_then(ast::Block::cast) + .and_then(|it| it.expr()) + { + if expr.syntax() == node { + return true; + } + } + false + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::check_action_range; + + #[test] + fn test_introduce_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_introduce_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()), + ); + } + + #[test] + fn test_introduce_var_part_of_expr_stmt() { + check_action_range( + " +fn foo() { + <|>1<|> + 1; +}", + " +fn foo() { + let <|>var_name = 1; + var_name + 1; +}", + |file, range| introduce_variable(file, range).map(|f| f()), + ); + } + + #[test] + fn test_introduce_var_last_expr() { + check_action_range( + " +fn foo() { + bar(<|>1 + 1<|>) +}", + " +fn foo() { + let <|>var_name = 1 + 1; + bar(var_name) +}", + |file, range| introduce_variable(file, range).map(|f| f()), + ); + } + + #[test] + fn test_introduce_var_last_full_expr() { + check_action_range( + " +fn foo() { + <|>bar(1 + 1)<|> +}", + " +fn foo() { + let <|>var_name = bar(1 + 1); + var_name +}", + |file, range| introduce_variable(file, range).map(|f| f()), + ); + } + +} diff --git a/crates/ra_editor/src/lib.rs b/crates/ra_editor/src/lib.rs index 46d521e4b..ac283e2e0 100644 --- a/crates/ra_editor/src/lib.rs +++ b/crates/ra_editor/src/lib.rs @@ -19,7 +19,7 @@ pub use self::{ typing::{join_lines, on_enter, on_eq_typed}, diagnostics::diagnostics }; -use ra_text_edit::{TextEdit, TextEditBuilder}; +use ra_text_edit::TextEditBuilder; use ra_syntax::{ algo::find_leaf_at_offset, ast::{self, AstNode}, -- cgit v1.2.3