diff options
26 files changed, 582 insertions, 580 deletions
diff --git a/Cargo.lock b/Cargo.lock index 957190fdb..15cd4386c 100644 --- a/Cargo.lock +++ b/Cargo.lock | |||
@@ -913,6 +913,19 @@ name = "ra_arena" | |||
913 | version = "0.1.0" | 913 | version = "0.1.0" |
914 | 914 | ||
915 | [[package]] | 915 | [[package]] |
916 | name = "ra_assists" | ||
917 | version = "0.1.0" | ||
918 | dependencies = [ | ||
919 | "join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||
920 | "ra_db 0.1.0", | ||
921 | "ra_hir 0.1.0", | ||
922 | "ra_ide_api_light 0.1.0", | ||
923 | "ra_syntax 0.1.0", | ||
924 | "ra_text_edit 0.1.0", | ||
925 | "test_utils 0.1.0", | ||
926 | ] | ||
927 | |||
928 | [[package]] | ||
916 | name = "ra_cli" | 929 | name = "ra_cli" |
917 | version = "0.1.0" | 930 | version = "0.1.0" |
918 | dependencies = [ | 931 | dependencies = [ |
@@ -970,6 +983,7 @@ dependencies = [ | |||
970 | "join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", | 983 | "join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", |
971 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", | 984 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", |
972 | "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", | 985 | "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", |
986 | "ra_assists 0.1.0", | ||
973 | "ra_db 0.1.0", | 987 | "ra_db 0.1.0", |
974 | "ra_hir 0.1.0", | 988 | "ra_hir 0.1.0", |
975 | "ra_ide_api_light 0.1.0", | 989 | "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 000000000..20bc253e3 --- /dev/null +++ b/crates/ra_assists/Cargo.toml | |||
@@ -0,0 +1,17 @@ | |||
1 | [package] | ||
2 | edition = "2018" | ||
3 | name = "ra_assists" | ||
4 | version = "0.1.0" | ||
5 | authors = ["Aleksey Kladov <[email protected]>"] | ||
6 | |||
7 | [dependencies] | ||
8 | join_to_string = "0.1.3" | ||
9 | |||
10 | ra_ide_api_light = { path = "../ra_ide_api_light" } | ||
11 | ra_syntax = { path = "../ra_syntax" } | ||
12 | ra_text_edit = { path = "../ra_text_edit" } | ||
13 | ra_db = { path = "../ra_db" } | ||
14 | hir = { path = "../ra_hir", package = "ra_hir" } | ||
15 | |||
16 | [dev-dependencies] | ||
17 | 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 index 6e964d011..01a4079f6 100644 --- a/crates/ra_ide_api_light/src/assists/add_derive.rs +++ b/crates/ra_assists/src/add_derive.rs | |||
@@ -1,12 +1,13 @@ | |||
1 | use hir::db::HirDatabase; | ||
1 | use ra_syntax::{ | 2 | use ra_syntax::{ |
2 | ast::{self, AstNode, AttrsOwner}, | 3 | ast::{self, AstNode, AttrsOwner}, |
3 | SyntaxKind::{WHITESPACE, COMMENT}, | 4 | SyntaxKind::{WHITESPACE, COMMENT}, |
4 | TextUnit, | 5 | TextUnit, |
5 | }; | 6 | }; |
6 | 7 | ||
7 | use crate::assists::{AssistCtx, Assist}; | 8 | use crate::{AssistCtx, Assist}; |
8 | 9 | ||
9 | pub fn add_derive(ctx: AssistCtx) -> Option<Assist> { | 10 | pub(crate) fn add_derive(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
10 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; | 11 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; |
11 | let node_start = derive_insertion_offset(nominal)?; | 12 | let node_start = derive_insertion_offset(nominal)?; |
12 | ctx.build("add `#[derive]`", |edit| { | 13 | ctx.build("add `#[derive]`", |edit| { |
@@ -39,7 +40,7 @@ fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option<TextUnit> { | |||
39 | #[cfg(test)] | 40 | #[cfg(test)] |
40 | mod tests { | 41 | mod tests { |
41 | use super::*; | 42 | use super::*; |
42 | use crate::assists::check_assist; | 43 | use crate::helpers::check_assist; |
43 | 44 | ||
44 | #[test] | 45 | #[test] |
45 | fn add_derive_new() { | 46 | 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 index 2eda7cae2..699508f91 100644 --- a/crates/ra_ide_api_light/src/assists/add_impl.rs +++ b/crates/ra_assists/src/add_impl.rs | |||
@@ -1,12 +1,13 @@ | |||
1 | use join_to_string::join; | 1 | use join_to_string::join; |
2 | use hir::db::HirDatabase; | ||
2 | use ra_syntax::{ | 3 | use ra_syntax::{ |
3 | ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, | 4 | ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, |
4 | TextUnit, | 5 | TextUnit, |
5 | }; | 6 | }; |
6 | 7 | ||
7 | use crate::assists::{AssistCtx, Assist}; | 8 | use crate::{AssistCtx, Assist}; |
8 | 9 | ||
9 | pub fn add_impl(ctx: AssistCtx) -> Option<Assist> { | 10 | pub(crate) fn add_impl(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
10 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; | 11 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; |
11 | let name = nominal.name()?; | 12 | let name = nominal.name()?; |
12 | ctx.build("add impl", |edit| { | 13 | ctx.build("add impl", |edit| { |
@@ -42,7 +43,7 @@ pub fn add_impl(ctx: AssistCtx) -> Option<Assist> { | |||
42 | #[cfg(test)] | 43 | #[cfg(test)] |
43 | mod tests { | 44 | mod tests { |
44 | use super::*; | 45 | use super::*; |
45 | use crate::assists::check_assist; | 46 | use crate::helpers::check_assist; |
46 | 47 | ||
47 | #[test] | 48 | #[test] |
48 | fn test_add_impl() { | 49 | 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 000000000..6d09bde52 --- /dev/null +++ b/crates/ra_assists/src/assist_ctx.rs | |||
@@ -0,0 +1,154 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_text_edit::TextEditBuilder; | ||
3 | use ra_db::FileRange; | ||
4 | use ra_syntax::{ | ||
5 | SourceFile, TextRange, AstNode, TextUnit, SyntaxNode, | ||
6 | algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, | ||
7 | }; | ||
8 | use ra_ide_api_light::formatting::{leading_indent, reindent}; | ||
9 | |||
10 | use crate::{AssistLabel, AssistAction}; | ||
11 | |||
12 | pub(crate) enum Assist { | ||
13 | Unresolved(AssistLabel), | ||
14 | Resolved(AssistLabel, AssistAction), | ||
15 | } | ||
16 | |||
17 | /// `AssistCtx` allows to apply an assist or check if it could be applied. | ||
18 | /// | ||
19 | /// Assists use a somewhat overengineered approach, given the current needs. The | ||
20 | /// assists workflow consists of two phases. In the first phase, a user asks for | ||
21 | /// the list of available assists. In the second phase, the user picks a | ||
22 | /// particular assist and it gets applied. | ||
23 | /// | ||
24 | /// There are two peculiarities here: | ||
25 | /// | ||
26 | /// * first, we ideally avoid computing more things then necessary to answer | ||
27 | /// "is assist applicable" in the first phase. | ||
28 | /// * second, when we are applying assist, we don't have a guarantee that there | ||
29 | /// weren't any changes between the point when user asked for assists and when | ||
30 | /// they applied a particular assist. So, when applying assist, we need to do | ||
31 | /// all the checks from scratch. | ||
32 | /// | ||
33 | /// To avoid repeating the same code twice for both "check" and "apply" | ||
34 | /// functions, we use an approach reminiscent of that of Django's function based | ||
35 | /// views dealing with forms. Each assist receives a runtime parameter, | ||
36 | /// `should_compute_edit`. It first check if an edit is applicable (potentially | ||
37 | /// computing info required to compute the actual edit). If it is applicable, | ||
38 | /// and `should_compute_edit` is `true`, it then computes the actual edit. | ||
39 | /// | ||
40 | /// So, to implement the original assists workflow, we can first apply each edit | ||
41 | /// with `should_compute_edit = false`, and then applying the selected edit | ||
42 | /// again, with `should_compute_edit = true` this time. | ||
43 | /// | ||
44 | /// Note, however, that we don't actually use such two-phase logic at the | ||
45 | /// moment, because the LSP API is pretty awkward in this place, and it's much | ||
46 | /// easier to just compute the edit eagerly :-)#[derive(Debug, Clone)] | ||
47 | #[derive(Debug)] | ||
48 | pub(crate) struct AssistCtx<'a, DB> { | ||
49 | pub(crate) db: &'a DB, | ||
50 | pub(crate) frange: FileRange, | ||
51 | source_file: &'a SourceFile, | ||
52 | should_compute_edit: bool, | ||
53 | } | ||
54 | |||
55 | impl<'a, DB> Clone for AssistCtx<'a, DB> { | ||
56 | fn clone(&self) -> Self { | ||
57 | AssistCtx { | ||
58 | db: self.db, | ||
59 | frange: self.frange, | ||
60 | source_file: self.source_file, | ||
61 | should_compute_edit: self.should_compute_edit, | ||
62 | } | ||
63 | } | ||
64 | } | ||
65 | |||
66 | impl<'a, DB: HirDatabase> AssistCtx<'a, DB> { | ||
67 | pub(crate) fn with_ctx<F, T>(db: &DB, frange: FileRange, should_compute_edit: bool, f: F) -> T | ||
68 | where | ||
69 | F: FnOnce(AssistCtx<DB>) -> T, | ||
70 | { | ||
71 | let source_file = &db.parse(frange.file_id); | ||
72 | let ctx = AssistCtx { | ||
73 | db, | ||
74 | frange, | ||
75 | source_file, | ||
76 | should_compute_edit, | ||
77 | }; | ||
78 | f(ctx) | ||
79 | } | ||
80 | |||
81 | pub(crate) fn build( | ||
82 | self, | ||
83 | label: impl Into<String>, | ||
84 | f: impl FnOnce(&mut AssistBuilder), | ||
85 | ) -> Option<Assist> { | ||
86 | let label = AssistLabel { | ||
87 | label: label.into(), | ||
88 | }; | ||
89 | if !self.should_compute_edit { | ||
90 | return Some(Assist::Unresolved(label)); | ||
91 | } | ||
92 | let action = { | ||
93 | let mut edit = AssistBuilder::default(); | ||
94 | f(&mut edit); | ||
95 | edit.build() | ||
96 | }; | ||
97 | Some(Assist::Resolved(label, action)) | ||
98 | } | ||
99 | |||
100 | pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { | ||
101 | find_leaf_at_offset(self.source_file.syntax(), self.frange.range.start()) | ||
102 | } | ||
103 | |||
104 | pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> { | ||
105 | find_node_at_offset(self.source_file.syntax(), self.frange.range.start()) | ||
106 | } | ||
107 | pub(crate) fn covering_node(&self) -> &'a SyntaxNode { | ||
108 | find_covering_node(self.source_file.syntax(), self.frange.range) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | #[derive(Default)] | ||
113 | pub(crate) struct AssistBuilder { | ||
114 | edit: TextEditBuilder, | ||
115 | cursor_position: Option<TextUnit>, | ||
116 | } | ||
117 | |||
118 | impl AssistBuilder { | ||
119 | pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { | ||
120 | self.edit.replace(range, replace_with.into()) | ||
121 | } | ||
122 | |||
123 | pub(crate) fn replace_node_and_indent( | ||
124 | &mut self, | ||
125 | node: &SyntaxNode, | ||
126 | replace_with: impl Into<String>, | ||
127 | ) { | ||
128 | let mut replace_with = replace_with.into(); | ||
129 | if let Some(indent) = leading_indent(node) { | ||
130 | replace_with = reindent(&replace_with, indent) | ||
131 | } | ||
132 | self.replace(node.range(), replace_with) | ||
133 | } | ||
134 | |||
135 | #[allow(unused)] | ||
136 | pub(crate) fn delete(&mut self, range: TextRange) { | ||
137 | self.edit.delete(range) | ||
138 | } | ||
139 | |||
140 | pub(crate) fn insert(&mut self, offset: TextUnit, text: impl Into<String>) { | ||
141 | self.edit.insert(offset, text.into()) | ||
142 | } | ||
143 | |||
144 | pub(crate) fn set_cursor(&mut self, offset: TextUnit) { | ||
145 | self.cursor_position = Some(offset) | ||
146 | } | ||
147 | |||
148 | fn build(self) -> AssistAction { | ||
149 | AssistAction { | ||
150 | edit: self.edit.finish(), | ||
151 | cursor_position: self.cursor_position, | ||
152 | } | ||
153 | } | ||
154 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/change_visibility.rs b/crates/ra_assists/src/change_visibility.rs index 6e8bc2632..4cd32985e 100644 --- a/crates/ra_ide_api_light/src/assists/change_visibility.rs +++ b/crates/ra_assists/src/change_visibility.rs | |||
@@ -1,19 +1,20 @@ | |||
1 | use hir::db::HirDatabase; | ||
1 | use ra_syntax::{ | 2 | use ra_syntax::{ |
2 | AstNode, SyntaxNode, TextUnit, | 3 | AstNode, SyntaxNode, TextUnit, |
3 | ast::{self, VisibilityOwner, NameOwner}, | 4 | ast::{self, VisibilityOwner, NameOwner}, |
4 | 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}, | 5 | 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}, |
5 | }; | 6 | }; |
6 | 7 | ||
7 | use crate::assists::{AssistCtx, Assist}; | 8 | use crate::{AssistCtx, Assist}; |
8 | 9 | ||
9 | pub fn change_visibility(ctx: AssistCtx) -> Option<Assist> { | 10 | pub(crate) fn change_visibility(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
10 | if let Some(vis) = ctx.node_at_offset::<ast::Visibility>() { | 11 | if let Some(vis) = ctx.node_at_offset::<ast::Visibility>() { |
11 | return change_vis(ctx, vis); | 12 | return change_vis(ctx, vis); |
12 | } | 13 | } |
13 | add_vis(ctx) | 14 | add_vis(ctx) |
14 | } | 15 | } |
15 | 16 | ||
16 | fn add_vis(ctx: AssistCtx) -> Option<Assist> { | 17 | fn add_vis(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
17 | let item_keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() { | 18 | let item_keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() { |
18 | FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, | 19 | FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, |
19 | _ => false, | 20 | _ => false, |
@@ -57,7 +58,7 @@ fn vis_offset(node: &SyntaxNode) -> TextUnit { | |||
57 | .unwrap_or(node.range().start()) | 58 | .unwrap_or(node.range().start()) |
58 | } | 59 | } |
59 | 60 | ||
60 | fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> { | 61 | fn change_vis(ctx: AssistCtx<impl HirDatabase>, vis: &ast::Visibility) -> Option<Assist> { |
61 | if vis.syntax().text() == "pub" { | 62 | if vis.syntax().text() == "pub" { |
62 | return ctx.build("chage to pub(crate)", |edit| { | 63 | return ctx.build("chage to pub(crate)", |edit| { |
63 | edit.replace(vis.syntax().range(), "pub(crate)"); | 64 | edit.replace(vis.syntax().range(), "pub(crate)"); |
@@ -76,7 +77,7 @@ fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> { | |||
76 | #[cfg(test)] | 77 | #[cfg(test)] |
77 | mod tests { | 78 | mod tests { |
78 | use super::*; | 79 | use super::*; |
79 | use crate::assists::check_assist; | 80 | use crate::helpers::check_assist; |
80 | 81 | ||
81 | #[test] | 82 | #[test] |
82 | fn change_visibility_adds_pub_crate_to_items() { | 83 | 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 000000000..9aa37d94c --- /dev/null +++ b/crates/ra_assists/src/fill_match_arms.rs | |||
@@ -0,0 +1,145 @@ | |||
1 | use std::fmt::Write; | ||
2 | |||
3 | use hir::{ | ||
4 | AdtDef, Ty, FieldSource, source_binder, | ||
5 | db::HirDatabase, | ||
6 | }; | ||
7 | use ra_syntax::ast::{self, AstNode}; | ||
8 | |||
9 | use crate::{AssistCtx, Assist}; | ||
10 | |||
11 | pub(crate) fn fill_match_arms(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
12 | let match_expr = ctx.node_at_offset::<ast::MatchExpr>()?; | ||
13 | |||
14 | // We already have some match arms, so we don't provide any assists. | ||
15 | match match_expr.match_arm_list() { | ||
16 | Some(arm_list) if arm_list.arms().count() > 0 => { | ||
17 | return None; | ||
18 | } | ||
19 | _ => {} | ||
20 | } | ||
21 | |||
22 | let expr = match_expr.expr()?; | ||
23 | let function = | ||
24 | source_binder::function_from_child_node(ctx.db, ctx.frange.file_id, expr.syntax())?; | ||
25 | let infer_result = function.infer(ctx.db); | ||
26 | let syntax_mapping = function.body_syntax_mapping(ctx.db); | ||
27 | let node_expr = syntax_mapping.node_expr(expr)?; | ||
28 | let match_expr_ty = infer_result[node_expr].clone(); | ||
29 | let enum_def = match match_expr_ty { | ||
30 | Ty::Adt { | ||
31 | def_id: AdtDef::Enum(e), | ||
32 | .. | ||
33 | } => e, | ||
34 | _ => return None, | ||
35 | }; | ||
36 | let enum_name = enum_def.name(ctx.db)?; | ||
37 | let db = ctx.db; | ||
38 | |||
39 | ctx.build("fill match arms", |edit| { | ||
40 | let mut buf = format!("match {} {{\n", expr.syntax().text().to_string()); | ||
41 | let variants = enum_def.variants(db); | ||
42 | for variant in variants { | ||
43 | let name = match variant.name(db) { | ||
44 | Some(it) => it, | ||
45 | None => continue, | ||
46 | }; | ||
47 | write!(&mut buf, " {}::{}", enum_name, name.to_string()).unwrap(); | ||
48 | |||
49 | let pat = variant | ||
50 | .fields(db) | ||
51 | .into_iter() | ||
52 | .map(|field| { | ||
53 | let name = field.name(db).to_string(); | ||
54 | let (_, source) = field.source(db); | ||
55 | match source { | ||
56 | FieldSource::Named(_) => name, | ||
57 | FieldSource::Pos(_) => "_".to_string(), | ||
58 | } | ||
59 | }) | ||
60 | .collect::<Vec<_>>(); | ||
61 | |||
62 | match pat.first().map(|s| s.as_str()) { | ||
63 | Some("_") => write!(&mut buf, "({})", pat.join(", ")).unwrap(), | ||
64 | Some(_) => write!(&mut buf, "{{{}}}", pat.join(", ")).unwrap(), | ||
65 | None => (), | ||
66 | }; | ||
67 | |||
68 | buf.push_str(" => (),\n"); | ||
69 | } | ||
70 | buf.push_str("}"); | ||
71 | edit.set_cursor(expr.syntax().range().start()); | ||
72 | edit.replace_node_and_indent(match_expr.syntax(), buf); | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | #[cfg(test)] | ||
77 | mod tests { | ||
78 | use crate::helpers::check_assist; | ||
79 | |||
80 | use super::fill_match_arms; | ||
81 | |||
82 | #[test] | ||
83 | fn fill_match_arms_empty_body() { | ||
84 | check_assist( | ||
85 | fill_match_arms, | ||
86 | r#" | ||
87 | enum A { | ||
88 | As, | ||
89 | Bs, | ||
90 | Cs(String), | ||
91 | Ds(String, String), | ||
92 | Es{x: usize, y: usize} | ||
93 | } | ||
94 | |||
95 | fn main() { | ||
96 | let a = A::As; | ||
97 | match a<|> {} | ||
98 | } | ||
99 | "#, | ||
100 | r#" | ||
101 | enum A { | ||
102 | As, | ||
103 | Bs, | ||
104 | Cs(String), | ||
105 | Ds(String, String), | ||
106 | Es{x: usize, y: usize} | ||
107 | } | ||
108 | |||
109 | fn main() { | ||
110 | let a = A::As; | ||
111 | match <|>a { | ||
112 | A::As => (), | ||
113 | A::Bs => (), | ||
114 | A::Cs(_) => (), | ||
115 | A::Ds(_, _) => (), | ||
116 | A::Es{x, y} => (), | ||
117 | } | ||
118 | } | ||
119 | "#, | ||
120 | ); | ||
121 | } | ||
122 | #[test] | ||
123 | fn fill_match_arms_no_body() { | ||
124 | check_assist( | ||
125 | fill_match_arms, | ||
126 | r#" | ||
127 | enum E { X, Y} | ||
128 | |||
129 | fn main() { | ||
130 | match E::X<|> | ||
131 | } | ||
132 | "#, | ||
133 | r#" | ||
134 | enum E { X, Y} | ||
135 | |||
136 | fn main() { | ||
137 | match <|>E::X { | ||
138 | E::X => (), | ||
139 | E::Y => (), | ||
140 | } | ||
141 | } | ||
142 | "#, | ||
143 | ); | ||
144 | } | ||
145 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/flip_comma.rs b/crates/ra_assists/src/flip_comma.rs index a343413cc..a49820c29 100644 --- a/crates/ra_ide_api_light/src/assists/flip_comma.rs +++ b/crates/ra_assists/src/flip_comma.rs | |||
@@ -1,11 +1,12 @@ | |||
1 | use hir::db::HirDatabase; | ||
1 | use ra_syntax::{ | 2 | use ra_syntax::{ |
2 | Direction, | 3 | Direction, |
3 | SyntaxKind::COMMA, | 4 | SyntaxKind::COMMA, |
4 | }; | 5 | }; |
5 | 6 | ||
6 | use crate::assists::{non_trivia_sibling, AssistCtx, Assist}; | 7 | use crate::{AssistCtx, Assist, non_trivia_sibling}; |
7 | 8 | ||
8 | pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> { | 9 | pub(crate) fn flip_comma(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
9 | let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?; | 10 | let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?; |
10 | let prev = non_trivia_sibling(comma, Direction::Prev)?; | 11 | let prev = non_trivia_sibling(comma, Direction::Prev)?; |
11 | let next = non_trivia_sibling(comma, Direction::Next)?; | 12 | let next = non_trivia_sibling(comma, Direction::Next)?; |
@@ -18,7 +19,8 @@ pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> { | |||
18 | #[cfg(test)] | 19 | #[cfg(test)] |
19 | mod tests { | 20 | mod tests { |
20 | use super::*; | 21 | use super::*; |
21 | use crate::assists::check_assist; | 22 | |
23 | use crate::helpers::check_assist; | ||
22 | 24 | ||
23 | #[test] | 25 | #[test] |
24 | fn flip_comma_works_for_function_parameters() { | 26 | 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 index ed13bddc4..c937a816c 100644 --- a/crates/ra_ide_api_light/src/assists/introduce_variable.rs +++ b/crates/ra_assists/src/introduce_variable.rs | |||
@@ -1,3 +1,4 @@ | |||
1 | use hir::db::HirDatabase; | ||
1 | use ra_syntax::{ | 2 | use ra_syntax::{ |
2 | ast::{self, AstNode}, | 3 | ast::{self, AstNode}, |
3 | SyntaxKind::{ | 4 | SyntaxKind::{ |
@@ -5,9 +6,9 @@ use ra_syntax::{ | |||
5 | }, SyntaxNode, TextUnit, | 6 | }, SyntaxNode, TextUnit, |
6 | }; | 7 | }; |
7 | 8 | ||
8 | use crate::assists::{AssistCtx, Assist}; | 9 | use crate::{AssistCtx, Assist}; |
9 | 10 | ||
10 | pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option<Assist> { | 11 | pub(crate) fn introduce_variable<'a>(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
11 | let node = ctx.covering_node(); | 12 | let node = ctx.covering_node(); |
12 | if !valid_covering_node(node) { | 13 | if !valid_covering_node(node) { |
13 | return None; | 14 | return None; |
@@ -103,7 +104,7 @@ fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> { | |||
103 | #[cfg(test)] | 104 | #[cfg(test)] |
104 | mod tests { | 105 | mod tests { |
105 | use super::*; | 106 | use super::*; |
106 | use crate::assists::{ check_assist, check_assist_not_applicable, check_assist_range }; | 107 | use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range}; |
107 | 108 | ||
108 | #[test] | 109 | #[test] |
109 | fn test_introduce_var_simple() { | 110 | 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 000000000..4e97a84c2 --- /dev/null +++ b/crates/ra_assists/src/lib.rs | |||
@@ -0,0 +1,170 @@ | |||
1 | //! `ra_assits` crate provides a bunch of code assists, aslo known as code | ||
2 | //! actions (in LSP) or intentions (in IntelliJ). | ||
3 | //! | ||
4 | //! An assist is a micro-refactoring, which is automatically activated in | ||
5 | //! certain context. For example, if the cursor is over `,`, a "swap `,`" assist | ||
6 | //! becomes available. | ||
7 | |||
8 | mod assist_ctx; | ||
9 | |||
10 | use ra_text_edit::TextEdit; | ||
11 | use ra_syntax::{TextUnit, SyntaxNode, Direction}; | ||
12 | use ra_db::FileRange; | ||
13 | use hir::db::HirDatabase; | ||
14 | |||
15 | pub(crate) use crate::assist_ctx::{AssistCtx, Assist}; | ||
16 | |||
17 | #[derive(Debug)] | ||
18 | pub struct AssistLabel { | ||
19 | /// Short description of the assist, as shown in the UI. | ||
20 | pub label: String, | ||
21 | } | ||
22 | |||
23 | pub struct AssistAction { | ||
24 | pub edit: TextEdit, | ||
25 | pub cursor_position: Option<TextUnit>, | ||
26 | } | ||
27 | |||
28 | /// Return all the assists applicable at the given position. | ||
29 | /// | ||
30 | /// Assists are returned in the "unresolved" state, that is only labels are | ||
31 | /// returned, without actual edits. | ||
32 | pub fn applicable_assists<H>(db: &H, range: FileRange) -> Vec<AssistLabel> | ||
33 | where | ||
34 | H: HirDatabase + 'static, | ||
35 | { | ||
36 | AssistCtx::with_ctx(db, range, false, |ctx| { | ||
37 | all_assists() | ||
38 | .iter() | ||
39 | .filter_map(|f| f(ctx.clone())) | ||
40 | .map(|a| match a { | ||
41 | Assist::Unresolved(label) => label, | ||
42 | Assist::Resolved(..) => unreachable!(), | ||
43 | }) | ||
44 | .collect() | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | /// Return all the assists applicable at the given position. | ||
49 | /// | ||
50 | /// Assists are returned in the "resolved" state, that is with edit fully | ||
51 | /// computed. | ||
52 | pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)> | ||
53 | where | ||
54 | H: HirDatabase + 'static, | ||
55 | { | ||
56 | AssistCtx::with_ctx(db, range, false, |ctx| { | ||
57 | all_assists() | ||
58 | .iter() | ||
59 | .filter_map(|f| f(ctx.clone())) | ||
60 | .map(|a| match a { | ||
61 | Assist::Resolved(label, action) => (label, action), | ||
62 | Assist::Unresolved(..) => unreachable!(), | ||
63 | }) | ||
64 | .collect() | ||
65 | }) | ||
66 | } | ||
67 | |||
68 | mod add_derive; | ||
69 | mod add_impl; | ||
70 | mod flip_comma; | ||
71 | mod change_visibility; | ||
72 | mod fill_match_arms; | ||
73 | mod introduce_variable; | ||
74 | mod replace_if_let_with_match; | ||
75 | mod split_import; | ||
76 | fn all_assists<DB: HirDatabase>() -> &'static [fn(AssistCtx<DB>) -> Option<Assist>] { | ||
77 | &[ | ||
78 | add_derive::add_derive, | ||
79 | add_impl::add_impl, | ||
80 | change_visibility::change_visibility, | ||
81 | fill_match_arms::fill_match_arms, | ||
82 | flip_comma::flip_comma, | ||
83 | introduce_variable::introduce_variable, | ||
84 | replace_if_let_with_match::replace_if_let_with_match, | ||
85 | split_import::split_import, | ||
86 | ] | ||
87 | } | ||
88 | |||
89 | fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { | ||
90 | node.siblings(direction) | ||
91 | .skip(1) | ||
92 | .find(|node| !node.kind().is_trivia()) | ||
93 | } | ||
94 | |||
95 | #[cfg(test)] | ||
96 | mod helpers { | ||
97 | use hir::mock::MockDatabase; | ||
98 | use ra_syntax::TextRange; | ||
99 | use ra_db::FileRange; | ||
100 | use test_utils::{extract_offset, assert_eq_text, add_cursor, extract_range}; | ||
101 | |||
102 | use crate::{AssistCtx, Assist}; | ||
103 | |||
104 | pub(crate) fn check_assist( | ||
105 | assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>, | ||
106 | before: &str, | ||
107 | after: &str, | ||
108 | ) { | ||
109 | let (before_cursor_pos, before) = extract_offset(before); | ||
110 | let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); | ||
111 | let frange = FileRange { | ||
112 | file_id, | ||
113 | range: TextRange::offset_len(before_cursor_pos, 0.into()), | ||
114 | }; | ||
115 | let assist = | ||
116 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | ||
117 | let action = match assist { | ||
118 | Assist::Unresolved(_) => unreachable!(), | ||
119 | Assist::Resolved(_, it) => it, | ||
120 | }; | ||
121 | |||
122 | let actual = action.edit.apply(&before); | ||
123 | let actual_cursor_pos = match action.cursor_position { | ||
124 | None => action | ||
125 | .edit | ||
126 | .apply_to_offset(before_cursor_pos) | ||
127 | .expect("cursor position is affected by the edit"), | ||
128 | Some(off) => off, | ||
129 | }; | ||
130 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
131 | assert_eq_text!(after, &actual); | ||
132 | } | ||
133 | |||
134 | pub(crate) fn check_assist_range( | ||
135 | assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>, | ||
136 | before: &str, | ||
137 | after: &str, | ||
138 | ) { | ||
139 | let (range, before) = extract_range(before); | ||
140 | let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); | ||
141 | let frange = FileRange { file_id, range }; | ||
142 | let assist = | ||
143 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | ||
144 | let action = match assist { | ||
145 | Assist::Unresolved(_) => unreachable!(), | ||
146 | Assist::Resolved(_, it) => it, | ||
147 | }; | ||
148 | |||
149 | let mut actual = action.edit.apply(&before); | ||
150 | if let Some(pos) = action.cursor_position { | ||
151 | actual = add_cursor(&actual, pos); | ||
152 | } | ||
153 | assert_eq_text!(after, &actual); | ||
154 | } | ||
155 | |||
156 | pub(crate) fn check_assist_not_applicable( | ||
157 | assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>, | ||
158 | before: &str, | ||
159 | ) { | ||
160 | let (before_cursor_pos, before) = extract_offset(before); | ||
161 | let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); | ||
162 | let frange = FileRange { | ||
163 | file_id, | ||
164 | range: TextRange::offset_len(before_cursor_pos, 0.into()), | ||
165 | }; | ||
166 | let assist = AssistCtx::with_ctx(&db, frange, true, assist); | ||
167 | assert!(assist.is_none()); | ||
168 | } | ||
169 | |||
170 | } | ||
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 index 71880b919..f6af47ec9 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 @@ | |||
1 | use ra_syntax::{AstNode, ast}; | 1 | use ra_syntax::{AstNode, ast}; |
2 | use ra_ide_api_light::formatting::extract_trivial_expression; | ||
3 | use hir::db::HirDatabase; | ||
2 | 4 | ||
3 | use crate::{ | 5 | use crate::{AssistCtx, Assist}; |
4 | assists::{AssistCtx, Assist}, | ||
5 | formatting::extract_trivial_expression, | ||
6 | }; | ||
7 | 6 | ||
8 | pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option<Assist> { | 7 | pub(crate) fn replace_if_let_with_match(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
9 | let if_expr: &ast::IfExpr = ctx.node_at_offset()?; | 8 | let if_expr: &ast::IfExpr = ctx.node_at_offset()?; |
10 | let cond = if_expr.condition()?; | 9 | let cond = if_expr.condition()?; |
11 | let pat = cond.pat()?; | 10 | let pat = cond.pat()?; |
@@ -51,7 +50,7 @@ fn format_arm(block: &ast::Block) -> String { | |||
51 | #[cfg(test)] | 50 | #[cfg(test)] |
52 | mod tests { | 51 | mod tests { |
53 | use super::*; | 52 | use super::*; |
54 | use crate::assists::check_assist; | 53 | use crate::helpers::check_assist; |
55 | 54 | ||
56 | #[test] | 55 | #[test] |
57 | fn test_replace_if_let_with_match_unwraps_simple_expressions() { | 56 | 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 index e4015f07d..7e34be087 100644 --- a/crates/ra_ide_api_light/src/assists/split_import.rs +++ b/crates/ra_assists/src/split_import.rs | |||
@@ -1,12 +1,13 @@ | |||
1 | use hir::db::HirDatabase; | ||
1 | use ra_syntax::{ | 2 | use ra_syntax::{ |
2 | TextUnit, AstNode, SyntaxKind::COLONCOLON, | 3 | TextUnit, AstNode, SyntaxKind::COLONCOLON, |
3 | ast, | 4 | ast, |
4 | algo::generate, | 5 | algo::generate, |
5 | }; | 6 | }; |
6 | 7 | ||
7 | use crate::assists::{AssistCtx, Assist}; | 8 | use crate::{AssistCtx, Assist}; |
8 | 9 | ||
9 | pub fn split_import(ctx: AssistCtx) -> Option<Assist> { | 10 | pub(crate) fn split_import(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
10 | let colon_colon = ctx | 11 | let colon_colon = ctx |
11 | .leaf_at_offset() | 12 | .leaf_at_offset() |
12 | .find(|leaf| leaf.kind() == COLONCOLON)?; | 13 | .find(|leaf| leaf.kind() == COLONCOLON)?; |
@@ -34,7 +35,7 @@ pub fn split_import(ctx: AssistCtx) -> Option<Assist> { | |||
34 | #[cfg(test)] | 35 | #[cfg(test)] |
35 | mod tests { | 36 | mod tests { |
36 | use super::*; | 37 | use super::*; |
37 | use crate::assists::check_assist; | 38 | use crate::helpers::check_assist; |
38 | 39 | ||
39 | #[test] | 40 | #[test] |
40 | fn test_split_import() { | 41 | fn test_split_import() { |
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 { | |||
70 | /// Database which stores all significant input facts: source code and project | 70 | /// Database which stores all significant input facts: source code and project |
71 | /// model. Everything else in rust-analyzer is derived from these queries. | 71 | /// model. Everything else in rust-analyzer is derived from these queries. |
72 | #[salsa::query_group(SourceDatabaseStorage)] | 72 | #[salsa::query_group(SourceDatabaseStorage)] |
73 | pub trait SourceDatabase: CheckCanceled { | 73 | pub trait SourceDatabase: CheckCanceled + std::fmt::Debug { |
74 | /// Text of the file. | 74 | /// Text of the file. |
75 | #[salsa::input] | 75 | #[salsa::input] |
76 | fn file_text(&self, file_id: FileId) -> Arc<String>; | 76 | fn file_text(&self, file_id: FileId) -> Arc<String>; |
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 { | |||
18 | } | 18 | } |
19 | 19 | ||
20 | pub mod db; | 20 | pub mod db; |
21 | #[cfg(test)] | 21 | pub mod mock; |
22 | mod mock; | ||
23 | mod query_definitions; | 22 | mod query_definitions; |
24 | mod path; | 23 | mod path; |
25 | pub mod source_binder; | 24 | 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); | |||
17 | db::PersistentHirDatabaseStorage | 17 | db::PersistentHirDatabaseStorage |
18 | )] | 18 | )] |
19 | #[derive(Debug)] | 19 | #[derive(Debug)] |
20 | pub(crate) struct MockDatabase { | 20 | pub struct MockDatabase { |
21 | events: Mutex<Option<Vec<salsa::Event<MockDatabase>>>>, | 21 | events: Mutex<Option<Vec<salsa::Event<MockDatabase>>>>, |
22 | runtime: salsa::Runtime<MockDatabase>, | 22 | runtime: salsa::Runtime<MockDatabase>, |
23 | interner: Arc<HirInterner>, | 23 | interner: Arc<HirInterner>, |
@@ -27,13 +27,13 @@ pub(crate) struct MockDatabase { | |||
27 | impl panic::RefUnwindSafe for MockDatabase {} | 27 | impl panic::RefUnwindSafe for MockDatabase {} |
28 | 28 | ||
29 | impl MockDatabase { | 29 | impl MockDatabase { |
30 | pub(crate) fn with_files(fixture: &str) -> (MockDatabase, SourceRoot) { | 30 | pub fn with_files(fixture: &str) -> (MockDatabase, SourceRoot) { |
31 | let (db, source_root, position) = MockDatabase::from_fixture(fixture); | 31 | let (db, source_root, position) = MockDatabase::from_fixture(fixture); |
32 | assert!(position.is_none()); | 32 | assert!(position.is_none()); |
33 | (db, source_root) | 33 | (db, source_root) |
34 | } | 34 | } |
35 | 35 | ||
36 | pub(crate) fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId) { | 36 | pub fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId) { |
37 | let mut db = MockDatabase::default(); | 37 | let mut db = MockDatabase::default(); |
38 | let mut source_root = SourceRoot::default(); | 38 | let mut source_root = SourceRoot::default(); |
39 | let file_id = db.add_file(WORKSPACE, &mut source_root, "/main.rs", text); | 39 | let file_id = db.add_file(WORKSPACE, &mut source_root, "/main.rs", text); |
@@ -41,7 +41,7 @@ impl MockDatabase { | |||
41 | (db, source_root, file_id) | 41 | (db, source_root, file_id) |
42 | } | 42 | } |
43 | 43 | ||
44 | pub(crate) fn with_position(fixture: &str) -> (MockDatabase, FilePosition) { | 44 | pub fn with_position(fixture: &str) -> (MockDatabase, FilePosition) { |
45 | let (db, _, position) = MockDatabase::from_fixture(fixture); | 45 | let (db, _, position) = MockDatabase::from_fixture(fixture); |
46 | let position = position.expect("expected a marker ( <|> )"); | 46 | let position = position.expect("expected a marker ( <|> )"); |
47 | (db, position) | 47 | (db, position) |
@@ -166,13 +166,13 @@ impl AsRef<HirInterner> for MockDatabase { | |||
166 | } | 166 | } |
167 | 167 | ||
168 | impl MockDatabase { | 168 | impl MockDatabase { |
169 | pub(crate) fn log(&self, f: impl FnOnce()) -> Vec<salsa::Event<MockDatabase>> { | 169 | pub fn log(&self, f: impl FnOnce()) -> Vec<salsa::Event<MockDatabase>> { |
170 | *self.events.lock() = Some(Vec::new()); | 170 | *self.events.lock() = Some(Vec::new()); |
171 | f(); | 171 | f(); |
172 | self.events.lock().take().unwrap() | 172 | self.events.lock().take().unwrap() |
173 | } | 173 | } |
174 | 174 | ||
175 | pub(crate) fn log_executed(&self, f: impl FnOnce()) -> Vec<String> { | 175 | pub fn log_executed(&self, f: impl FnOnce()) -> Vec<String> { |
176 | let events = self.log(f); | 176 | let events = self.log(f); |
177 | events | 177 | events |
178 | .into_iter() | 178 | .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" } | |||
24 | ra_db = { path = "../ra_db" } | 24 | ra_db = { path = "../ra_db" } |
25 | hir = { path = "../ra_hir", package = "ra_hir" } | 25 | hir = { path = "../ra_hir", package = "ra_hir" } |
26 | test_utils = { path = "../test_utils" } | 26 | test_utils = { path = "../test_utils" } |
27 | ra_assists = { path = "../ra_assists" } | ||
27 | 28 | ||
28 | [dev-dependencies] | 29 | [dev-dependencies] |
29 | insta = "0.6.1" | 30 | 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 @@ | |||
1 | mod fill_match_arm; | 1 | use ra_db::{FileRange, FilePosition}; |
2 | 2 | ||
3 | use ra_syntax::{ | 3 | use crate::{SourceFileEdit, SourceChange, db::RootDatabase}; |
4 | TextRange, SourceFile, AstNode, | 4 | |
5 | algo::find_node_at_offset, | 5 | pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<SourceChange> { |
6 | }; | 6 | ra_assists::assists(db, frange) |
7 | use ra_ide_api_light::{ | 7 | .into_iter() |
8 | LocalEdit, | 8 | .map(|(label, action)| { |
9 | assists::{ | 9 | let file_id = frange.file_id; |
10 | Assist, | 10 | let file_edit = SourceFileEdit { |
11 | AssistBuilder | 11 | file_id, |
12 | } | 12 | edit: action.edit, |
13 | }; | 13 | }; |
14 | use crate::{ | 14 | SourceChange { |
15 | db::RootDatabase, | 15 | label: label.label, |
16 | FileId | 16 | source_file_edits: vec![file_edit], |
17 | }; | 17 | file_system_edits: vec![], |
18 | 18 | cursor_position: action | |
19 | /// Return all the assists applicable at the given position. | 19 | .cursor_position |
20 | pub(crate) fn assists( | 20 | .map(|offset| FilePosition { offset, file_id }), |
21 | db: &RootDatabase, | 21 | } |
22 | file_id: FileId, | 22 | }) |
23 | file: &SourceFile, | ||
24 | range: TextRange, | ||
25 | ) -> Vec<LocalEdit> { | ||
26 | let ctx = AssistCtx::new(db, file_id, file, range); | ||
27 | [fill_match_arm::fill_match_arm] | ||
28 | .iter() | ||
29 | .filter_map(|&assist| ctx.clone().apply(assist)) | ||
30 | .collect() | 23 | .collect() |
31 | } | 24 | } |
32 | |||
33 | #[derive(Debug, Clone)] | ||
34 | pub struct AssistCtx<'a> { | ||
35 | file_id: FileId, | ||
36 | source_file: &'a SourceFile, | ||
37 | db: &'a RootDatabase, | ||
38 | range: TextRange, | ||
39 | should_compute_edit: bool, | ||
40 | } | ||
41 | |||
42 | impl<'a> AssistCtx<'a> { | ||
43 | pub(crate) fn new( | ||
44 | db: &'a RootDatabase, | ||
45 | file_id: FileId, | ||
46 | source_file: &'a SourceFile, | ||
47 | range: TextRange, | ||
48 | ) -> AssistCtx<'a> { | ||
49 | AssistCtx { | ||
50 | source_file, | ||
51 | file_id, | ||
52 | db, | ||
53 | range, | ||
54 | should_compute_edit: false, | ||
55 | } | ||
56 | } | ||
57 | |||
58 | pub fn apply(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> Option<LocalEdit> { | ||
59 | self.should_compute_edit = true; | ||
60 | match assist(self) { | ||
61 | None => None, | ||
62 | Some(Assist::Edit(e)) => Some(e), | ||
63 | Some(Assist::Applicable) => unreachable!(), | ||
64 | } | ||
65 | } | ||
66 | |||
67 | #[allow(unused)] | ||
68 | pub fn check(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> bool { | ||
69 | self.should_compute_edit = false; | ||
70 | match assist(self) { | ||
71 | None => false, | ||
72 | Some(Assist::Edit(_)) => unreachable!(), | ||
73 | Some(Assist::Applicable) => true, | ||
74 | } | ||
75 | } | ||
76 | |||
77 | fn build(self, label: impl Into<String>, f: impl FnOnce(&mut AssistBuilder)) -> Option<Assist> { | ||
78 | if !self.should_compute_edit { | ||
79 | return Some(Assist::Applicable); | ||
80 | } | ||
81 | let mut edit = AssistBuilder::default(); | ||
82 | f(&mut edit); | ||
83 | Some(edit.build(label)) | ||
84 | } | ||
85 | |||
86 | pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> { | ||
87 | find_node_at_offset(self.source_file.syntax(), self.range.start()) | ||
88 | } | ||
89 | } | ||
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 @@ | |||
1 | use std::fmt::Write; | ||
2 | use hir::{ | ||
3 | AdtDef, | ||
4 | source_binder, | ||
5 | Ty, | ||
6 | FieldSource, | ||
7 | }; | ||
8 | use ra_ide_api_light::{ | ||
9 | assists::{ | ||
10 | Assist, | ||
11 | AssistBuilder | ||
12 | } | ||
13 | }; | ||
14 | use ra_syntax::{ | ||
15 | ast::{ | ||
16 | self, | ||
17 | AstNode, | ||
18 | } | ||
19 | }; | ||
20 | |||
21 | use crate::assists::AssistCtx; | ||
22 | |||
23 | pub fn fill_match_arm(ctx: AssistCtx) -> Option<Assist> { | ||
24 | let match_expr = ctx.node_at_offset::<ast::MatchExpr>()?; | ||
25 | |||
26 | // We already have some match arms, so we don't provide any assists. | ||
27 | match match_expr.match_arm_list() { | ||
28 | Some(arm_list) if arm_list.arms().count() > 0 => { | ||
29 | return None; | ||
30 | } | ||
31 | _ => {} | ||
32 | } | ||
33 | |||
34 | let expr = match_expr.expr()?; | ||
35 | let function = source_binder::function_from_child_node(ctx.db, ctx.file_id, expr.syntax())?; | ||
36 | let infer_result = function.infer(ctx.db); | ||
37 | let syntax_mapping = function.body_syntax_mapping(ctx.db); | ||
38 | let node_expr = syntax_mapping.node_expr(expr)?; | ||
39 | let match_expr_ty = infer_result[node_expr].clone(); | ||
40 | match match_expr_ty { | ||
41 | Ty::Adt { def_id, .. } => match def_id { | ||
42 | AdtDef::Enum(e) => { | ||
43 | let mut buf = format!("match {} {{\n", expr.syntax().text().to_string()); | ||
44 | let variants = e.variants(ctx.db); | ||
45 | for variant in variants { | ||
46 | let name = variant.name(ctx.db)?; | ||
47 | write!( | ||
48 | &mut buf, | ||
49 | " {}::{}", | ||
50 | e.name(ctx.db)?.to_string(), | ||
51 | name.to_string() | ||
52 | ) | ||
53 | .expect("write fmt"); | ||
54 | |||
55 | let pat = variant | ||
56 | .fields(ctx.db) | ||
57 | .into_iter() | ||
58 | .map(|field| { | ||
59 | let name = field.name(ctx.db).to_string(); | ||
60 | let (_, source) = field.source(ctx.db); | ||
61 | match source { | ||
62 | FieldSource::Named(_) => name, | ||
63 | FieldSource::Pos(_) => "_".to_string(), | ||
64 | } | ||
65 | }) | ||
66 | .collect::<Vec<_>>(); | ||
67 | |||
68 | match pat.first().map(|s| s.as_str()) { | ||
69 | Some("_") => write!(&mut buf, "({})", pat.join(", ")).expect("write fmt"), | ||
70 | Some(_) => write!(&mut buf, "{{{}}}", pat.join(", ")).expect("write fmt"), | ||
71 | None => (), | ||
72 | }; | ||
73 | |||
74 | buf.push_str(" => (),\n"); | ||
75 | } | ||
76 | buf.push_str("}"); | ||
77 | ctx.build("fill match arms", |edit: &mut AssistBuilder| { | ||
78 | edit.replace_node_and_indent(match_expr.syntax(), buf); | ||
79 | }) | ||
80 | } | ||
81 | _ => None, | ||
82 | }, | ||
83 | _ => None, | ||
84 | } | ||
85 | } | ||
86 | |||
87 | #[cfg(test)] | ||
88 | mod tests { | ||
89 | use insta::assert_debug_snapshot_matches; | ||
90 | |||
91 | use ra_syntax::{TextRange, TextUnit}; | ||
92 | |||
93 | use crate::{ | ||
94 | FileRange, | ||
95 | mock_analysis::{analysis_and_position, single_file_with_position} | ||
96 | }; | ||
97 | use ra_db::SourceDatabase; | ||
98 | |||
99 | fn test_assit(name: &str, code: &str) { | ||
100 | let (analysis, position) = if code.contains("//-") { | ||
101 | analysis_and_position(code) | ||
102 | } else { | ||
103 | single_file_with_position(code) | ||
104 | }; | ||
105 | let frange = FileRange { | ||
106 | file_id: position.file_id, | ||
107 | range: TextRange::offset_len(position.offset, TextUnit::from(1)), | ||
108 | }; | ||
109 | let source_file = analysis | ||
110 | .with_db(|db| db.parse(frange.file_id)) | ||
111 | .expect("source file"); | ||
112 | let ret = analysis | ||
113 | .with_db(|db| crate::assists::assists(db, frange.file_id, &source_file, frange.range)) | ||
114 | .expect("assists"); | ||
115 | |||
116 | assert_debug_snapshot_matches!(name, ret); | ||
117 | } | ||
118 | |||
119 | #[test] | ||
120 | fn test_fill_match_arm() { | ||
121 | test_assit( | ||
122 | "fill_match_arm1", | ||
123 | r#" | ||
124 | enum A { | ||
125 | As, | ||
126 | Bs, | ||
127 | Cs(String), | ||
128 | Ds(String, String), | ||
129 | Es{x: usize, y: usize} | ||
130 | } | ||
131 | |||
132 | fn main() { | ||
133 | let a = A::As; | ||
134 | match a<|> | ||
135 | } | ||
136 | "#, | ||
137 | ); | ||
138 | |||
139 | test_assit( | ||
140 | "fill_match_arm2", | ||
141 | r#" | ||
142 | enum A { | ||
143 | As, | ||
144 | Bs, | ||
145 | Cs(String), | ||
146 | Ds(String, String), | ||
147 | Es{x: usize, y: usize} | ||
148 | } | ||
149 | |||
150 | fn main() { | ||
151 | let a = A::As; | ||
152 | match a<|> {} | ||
153 | } | ||
154 | "#, | ||
155 | ); | ||
156 | } | ||
157 | } | ||
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 @@ | |||
1 | --- | ||
2 | created: "2019-02-03T15:38:46.094184+00:00" | ||
3 | creator: [email protected] | ||
4 | expression: ret | ||
5 | source: crates/ra_ide_api/src/assits/fill_match_arm.rs | ||
6 | --- | ||
7 | [ | ||
8 | LocalEdit { | ||
9 | label: "fill match arms", | ||
10 | edit: TextEdit { | ||
11 | atoms: [ | ||
12 | AtomTextEdit { | ||
13 | delete: [211; 218), | ||
14 | insert: "match a {\n A::As => (),\n A::Bs => (),\n A::Cs(_) => (),\n A::Ds(_, _) => (),\n A::Es{x, y} => (),\n }" | ||
15 | } | ||
16 | ] | ||
17 | }, | ||
18 | cursor_position: None | ||
19 | } | ||
20 | ] | ||
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 @@ | |||
1 | --- | ||
2 | created: "2019-02-03T15:41:34.640074+00:00" | ||
3 | creator: [email protected] | ||
4 | expression: ret | ||
5 | source: crates/ra_ide_api/src/assits/fill_match_arm.rs | ||
6 | --- | ||
7 | [ | ||
8 | LocalEdit { | ||
9 | label: "fill match arms", | ||
10 | edit: TextEdit { | ||
11 | atoms: [ | ||
12 | AtomTextEdit { | ||
13 | delete: [211; 221), | ||
14 | insert: "match a {\n A::As => (),\n A::Bs => (),\n A::Cs(_) => (),\n A::Ds(_, _) => (),\n A::Es{x, y} => (),\n }" | ||
15 | } | ||
16 | ] | ||
17 | }, | ||
18 | cursor_position: None | ||
19 | } | ||
20 | ] | ||
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::{ | |||
19 | 19 | ||
20 | use crate::{ | 20 | use crate::{ |
21 | AnalysisChange, | 21 | AnalysisChange, |
22 | CrateId, db, Diagnostic, FileId, FilePosition, FileRange, FileSystemEdit, | 22 | CrateId, db, Diagnostic, FileId, FilePosition, FileSystemEdit, |
23 | Query, RootChange, SourceChange, SourceFileEdit, | 23 | Query, RootChange, SourceChange, SourceFileEdit, |
24 | symbol_index::{FileSymbol, SymbolsDatabase}, | 24 | symbol_index::{FileSymbol, SymbolsDatabase}, |
25 | status::syntax_tree_stats | 25 | status::syntax_tree_stats |
@@ -236,15 +236,6 @@ impl db::RootDatabase { | |||
236 | res | 236 | res |
237 | } | 237 | } |
238 | 238 | ||
239 | pub(crate) fn assists(&self, frange: FileRange) -> Vec<SourceChange> { | ||
240 | let file = self.parse(frange.file_id); | ||
241 | ra_ide_api_light::assists::assists(&file, frange.range) | ||
242 | .into_iter() | ||
243 | .chain(crate::assists::assists(self, frange.file_id, &file, frange.range).into_iter()) | ||
244 | .map(|local_edit| SourceChange::from_local_edit(frange.file_id, local_edit)) | ||
245 | .collect() | ||
246 | } | ||
247 | |||
248 | pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Vec<FileSymbol> { | 239 | pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Vec<FileSymbol> { |
249 | let name = name_ref.text(); | 240 | let name = name_ref.text(); |
250 | let mut query = Query::new(name.to_string()); | 241 | 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 { | |||
477 | /// Computes assists (aks code actons aka intentions) for the given | 477 | /// Computes assists (aks code actons aka intentions) for the given |
478 | /// position. | 478 | /// position. |
479 | pub fn assists(&self, frange: FileRange) -> Cancelable<Vec<SourceChange>> { | 479 | pub fn assists(&self, frange: FileRange) -> Cancelable<Vec<SourceChange>> { |
480 | self.with_db(|db| db.assists(frange)) | 480 | self.with_db(|db| assists::assists(db, frange)) |
481 | } | 481 | } |
482 | 482 | ||
483 | /// Computes the set of diagnostics for the given file. | 483 | /// 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 @@ | |||
1 | //! This modules contains various "assists": suggestions for source code edits | ||
2 | //! which are likely to occur at a given cursor position. For example, if the | ||
3 | //! cursor is on the `,`, a possible assist is swapping the elements around the | ||
4 | //! comma. | ||
5 | |||
6 | mod flip_comma; | ||
7 | mod add_derive; | ||
8 | mod add_impl; | ||
9 | mod introduce_variable; | ||
10 | mod change_visibility; | ||
11 | mod split_import; | ||
12 | mod replace_if_let_with_match; | ||
13 | |||
14 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
15 | use ra_syntax::{ | ||
16 | Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode, | ||
17 | algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, | ||
18 | }; | ||
19 | use itertools::Itertools; | ||
20 | |||
21 | use crate::formatting::leading_indent; | ||
22 | |||
23 | pub use self::{ | ||
24 | flip_comma::flip_comma, | ||
25 | add_derive::add_derive, | ||
26 | add_impl::add_impl, | ||
27 | introduce_variable::introduce_variable, | ||
28 | change_visibility::change_visibility, | ||
29 | split_import::split_import, | ||
30 | replace_if_let_with_match::replace_if_let_with_match, | ||
31 | }; | ||
32 | |||
33 | /// Return all the assists applicable at the given position. | ||
34 | pub fn assists(file: &SourceFile, range: TextRange) -> Vec<LocalEdit> { | ||
35 | let ctx = AssistCtx::new(file, range); | ||
36 | [ | ||
37 | flip_comma, | ||
38 | add_derive, | ||
39 | add_impl, | ||
40 | introduce_variable, | ||
41 | change_visibility, | ||
42 | split_import, | ||
43 | replace_if_let_with_match, | ||
44 | ] | ||
45 | .iter() | ||
46 | .filter_map(|&assist| ctx.clone().apply(assist)) | ||
47 | .collect() | ||
48 | } | ||
49 | |||
50 | #[derive(Debug)] | ||
51 | pub struct LocalEdit { | ||
52 | pub label: String, | ||
53 | pub edit: TextEdit, | ||
54 | pub cursor_position: Option<TextUnit>, | ||
55 | } | ||
56 | |||
57 | fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { | ||
58 | node.siblings(direction) | ||
59 | .skip(1) | ||
60 | .find(|node| !node.kind().is_trivia()) | ||
61 | } | ||
62 | |||
63 | /// `AssistCtx` allows to apply an assist or check if it could be applied. | ||
64 | /// | ||
65 | /// Assists use a somewhat overengineered approach, given the current needs. The | ||
66 | /// assists workflow consists of two phases. In the first phase, a user asks for | ||
67 | /// the list of available assists. In the second phase, the user picks a | ||
68 | /// particular assist and it gets applied. | ||
69 | /// | ||
70 | /// There are two peculiarities here: | ||
71 | /// | ||
72 | /// * first, we ideally avoid computing more things then necessary to answer | ||
73 | /// "is assist applicable" in the first phase. | ||
74 | /// * second, when we are applying assist, we don't have a guarantee that there | ||
75 | /// weren't any changes between the point when user asked for assists and when | ||
76 | /// they applied a particular assist. So, when applying assist, we need to do | ||
77 | /// all the checks from scratch. | ||
78 | /// | ||
79 | /// To avoid repeating the same code twice for both "check" and "apply" | ||
80 | /// functions, we use an approach reminiscent of that of Django's function based | ||
81 | /// views dealing with forms. Each assist receives a runtime parameter, | ||
82 | /// `should_compute_edit`. It first check if an edit is applicable (potentially | ||
83 | /// computing info required to compute the actual edit). If it is applicable, | ||
84 | /// and `should_compute_edit` is `true`, it then computes the actual edit. | ||
85 | /// | ||
86 | /// So, to implement the original assists workflow, we can first apply each edit | ||
87 | /// with `should_compute_edit = false`, and then applying the selected edit | ||
88 | /// again, with `should_compute_edit = true` this time. | ||
89 | /// | ||
90 | /// Note, however, that we don't actually use such two-phase logic at the | ||
91 | /// moment, because the LSP API is pretty awkward in this place, and it's much | ||
92 | /// easier to just compute the edit eagerly :-) | ||
93 | #[derive(Debug, Clone)] | ||
94 | pub struct AssistCtx<'a> { | ||
95 | source_file: &'a SourceFile, | ||
96 | range: TextRange, | ||
97 | should_compute_edit: bool, | ||
98 | } | ||
99 | |||
100 | #[derive(Debug)] | ||
101 | pub enum Assist { | ||
102 | Applicable, | ||
103 | Edit(LocalEdit), | ||
104 | } | ||
105 | |||
106 | #[derive(Default)] | ||
107 | pub struct AssistBuilder { | ||
108 | edit: TextEditBuilder, | ||
109 | cursor_position: Option<TextUnit>, | ||
110 | } | ||
111 | |||
112 | impl<'a> AssistCtx<'a> { | ||
113 | pub fn new(source_file: &'a SourceFile, range: TextRange) -> AssistCtx { | ||
114 | AssistCtx { | ||
115 | source_file, | ||
116 | range, | ||
117 | should_compute_edit: false, | ||
118 | } | ||
119 | } | ||
120 | |||
121 | pub fn apply(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> Option<LocalEdit> { | ||
122 | self.should_compute_edit = true; | ||
123 | match assist(self) { | ||
124 | None => None, | ||
125 | Some(Assist::Edit(e)) => Some(e), | ||
126 | Some(Assist::Applicable) => unreachable!(), | ||
127 | } | ||
128 | } | ||
129 | |||
130 | pub fn check(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> bool { | ||
131 | self.should_compute_edit = false; | ||
132 | match assist(self) { | ||
133 | None => false, | ||
134 | Some(Assist::Edit(_)) => unreachable!(), | ||
135 | Some(Assist::Applicable) => true, | ||
136 | } | ||
137 | } | ||
138 | |||
139 | fn build(self, label: impl Into<String>, f: impl FnOnce(&mut AssistBuilder)) -> Option<Assist> { | ||
140 | if !self.should_compute_edit { | ||
141 | return Some(Assist::Applicable); | ||
142 | } | ||
143 | let mut edit = AssistBuilder::default(); | ||
144 | f(&mut edit); | ||
145 | Some(edit.build(label)) | ||
146 | } | ||
147 | |||
148 | pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { | ||
149 | find_leaf_at_offset(self.source_file.syntax(), self.range.start()) | ||
150 | } | ||
151 | pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> { | ||
152 | find_node_at_offset(self.source_file.syntax(), self.range.start()) | ||
153 | } | ||
154 | pub(crate) fn covering_node(&self) -> &'a SyntaxNode { | ||
155 | find_covering_node(self.source_file.syntax(), self.range) | ||
156 | } | ||
157 | } | ||
158 | |||
159 | impl AssistBuilder { | ||
160 | fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { | ||
161 | self.edit.replace(range, replace_with.into()) | ||
162 | } | ||
163 | pub fn replace_node_and_indent(&mut self, node: &SyntaxNode, replace_with: impl Into<String>) { | ||
164 | let mut replace_with = replace_with.into(); | ||
165 | if let Some(indent) = leading_indent(node) { | ||
166 | replace_with = reindent(&replace_with, indent) | ||
167 | } | ||
168 | self.replace(node.range(), replace_with) | ||
169 | } | ||
170 | #[allow(unused)] | ||
171 | fn delete(&mut self, range: TextRange) { | ||
172 | self.edit.delete(range) | ||
173 | } | ||
174 | fn insert(&mut self, offset: TextUnit, text: impl Into<String>) { | ||
175 | self.edit.insert(offset, text.into()) | ||
176 | } | ||
177 | fn set_cursor(&mut self, offset: TextUnit) { | ||
178 | self.cursor_position = Some(offset) | ||
179 | } | ||
180 | pub fn build(self, label: impl Into<String>) -> Assist { | ||
181 | Assist::Edit(LocalEdit { | ||
182 | label: label.into(), | ||
183 | cursor_position: self.cursor_position, | ||
184 | edit: self.edit.finish(), | ||
185 | }) | ||
186 | } | ||
187 | } | ||
188 | |||
189 | fn reindent(text: &str, indent: &str) -> String { | ||
190 | let indent = format!("\n{}", indent); | ||
191 | text.lines().intersperse(&indent).collect() | ||
192 | } | ||
193 | |||
194 | #[cfg(test)] | ||
195 | fn check_assist(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) { | ||
196 | crate::test_utils::check_action(before, after, |file, off| { | ||
197 | let range = TextRange::offset_len(off, 0.into()); | ||
198 | AssistCtx::new(file, range).apply(assist) | ||
199 | }) | ||
200 | } | ||
201 | |||
202 | #[cfg(test)] | ||
203 | fn check_assist_not_applicable(assist: fn(AssistCtx) -> Option<Assist>, text: &str) { | ||
204 | crate::test_utils::check_action_not_applicable(text, |file, off| { | ||
205 | let range = TextRange::offset_len(off, 0.into()); | ||
206 | AssistCtx::new(file, range).apply(assist) | ||
207 | }) | ||
208 | } | ||
209 | |||
210 | #[cfg(test)] | ||
211 | fn check_assist_range(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) { | ||
212 | crate::test_utils::check_action_range(before, after, |file, range| { | ||
213 | AssistCtx::new(file, range).apply(assist) | ||
214 | }) | ||
215 | } | ||
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 @@ | |||
1 | use itertools::Itertools; | ||
1 | use ra_syntax::{ | 2 | use ra_syntax::{ |
2 | AstNode, | 3 | AstNode, |
3 | SyntaxNode, SyntaxKind::*, | 4 | SyntaxNode, SyntaxKind::*, |
@@ -5,8 +6,13 @@ use ra_syntax::{ | |||
5 | algo::generate, | 6 | algo::generate, |
6 | }; | 7 | }; |
7 | 8 | ||
9 | pub fn reindent(text: &str, indent: &str) -> String { | ||
10 | let indent = format!("\n{}", indent); | ||
11 | text.lines().intersperse(&indent).collect() | ||
12 | } | ||
13 | |||
8 | /// If the node is on the beginning of the line, calculate indent. | 14 | /// If the node is on the beginning of the line, calculate indent. |
9 | pub(crate) fn leading_indent(node: &SyntaxNode) -> Option<&str> { | 15 | pub fn leading_indent(node: &SyntaxNode) -> Option<&str> { |
10 | for leaf in prev_leaves(node) { | 16 | for leaf in prev_leaves(node) { |
11 | if let Some(ws) = ast::Whitespace::cast(leaf) { | 17 | if let Some(ws) = ast::Whitespace::cast(leaf) { |
12 | let ws_text = ws.text(); | 18 | let ws_text = ws.text(); |
@@ -32,7 +38,7 @@ fn prev_leaf(node: &SyntaxNode) -> Option<&SyntaxNode> { | |||
32 | .last() | 38 | .last() |
33 | } | 39 | } |
34 | 40 | ||
35 | pub(crate) fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> { | 41 | pub fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> { |
36 | let expr = block.expr()?; | 42 | let expr = block.expr()?; |
37 | if expr.syntax().text().contains('\n') { | 43 | if expr.syntax().text().contains('\n') { |
38 | return None; | 44 | 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 @@ | |||
3 | //! This usually means functions which take syntax tree as an input and produce | 3 | //! This usually means functions which take syntax tree as an input and produce |
4 | //! an edit or some auxiliary info. | 4 | //! an edit or some auxiliary info. |
5 | 5 | ||
6 | pub mod assists; | 6 | pub mod formatting; |
7 | mod extend_selection; | 7 | mod extend_selection; |
8 | mod folding_ranges; | 8 | mod folding_ranges; |
9 | mod line_index; | 9 | mod line_index; |
@@ -14,10 +14,15 @@ mod test_utils; | |||
14 | mod join_lines; | 14 | mod join_lines; |
15 | mod typing; | 15 | mod typing; |
16 | mod diagnostics; | 16 | mod diagnostics; |
17 | pub(crate) mod formatting; | 17 | |
18 | #[derive(Debug)] | ||
19 | pub struct LocalEdit { | ||
20 | pub label: String, | ||
21 | pub edit: ra_text_edit::TextEdit, | ||
22 | pub cursor_position: Option<TextUnit>, | ||
23 | } | ||
18 | 24 | ||
19 | pub use self::{ | 25 | pub use self::{ |
20 | assists::LocalEdit, | ||
21 | extend_selection::extend_selection, | 26 | extend_selection::extend_selection, |
22 | folding_ranges::{folding_ranges, Fold, FoldKind}, | 27 | folding_ranges::{folding_ranges, Fold, FoldKind}, |
23 | line_index::{LineCol, LineIndex}, | 28 | 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 @@ | |||
1 | use ra_syntax::{SourceFile, TextRange, TextUnit}; | 1 | use ra_syntax::{SourceFile, TextUnit}; |
2 | 2 | ||
3 | use crate::LocalEdit; | 3 | use crate::LocalEdit; |
4 | pub use test_utils::*; | 4 | pub use test_utils::*; |
@@ -22,32 +22,3 @@ pub fn check_action<F: Fn(&SourceFile, TextUnit) -> Option<LocalEdit>>( | |||
22 | let actual = add_cursor(&actual, actual_cursor_pos); | 22 | let actual = add_cursor(&actual, actual_cursor_pos); |
23 | assert_eq_text!(after, &actual); | 23 | assert_eq_text!(after, &actual); |
24 | } | 24 | } |
25 | |||
26 | pub fn check_action_not_applicable<F: Fn(&SourceFile, TextUnit) -> Option<LocalEdit>>( | ||
27 | text: &str, | ||
28 | f: F, | ||
29 | ) { | ||
30 | let (text_cursor_pos, text) = extract_offset(text); | ||
31 | let file = SourceFile::parse(&text); | ||
32 | assert!( | ||
33 | f(&file, text_cursor_pos).is_none(), | ||
34 | "code action is applicable but it shouldn't" | ||
35 | ); | ||
36 | } | ||
37 | |||
38 | pub fn check_action_range<F: Fn(&SourceFile, TextRange) -> Option<LocalEdit>>( | ||
39 | before: &str, | ||
40 | after: &str, | ||
41 | f: F, | ||
42 | ) { | ||
43 | let (range, before) = extract_range(before); | ||
44 | let file = SourceFile::parse(&before); | ||
45 | let result = f(&file, range).expect("code action is not applicable"); | ||
46 | let actual = result.edit.apply(&before); | ||
47 | let actual_cursor_pos = match result.cursor_position { | ||
48 | None => result.edit.apply_to_offset(range.start()).unwrap(), | ||
49 | Some(off) => off, | ||
50 | }; | ||
51 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
52 | assert_eq_text!(after, &actual); | ||
53 | } | ||