aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/ra_assists/Cargo.toml17
-rw-r--r--crates/ra_assists/src/add_derive.rs (renamed from crates/ra_ide_api_light/src/assists/add_derive.rs)7
-rw-r--r--crates/ra_assists/src/add_impl.rs (renamed from crates/ra_ide_api_light/src/assists/add_impl.rs)7
-rw-r--r--crates/ra_assists/src/assist_ctx.rs154
-rw-r--r--crates/ra_assists/src/change_visibility.rs (renamed from crates/ra_ide_api_light/src/assists/change_visibility.rs)11
-rw-r--r--crates/ra_assists/src/fill_match_arms.rs145
-rw-r--r--crates/ra_assists/src/flip_comma.rs (renamed from crates/ra_ide_api_light/src/assists/flip_comma.rs)8
-rw-r--r--crates/ra_assists/src/introduce_variable.rs (renamed from crates/ra_ide_api_light/src/assists/introduce_variable.rs)7
-rw-r--r--crates/ra_assists/src/lib.rs170
-rw-r--r--crates/ra_assists/src/replace_if_let_with_match.rs (renamed from crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs)11
-rw-r--r--crates/ra_assists/src/split_import.rs (renamed from crates/ra_ide_api_light/src/assists/split_import.rs)7
-rw-r--r--crates/ra_db/src/lib.rs2
-rw-r--r--crates/ra_hir/src/lib.rs3
-rw-r--r--crates/ra_hir/src/mock.rs12
-rw-r--r--crates/ra_ide_api/Cargo.toml1
-rw-r--r--crates/ra_ide_api/src/assists.rs109
-rw-r--r--crates/ra_ide_api/src/assists/fill_match_arm.rs157
-rw-r--r--crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm1.snap20
-rw-r--r--crates/ra_ide_api/src/assists/snapshots/tests__fill_match_arm2.snap20
-rw-r--r--crates/ra_ide_api/src/imp.rs11
-rw-r--r--crates/ra_ide_api/src/lib.rs2
-rw-r--r--crates/ra_ide_api_light/src/assists.rs215
-rw-r--r--crates/ra_ide_api_light/src/formatting.rs10
-rw-r--r--crates/ra_ide_api_light/src/lib.rs11
-rw-r--r--crates/ra_ide_api_light/src/test_utils.rs31
25 files changed, 568 insertions, 580 deletions
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]
2edition = "2018"
3name = "ra_assists"
4version = "0.1.0"
5authors = ["Aleksey Kladov <[email protected]>"]
6
7[dependencies]
8join_to_string = "0.1.3"
9
10ra_ide_api_light = { path = "../ra_ide_api_light" }
11ra_syntax = { path = "../ra_syntax" }
12ra_text_edit = { path = "../ra_text_edit" }
13ra_db = { path = "../ra_db" }
14hir = { path = "../ra_hir", package = "ra_hir" }
15
16[dev-dependencies]
17test_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 @@
1use hir::db::HirDatabase;
1use ra_syntax::{ 2use 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
7use crate::assists::{AssistCtx, Assist}; 8use crate::{AssistCtx, Assist};
8 9
9pub fn add_derive(ctx: AssistCtx) -> Option<Assist> { 10pub(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)]
40mod tests { 41mod 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 @@
1use join_to_string::join; 1use join_to_string::join;
2use hir::db::HirDatabase;
2use ra_syntax::{ 3use ra_syntax::{
3 ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, 4 ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner},
4 TextUnit, 5 TextUnit,
5}; 6};
6 7
7use crate::assists::{AssistCtx, Assist}; 8use crate::{AssistCtx, Assist};
8 9
9pub fn add_impl(ctx: AssistCtx) -> Option<Assist> { 10pub(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)]
43mod tests { 44mod 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 @@
1use hir::db::HirDatabase;
2use ra_text_edit::TextEditBuilder;
3use ra_db::FileRange;
4use ra_syntax::{
5 SourceFile, TextRange, AstNode, TextUnit, SyntaxNode,
6 algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset},
7};
8use ra_ide_api_light::formatting::{leading_indent, reindent};
9
10use crate::{AssistLabel, AssistAction};
11
12pub(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)]
48pub(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
55impl<'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
66impl<'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)]
113pub(crate) struct AssistBuilder {
114 edit: TextEditBuilder,
115 cursor_position: Option<TextUnit>,
116}
117
118impl 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 @@
1use hir::db::HirDatabase;
1use ra_syntax::{ 2use 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
7use crate::assists::{AssistCtx, Assist}; 8use crate::{AssistCtx, Assist};
8 9
9pub fn change_visibility(ctx: AssistCtx) -> Option<Assist> { 10pub(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
16fn add_vis(ctx: AssistCtx) -> Option<Assist> { 17fn 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
60fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> { 61fn 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)]
77mod tests { 78mod 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 @@
1use std::fmt::Write;
2
3use hir::{
4 AdtDef, Ty, FieldSource, source_binder,
5 db::HirDatabase,
6};
7use ra_syntax::ast::{self, AstNode};
8
9use crate::{AssistCtx, Assist};
10
11pub(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)]
77mod 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 @@
1use hir::db::HirDatabase;
1use ra_syntax::{ 2use ra_syntax::{
2 Direction, 3 Direction,
3 SyntaxKind::COMMA, 4 SyntaxKind::COMMA,
4}; 5};
5 6
6use crate::assists::{non_trivia_sibling, AssistCtx, Assist}; 7use crate::{AssistCtx, Assist, non_trivia_sibling};
7 8
8pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> { 9pub(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)]
19mod tests { 20mod 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 @@
1use hir::db::HirDatabase;
1use ra_syntax::{ 2use 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
8use crate::assists::{AssistCtx, Assist}; 9use crate::{AssistCtx, Assist};
9 10
10pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option<Assist> { 11pub(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)]
104mod tests { 105mod 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
8mod assist_ctx;
9
10use ra_text_edit::TextEdit;
11use ra_syntax::{TextUnit, SyntaxNode, Direction};
12use ra_db::FileRange;
13use hir::db::HirDatabase;
14
15pub(crate) use crate::assist_ctx::{AssistCtx, Assist};
16
17#[derive(Debug)]
18pub struct AssistLabel {
19 /// Short description of the assist, as shown in the UI.
20 pub label: String,
21}
22
23pub 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.
32pub fn applicable_assists<H>(db: &H, range: FileRange) -> Vec<AssistLabel>
33where
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.
52pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)>
53where
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
68mod add_derive;
69mod add_impl;
70mod flip_comma;
71mod change_visibility;
72mod fill_match_arms;
73mod introduce_variable;
74mod replace_if_let_with_match;
75mod split_import;
76fn 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
89fn 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)]
96mod 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 @@
1use ra_syntax::{AstNode, ast}; 1use ra_syntax::{AstNode, ast};
2use ra_ide_api_light::formatting::extract_trivial_expression;
3use hir::db::HirDatabase;
2 4
3use crate::{ 5use crate::{AssistCtx, Assist};
4 assists::{AssistCtx, Assist},
5 formatting::extract_trivial_expression,
6};
7 6
8pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option<Assist> { 7pub(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)]
52mod tests { 51mod 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 @@
1use hir::db::HirDatabase;
1use ra_syntax::{ 2use 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
7use crate::assists::{AssistCtx, Assist}; 8use crate::{AssistCtx, Assist};
8 9
9pub fn split_import(ctx: AssistCtx) -> Option<Assist> { 10pub(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)]
35mod tests { 36mod 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)]
73pub trait SourceDatabase: CheckCanceled { 73pub 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
20pub mod db; 20pub mod db;
21#[cfg(test)] 21pub mod mock;
22mod mock;
23mod query_definitions; 22mod query_definitions;
24mod path; 23mod path;
25pub mod source_binder; 24pub 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)]
20pub(crate) struct MockDatabase { 20pub 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 {
27impl panic::RefUnwindSafe for MockDatabase {} 27impl panic::RefUnwindSafe for MockDatabase {}
28 28
29impl MockDatabase { 29impl 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
168impl MockDatabase { 168impl 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" }
24ra_db = { path = "../ra_db" } 24ra_db = { path = "../ra_db" }
25hir = { path = "../ra_hir", package = "ra_hir" } 25hir = { path = "../ra_hir", package = "ra_hir" }
26test_utils = { path = "../test_utils" } 26test_utils = { path = "../test_utils" }
27ra_assists = { path = "../ra_assists" }
27 28
28[dev-dependencies] 29[dev-dependencies]
29insta = "0.6.1" 30insta = "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 @@
1mod fill_match_arm; 1use ra_db::{FileRange, FilePosition};
2 2
3use ra_syntax::{ 3use crate::{SourceFileEdit, SourceChange, db::RootDatabase};
4 TextRange, SourceFile, AstNode, 4
5 algo::find_node_at_offset, 5pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<SourceChange> {
6}; 6 ra_assists::assists(db, frange)
7use 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 };
14use 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
20pub(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)]
34pub 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
42impl<'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 @@
1use std::fmt::Write;
2use hir::{
3 AdtDef,
4 source_binder,
5 Ty,
6 FieldSource,
7};
8use ra_ide_api_light::{
9 assists::{
10 Assist,
11 AssistBuilder
12 }
13};
14use ra_syntax::{
15 ast::{
16 self,
17 AstNode,
18 }
19};
20
21use crate::assists::AssistCtx;
22
23pub 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)]
88mod 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---
2created: "2019-02-03T15:38:46.094184+00:00"
3creator: [email protected]
4expression: ret
5source: 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---
2created: "2019-02-03T15:41:34.640074+00:00"
3creator: [email protected]
4expression: ret
5source: 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
20use crate::{ 20use 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
6mod flip_comma;
7mod add_derive;
8mod add_impl;
9mod introduce_variable;
10mod change_visibility;
11mod split_import;
12mod replace_if_let_with_match;
13
14use ra_text_edit::{TextEdit, TextEditBuilder};
15use ra_syntax::{
16 Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode,
17 algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset},
18};
19use itertools::Itertools;
20
21use crate::formatting::leading_indent;
22
23pub 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.
34pub 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)]
51pub struct LocalEdit {
52 pub label: String,
53 pub edit: TextEdit,
54 pub cursor_position: Option<TextUnit>,
55}
56
57fn 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)]
94pub struct AssistCtx<'a> {
95 source_file: &'a SourceFile,
96 range: TextRange,
97 should_compute_edit: bool,
98}
99
100#[derive(Debug)]
101pub enum Assist {
102 Applicable,
103 Edit(LocalEdit),
104}
105
106#[derive(Default)]
107pub struct AssistBuilder {
108 edit: TextEditBuilder,
109 cursor_position: Option<TextUnit>,
110}
111
112impl<'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
159impl 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
189fn reindent(text: &str, indent: &str) -> String {
190 let indent = format!("\n{}", indent);
191 text.lines().intersperse(&indent).collect()
192}
193
194#[cfg(test)]
195fn 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)]
203fn 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)]
211fn 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 @@
1use itertools::Itertools;
1use ra_syntax::{ 2use 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
9pub 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.
9pub(crate) fn leading_indent(node: &SyntaxNode) -> Option<&str> { 15pub 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
35pub(crate) fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> { 41pub 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
6pub mod assists; 6pub mod formatting;
7mod extend_selection; 7mod extend_selection;
8mod folding_ranges; 8mod folding_ranges;
9mod line_index; 9mod line_index;
@@ -14,10 +14,15 @@ mod test_utils;
14mod join_lines; 14mod join_lines;
15mod typing; 15mod typing;
16mod diagnostics; 16mod diagnostics;
17pub(crate) mod formatting; 17
18#[derive(Debug)]
19pub struct LocalEdit {
20 pub label: String,
21 pub edit: ra_text_edit::TextEdit,
22 pub cursor_position: Option<TextUnit>,
23}
18 24
19pub use self::{ 25pub 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 @@
1use ra_syntax::{SourceFile, TextRange, TextUnit}; 1use ra_syntax::{SourceFile, TextUnit};
2 2
3use crate::LocalEdit; 3use crate::LocalEdit;
4pub use test_utils::*; 4pub 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
26pub 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
38pub 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}