From 0c5fd8f7cbf04eda763e55bc9a38dad5f7ec917d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 3 Feb 2019 21:26:35 +0300 Subject: [PATCH] move assists to a separate crate --- Cargo.lock | 14 ++ crates/ra_assists/Cargo.toml | 17 ++ .../assists => ra_assists/src}/add_derive.rs | 7 +- .../assists => ra_assists/src}/add_impl.rs | 7 +- crates/ra_assists/src/assist_ctx.rs | 154 +++++++++++++ .../src}/change_visibility.rs | 11 +- crates/ra_assists/src/fill_match_arms.rs | 145 ++++++++++++ .../assists => ra_assists/src}/flip_comma.rs | 8 +- .../src}/introduce_variable.rs | 7 +- crates/ra_assists/src/lib.rs | 170 ++++++++++++++ .../src}/replace_if_let_with_match.rs | 11 +- .../src}/split_import.rs | 7 +- 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 | 105 ++------- .../ra_ide_api/src/assists/fill_match_arm.rs | 157 ------------- .../snapshots/tests__fill_match_arm1.snap | 20 -- .../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/formatting.rs | 10 +- crates/ra_ide_api_light/src/lib.rs | 11 +- crates/ra_ide_api_light/src/test_utils.rs | 31 +-- 26 files changed, 580 insertions(+), 578 deletions(-) create mode 100644 crates/ra_assists/Cargo.toml rename crates/{ra_ide_api_light/src/assists => ra_assists/src}/add_derive.rs (92%) rename crates/{ra_ide_api_light/src/assists => ra_assists/src}/add_impl.rs (91%) create mode 100644 crates/ra_assists/src/assist_ctx.rs rename crates/{ra_ide_api_light/src/assists => ra_assists/src}/change_visibility.rs (92%) create mode 100644 crates/ra_assists/src/fill_match_arms.rs rename crates/{ra_ide_api_light/src/assists => ra_assists/src}/flip_comma.rs (77%) rename crates/{ra_ide_api_light/src/assists => ra_assists/src}/introduce_variable.rs (97%) create mode 100644 crates/ra_assists/src/lib.rs rename crates/{ra_ide_api_light/src/assists => ra_assists/src}/replace_if_let_with_match.rs (88%) rename crates/{ra_ide_api_light/src/assists => ra_assists/src}/split_import.rs (88%) 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 diff --git a/Cargo.lock b/Cargo.lock index 957190fdb56..15cd4386c59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,6 +912,19 @@ dependencies = [ name = "ra_arena" version = "0.1.0" +[[package]] +name = "ra_assists" +version = "0.1.0" +dependencies = [ + "join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ra_db 0.1.0", + "ra_hir 0.1.0", + "ra_ide_api_light 0.1.0", + "ra_syntax 0.1.0", + "ra_text_edit 0.1.0", + "test_utils 0.1.0", +] + [[package]] name = "ra_cli" version = "0.1.0" @@ -970,6 +983,7 @@ dependencies = [ "join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ra_assists 0.1.0", "ra_db 0.1.0", "ra_hir 0.1.0", "ra_ide_api_light 0.1.0", diff --git a/crates/ra_assists/Cargo.toml b/crates/ra_assists/Cargo.toml new file mode 100644 index 00000000000..20bc253e30b --- /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_ide_api_light/src/assists/add_derive.rs b/crates/ra_assists/src/add_derive.rs similarity index 92% rename from crates/ra_ide_api_light/src/assists/add_derive.rs rename to crates/ra_assists/src/add_derive.rs index 6e964d011bd..01a4079f68b 100644 --- a/crates/ra_ide_api_light/src/assists/add_derive.rs +++ b/crates/ra_assists/src/add_derive.rs @@ -1,12 +1,13 @@ +use hir::db::HirDatabase; use ra_syntax::{ ast::{self, AstNode, AttrsOwner}, SyntaxKind::{WHITESPACE, COMMENT}, TextUnit, }; -use crate::assists::{AssistCtx, Assist}; +use crate::{AssistCtx, Assist}; -pub fn add_derive(ctx: AssistCtx) -> Option { +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| { @@ -39,7 +40,7 @@ fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::assists::check_assist; + use crate::helpers::check_assist; #[test] fn add_derive_new() { diff --git a/crates/ra_ide_api_light/src/assists/add_impl.rs b/crates/ra_assists/src/add_impl.rs similarity index 91% rename from crates/ra_ide_api_light/src/assists/add_impl.rs rename to crates/ra_assists/src/add_impl.rs index 2eda7cae208..699508f9151 100644 --- a/crates/ra_ide_api_light/src/assists/add_impl.rs +++ b/crates/ra_assists/src/add_impl.rs @@ -1,12 +1,13 @@ use join_to_string::join; +use hir::db::HirDatabase; use ra_syntax::{ ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, TextUnit, }; -use crate::assists::{AssistCtx, Assist}; +use crate::{AssistCtx, Assist}; -pub fn add_impl(ctx: AssistCtx) -> Option { +pub(crate) fn add_impl(ctx: AssistCtx) -> Option { let nominal = ctx.node_at_offset::()?; let name = nominal.name()?; ctx.build("add impl", |edit| { @@ -42,7 +43,7 @@ pub fn add_impl(ctx: AssistCtx) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::assists::check_assist; + use crate::helpers::check_assist; #[test] fn test_add_impl() { diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs new file mode 100644 index 00000000000..6d09bde52af --- /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_ide_api_light/src/assists/change_visibility.rs b/crates/ra_assists/src/change_visibility.rs similarity index 92% rename from crates/ra_ide_api_light/src/assists/change_visibility.rs rename to crates/ra_assists/src/change_visibility.rs index 6e8bc263241..4cd32985e09 100644 --- a/crates/ra_ide_api_light/src/assists/change_visibility.rs +++ b/crates/ra_assists/src/change_visibility.rs @@ -1,19 +1,20 @@ +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::assists::{AssistCtx, Assist}; +use crate::{AssistCtx, Assist}; -pub fn change_visibility(ctx: AssistCtx) -> Option { +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 { +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, @@ -57,7 +58,7 @@ fn vis_offset(node: &SyntaxNode) -> TextUnit { .unwrap_or(node.range().start()) } -fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option { +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)"); @@ -76,7 +77,7 @@ fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::assists::check_assist; + use crate::helpers::check_assist; #[test] fn change_visibility_adds_pub_crate_to_items() { 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 00000000000..9aa37d94cdb --- /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_ide_api_light/src/assists/flip_comma.rs b/crates/ra_assists/src/flip_comma.rs similarity index 77% rename from crates/ra_ide_api_light/src/assists/flip_comma.rs rename to crates/ra_assists/src/flip_comma.rs index a343413cc57..a49820c2957 100644 --- a/crates/ra_ide_api_light/src/assists/flip_comma.rs +++ b/crates/ra_assists/src/flip_comma.rs @@ -1,11 +1,12 @@ +use hir::db::HirDatabase; use ra_syntax::{ Direction, SyntaxKind::COMMA, }; -use crate::assists::{non_trivia_sibling, AssistCtx, Assist}; +use crate::{AssistCtx, Assist, non_trivia_sibling}; -pub fn flip_comma(ctx: AssistCtx) -> Option { +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)?; @@ -18,7 +19,8 @@ pub fn flip_comma(ctx: AssistCtx) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::assists::check_assist; + + use crate::helpers::check_assist; #[test] fn flip_comma_works_for_function_parameters() { diff --git a/crates/ra_ide_api_light/src/assists/introduce_variable.rs b/crates/ra_assists/src/introduce_variable.rs similarity index 97% rename from crates/ra_ide_api_light/src/assists/introduce_variable.rs rename to crates/ra_assists/src/introduce_variable.rs index ed13bddc48e..c937a816cda 100644 --- a/crates/ra_ide_api_light/src/assists/introduce_variable.rs +++ b/crates/ra_assists/src/introduce_variable.rs @@ -1,3 +1,4 @@ +use hir::db::HirDatabase; use ra_syntax::{ ast::{self, AstNode}, SyntaxKind::{ @@ -5,9 +6,9 @@ use ra_syntax::{ }, SyntaxNode, TextUnit, }; -use crate::assists::{AssistCtx, Assist}; +use crate::{AssistCtx, Assist}; -pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option { +pub(crate) fn introduce_variable<'a>(ctx: AssistCtx) -> Option { let node = ctx.covering_node(); if !valid_covering_node(node) { return None; @@ -103,7 +104,7 @@ fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> { #[cfg(test)] mod tests { use super::*; - use crate::assists::{ check_assist, check_assist_not_applicable, check_assist_range }; + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range}; #[test] fn test_introduce_var_simple() { diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs new file mode 100644 index 00000000000..4e97a84c20d --- /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_ide_api_light/src/assists/replace_if_let_with_match.rs b/crates/ra_assists/src/replace_if_let_with_match.rs similarity index 88% rename from crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs rename to crates/ra_assists/src/replace_if_let_with_match.rs index 71880b91982..f6af47ec913 100644 --- a/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs +++ b/crates/ra_assists/src/replace_if_let_with_match.rs @@ -1,11 +1,10 @@ use ra_syntax::{AstNode, ast}; +use ra_ide_api_light::formatting::extract_trivial_expression; +use hir::db::HirDatabase; -use crate::{ - assists::{AssistCtx, Assist}, - formatting::extract_trivial_expression, -}; +use crate::{AssistCtx, Assist}; -pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option { +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()?; @@ -51,7 +50,7 @@ fn format_arm(block: &ast::Block) -> String { #[cfg(test)] mod tests { use super::*; - use crate::assists::check_assist; + use crate::helpers::check_assist; #[test] fn test_replace_if_let_with_match_unwraps_simple_expressions() { diff --git a/crates/ra_ide_api_light/src/assists/split_import.rs b/crates/ra_assists/src/split_import.rs similarity index 88% rename from crates/ra_ide_api_light/src/assists/split_import.rs rename to crates/ra_assists/src/split_import.rs index e4015f07da6..7e34be0878f 100644 --- a/crates/ra_ide_api_light/src/assists/split_import.rs +++ b/crates/ra_assists/src/split_import.rs @@ -1,12 +1,13 @@ +use hir::db::HirDatabase; use ra_syntax::{ TextUnit, AstNode, SyntaxKind::COLONCOLON, ast, algo::generate, }; -use crate::assists::{AssistCtx, Assist}; +use crate::{AssistCtx, Assist}; -pub fn split_import(ctx: AssistCtx) -> Option { +pub(crate) fn split_import(ctx: AssistCtx) -> Option { let colon_colon = ctx .leaf_at_offset() .find(|leaf| leaf.kind() == COLONCOLON)?; @@ -34,7 +35,7 @@ pub fn split_import(ctx: AssistCtx) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::assists::check_assist; + use crate::helpers::check_assist; #[test] fn test_split_import() { diff --git a/crates/ra_db/src/lib.rs b/crates/ra_db/src/lib.rs index 926cf0bd598..66634e05b37 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 54da5559857..a9cd955cf05 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 00a07d1a18a..87095fb2192 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 54de9b2e3ab..95cccf8cf94 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 2da251df5d1..2a96fdf471d 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_db::{FileRange, FilePosition}; -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 -}; +use crate::{SourceFileEdit, SourceChange, db::RootDatabase}; -/// 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)) +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 6ae829d8569..00000000000 --- 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 980726d92f6..00000000000 --- 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 cee0efe74a6..00000000000 --- 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 fd8637ad249..b139efabf6d 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 3a187d7a542..8beaba5de3b 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 e578805f107..00000000000 --- 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/formatting.rs b/crates/ra_ide_api_light/src/formatting.rs index 1f34b85d693..46ffa7d9604 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 9dd72701d70..17044270c60 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 22ded243553..bfac0fce369 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); -}