From 0c5fd8f7cbf04eda763e55bc9a38dad5f7ec917d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 3 Feb 2019 21:26:35 +0300 Subject: move assists to a separate crate --- crates/ra_assists/Cargo.toml | 17 + crates/ra_assists/src/add_derive.rs | 85 ++++ crates/ra_assists/src/add_impl.rs | 67 ++++ crates/ra_assists/src/assist_ctx.rs | 154 ++++++++ crates/ra_assists/src/change_visibility.rs | 166 ++++++++ crates/ra_assists/src/fill_match_arms.rs | 145 +++++++ crates/ra_assists/src/flip_comma.rs | 33 ++ crates/ra_assists/src/introduce_variable.rs | 432 +++++++++++++++++++++ crates/ra_assists/src/lib.rs | 170 ++++++++ crates/ra_assists/src/replace_if_let_with_match.rs | 80 ++++ crates/ra_assists/src/split_import.rs | 57 +++ 11 files changed, 1406 insertions(+) create mode 100644 crates/ra_assists/Cargo.toml create mode 100644 crates/ra_assists/src/add_derive.rs create mode 100644 crates/ra_assists/src/add_impl.rs create mode 100644 crates/ra_assists/src/assist_ctx.rs create mode 100644 crates/ra_assists/src/change_visibility.rs create mode 100644 crates/ra_assists/src/fill_match_arms.rs create mode 100644 crates/ra_assists/src/flip_comma.rs create mode 100644 crates/ra_assists/src/introduce_variable.rs create mode 100644 crates/ra_assists/src/lib.rs create mode 100644 crates/ra_assists/src/replace_if_let_with_match.rs create mode 100644 crates/ra_assists/src/split_import.rs (limited to 'crates/ra_assists') diff --git a/crates/ra_assists/Cargo.toml b/crates/ra_assists/Cargo.toml new file mode 100644 index 000000000..20bc253e3 --- /dev/null +++ b/crates/ra_assists/Cargo.toml @@ -0,0 +1,17 @@ +[package] +edition = "2018" +name = "ra_assists" +version = "0.1.0" +authors = ["Aleksey Kladov "] + +[dependencies] +join_to_string = "0.1.3" + +ra_ide_api_light = { path = "../ra_ide_api_light" } +ra_syntax = { path = "../ra_syntax" } +ra_text_edit = { path = "../ra_text_edit" } +ra_db = { path = "../ra_db" } +hir = { path = "../ra_hir", package = "ra_hir" } + +[dev-dependencies] +test_utils = { path = "../test_utils" } diff --git a/crates/ra_assists/src/add_derive.rs b/crates/ra_assists/src/add_derive.rs new file mode 100644 index 000000000..01a4079f6 --- /dev/null +++ b/crates/ra_assists/src/add_derive.rs @@ -0,0 +1,85 @@ +use hir::db::HirDatabase; +use ra_syntax::{ + ast::{self, AstNode, AttrsOwner}, + SyntaxKind::{WHITESPACE, COMMENT}, + TextUnit, +}; + +use crate::{AssistCtx, Assist}; + +pub(crate) fn add_derive(ctx: AssistCtx) -> Option { + let nominal = ctx.node_at_offset::()?; + let node_start = derive_insertion_offset(nominal)?; + ctx.build("add `#[derive]`", |edit| { + let derive_attr = nominal + .attrs() + .filter_map(|x| x.as_call()) + .filter(|(name, _arg)| name == "derive") + .map(|(_name, arg)| arg) + .next(); + let offset = match derive_attr { + None => { + edit.insert(node_start, "#[derive()]\n"); + node_start + TextUnit::of_str("#[derive(") + } + Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), + }; + edit.set_cursor(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::helpers::check_assist; + + #[test] + fn add_derive_new() { + check_assist( + add_derive, + "struct Foo { a: i32, <|>}", + "#[derive(<|>)]\nstruct Foo { a: i32, }", + ); + check_assist( + add_derive, + "struct Foo { <|> a: i32, }", + "#[derive(<|>)]\nstruct Foo { a: i32, }", + ); + } + + #[test] + fn add_derive_existing() { + check_assist( + add_derive, + "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", + "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", + ); + } + + #[test] + fn add_derive_new_with_doc_comment() { + check_assist( + add_derive, + " +/// `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, } + ", + ); + } +} diff --git a/crates/ra_assists/src/add_impl.rs b/crates/ra_assists/src/add_impl.rs new file mode 100644 index 000000000..699508f91 --- /dev/null +++ b/crates/ra_assists/src/add_impl.rs @@ -0,0 +1,67 @@ +use join_to_string::join; +use hir::db::HirDatabase; +use ra_syntax::{ + ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, + TextUnit, +}; + +use crate::{AssistCtx, Assist}; + +pub(crate) fn add_impl(ctx: AssistCtx) -> Option { + let nominal = ctx.node_at_offset::()?; + let name = nominal.name()?; + ctx.build("add impl", |edit| { + let type_params = nominal.type_param_list(); + 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"); + edit.set_cursor(start_offset + TextUnit::of_str(&buf)); + buf.push_str("\n}"); + edit.insert(start_offset, buf); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::check_assist; + + #[test] + fn test_add_impl() { + check_assist( + add_impl, + "struct Foo {<|>}\n", + "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", + ); + check_assist( + add_impl, + "struct Foo {<|>}", + "struct Foo {}\n\nimpl Foo {\n<|>\n}", + ); + check_assist( + add_impl, + "struct Foo<'a, T: Foo<'a>> {<|>}", + "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", + ); + } + +} diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs new file mode 100644 index 000000000..6d09bde52 --- /dev/null +++ b/crates/ra_assists/src/assist_ctx.rs @@ -0,0 +1,154 @@ +use hir::db::HirDatabase; +use ra_text_edit::TextEditBuilder; +use ra_db::FileRange; +use ra_syntax::{ + SourceFile, TextRange, AstNode, TextUnit, SyntaxNode, + algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, +}; +use ra_ide_api_light::formatting::{leading_indent, reindent}; + +use crate::{AssistLabel, AssistAction}; + +pub(crate) enum Assist { + Unresolved(AssistLabel), + Resolved(AssistLabel, AssistAction), +} + +/// `AssistCtx` allows to apply an assist or check if it could be applied. +/// +/// Assists use a somewhat overengineered approach, given the current needs. The +/// assists workflow consists of two phases. In the first phase, a user asks for +/// the list of available assists. In the second phase, the user picks a +/// particular assist and it gets applied. +/// +/// There are two peculiarities here: +/// +/// * first, we ideally avoid computing more things then necessary to answer +/// "is assist applicable" in the first phase. +/// * second, when we are applying assist, we don't have a guarantee that there +/// weren't any changes between the point when user asked for assists and when +/// they applied a particular assist. So, when applying assist, we need to do +/// all the checks from scratch. +/// +/// To avoid repeating the same code twice for both "check" and "apply" +/// functions, we use an approach reminiscent of that of Django's function based +/// views dealing with forms. Each assist receives a runtime parameter, +/// `should_compute_edit`. It first check if an edit is applicable (potentially +/// computing info required to compute the actual edit). If it is applicable, +/// and `should_compute_edit` is `true`, it then computes the actual edit. +/// +/// So, to implement the original assists workflow, we can first apply each edit +/// with `should_compute_edit = false`, and then applying the selected edit +/// again, with `should_compute_edit = true` this time. +/// +/// Note, however, that we don't actually use such two-phase logic at the +/// moment, because the LSP API is pretty awkward in this place, and it's much +/// easier to just compute the edit eagerly :-)#[derive(Debug, Clone)] +#[derive(Debug)] +pub(crate) struct AssistCtx<'a, DB> { + pub(crate) db: &'a DB, + pub(crate) frange: FileRange, + source_file: &'a SourceFile, + should_compute_edit: bool, +} + +impl<'a, DB> Clone for AssistCtx<'a, DB> { + fn clone(&self) -> Self { + AssistCtx { + db: self.db, + frange: self.frange, + source_file: self.source_file, + should_compute_edit: self.should_compute_edit, + } + } +} + +impl<'a, DB: HirDatabase> AssistCtx<'a, DB> { + pub(crate) fn with_ctx(db: &DB, frange: FileRange, should_compute_edit: bool, f: F) -> T + where + F: FnOnce(AssistCtx) -> T, + { + let source_file = &db.parse(frange.file_id); + let ctx = AssistCtx { + db, + frange, + source_file, + should_compute_edit, + }; + f(ctx) + } + + pub(crate) fn build( + self, + label: impl Into, + f: impl FnOnce(&mut AssistBuilder), + ) -> Option { + let label = AssistLabel { + label: label.into(), + }; + if !self.should_compute_edit { + return Some(Assist::Unresolved(label)); + } + let action = { + let mut edit = AssistBuilder::default(); + f(&mut edit); + edit.build() + }; + Some(Assist::Resolved(label, action)) + } + + pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { + find_leaf_at_offset(self.source_file.syntax(), self.frange.range.start()) + } + + pub(crate) fn node_at_offset(&self) -> Option<&'a N> { + find_node_at_offset(self.source_file.syntax(), self.frange.range.start()) + } + pub(crate) fn covering_node(&self) -> &'a SyntaxNode { + find_covering_node(self.source_file.syntax(), self.frange.range) + } +} + +#[derive(Default)] +pub(crate) struct AssistBuilder { + edit: TextEditBuilder, + cursor_position: Option, +} + +impl AssistBuilder { + pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into) { + self.edit.replace(range, replace_with.into()) + } + + pub(crate) fn replace_node_and_indent( + &mut self, + node: &SyntaxNode, + replace_with: impl Into, + ) { + let mut replace_with = replace_with.into(); + if let Some(indent) = leading_indent(node) { + replace_with = reindent(&replace_with, indent) + } + self.replace(node.range(), replace_with) + } + + #[allow(unused)] + pub(crate) fn delete(&mut self, range: TextRange) { + self.edit.delete(range) + } + + pub(crate) fn insert(&mut self, offset: TextUnit, text: impl Into) { + self.edit.insert(offset, text.into()) + } + + pub(crate) fn set_cursor(&mut self, offset: TextUnit) { + self.cursor_position = Some(offset) + } + + fn build(self) -> AssistAction { + AssistAction { + edit: self.edit.finish(), + cursor_position: self.cursor_position, + } + } +} diff --git a/crates/ra_assists/src/change_visibility.rs b/crates/ra_assists/src/change_visibility.rs new file mode 100644 index 000000000..4cd32985e --- /dev/null +++ b/crates/ra_assists/src/change_visibility.rs @@ -0,0 +1,166 @@ +use hir::db::HirDatabase; +use ra_syntax::{ + AstNode, SyntaxNode, TextUnit, + ast::{self, VisibilityOwner, NameOwner}, + SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF, IDENT, WHITESPACE, COMMENT, ATTR}, +}; + +use crate::{AssistCtx, Assist}; + +pub(crate) fn change_visibility(ctx: AssistCtx) -> Option { + if let Some(vis) = ctx.node_at_offset::() { + return change_vis(ctx, vis); + } + add_vis(ctx) +} + +fn add_vis(ctx: AssistCtx) -> Option { + let item_keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() { + FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, + _ => false, + }); + + let offset = if let Some(keyword) = item_keyword { + let parent = keyword.parent()?; + let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; + // Parent is not a definition, can't add visibility + if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) { + return None; + } + // Already have visibility, do nothing + if parent.children().any(|child| child.kind() == VISIBILITY) { + return None; + } + vis_offset(parent) + } else { + let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?; + let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?; + if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() { + return None; + } + vis_offset(field.syntax()) + }; + + ctx.build("make pub(crate)", |edit| { + edit.insert(offset, "pub(crate) "); + edit.set_cursor(offset); + }) +} + +fn vis_offset(node: &SyntaxNode) -> TextUnit { + node.children() + .skip_while(|it| match it.kind() { + WHITESPACE | COMMENT | ATTR => true, + _ => false, + }) + .next() + .map(|it| it.range().start()) + .unwrap_or(node.range().start()) +} + +fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option { + if vis.syntax().text() == "pub" { + return ctx.build("chage to pub(crate)", |edit| { + edit.replace(vis.syntax().range(), "pub(crate)"); + edit.set_cursor(vis.syntax().range().start()); + }); + } + if vis.syntax().text() == "pub(crate)" { + return ctx.build("chage to pub", |edit| { + edit.replace(vis.syntax().range(), "pub"); + edit.set_cursor(vis.syntax().range().start()); + }); + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::check_assist; + + #[test] + fn change_visibility_adds_pub_crate_to_items() { + check_assist( + change_visibility, + "<|>fn foo() {}", + "<|>pub(crate) fn foo() {}", + ); + check_assist( + change_visibility, + "f<|>n foo() {}", + "<|>pub(crate) fn foo() {}", + ); + check_assist( + change_visibility, + "<|>struct Foo {}", + "<|>pub(crate) struct Foo {}", + ); + check_assist( + change_visibility, + "<|>mod foo {}", + "<|>pub(crate) mod foo {}", + ); + check_assist( + change_visibility, + "<|>trait Foo {}", + "<|>pub(crate) trait Foo {}", + ); + check_assist(change_visibility, "m<|>od {}", "<|>pub(crate) mod {}"); + check_assist( + change_visibility, + "unsafe f<|>n foo() {}", + "<|>pub(crate) unsafe fn foo() {}", + ); + } + + #[test] + fn change_visibility_works_with_struct_fields() { + check_assist( + change_visibility, + "struct S { <|>field: u32 }", + "struct S { <|>pub(crate) field: u32 }", + ) + } + + #[test] + fn change_visibility_pub_to_pub_crate() { + check_assist( + change_visibility, + "<|>pub fn foo() {}", + "<|>pub(crate) fn foo() {}", + ) + } + + #[test] + fn change_visibility_pub_crate_to_pub() { + check_assist( + change_visibility, + "<|>pub(crate) fn foo() {}", + "<|>pub fn foo() {}", + ) + } + + #[test] + fn change_visibility_handles_comment_attrs() { + check_assist( + change_visibility, + " + /// docs + + // comments + + #[derive(Debug)] + <|>struct Foo; + ", + " + /// docs + + // comments + + #[derive(Debug)] + <|>pub(crate) struct Foo; + ", + ) + } +} diff --git a/crates/ra_assists/src/fill_match_arms.rs b/crates/ra_assists/src/fill_match_arms.rs new file mode 100644 index 000000000..9aa37d94c --- /dev/null +++ b/crates/ra_assists/src/fill_match_arms.rs @@ -0,0 +1,145 @@ +use std::fmt::Write; + +use hir::{ + AdtDef, Ty, FieldSource, source_binder, + db::HirDatabase, +}; +use ra_syntax::ast::{self, AstNode}; + +use crate::{AssistCtx, Assist}; + +pub(crate) fn fill_match_arms(ctx: AssistCtx) -> Option { + let match_expr = ctx.node_at_offset::()?; + + // We already have some match arms, so we don't provide any assists. + match match_expr.match_arm_list() { + Some(arm_list) if arm_list.arms().count() > 0 => { + return None; + } + _ => {} + } + + let expr = match_expr.expr()?; + let function = + source_binder::function_from_child_node(ctx.db, ctx.frange.file_id, expr.syntax())?; + let infer_result = function.infer(ctx.db); + let syntax_mapping = function.body_syntax_mapping(ctx.db); + let node_expr = syntax_mapping.node_expr(expr)?; + let match_expr_ty = infer_result[node_expr].clone(); + let enum_def = match match_expr_ty { + Ty::Adt { + def_id: AdtDef::Enum(e), + .. + } => e, + _ => return None, + }; + let enum_name = enum_def.name(ctx.db)?; + let db = ctx.db; + + ctx.build("fill match arms", |edit| { + let mut buf = format!("match {} {{\n", expr.syntax().text().to_string()); + let variants = enum_def.variants(db); + for variant in variants { + let name = match variant.name(db) { + Some(it) => it, + None => continue, + }; + write!(&mut buf, " {}::{}", enum_name, name.to_string()).unwrap(); + + let pat = variant + .fields(db) + .into_iter() + .map(|field| { + let name = field.name(db).to_string(); + let (_, source) = field.source(db); + match source { + FieldSource::Named(_) => name, + FieldSource::Pos(_) => "_".to_string(), + } + }) + .collect::>(); + + match pat.first().map(|s| s.as_str()) { + Some("_") => write!(&mut buf, "({})", pat.join(", ")).unwrap(), + Some(_) => write!(&mut buf, "{{{}}}", pat.join(", ")).unwrap(), + None => (), + }; + + buf.push_str(" => (),\n"); + } + buf.push_str("}"); + edit.set_cursor(expr.syntax().range().start()); + edit.replace_node_and_indent(match_expr.syntax(), buf); + }) +} + +#[cfg(test)] +mod tests { + use crate::helpers::check_assist; + + use super::fill_match_arms; + + #[test] + fn fill_match_arms_empty_body() { + check_assist( + fill_match_arms, + r#" + enum A { + As, + Bs, + Cs(String), + Ds(String, String), + Es{x: usize, y: usize} + } + + fn main() { + let a = A::As; + match a<|> {} + } + "#, + r#" + enum A { + As, + Bs, + Cs(String), + Ds(String, String), + Es{x: usize, y: usize} + } + + fn main() { + let a = A::As; + match <|>a { + A::As => (), + A::Bs => (), + A::Cs(_) => (), + A::Ds(_, _) => (), + A::Es{x, y} => (), + } + } + "#, + ); + } + #[test] + fn fill_match_arms_no_body() { + check_assist( + fill_match_arms, + r#" + enum E { X, Y} + + fn main() { + match E::X<|> + } + "#, + r#" + enum E { X, Y} + + fn main() { + match <|>E::X { + E::X => (), + E::Y => (), + } + } + "#, + ); + } +} diff --git a/crates/ra_assists/src/flip_comma.rs b/crates/ra_assists/src/flip_comma.rs new file mode 100644 index 000000000..a49820c29 --- /dev/null +++ b/crates/ra_assists/src/flip_comma.rs @@ -0,0 +1,33 @@ +use hir::db::HirDatabase; +use ra_syntax::{ + Direction, + SyntaxKind::COMMA, +}; + +use crate::{AssistCtx, Assist, non_trivia_sibling}; + +pub(crate) fn flip_comma(ctx: AssistCtx) -> Option { + let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?; + let prev = non_trivia_sibling(comma, Direction::Prev)?; + let next = non_trivia_sibling(comma, Direction::Next)?; + ctx.build("flip comma", |edit| { + edit.replace(prev.range(), next.text()); + edit.replace(next.range(), prev.text()); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::check_assist; + + #[test] + fn flip_comma_works_for_function_parameters() { + check_assist( + flip_comma, + "fn foo(x: i32,<|> y: Result<(), ()>) {}", + "fn foo(y: Result<(), ()>,<|> x: i32) {}", + ) + } +} diff --git a/crates/ra_assists/src/introduce_variable.rs b/crates/ra_assists/src/introduce_variable.rs new file mode 100644 index 000000000..c937a816c --- /dev/null +++ b/crates/ra_assists/src/introduce_variable.rs @@ -0,0 +1,432 @@ +use hir::db::HirDatabase; +use ra_syntax::{ + ast::{self, AstNode}, + SyntaxKind::{ + WHITESPACE, MATCH_ARM, LAMBDA_EXPR, PATH_EXPR, BREAK_EXPR, LOOP_EXPR, RETURN_EXPR, COMMENT + }, SyntaxNode, TextUnit, +}; + +use crate::{AssistCtx, Assist}; + +pub(crate) fn introduce_variable<'a>(ctx: AssistCtx) -> Option { + let node = ctx.covering_node(); + if !valid_covering_node(node) { + return None; + } + let expr = node.ancestors().filter_map(valid_target_expr).next()?; + let (anchor_stmt, wrap_in_block) = anchor_stmt(expr)?; + let indent = anchor_stmt.prev_sibling()?; + if indent.kind() != WHITESPACE { + return None; + } + ctx.build("introduce variable", move |edit| { + let mut buf = String::new(); + + let cursor_offset = if wrap_in_block { + buf.push_str("{ let var_name = "); + TextUnit::of_str("{ let ") + } else { + buf.push_str("let var_name = "); + TextUnit::of_str("let ") + }; + + expr.syntax().text().push_to(&mut buf); + let full_stmt = ast::ExprStmt::cast(anchor_stmt); + let is_full_stmt = if let Some(expr_stmt) = full_stmt { + Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) + } else { + false + }; + if is_full_stmt { + if !full_stmt.unwrap().has_semi() { + buf.push_str(";"); + } + 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); + if wrap_in_block { + edit.insert(anchor_stmt.range().end(), " }"); + } + } + edit.set_cursor(anchor_stmt.range().start() + cursor_offset); + }) +} + +fn valid_covering_node(node: &SyntaxNode) -> bool { + node.kind() != COMMENT +} +/// Check wether the node is a valid expression which can be extracted to a variable. +/// In general that's true for any expression, but in some cases that would produce invalid code. +fn valid_target_expr(node: &SyntaxNode) -> Option<&ast::Expr> { + return match node.kind() { + PATH_EXPR => None, + BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()), + RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), + LOOP_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), + _ => ast::Expr::cast(node), + }; +} + +/// Returns the syntax node which will follow the freshly introduced var +/// and a boolean indicating whether we have to wrap it within a { } block +/// to produce correct code. +/// It can be a statement, the last in a block expression or a wanna be block +/// expression like a lamba or match arm. +fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> { + expr.syntax().ancestors().find_map(|node| { + if ast::Stmt::cast(node).is_some() { + return Some((node, false)); + } + + if let Some(expr) = node + .parent() + .and_then(ast::Block::cast) + .and_then(|it| it.expr()) + { + if expr.syntax() == node { + return Some((node, false)); + } + } + + if let Some(parent) = node.parent() { + if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR { + return Some((node, true)); + } + } + + None + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range}; + + #[test] + fn test_introduce_var_simple() { + check_assist_range( + introduce_variable, + " +fn foo() { + foo(<|>1 + 1<|>); +}", + " +fn foo() { + let <|>var_name = 1 + 1; + foo(var_name); +}", + ); + } + + #[test] + fn test_introduce_var_expr_stmt() { + check_assist_range( + introduce_variable, + " +fn foo() { + <|>1 + 1<|>; +}", + " +fn foo() { + let <|>var_name = 1 + 1; +}", + ); + } + + #[test] + fn test_introduce_var_part_of_expr_stmt() { + check_assist_range( + introduce_variable, + " +fn foo() { + <|>1<|> + 1; +}", + " +fn foo() { + let <|>var_name = 1; + var_name + 1; +}", + ); + } + + #[test] + fn test_introduce_var_last_expr() { + check_assist_range( + introduce_variable, + " +fn foo() { + bar(<|>1 + 1<|>) +}", + " +fn foo() { + let <|>var_name = 1 + 1; + bar(var_name) +}", + ); + } + + #[test] + fn test_introduce_var_last_full_expr() { + check_assist_range( + introduce_variable, + " +fn foo() { + <|>bar(1 + 1)<|> +}", + " +fn foo() { + let <|>var_name = bar(1 + 1); + var_name +}", + ); + } + + #[test] + fn test_introduce_var_block_expr_second_to_last() { + check_assist_range( + introduce_variable, + " +fn foo() { + <|>{ let x = 0; x }<|> + something_else(); +}", + " +fn foo() { + let <|>var_name = { let x = 0; x }; + something_else(); +}", + ); + } + + #[test] + fn test_introduce_var_in_match_arm_no_block() { + check_assist_range( + introduce_variable, + " +fn main() { + let x = true; + let tuple = match x { + true => (<|>2 + 2<|>, true) + _ => (0, false) + }; +} +", + " +fn main() { + let x = true; + let tuple = match x { + true => { let <|>var_name = 2 + 2; (var_name, true) } + _ => (0, false) + }; +} +", + ); + } + + #[test] + fn test_introduce_var_in_match_arm_with_block() { + check_assist_range( + introduce_variable, + " +fn main() { + let x = true; + let tuple = match x { + true => { + let y = 1; + (<|>2 + y<|>, true) + } + _ => (0, false) + }; +} +", + " +fn main() { + let x = true; + let tuple = match x { + true => { + let y = 1; + let <|>var_name = 2 + y; + (var_name, true) + } + _ => (0, false) + }; +} +", + ); + } + + #[test] + fn test_introduce_var_in_closure_no_block() { + check_assist_range( + introduce_variable, + " +fn main() { + let lambda = |x: u32| <|>x * 2<|>; +} +", + " +fn main() { + let lambda = |x: u32| { let <|>var_name = x * 2; var_name }; +} +", + ); + } + + #[test] + fn test_introduce_var_in_closure_with_block() { + check_assist_range( + introduce_variable, + " +fn main() { + let lambda = |x: u32| { <|>x * 2<|> }; +} +", + " +fn main() { + let lambda = |x: u32| { let <|>var_name = x * 2; var_name }; +} +", + ); + } + + #[test] + fn test_introduce_var_path_simple() { + check_assist( + introduce_variable, + " +fn main() { + let o = S<|>ome(true); +} +", + " +fn main() { + let <|>var_name = Some(true); + let o = var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_path_method() { + check_assist( + introduce_variable, + " +fn main() { + let v = b<|>ar.foo(); +} +", + " +fn main() { + let <|>var_name = bar.foo(); + let v = var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_return() { + check_assist( + introduce_variable, + " +fn foo() -> u32 { + r<|>eturn 2 + 2; +} +", + " +fn foo() -> u32 { + let <|>var_name = 2 + 2; + return var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_break() { + check_assist( + introduce_variable, + " +fn main() { + let result = loop { + b<|>reak 2 + 2; + }; +} +", + " +fn main() { + let result = loop { + let <|>var_name = 2 + 2; + break var_name; + }; +} +", + ); + } + + #[test] + fn test_introduce_var_for_cast() { + check_assist( + introduce_variable, + " +fn main() { + let v = 0f32 a<|>s u32; +} +", + " +fn main() { + let <|>var_name = 0f32 as u32; + let v = var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_for_return_not_applicable() { + check_assist_not_applicable( + introduce_variable, + " +fn foo() { + r<|>eturn; +} +", + ); + } + + #[test] + fn test_introduce_var_for_break_not_applicable() { + check_assist_not_applicable( + introduce_variable, + " +fn main() { + loop { + b<|>reak; + }; +} +", + ); + } + + #[test] + fn test_introduce_var_in_comment_not_applicable() { + check_assist_not_applicable( + introduce_variable, + " +fn main() { + let x = true; + let tuple = match x { + // c<|>omment + true => (2 + 2, true) + _ => (0, false) + }; +} +", + ); + } +} diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs new file mode 100644 index 000000000..4e97a84c2 --- /dev/null +++ b/crates/ra_assists/src/lib.rs @@ -0,0 +1,170 @@ +//! `ra_assits` crate provides a bunch of code assists, aslo known as code +//! actions (in LSP) or intentions (in IntelliJ). +//! +//! An assist is a micro-refactoring, which is automatically activated in +//! certain context. For example, if the cursor is over `,`, a "swap `,`" assist +//! becomes available. + +mod assist_ctx; + +use ra_text_edit::TextEdit; +use ra_syntax::{TextUnit, SyntaxNode, Direction}; +use ra_db::FileRange; +use hir::db::HirDatabase; + +pub(crate) use crate::assist_ctx::{AssistCtx, Assist}; + +#[derive(Debug)] +pub struct AssistLabel { + /// Short description of the assist, as shown in the UI. + pub label: String, +} + +pub struct AssistAction { + pub edit: TextEdit, + pub cursor_position: Option, +} + +/// Return all the assists applicable at the given position. +/// +/// Assists are returned in the "unresolved" state, that is only labels are +/// returned, without actual edits. +pub fn applicable_assists(db: &H, range: FileRange) -> Vec +where + H: HirDatabase + 'static, +{ + AssistCtx::with_ctx(db, range, false, |ctx| { + all_assists() + .iter() + .filter_map(|f| f(ctx.clone())) + .map(|a| match a { + Assist::Unresolved(label) => label, + Assist::Resolved(..) => unreachable!(), + }) + .collect() + }) +} + +/// Return all the assists applicable at the given position. +/// +/// Assists are returned in the "resolved" state, that is with edit fully +/// computed. +pub fn assists(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)> +where + H: HirDatabase + 'static, +{ + AssistCtx::with_ctx(db, range, false, |ctx| { + all_assists() + .iter() + .filter_map(|f| f(ctx.clone())) + .map(|a| match a { + Assist::Resolved(label, action) => (label, action), + Assist::Unresolved(..) => unreachable!(), + }) + .collect() + }) +} + +mod add_derive; +mod add_impl; +mod flip_comma; +mod change_visibility; +mod fill_match_arms; +mod introduce_variable; +mod replace_if_let_with_match; +mod split_import; +fn all_assists() -> &'static [fn(AssistCtx) -> Option] { + &[ + add_derive::add_derive, + add_impl::add_impl, + change_visibility::change_visibility, + fill_match_arms::fill_match_arms, + flip_comma::flip_comma, + introduce_variable::introduce_variable, + replace_if_let_with_match::replace_if_let_with_match, + split_import::split_import, + ] +} + +fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { + node.siblings(direction) + .skip(1) + .find(|node| !node.kind().is_trivia()) +} + +#[cfg(test)] +mod helpers { + use hir::mock::MockDatabase; + use ra_syntax::TextRange; + use ra_db::FileRange; + use test_utils::{extract_offset, assert_eq_text, add_cursor, extract_range}; + + use crate::{AssistCtx, Assist}; + + pub(crate) fn check_assist( + assist: fn(AssistCtx) -> Option, + before: &str, + after: &str, + ) { + let (before_cursor_pos, before) = extract_offset(before); + let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); + let frange = FileRange { + file_id, + range: TextRange::offset_len(before_cursor_pos, 0.into()), + }; + let assist = + AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); + let action = match assist { + Assist::Unresolved(_) => unreachable!(), + Assist::Resolved(_, it) => it, + }; + + let actual = action.edit.apply(&before); + let actual_cursor_pos = match action.cursor_position { + None => action + .edit + .apply_to_offset(before_cursor_pos) + .expect("cursor position is affected by the edit"), + Some(off) => off, + }; + let actual = add_cursor(&actual, actual_cursor_pos); + assert_eq_text!(after, &actual); + } + + pub(crate) fn check_assist_range( + assist: fn(AssistCtx) -> Option, + before: &str, + after: &str, + ) { + let (range, before) = extract_range(before); + let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); + let frange = FileRange { file_id, range }; + let assist = + AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); + let action = match assist { + Assist::Unresolved(_) => unreachable!(), + Assist::Resolved(_, it) => it, + }; + + let mut actual = action.edit.apply(&before); + if let Some(pos) = action.cursor_position { + actual = add_cursor(&actual, pos); + } + assert_eq_text!(after, &actual); + } + + pub(crate) fn check_assist_not_applicable( + assist: fn(AssistCtx) -> Option, + before: &str, + ) { + let (before_cursor_pos, before) = extract_offset(before); + let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); + let frange = FileRange { + file_id, + range: TextRange::offset_len(before_cursor_pos, 0.into()), + }; + let assist = AssistCtx::with_ctx(&db, frange, true, assist); + assert!(assist.is_none()); + } + +} diff --git a/crates/ra_assists/src/replace_if_let_with_match.rs b/crates/ra_assists/src/replace_if_let_with_match.rs new file mode 100644 index 000000000..f6af47ec9 --- /dev/null +++ b/crates/ra_assists/src/replace_if_let_with_match.rs @@ -0,0 +1,80 @@ +use ra_syntax::{AstNode, ast}; +use ra_ide_api_light::formatting::extract_trivial_expression; +use hir::db::HirDatabase; + +use crate::{AssistCtx, Assist}; + +pub(crate) fn replace_if_let_with_match(ctx: AssistCtx) -> Option { + let if_expr: &ast::IfExpr = ctx.node_at_offset()?; + let cond = if_expr.condition()?; + let pat = cond.pat()?; + let expr = cond.expr()?; + let then_block = if_expr.then_branch()?; + let else_block = match if_expr.else_branch()? { + ast::ElseBranchFlavor::Block(it) => it, + ast::ElseBranchFlavor::IfExpr(_) => return None, + }; + + ctx.build("replace with match", |edit| { + let match_expr = build_match_expr(expr, pat, then_block, else_block); + edit.replace_node_and_indent(if_expr.syntax(), match_expr); + edit.set_cursor(if_expr.syntax().range().start()) + }) +} + +fn build_match_expr( + expr: &ast::Expr, + pat1: &ast::Pat, + arm1: &ast::Block, + arm2: &ast::Block, +) -> String { + let mut buf = String::new(); + buf.push_str(&format!("match {} {{\n", expr.syntax().text())); + buf.push_str(&format!( + " {} => {}\n", + pat1.syntax().text(), + format_arm(arm1) + )); + buf.push_str(&format!(" _ => {}\n", format_arm(arm2))); + buf.push_str("}"); + buf +} + +fn format_arm(block: &ast::Block) -> String { + match extract_trivial_expression(block) { + None => block.syntax().text().to_string(), + Some(e) => format!("{},", e.syntax().text()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::check_assist; + + #[test] + fn test_replace_if_let_with_match_unwraps_simple_expressions() { + check_assist( + replace_if_let_with_match, + " +impl VariantData { + pub fn is_struct(&self) -> bool { + if <|>let VariantData::Struct(..) = *self { + true + } else { + false + } + } +} ", + " +impl VariantData { + pub fn is_struct(&self) -> bool { + <|>match *self { + VariantData::Struct(..) => true, + _ => false, + } + } +} ", + ) + } +} diff --git a/crates/ra_assists/src/split_import.rs b/crates/ra_assists/src/split_import.rs new file mode 100644 index 000000000..7e34be087 --- /dev/null +++ b/crates/ra_assists/src/split_import.rs @@ -0,0 +1,57 @@ +use hir::db::HirDatabase; +use ra_syntax::{ + TextUnit, AstNode, SyntaxKind::COLONCOLON, + ast, + algo::generate, +}; + +use crate::{AssistCtx, Assist}; + +pub(crate) fn split_import(ctx: AssistCtx) -> Option { + let colon_colon = ctx + .leaf_at_offset() + .find(|leaf| leaf.kind() == COLONCOLON)?; + let path = colon_colon.parent().and_then(ast::Path::cast)?; + let top_path = generate(Some(path), |it| it.parent_path()).last()?; + + let use_tree = top_path.syntax().ancestors().find_map(ast::UseTree::cast); + if use_tree.is_none() { + return None; + } + + let l_curly = colon_colon.range().end(); + let r_curly = match top_path.syntax().parent().and_then(ast::UseTree::cast) { + Some(tree) => tree.syntax().range().end(), + None => top_path.syntax().range().end(), + }; + + ctx.build("split import", |edit| { + edit.insert(l_curly, "{"); + edit.insert(r_curly, "}"); + edit.set_cursor(l_curly + TextUnit::of_str("{")); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::check_assist; + + #[test] + fn test_split_import() { + check_assist( + split_import, + "use crate::<|>db::RootDatabase;", + "use crate::{<|>db::RootDatabase};", + ) + } + + #[test] + fn split_import_works_with_trees() { + check_assist( + split_import, + "use algo:<|>:visitor::{Visitor, visit}", + "use algo::{<|>visitor::{Visitor, visit}}", + ) + } +} -- cgit v1.2.3