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 +++ crates/ra_db/src/lib.rs | 2 +- crates/ra_hir/src/lib.rs | 3 +- crates/ra_hir/src/mock.rs | 12 +- crates/ra_ide_api/Cargo.toml | 1 + crates/ra_ide_api/src/assists.rs | 109 ++---- crates/ra_ide_api/src/assists/fill_match_arm.rs | 157 -------- .../assists/snapshots/tests__fill_match_arm1.snap | 20 - .../assists/snapshots/tests__fill_match_arm2.snap | 20 - crates/ra_ide_api/src/imp.rs | 11 +- crates/ra_ide_api/src/lib.rs | 2 +- crates/ra_ide_api_light/src/assists.rs | 215 ---------- crates/ra_ide_api_light/src/assists/add_derive.rs | 84 ---- crates/ra_ide_api_light/src/assists/add_impl.rs | 66 ---- .../src/assists/change_visibility.rs | 165 -------- crates/ra_ide_api_light/src/assists/flip_comma.rs | 31 -- .../src/assists/introduce_variable.rs | 431 -------------------- .../src/assists/replace_if_let_with_match.rs | 81 ---- .../ra_ide_api_light/src/assists/split_import.rs | 56 --- crates/ra_ide_api_light/src/formatting.rs | 10 +- crates/ra_ide_api_light/src/lib.rs | 11 +- crates/ra_ide_api_light/src/test_utils.rs | 31 +- 32 files changed, 1456 insertions(+), 1468 deletions(-) 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 delete mode 100644 crates/ra_ide_api/src/assists/fill_match_arm.rs delete mode 100644 crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm1.snap delete mode 100644 crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm2.snap delete mode 100644 crates/ra_ide_api_light/src/assists.rs delete mode 100644 crates/ra_ide_api_light/src/assists/add_derive.rs delete mode 100644 crates/ra_ide_api_light/src/assists/add_impl.rs delete mode 100644 crates/ra_ide_api_light/src/assists/change_visibility.rs delete mode 100644 crates/ra_ide_api_light/src/assists/flip_comma.rs delete mode 100644 crates/ra_ide_api_light/src/assists/introduce_variable.rs delete mode 100644 crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs delete mode 100644 crates/ra_ide_api_light/src/assists/split_import.rs (limited to 'crates') 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}}", + ) + } +} diff --git a/crates/ra_db/src/lib.rs b/crates/ra_db/src/lib.rs index 926cf0bd5..66634e05b 100644 --- a/crates/ra_db/src/lib.rs +++ b/crates/ra_db/src/lib.rs @@ -70,7 +70,7 @@ pub struct FileRange { /// Database which stores all significant input facts: source code and project /// model. Everything else in rust-analyzer is derived from these queries. #[salsa::query_group(SourceDatabaseStorage)] -pub trait SourceDatabase: CheckCanceled { +pub trait SourceDatabase: CheckCanceled + std::fmt::Debug { /// Text of the file. #[salsa::input] fn file_text(&self, file_id: FileId) -> Arc; diff --git a/crates/ra_hir/src/lib.rs b/crates/ra_hir/src/lib.rs index 54da55598..a9cd955cf 100644 --- a/crates/ra_hir/src/lib.rs +++ b/crates/ra_hir/src/lib.rs @@ -18,8 +18,7 @@ macro_rules! impl_froms { } pub mod db; -#[cfg(test)] -mod mock; +pub mod mock; mod query_definitions; mod path; pub mod source_binder; diff --git a/crates/ra_hir/src/mock.rs b/crates/ra_hir/src/mock.rs index 00a07d1a1..87095fb21 100644 --- a/crates/ra_hir/src/mock.rs +++ b/crates/ra_hir/src/mock.rs @@ -17,7 +17,7 @@ pub const WORKSPACE: SourceRootId = SourceRootId(0); db::PersistentHirDatabaseStorage )] #[derive(Debug)] -pub(crate) struct MockDatabase { +pub struct MockDatabase { events: Mutex>>>, runtime: salsa::Runtime, interner: Arc, @@ -27,13 +27,13 @@ pub(crate) struct MockDatabase { impl panic::RefUnwindSafe for MockDatabase {} impl MockDatabase { - pub(crate) fn with_files(fixture: &str) -> (MockDatabase, SourceRoot) { + pub fn with_files(fixture: &str) -> (MockDatabase, SourceRoot) { let (db, source_root, position) = MockDatabase::from_fixture(fixture); assert!(position.is_none()); (db, source_root) } - pub(crate) fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId) { + pub fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId) { let mut db = MockDatabase::default(); let mut source_root = SourceRoot::default(); let file_id = db.add_file(WORKSPACE, &mut source_root, "/main.rs", text); @@ -41,7 +41,7 @@ impl MockDatabase { (db, source_root, file_id) } - pub(crate) fn with_position(fixture: &str) -> (MockDatabase, FilePosition) { + pub fn with_position(fixture: &str) -> (MockDatabase, FilePosition) { let (db, _, position) = MockDatabase::from_fixture(fixture); let position = position.expect("expected a marker ( <|> )"); (db, position) @@ -166,13 +166,13 @@ impl AsRef for MockDatabase { } impl MockDatabase { - pub(crate) fn log(&self, f: impl FnOnce()) -> Vec> { + pub fn log(&self, f: impl FnOnce()) -> Vec> { *self.events.lock() = Some(Vec::new()); f(); self.events.lock().take().unwrap() } - pub(crate) fn log_executed(&self, f: impl FnOnce()) -> Vec { + pub fn log_executed(&self, f: impl FnOnce()) -> Vec { let events = self.log(f); events .into_iter() diff --git a/crates/ra_ide_api/Cargo.toml b/crates/ra_ide_api/Cargo.toml index 54de9b2e3..95cccf8cf 100644 --- a/crates/ra_ide_api/Cargo.toml +++ b/crates/ra_ide_api/Cargo.toml @@ -24,6 +24,7 @@ ra_text_edit = { path = "../ra_text_edit" } ra_db = { path = "../ra_db" } hir = { path = "../ra_hir", package = "ra_hir" } test_utils = { path = "../test_utils" } +ra_assists = { path = "../ra_assists" } [dev-dependencies] insta = "0.6.1" diff --git a/crates/ra_ide_api/src/assists.rs b/crates/ra_ide_api/src/assists.rs index 2da251df5..2a96fdf47 100644 --- a/crates/ra_ide_api/src/assists.rs +++ b/crates/ra_ide_api/src/assists.rs @@ -1,89 +1,24 @@ -mod fill_match_arm; - -use ra_syntax::{ - TextRange, SourceFile, AstNode, - algo::find_node_at_offset, -}; -use ra_ide_api_light::{ - LocalEdit, - assists::{ - Assist, - AssistBuilder - } -}; -use crate::{ - db::RootDatabase, - FileId -}; - -/// Return all the assists applicable at the given position. -pub(crate) fn assists( - db: &RootDatabase, - file_id: FileId, - file: &SourceFile, - range: TextRange, -) -> Vec { - let ctx = AssistCtx::new(db, file_id, file, range); - [fill_match_arm::fill_match_arm] - .iter() - .filter_map(|&assist| ctx.clone().apply(assist)) +use ra_db::{FileRange, FilePosition}; + +use crate::{SourceFileEdit, SourceChange, db::RootDatabase}; + +pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec { + ra_assists::assists(db, frange) + .into_iter() + .map(|(label, action)| { + let file_id = frange.file_id; + let file_edit = SourceFileEdit { + file_id, + edit: action.edit, + }; + SourceChange { + label: label.label, + source_file_edits: vec![file_edit], + file_system_edits: vec![], + cursor_position: action + .cursor_position + .map(|offset| FilePosition { offset, file_id }), + } + }) .collect() } - -#[derive(Debug, Clone)] -pub struct AssistCtx<'a> { - file_id: FileId, - source_file: &'a SourceFile, - db: &'a RootDatabase, - range: TextRange, - should_compute_edit: bool, -} - -impl<'a> AssistCtx<'a> { - pub(crate) fn new( - db: &'a RootDatabase, - file_id: FileId, - source_file: &'a SourceFile, - range: TextRange, - ) -> AssistCtx<'a> { - AssistCtx { - source_file, - file_id, - db, - range, - should_compute_edit: false, - } - } - - pub fn apply(mut self, assist: fn(AssistCtx) -> Option) -> Option { - self.should_compute_edit = true; - match assist(self) { - None => None, - Some(Assist::Edit(e)) => Some(e), - Some(Assist::Applicable) => unreachable!(), - } - } - - #[allow(unused)] - pub fn check(mut self, assist: fn(AssistCtx) -> Option) -> bool { - self.should_compute_edit = false; - match assist(self) { - None => false, - Some(Assist::Edit(_)) => unreachable!(), - Some(Assist::Applicable) => true, - } - } - - fn build(self, label: impl Into, f: impl FnOnce(&mut AssistBuilder)) -> Option { - if !self.should_compute_edit { - return Some(Assist::Applicable); - } - let mut edit = AssistBuilder::default(); - f(&mut edit); - Some(edit.build(label)) - } - - pub(crate) fn node_at_offset(&self) -> Option<&'a N> { - find_node_at_offset(self.source_file.syntax(), self.range.start()) - } -} diff --git a/crates/ra_ide_api/src/assists/fill_match_arm.rs b/crates/ra_ide_api/src/assists/fill_match_arm.rs deleted file mode 100644 index 6ae829d85..000000000 --- a/crates/ra_ide_api/src/assists/fill_match_arm.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::fmt::Write; -use hir::{ - AdtDef, - source_binder, - Ty, - FieldSource, -}; -use ra_ide_api_light::{ - assists::{ - Assist, - AssistBuilder - } -}; -use ra_syntax::{ - ast::{ - self, - AstNode, - } -}; - -use crate::assists::AssistCtx; - -pub fn fill_match_arm(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.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(); - match match_expr_ty { - Ty::Adt { def_id, .. } => match def_id { - AdtDef::Enum(e) => { - let mut buf = format!("match {} {{\n", expr.syntax().text().to_string()); - let variants = e.variants(ctx.db); - for variant in variants { - let name = variant.name(ctx.db)?; - write!( - &mut buf, - " {}::{}", - e.name(ctx.db)?.to_string(), - name.to_string() - ) - .expect("write fmt"); - - let pat = variant - .fields(ctx.db) - .into_iter() - .map(|field| { - let name = field.name(ctx.db).to_string(); - let (_, source) = field.source(ctx.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(", ")).expect("write fmt"), - Some(_) => write!(&mut buf, "{{{}}}", pat.join(", ")).expect("write fmt"), - None => (), - }; - - buf.push_str(" => (),\n"); - } - buf.push_str("}"); - ctx.build("fill match arms", |edit: &mut AssistBuilder| { - edit.replace_node_and_indent(match_expr.syntax(), buf); - }) - } - _ => None, - }, - _ => None, - } -} - -#[cfg(test)] -mod tests { - use insta::assert_debug_snapshot_matches; - - use ra_syntax::{TextRange, TextUnit}; - - use crate::{ - FileRange, - mock_analysis::{analysis_and_position, single_file_with_position} -}; - use ra_db::SourceDatabase; - - fn test_assit(name: &str, code: &str) { - let (analysis, position) = if code.contains("//-") { - analysis_and_position(code) - } else { - single_file_with_position(code) - }; - let frange = FileRange { - file_id: position.file_id, - range: TextRange::offset_len(position.offset, TextUnit::from(1)), - }; - let source_file = analysis - .with_db(|db| db.parse(frange.file_id)) - .expect("source file"); - let ret = analysis - .with_db(|db| crate::assists::assists(db, frange.file_id, &source_file, frange.range)) - .expect("assists"); - - assert_debug_snapshot_matches!(name, ret); - } - - #[test] - fn test_fill_match_arm() { - test_assit( - "fill_match_arm1", - r#" - enum A { - As, - Bs, - Cs(String), - Ds(String, String), - Es{x: usize, y: usize} - } - - fn main() { - let a = A::As; - match a<|> - } - "#, - ); - - test_assit( - "fill_match_arm2", - r#" - enum A { - As, - Bs, - Cs(String), - Ds(String, String), - Es{x: usize, y: usize} - } - - fn main() { - let a = A::As; - match a<|> {} - } - "#, - ); - } -} diff --git a/crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm1.snap b/crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm1.snap deleted file mode 100644 index 980726d92..000000000 --- a/crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm1.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -created: "2019-02-03T15:38:46.094184+00:00" -creator: insta@0.5.2 -expression: ret -source: crates/ra_ide_api/src/assits/fill_match_arm.rs ---- -[ - LocalEdit { - label: "fill match arms", - edit: TextEdit { - atoms: [ - AtomTextEdit { - delete: [211; 218), - insert: "match a {\n A::As => (),\n A::Bs => (),\n A::Cs(_) => (),\n A::Ds(_, _) => (),\n A::Es{x, y} => (),\n }" - } - ] - }, - cursor_position: None - } -] diff --git a/crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm2.snap b/crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm2.snap deleted file mode 100644 index cee0efe74..000000000 --- a/crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm2.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -created: "2019-02-03T15:41:34.640074+00:00" -creator: insta@0.5.2 -expression: ret -source: crates/ra_ide_api/src/assits/fill_match_arm.rs ---- -[ - LocalEdit { - label: "fill match arms", - edit: TextEdit { - atoms: [ - AtomTextEdit { - delete: [211; 221), - insert: "match a {\n A::As => (),\n A::Bs => (),\n A::Cs(_) => (),\n A::Ds(_, _) => (),\n A::Es{x, y} => (),\n }" - } - ] - }, - cursor_position: None - } -] diff --git a/crates/ra_ide_api/src/imp.rs b/crates/ra_ide_api/src/imp.rs index fd8637ad2..b139efabf 100644 --- a/crates/ra_ide_api/src/imp.rs +++ b/crates/ra_ide_api/src/imp.rs @@ -19,7 +19,7 @@ use ra_syntax::{ use crate::{ AnalysisChange, - CrateId, db, Diagnostic, FileId, FilePosition, FileRange, FileSystemEdit, + CrateId, db, Diagnostic, FileId, FilePosition, FileSystemEdit, Query, RootChange, SourceChange, SourceFileEdit, symbol_index::{FileSymbol, SymbolsDatabase}, status::syntax_tree_stats @@ -236,15 +236,6 @@ impl db::RootDatabase { res } - pub(crate) fn assists(&self, frange: FileRange) -> Vec { - let file = self.parse(frange.file_id); - ra_ide_api_light::assists::assists(&file, frange.range) - .into_iter() - .chain(crate::assists::assists(self, frange.file_id, &file, frange.range).into_iter()) - .map(|local_edit| SourceChange::from_local_edit(frange.file_id, local_edit)) - .collect() - } - pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Vec { let name = name_ref.text(); let mut query = Query::new(name.to_string()); diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 3a187d7a5..8beaba5de 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -477,7 +477,7 @@ impl Analysis { /// Computes assists (aks code actons aka intentions) for the given /// position. pub fn assists(&self, frange: FileRange) -> Cancelable> { - self.with_db(|db| db.assists(frange)) + self.with_db(|db| assists::assists(db, frange)) } /// Computes the set of diagnostics for the given file. diff --git a/crates/ra_ide_api_light/src/assists.rs b/crates/ra_ide_api_light/src/assists.rs deleted file mode 100644 index e578805f1..000000000 --- a/crates/ra_ide_api_light/src/assists.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! This modules contains various "assists": suggestions for source code edits -//! which are likely to occur at a given cursor position. For example, if the -//! cursor is on the `,`, a possible assist is swapping the elements around the -//! comma. - -mod flip_comma; -mod add_derive; -mod add_impl; -mod introduce_variable; -mod change_visibility; -mod split_import; -mod replace_if_let_with_match; - -use ra_text_edit::{TextEdit, TextEditBuilder}; -use ra_syntax::{ - Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode, - algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, -}; -use itertools::Itertools; - -use crate::formatting::leading_indent; - -pub use self::{ - flip_comma::flip_comma, - add_derive::add_derive, - add_impl::add_impl, - introduce_variable::introduce_variable, - change_visibility::change_visibility, - split_import::split_import, - replace_if_let_with_match::replace_if_let_with_match, -}; - -/// Return all the assists applicable at the given position. -pub fn assists(file: &SourceFile, range: TextRange) -> Vec { - let ctx = AssistCtx::new(file, range); - [ - flip_comma, - add_derive, - add_impl, - introduce_variable, - change_visibility, - split_import, - replace_if_let_with_match, - ] - .iter() - .filter_map(|&assist| ctx.clone().apply(assist)) - .collect() -} - -#[derive(Debug)] -pub struct LocalEdit { - pub label: String, - pub edit: TextEdit, - pub cursor_position: Option, -} - -fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { - node.siblings(direction) - .skip(1) - .find(|node| !node.kind().is_trivia()) -} - -/// `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)] -pub struct AssistCtx<'a> { - source_file: &'a SourceFile, - range: TextRange, - should_compute_edit: bool, -} - -#[derive(Debug)] -pub enum Assist { - Applicable, - Edit(LocalEdit), -} - -#[derive(Default)] -pub struct AssistBuilder { - edit: TextEditBuilder, - cursor_position: Option, -} - -impl<'a> AssistCtx<'a> { - pub fn new(source_file: &'a SourceFile, range: TextRange) -> AssistCtx { - AssistCtx { - source_file, - range, - should_compute_edit: false, - } - } - - pub fn apply(mut self, assist: fn(AssistCtx) -> Option) -> Option { - self.should_compute_edit = true; - match assist(self) { - None => None, - Some(Assist::Edit(e)) => Some(e), - Some(Assist::Applicable) => unreachable!(), - } - } - - pub fn check(mut self, assist: fn(AssistCtx) -> Option) -> bool { - self.should_compute_edit = false; - match assist(self) { - None => false, - Some(Assist::Edit(_)) => unreachable!(), - Some(Assist::Applicable) => true, - } - } - - fn build(self, label: impl Into, f: impl FnOnce(&mut AssistBuilder)) -> Option { - if !self.should_compute_edit { - return Some(Assist::Applicable); - } - let mut edit = AssistBuilder::default(); - f(&mut edit); - Some(edit.build(label)) - } - - pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { - find_leaf_at_offset(self.source_file.syntax(), self.range.start()) - } - pub(crate) fn node_at_offset(&self) -> Option<&'a N> { - find_node_at_offset(self.source_file.syntax(), self.range.start()) - } - pub(crate) fn covering_node(&self) -> &'a SyntaxNode { - find_covering_node(self.source_file.syntax(), self.range) - } -} - -impl AssistBuilder { - fn replace(&mut self, range: TextRange, replace_with: impl Into) { - self.edit.replace(range, replace_with.into()) - } - pub 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)] - fn delete(&mut self, range: TextRange) { - self.edit.delete(range) - } - fn insert(&mut self, offset: TextUnit, text: impl Into) { - self.edit.insert(offset, text.into()) - } - fn set_cursor(&mut self, offset: TextUnit) { - self.cursor_position = Some(offset) - } - pub fn build(self, label: impl Into) -> Assist { - Assist::Edit(LocalEdit { - label: label.into(), - cursor_position: self.cursor_position, - edit: self.edit.finish(), - }) - } -} - -fn reindent(text: &str, indent: &str) -> String { - let indent = format!("\n{}", indent); - text.lines().intersperse(&indent).collect() -} - -#[cfg(test)] -fn check_assist(assist: fn(AssistCtx) -> Option, before: &str, after: &str) { - crate::test_utils::check_action(before, after, |file, off| { - let range = TextRange::offset_len(off, 0.into()); - AssistCtx::new(file, range).apply(assist) - }) -} - -#[cfg(test)] -fn check_assist_not_applicable(assist: fn(AssistCtx) -> Option, text: &str) { - crate::test_utils::check_action_not_applicable(text, |file, off| { - let range = TextRange::offset_len(off, 0.into()); - AssistCtx::new(file, range).apply(assist) - }) -} - -#[cfg(test)] -fn check_assist_range(assist: fn(AssistCtx) -> Option, before: &str, after: &str) { - crate::test_utils::check_action_range(before, after, |file, range| { - AssistCtx::new(file, range).apply(assist) - }) -} diff --git a/crates/ra_ide_api_light/src/assists/add_derive.rs b/crates/ra_ide_api_light/src/assists/add_derive.rs deleted file mode 100644 index 6e964d011..000000000 --- a/crates/ra_ide_api_light/src/assists/add_derive.rs +++ /dev/null @@ -1,84 +0,0 @@ -use ra_syntax::{ - ast::{self, AstNode, AttrsOwner}, - SyntaxKind::{WHITESPACE, COMMENT}, - TextUnit, -}; - -use crate::assists::{AssistCtx, Assist}; - -pub 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::assists::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_ide_api_light/src/assists/add_impl.rs b/crates/ra_ide_api_light/src/assists/add_impl.rs deleted file mode 100644 index 2eda7cae2..000000000 --- a/crates/ra_ide_api_light/src/assists/add_impl.rs +++ /dev/null @@ -1,66 +0,0 @@ -use join_to_string::join; -use ra_syntax::{ - ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, - TextUnit, -}; - -use crate::assists::{AssistCtx, Assist}; - -pub 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::assists::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_ide_api_light/src/assists/change_visibility.rs b/crates/ra_ide_api_light/src/assists/change_visibility.rs deleted file mode 100644 index 6e8bc2632..000000000 --- a/crates/ra_ide_api_light/src/assists/change_visibility.rs +++ /dev/null @@ -1,165 +0,0 @@ -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::assists::{AssistCtx, Assist}; - -pub 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::assists::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_ide_api_light/src/assists/flip_comma.rs b/crates/ra_ide_api_light/src/assists/flip_comma.rs deleted file mode 100644 index a343413cc..000000000 --- a/crates/ra_ide_api_light/src/assists/flip_comma.rs +++ /dev/null @@ -1,31 +0,0 @@ -use ra_syntax::{ - Direction, - SyntaxKind::COMMA, -}; - -use crate::assists::{non_trivia_sibling, AssistCtx, Assist}; - -pub 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::assists::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_ide_api_light/src/assists/introduce_variable.rs b/crates/ra_ide_api_light/src/assists/introduce_variable.rs deleted file mode 100644 index ed13bddc4..000000000 --- a/crates/ra_ide_api_light/src/assists/introduce_variable.rs +++ /dev/null @@ -1,431 +0,0 @@ -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::assists::{AssistCtx, Assist}; - -pub 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::assists::{ 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_ide_api_light/src/assists/replace_if_let_with_match.rs b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs deleted file mode 100644 index 71880b919..000000000 --- a/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs +++ /dev/null @@ -1,81 +0,0 @@ -use ra_syntax::{AstNode, ast}; - -use crate::{ - assists::{AssistCtx, Assist}, - formatting::extract_trivial_expression, -}; - -pub 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::assists::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_ide_api_light/src/assists/split_import.rs b/crates/ra_ide_api_light/src/assists/split_import.rs deleted file mode 100644 index e4015f07d..000000000 --- a/crates/ra_ide_api_light/src/assists/split_import.rs +++ /dev/null @@ -1,56 +0,0 @@ -use ra_syntax::{ - TextUnit, AstNode, SyntaxKind::COLONCOLON, - ast, - algo::generate, -}; - -use crate::assists::{AssistCtx, Assist}; - -pub 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::assists::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}}", - ) - } -} diff --git a/crates/ra_ide_api_light/src/formatting.rs b/crates/ra_ide_api_light/src/formatting.rs index 1f34b85d6..46ffa7d96 100644 --- a/crates/ra_ide_api_light/src/formatting.rs +++ b/crates/ra_ide_api_light/src/formatting.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use ra_syntax::{ AstNode, SyntaxNode, SyntaxKind::*, @@ -5,8 +6,13 @@ use ra_syntax::{ algo::generate, }; +pub fn reindent(text: &str, indent: &str) -> String { + let indent = format!("\n{}", indent); + text.lines().intersperse(&indent).collect() +} + /// If the node is on the beginning of the line, calculate indent. -pub(crate) fn leading_indent(node: &SyntaxNode) -> Option<&str> { +pub fn leading_indent(node: &SyntaxNode) -> Option<&str> { for leaf in prev_leaves(node) { if let Some(ws) = ast::Whitespace::cast(leaf) { let ws_text = ws.text(); @@ -32,7 +38,7 @@ fn prev_leaf(node: &SyntaxNode) -> Option<&SyntaxNode> { .last() } -pub(crate) fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> { +pub fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> { let expr = block.expr()?; if expr.syntax().text().contains('\n') { return None; diff --git a/crates/ra_ide_api_light/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs index 9dd72701d..17044270c 100644 --- a/crates/ra_ide_api_light/src/lib.rs +++ b/crates/ra_ide_api_light/src/lib.rs @@ -3,7 +3,7 @@ //! This usually means functions which take syntax tree as an input and produce //! an edit or some auxiliary info. -pub mod assists; +pub mod formatting; mod extend_selection; mod folding_ranges; mod line_index; @@ -14,10 +14,15 @@ mod test_utils; mod join_lines; mod typing; mod diagnostics; -pub(crate) mod formatting; + +#[derive(Debug)] +pub struct LocalEdit { + pub label: String, + pub edit: ra_text_edit::TextEdit, + pub cursor_position: Option, +} pub use self::{ - assists::LocalEdit, extend_selection::extend_selection, folding_ranges::{folding_ranges, Fold, FoldKind}, line_index::{LineCol, LineIndex}, diff --git a/crates/ra_ide_api_light/src/test_utils.rs b/crates/ra_ide_api_light/src/test_utils.rs index 22ded2435..bfac0fce3 100644 --- a/crates/ra_ide_api_light/src/test_utils.rs +++ b/crates/ra_ide_api_light/src/test_utils.rs @@ -1,4 +1,4 @@ -use ra_syntax::{SourceFile, TextRange, TextUnit}; +use ra_syntax::{SourceFile, TextUnit}; use crate::LocalEdit; pub use test_utils::*; @@ -22,32 +22,3 @@ pub fn check_action Option>( let actual = add_cursor(&actual, actual_cursor_pos); assert_eq_text!(after, &actual); } - -pub fn check_action_not_applicable Option>( - text: &str, - f: F, -) { - let (text_cursor_pos, text) = extract_offset(text); - let file = SourceFile::parse(&text); - assert!( - f(&file, text_cursor_pos).is_none(), - "code action is applicable but it shouldn't" - ); -} - -pub fn check_action_range Option>( - before: &str, - after: &str, - f: F, -) { - let (range, before) = extract_range(before); - let file = SourceFile::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); -} -- cgit v1.2.3