diff options
Diffstat (limited to 'crates/ra_ide_api')
-rw-r--r-- | crates/ra_ide_api/Cargo.toml | 5 | ||||
-rw-r--r-- | crates/ra_ide_api/src/call_info.rs | 183 | ||||
-rw-r--r-- | crates/ra_ide_api/src/change.rs | 2 | ||||
-rw-r--r-- | crates/ra_ide_api/src/completion/complete_path.rs | 4 | ||||
-rw-r--r-- | crates/ra_ide_api/src/completion/complete_postfix.rs | 6 | ||||
-rw-r--r-- | crates/ra_ide_api/src/completion/completion_item.rs | 12 | ||||
-rw-r--r-- | crates/ra_ide_api/src/completion/presentation.rs | 74 | ||||
-rw-r--r-- | crates/ra_ide_api/src/db.rs | 1 | ||||
-rw-r--r-- | crates/ra_ide_api/src/diagnostics.rs | 9 | ||||
-rw-r--r-- | crates/ra_ide_api/src/display/function_signature.rs | 112 | ||||
-rw-r--r-- | crates/ra_ide_api/src/extend_selection.rs | 82 | ||||
-rw-r--r-- | crates/ra_ide_api/src/impls.rs | 4 | ||||
-rw-r--r-- | crates/ra_ide_api/src/lib.rs | 129 | ||||
-rw-r--r-- | crates/ra_ide_api/src/parent_module.rs | 5 | ||||
-rw-r--r-- | crates/ra_ide_api/src/references/rename.rs | 24 | ||||
-rw-r--r-- | crates/ra_ide_api/src/references/search_scope.rs | 7 | ||||
-rw-r--r-- | crates/ra_ide_api/src/runnables.rs | 14 | ||||
-rw-r--r-- | crates/ra_ide_api/src/source_change.rs | 119 | ||||
-rw-r--r-- | crates/ra_ide_api/src/typing.rs | 359 |
19 files changed, 772 insertions, 379 deletions
diff --git a/crates/ra_ide_api/Cargo.toml b/crates/ra_ide_api/Cargo.toml index f66f0a6ba..bf6ef12f3 100644 --- a/crates/ra_ide_api/Cargo.toml +++ b/crates/ra_ide_api/Cargo.toml | |||
@@ -27,10 +27,13 @@ ra_db = { path = "../ra_db" } | |||
27 | ra_cfg = { path = "../ra_cfg" } | 27 | ra_cfg = { path = "../ra_cfg" } |
28 | ra_fmt = { path = "../ra_fmt" } | 28 | ra_fmt = { path = "../ra_fmt" } |
29 | ra_prof = { path = "../ra_prof" } | 29 | ra_prof = { path = "../ra_prof" } |
30 | hir = { path = "../ra_hir", package = "ra_hir" } | ||
31 | test_utils = { path = "../test_utils" } | 30 | test_utils = { path = "../test_utils" } |
32 | ra_assists = { path = "../ra_assists" } | 31 | ra_assists = { path = "../ra_assists" } |
33 | 32 | ||
33 | # ra_ide_api should depend only on the top-level `hir` package. if you need | ||
34 | # something from some `hir_xxx` subpackage, reexport the API via `hir`. | ||
35 | hir = { path = "../ra_hir", package = "ra_hir" } | ||
36 | |||
34 | [dev-dependencies] | 37 | [dev-dependencies] |
35 | insta = "0.12.0" | 38 | insta = "0.12.0" |
36 | 39 | ||
diff --git a/crates/ra_ide_api/src/call_info.rs b/crates/ra_ide_api/src/call_info.rs index 7d18be483..3572825b5 100644 --- a/crates/ra_ide_api/src/call_info.rs +++ b/crates/ra_ide_api/src/call_info.rs | |||
@@ -2,9 +2,9 @@ | |||
2 | 2 | ||
3 | use ra_db::SourceDatabase; | 3 | use ra_db::SourceDatabase; |
4 | use ra_syntax::{ | 4 | use ra_syntax::{ |
5 | algo::find_node_at_offset, | 5 | algo::ancestors_at_offset, |
6 | ast::{self, ArgListOwner}, | 6 | ast::{self, ArgListOwner}, |
7 | AstNode, SyntaxNode, TextUnit, | 7 | match_ast, AstNode, SyntaxNode, TextUnit, |
8 | }; | 8 | }; |
9 | use test_utils::tested_by; | 9 | use test_utils::tested_by; |
10 | 10 | ||
@@ -20,24 +20,30 @@ pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Option<Cal | |||
20 | let name_ref = calling_node.name_ref()?; | 20 | let name_ref = calling_node.name_ref()?; |
21 | 21 | ||
22 | let analyzer = hir::SourceAnalyzer::new(db, position.file_id, name_ref.syntax(), None); | 22 | let analyzer = hir::SourceAnalyzer::new(db, position.file_id, name_ref.syntax(), None); |
23 | let function = match &calling_node { | 23 | let (mut call_info, has_self) = match &calling_node { |
24 | FnCallNode::CallExpr(expr) => { | 24 | FnCallNode::CallExpr(expr) => { |
25 | //FIXME: apply subst | 25 | //FIXME: apply subst |
26 | let (callable_def, _subst) = analyzer.type_of(db, &expr.expr()?)?.as_callable()?; | 26 | let (callable_def, _subst) = analyzer.type_of(db, &expr.expr()?)?.as_callable()?; |
27 | match callable_def { | 27 | match callable_def { |
28 | hir::CallableDef::Function(it) => it, | 28 | hir::CallableDef::Function(it) => { |
29 | //FIXME: handle other callables | 29 | (CallInfo::with_fn(db, it), it.data(db).has_self_param()) |
30 | _ => return None, | 30 | } |
31 | hir::CallableDef::Struct(it) => (CallInfo::with_struct(db, it)?, false), | ||
32 | hir::CallableDef::EnumVariant(it) => (CallInfo::with_enum_variant(db, it)?, false), | ||
31 | } | 33 | } |
32 | } | 34 | } |
33 | FnCallNode::MethodCallExpr(expr) => analyzer.resolve_method_call(&expr)?, | 35 | FnCallNode::MethodCallExpr(expr) => { |
36 | let function = analyzer.resolve_method_call(&expr)?; | ||
37 | (CallInfo::with_fn(db, function), function.data(db).has_self_param()) | ||
38 | } | ||
39 | FnCallNode::MacroCallExpr(expr) => { | ||
40 | let macro_def = analyzer.resolve_macro_call(db, &expr)?; | ||
41 | (CallInfo::with_macro(db, macro_def)?, false) | ||
42 | } | ||
34 | }; | 43 | }; |
35 | 44 | ||
36 | let mut call_info = CallInfo::new(db, function); | ||
37 | |||
38 | // If we have a calling expression let's find which argument we are on | 45 | // If we have a calling expression let's find which argument we are on |
39 | let num_params = call_info.parameters().len(); | 46 | let num_params = call_info.parameters().len(); |
40 | let has_self = function.data(db).has_self_param(); | ||
41 | 47 | ||
42 | if num_params == 1 { | 48 | if num_params == 1 { |
43 | if !has_self { | 49 | if !has_self { |
@@ -75,20 +81,25 @@ pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Option<Cal | |||
75 | Some(call_info) | 81 | Some(call_info) |
76 | } | 82 | } |
77 | 83 | ||
84 | #[derive(Debug)] | ||
78 | enum FnCallNode { | 85 | enum FnCallNode { |
79 | CallExpr(ast::CallExpr), | 86 | CallExpr(ast::CallExpr), |
80 | MethodCallExpr(ast::MethodCallExpr), | 87 | MethodCallExpr(ast::MethodCallExpr), |
88 | MacroCallExpr(ast::MacroCall), | ||
81 | } | 89 | } |
82 | 90 | ||
83 | impl FnCallNode { | 91 | impl FnCallNode { |
84 | fn with_node(syntax: &SyntaxNode, offset: TextUnit) -> Option<FnCallNode> { | 92 | fn with_node(syntax: &SyntaxNode, offset: TextUnit) -> Option<FnCallNode> { |
85 | if let Some(expr) = find_node_at_offset::<ast::CallExpr>(syntax, offset) { | 93 | ancestors_at_offset(syntax, offset).find_map(|node| { |
86 | return Some(FnCallNode::CallExpr(expr)); | 94 | match_ast! { |
87 | } | 95 | match node { |
88 | if let Some(expr) = find_node_at_offset::<ast::MethodCallExpr>(syntax, offset) { | 96 | ast::CallExpr(it) => { Some(FnCallNode::CallExpr(it)) }, |
89 | return Some(FnCallNode::MethodCallExpr(expr)); | 97 | ast::MethodCallExpr(it) => { Some(FnCallNode::MethodCallExpr(it)) }, |
90 | } | 98 | ast::MacroCall(it) => { Some(FnCallNode::MacroCallExpr(it)) }, |
91 | None | 99 | _ => { None }, |
100 | } | ||
101 | } | ||
102 | }) | ||
92 | } | 103 | } |
93 | 104 | ||
94 | fn name_ref(&self) -> Option<ast::NameRef> { | 105 | fn name_ref(&self) -> Option<ast::NameRef> { |
@@ -101,6 +112,8 @@ impl FnCallNode { | |||
101 | FnCallNode::MethodCallExpr(call_expr) => { | 112 | FnCallNode::MethodCallExpr(call_expr) => { |
102 | call_expr.syntax().children().filter_map(ast::NameRef::cast).nth(0) | 113 | call_expr.syntax().children().filter_map(ast::NameRef::cast).nth(0) |
103 | } | 114 | } |
115 | |||
116 | FnCallNode::MacroCallExpr(call_expr) => call_expr.path()?.segment()?.name_ref(), | ||
104 | } | 117 | } |
105 | } | 118 | } |
106 | 119 | ||
@@ -108,17 +121,36 @@ impl FnCallNode { | |||
108 | match self { | 121 | match self { |
109 | FnCallNode::CallExpr(expr) => expr.arg_list(), | 122 | FnCallNode::CallExpr(expr) => expr.arg_list(), |
110 | FnCallNode::MethodCallExpr(expr) => expr.arg_list(), | 123 | FnCallNode::MethodCallExpr(expr) => expr.arg_list(), |
124 | FnCallNode::MacroCallExpr(_) => None, | ||
111 | } | 125 | } |
112 | } | 126 | } |
113 | } | 127 | } |
114 | 128 | ||
115 | impl CallInfo { | 129 | impl CallInfo { |
116 | fn new(db: &RootDatabase, function: hir::Function) -> Self { | 130 | fn with_fn(db: &RootDatabase, function: hir::Function) -> Self { |
117 | let signature = FunctionSignature::from_hir(db, function); | 131 | let signature = FunctionSignature::from_hir(db, function); |
118 | 132 | ||
119 | CallInfo { signature, active_parameter: None } | 133 | CallInfo { signature, active_parameter: None } |
120 | } | 134 | } |
121 | 135 | ||
136 | fn with_struct(db: &RootDatabase, st: hir::Struct) -> Option<Self> { | ||
137 | let signature = FunctionSignature::from_struct(db, st)?; | ||
138 | |||
139 | Some(CallInfo { signature, active_parameter: None }) | ||
140 | } | ||
141 | |||
142 | fn with_enum_variant(db: &RootDatabase, variant: hir::EnumVariant) -> Option<Self> { | ||
143 | let signature = FunctionSignature::from_enum_variant(db, variant)?; | ||
144 | |||
145 | Some(CallInfo { signature, active_parameter: None }) | ||
146 | } | ||
147 | |||
148 | fn with_macro(db: &RootDatabase, macro_def: hir::MacroDef) -> Option<Self> { | ||
149 | let signature = FunctionSignature::from_macro(db, macro_def)?; | ||
150 | |||
151 | Some(CallInfo { signature, active_parameter: None }) | ||
152 | } | ||
153 | |||
122 | fn parameters(&self) -> &[String] { | 154 | fn parameters(&self) -> &[String] { |
123 | &self.signature.parameters | 155 | &self.signature.parameters |
124 | } | 156 | } |
@@ -415,6 +447,7 @@ pub fn foo(mut r: WriteHandler<()>) { | |||
415 | "#, | 447 | "#, |
416 | ); | 448 | ); |
417 | 449 | ||
450 | assert_eq!(info.label(), "fn finished(&mut self, ctx: &mut Self::Context)".to_string()); | ||
418 | assert_eq!(info.parameters(), ["&mut self", "ctx: &mut Self::Context"]); | 451 | assert_eq!(info.parameters(), ["&mut self", "ctx: &mut Self::Context"]); |
419 | assert_eq!(info.active_parameter, Some(1)); | 452 | assert_eq!(info.active_parameter, Some(1)); |
420 | assert_eq!( | 453 | assert_eq!( |
@@ -438,4 +471,118 @@ By default this method stops actor's `Context`."# | |||
438 | let call_info = analysis.call_info(position).unwrap(); | 471 | let call_info = analysis.call_info(position).unwrap(); |
439 | assert!(call_info.is_none()); | 472 | assert!(call_info.is_none()); |
440 | } | 473 | } |
474 | |||
475 | #[test] | ||
476 | fn test_nested_method_in_lamba() { | ||
477 | let info = call_info( | ||
478 | r#"struct Foo; | ||
479 | |||
480 | impl Foo { | ||
481 | fn bar(&self, _: u32) { } | ||
482 | } | ||
483 | |||
484 | fn bar(_: u32) { } | ||
485 | |||
486 | fn main() { | ||
487 | let foo = Foo; | ||
488 | std::thread::spawn(move || foo.bar(<|>)); | ||
489 | }"#, | ||
490 | ); | ||
491 | |||
492 | assert_eq!(info.parameters(), ["&self", "_: u32"]); | ||
493 | assert_eq!(info.active_parameter, Some(1)); | ||
494 | assert_eq!(info.label(), "fn bar(&self, _: u32)"); | ||
495 | } | ||
496 | |||
497 | #[test] | ||
498 | fn works_for_tuple_structs() { | ||
499 | let info = call_info( | ||
500 | r#" | ||
501 | /// A cool tuple struct | ||
502 | struct TS(u32, i32); | ||
503 | fn main() { | ||
504 | let s = TS(0, <|>); | ||
505 | }"#, | ||
506 | ); | ||
507 | |||
508 | assert_eq!(info.label(), "struct TS(u32, i32) -> TS"); | ||
509 | assert_eq!(info.doc().map(|it| it.into()), Some("A cool tuple struct".to_string())); | ||
510 | assert_eq!(info.active_parameter, Some(1)); | ||
511 | } | ||
512 | |||
513 | #[test] | ||
514 | #[should_panic] | ||
515 | fn cant_call_named_structs() { | ||
516 | let _ = call_info( | ||
517 | r#" | ||
518 | struct TS { x: u32, y: i32 } | ||
519 | fn main() { | ||
520 | let s = TS(<|>); | ||
521 | }"#, | ||
522 | ); | ||
523 | } | ||
524 | |||
525 | #[test] | ||
526 | fn works_for_enum_variants() { | ||
527 | let info = call_info( | ||
528 | r#" | ||
529 | enum E { | ||
530 | /// A Variant | ||
531 | A(i32), | ||
532 | /// Another | ||
533 | B, | ||
534 | /// And C | ||
535 | C { a: i32, b: i32 } | ||
536 | } | ||
537 | |||
538 | fn main() { | ||
539 | let a = E::A(<|>); | ||
540 | } | ||
541 | "#, | ||
542 | ); | ||
543 | |||
544 | assert_eq!(info.label(), "E::A(0: i32)"); | ||
545 | assert_eq!(info.doc().map(|it| it.into()), Some("A Variant".to_string())); | ||
546 | assert_eq!(info.active_parameter, Some(0)); | ||
547 | } | ||
548 | |||
549 | #[test] | ||
550 | #[should_panic] | ||
551 | fn cant_call_enum_records() { | ||
552 | let _ = call_info( | ||
553 | r#" | ||
554 | enum E { | ||
555 | /// A Variant | ||
556 | A(i32), | ||
557 | /// Another | ||
558 | B, | ||
559 | /// And C | ||
560 | C { a: i32, b: i32 } | ||
561 | } | ||
562 | |||
563 | fn main() { | ||
564 | let a = E::C(<|>); | ||
565 | } | ||
566 | "#, | ||
567 | ); | ||
568 | } | ||
569 | |||
570 | #[test] | ||
571 | fn fn_signature_for_macro() { | ||
572 | let info = call_info( | ||
573 | r#" | ||
574 | /// empty macro | ||
575 | macro_rules! foo { | ||
576 | () => {} | ||
577 | } | ||
578 | |||
579 | fn f() { | ||
580 | foo!(<|>); | ||
581 | } | ||
582 | "#, | ||
583 | ); | ||
584 | |||
585 | assert_eq!(info.label(), "foo!()"); | ||
586 | assert_eq!(info.doc().map(|it| it.into()), Some("empty macro".to_string())); | ||
587 | } | ||
441 | } | 588 | } |
diff --git a/crates/ra_ide_api/src/change.rs b/crates/ra_ide_api/src/change.rs index 050249c0e..39c5946c7 100644 --- a/crates/ra_ide_api/src/change.rs +++ b/crates/ra_ide_api/src/change.rs | |||
@@ -43,7 +43,7 @@ impl fmt::Debug for AnalysisChange { | |||
43 | if !self.libraries_added.is_empty() { | 43 | if !self.libraries_added.is_empty() { |
44 | d.field("libraries_added", &self.libraries_added.len()); | 44 | d.field("libraries_added", &self.libraries_added.len()); |
45 | } | 45 | } |
46 | if !self.crate_graph.is_some() { | 46 | if !self.crate_graph.is_none() { |
47 | d.field("crate_graph", &self.crate_graph); | 47 | d.field("crate_graph", &self.crate_graph); |
48 | } | 48 | } |
49 | d.finish() | 49 | d.finish() |
diff --git a/crates/ra_ide_api/src/completion/complete_path.rs b/crates/ra_ide_api/src/completion/complete_path.rs index 23dece73c..a58fdc036 100644 --- a/crates/ra_ide_api/src/completion/complete_path.rs +++ b/crates/ra_ide_api/src/completion/complete_path.rs | |||
@@ -50,7 +50,7 @@ pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) { | |||
50 | hir::ModuleDef::TypeAlias(a) => a.ty(ctx.db), | 50 | hir::ModuleDef::TypeAlias(a) => a.ty(ctx.db), |
51 | _ => unreachable!(), | 51 | _ => unreachable!(), |
52 | }; | 52 | }; |
53 | let krate = ctx.module.and_then(|m| m.krate(ctx.db)); | 53 | let krate = ctx.module.map(|m| m.krate()); |
54 | if let Some(krate) = krate { | 54 | if let Some(krate) = krate { |
55 | ty.iterate_impl_items(ctx.db, krate, |item| { | 55 | ty.iterate_impl_items(ctx.db, krate, |item| { |
56 | match item { | 56 | match item { |
@@ -67,7 +67,7 @@ pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) { | |||
67 | }); | 67 | }); |
68 | } | 68 | } |
69 | } | 69 | } |
70 | _ => return, | 70 | _ => {} |
71 | }; | 71 | }; |
72 | } | 72 | } |
73 | 73 | ||
diff --git a/crates/ra_ide_api/src/completion/complete_postfix.rs b/crates/ra_ide_api/src/completion/complete_postfix.rs index 555cecb73..60ed3518b 100644 --- a/crates/ra_ide_api/src/completion/complete_postfix.rs +++ b/crates/ra_ide_api/src/completion/complete_postfix.rs | |||
@@ -9,16 +9,14 @@ use crate::{ | |||
9 | }; | 9 | }; |
10 | use hir::{Ty, TypeCtor}; | 10 | use hir::{Ty, TypeCtor}; |
11 | use ra_syntax::{ast::AstNode, TextRange, TextUnit}; | 11 | use ra_syntax::{ast::AstNode, TextRange, TextUnit}; |
12 | use ra_text_edit::TextEditBuilder; | 12 | use ra_text_edit::TextEdit; |
13 | 13 | ||
14 | fn postfix_snippet(ctx: &CompletionContext, label: &str, detail: &str, snippet: &str) -> Builder { | 14 | fn postfix_snippet(ctx: &CompletionContext, label: &str, detail: &str, snippet: &str) -> Builder { |
15 | let edit = { | 15 | let edit = { |
16 | let receiver_range = | 16 | let receiver_range = |
17 | ctx.dot_receiver.as_ref().expect("no receiver available").syntax().text_range(); | 17 | ctx.dot_receiver.as_ref().expect("no receiver available").syntax().text_range(); |
18 | let delete_range = TextRange::from_to(receiver_range.start(), ctx.source_range().end()); | 18 | let delete_range = TextRange::from_to(receiver_range.start(), ctx.source_range().end()); |
19 | let mut builder = TextEditBuilder::default(); | 19 | TextEdit::replace(delete_range, snippet.to_string()) |
20 | builder.replace(delete_range, snippet.to_string()); | ||
21 | builder.finish() | ||
22 | }; | 20 | }; |
23 | CompletionItem::new(CompletionKind::Postfix, ctx.source_range(), label) | 21 | CompletionItem::new(CompletionKind::Postfix, ctx.source_range(), label) |
24 | .detail(detail) | 22 | .detail(detail) |
diff --git a/crates/ra_ide_api/src/completion/completion_item.rs b/crates/ra_ide_api/src/completion/completion_item.rs index 3e6933bc1..5c9c44704 100644 --- a/crates/ra_ide_api/src/completion/completion_item.rs +++ b/crates/ra_ide_api/src/completion/completion_item.rs | |||
@@ -4,7 +4,7 @@ use std::fmt; | |||
4 | 4 | ||
5 | use hir::Documentation; | 5 | use hir::Documentation; |
6 | use ra_syntax::TextRange; | 6 | use ra_syntax::TextRange; |
7 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 7 | use ra_text_edit::TextEdit; |
8 | 8 | ||
9 | /// `CompletionItem` describes a single completion variant in the editor pop-up. | 9 | /// `CompletionItem` describes a single completion variant in the editor pop-up. |
10 | /// It is basically a POD with various properties. To construct a | 10 | /// It is basically a POD with various properties. To construct a |
@@ -192,12 +192,10 @@ impl Builder { | |||
192 | let label = self.label; | 192 | let label = self.label; |
193 | let text_edit = match self.text_edit { | 193 | let text_edit = match self.text_edit { |
194 | Some(it) => it, | 194 | Some(it) => it, |
195 | None => { | 195 | None => TextEdit::replace( |
196 | let mut builder = TextEditBuilder::default(); | 196 | self.source_range, |
197 | builder | 197 | self.insert_text.unwrap_or_else(|| label.clone()), |
198 | .replace(self.source_range, self.insert_text.unwrap_or_else(|| label.clone())); | 198 | ), |
199 | builder.finish() | ||
200 | } | ||
201 | }; | 199 | }; |
202 | 200 | ||
203 | CompletionItem { | 201 | CompletionItem { |
diff --git a/crates/ra_ide_api/src/completion/presentation.rs b/crates/ra_ide_api/src/completion/presentation.rs index aed4ce6d4..65bb639ed 100644 --- a/crates/ra_ide_api/src/completion/presentation.rs +++ b/crates/ra_ide_api/src/completion/presentation.rs | |||
@@ -136,7 +136,7 @@ impl Completions { | |||
136 | for (idx, s) in docs.match_indices(¯o_name) { | 136 | for (idx, s) in docs.match_indices(¯o_name) { |
137 | let (before, after) = (&docs[..idx], &docs[idx + s.len()..]); | 137 | let (before, after) = (&docs[..idx], &docs[idx + s.len()..]); |
138 | // Ensure to match the full word | 138 | // Ensure to match the full word |
139 | if after.starts_with("!") | 139 | if after.starts_with('!') |
140 | && before | 140 | && before |
141 | .chars() | 141 | .chars() |
142 | .rev() | 142 | .rev() |
@@ -164,27 +164,32 @@ impl Completions { | |||
164 | name: Option<String>, | 164 | name: Option<String>, |
165 | macro_: hir::MacroDef, | 165 | macro_: hir::MacroDef, |
166 | ) { | 166 | ) { |
167 | let name = match name { | ||
168 | Some(it) => it, | ||
169 | None => return, | ||
170 | }; | ||
171 | |||
167 | let ast_node = macro_.source(ctx.db).ast; | 172 | let ast_node = macro_.source(ctx.db).ast; |
168 | if let Some(name) = name { | 173 | let detail = macro_label(&ast_node); |
169 | let detail = macro_label(&ast_node); | 174 | |
175 | let docs = macro_.docs(ctx.db); | ||
176 | let macro_declaration = format!("{}!", name); | ||
177 | |||
178 | let mut builder = | ||
179 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), ¯o_declaration) | ||
180 | .kind(CompletionItemKind::Macro) | ||
181 | .set_documentation(docs.clone()) | ||
182 | .detail(detail); | ||
170 | 183 | ||
171 | let docs = macro_.docs(ctx.db); | 184 | builder = if ctx.use_item_syntax.is_some() { |
185 | builder.insert_text(name) | ||
186 | } else { | ||
172 | let macro_braces_to_insert = | 187 | let macro_braces_to_insert = |
173 | self.guess_macro_braces(&name, docs.as_ref().map_or("", |s| s.as_str())); | 188 | self.guess_macro_braces(&name, docs.as_ref().map_or("", |s| s.as_str())); |
174 | let macro_declaration = name + "!"; | 189 | builder.insert_snippet(macro_declaration + macro_braces_to_insert) |
175 | 190 | }; | |
176 | let builder = CompletionItem::new( | ||
177 | CompletionKind::Reference, | ||
178 | ctx.source_range(), | ||
179 | ¯o_declaration, | ||
180 | ) | ||
181 | .kind(CompletionItemKind::Macro) | ||
182 | .set_documentation(docs) | ||
183 | .detail(detail) | ||
184 | .insert_snippet(macro_declaration + macro_braces_to_insert); | ||
185 | 191 | ||
186 | self.add(builder); | 192 | self.add(builder); |
187 | } | ||
188 | } | 193 | } |
189 | 194 | ||
190 | fn add_function_with_name( | 195 | fn add_function_with_name( |
@@ -220,7 +225,7 @@ impl Completions { | |||
220 | } else { | 225 | } else { |
221 | (format!("{}($0)", data.name()), format!("{}(…)", name)) | 226 | (format!("{}($0)", data.name()), format!("{}(…)", name)) |
222 | }; | 227 | }; |
223 | builder = builder.lookup_by(name.clone()).label(label).insert_snippet(snippet); | 228 | builder = builder.lookup_by(name).label(label).insert_snippet(snippet); |
224 | } | 229 | } |
225 | 230 | ||
226 | self.add(builder) | 231 | self.add(builder) |
@@ -281,10 +286,11 @@ fn has_non_default_type_params(def: hir::GenericDef, db: &db::RootDatabase) -> b | |||
281 | 286 | ||
282 | #[cfg(test)] | 287 | #[cfg(test)] |
283 | mod tests { | 288 | mod tests { |
284 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
285 | use insta::assert_debug_snapshot; | 289 | use insta::assert_debug_snapshot; |
286 | use test_utils::covers; | 290 | use test_utils::covers; |
287 | 291 | ||
292 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
293 | |||
288 | fn do_reference_completion(code: &str) -> Vec<CompletionItem> { | 294 | fn do_reference_completion(code: &str) -> Vec<CompletionItem> { |
289 | do_completion(code, CompletionKind::Reference) | 295 | do_completion(code, CompletionKind::Reference) |
290 | } | 296 | } |
@@ -576,4 +582,34 @@ mod tests { | |||
576 | "### | 582 | "### |
577 | ); | 583 | ); |
578 | } | 584 | } |
585 | |||
586 | #[test] | ||
587 | fn dont_insert_macro_call_braces_in_use() { | ||
588 | assert_debug_snapshot!( | ||
589 | do_reference_completion( | ||
590 | r" | ||
591 | //- /main.rs | ||
592 | use foo::<|>; | ||
593 | |||
594 | //- /foo/lib.rs | ||
595 | #[macro_export] | ||
596 | macro_rules frobnicate { | ||
597 | () => () | ||
598 | } | ||
599 | " | ||
600 | ), | ||
601 | @r###" | ||
602 | [ | ||
603 | CompletionItem { | ||
604 | label: "frobnicate!", | ||
605 | source_range: [9; 9), | ||
606 | delete: [9; 9), | ||
607 | insert: "frobnicate", | ||
608 | kind: Macro, | ||
609 | detail: "#[macro_export]\nmacro_rules! frobnicate", | ||
610 | }, | ||
611 | ] | ||
612 | "### | ||
613 | ) | ||
614 | } | ||
579 | } | 615 | } |
diff --git a/crates/ra_ide_api/src/db.rs b/crates/ra_ide_api/src/db.rs index 9146b647a..785e71808 100644 --- a/crates/ra_ide_api/src/db.rs +++ b/crates/ra_ide_api/src/db.rs | |||
@@ -23,6 +23,7 @@ use crate::{ | |||
23 | hir::db::InternDatabaseStorage, | 23 | hir::db::InternDatabaseStorage, |
24 | hir::db::AstDatabaseStorage, | 24 | hir::db::AstDatabaseStorage, |
25 | hir::db::DefDatabaseStorage, | 25 | hir::db::DefDatabaseStorage, |
26 | hir::db::DefDatabase2Storage, | ||
26 | hir::db::HirDatabaseStorage | 27 | hir::db::HirDatabaseStorage |
27 | )] | 28 | )] |
28 | #[derive(Debug)] | 29 | #[derive(Debug)] |
diff --git a/crates/ra_ide_api/src/diagnostics.rs b/crates/ra_ide_api/src/diagnostics.rs index 8743a3a79..1f1f5cd74 100644 --- a/crates/ra_ide_api/src/diagnostics.rs +++ b/crates/ra_ide_api/src/diagnostics.rs | |||
@@ -85,10 +85,9 @@ pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic> | |||
85 | }) | 85 | }) |
86 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { | 86 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { |
87 | let node = d.ast(db); | 87 | let node = d.ast(db); |
88 | let mut builder = TextEditBuilder::default(); | ||
89 | let replacement = format!("Ok({})", node.syntax()); | 88 | let replacement = format!("Ok({})", node.syntax()); |
90 | builder.replace(node.syntax().text_range(), replacement); | 89 | let edit = TextEdit::replace(node.syntax().text_range(), replacement); |
91 | let fix = SourceChange::source_file_edit_from("wrap with ok", file_id, builder.finish()); | 90 | let fix = SourceChange::source_file_edit_from("wrap with ok", file_id, edit); |
92 | res.borrow_mut().push(Diagnostic { | 91 | res.borrow_mut().push(Diagnostic { |
93 | range: d.highlight_range(), | 92 | range: d.highlight_range(), |
94 | message: d.message(), | 93 | message: d.message(), |
@@ -152,9 +151,7 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement( | |||
152 | let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); | 151 | let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); |
153 | let end = use_tree_list_node.text_range().end(); | 152 | let end = use_tree_list_node.text_range().end(); |
154 | let range = TextRange::from_to(start, end); | 153 | let range = TextRange::from_to(start, end); |
155 | let mut edit_builder = TextEditBuilder::default(); | 154 | return Some(TextEdit::delete(range)); |
156 | edit_builder.delete(range); | ||
157 | return Some(edit_builder.finish()); | ||
158 | } | 155 | } |
159 | None | 156 | None |
160 | } | 157 | } |
diff --git a/crates/ra_ide_api/src/display/function_signature.rs b/crates/ra_ide_api/src/display/function_signature.rs index 43f022ccd..9075ca443 100644 --- a/crates/ra_ide_api/src/display/function_signature.rs +++ b/crates/ra_ide_api/src/display/function_signature.rs | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | use std::fmt::{self, Display}; | 3 | use std::fmt::{self, Display}; |
4 | 4 | ||
5 | use hir::{Docs, Documentation, HasSource}; | 5 | use hir::{Docs, Documentation, HasSource, HirDisplay}; |
6 | use join_to_string::join; | 6 | use join_to_string::join; |
7 | use ra_syntax::ast::{self, AstNode, NameOwner, VisibilityOwner}; | 7 | use ra_syntax::ast::{self, AstNode, NameOwner, VisibilityOwner}; |
8 | use std::convert::From; | 8 | use std::convert::From; |
@@ -12,9 +12,18 @@ use crate::{ | |||
12 | display::{generic_parameters, where_predicates}, | 12 | display::{generic_parameters, where_predicates}, |
13 | }; | 13 | }; |
14 | 14 | ||
15 | #[derive(Debug)] | ||
16 | pub enum CallableKind { | ||
17 | Function, | ||
18 | StructConstructor, | ||
19 | VariantConstructor, | ||
20 | Macro, | ||
21 | } | ||
22 | |||
15 | /// Contains information about a function signature | 23 | /// Contains information about a function signature |
16 | #[derive(Debug)] | 24 | #[derive(Debug)] |
17 | pub struct FunctionSignature { | 25 | pub struct FunctionSignature { |
26 | pub kind: CallableKind, | ||
18 | /// Optional visibility | 27 | /// Optional visibility |
19 | pub visibility: Option<String>, | 28 | pub visibility: Option<String>, |
20 | /// Name of the function | 29 | /// Name of the function |
@@ -42,6 +51,99 @@ impl FunctionSignature { | |||
42 | let ast_node = function.source(db).ast; | 51 | let ast_node = function.source(db).ast; |
43 | FunctionSignature::from(&ast_node).with_doc_opt(doc) | 52 | FunctionSignature::from(&ast_node).with_doc_opt(doc) |
44 | } | 53 | } |
54 | |||
55 | pub(crate) fn from_struct(db: &db::RootDatabase, st: hir::Struct) -> Option<Self> { | ||
56 | let node: ast::StructDef = st.source(db).ast; | ||
57 | match node.kind() { | ||
58 | ast::StructKind::Named(_) => return None, | ||
59 | _ => (), | ||
60 | }; | ||
61 | |||
62 | let params = st | ||
63 | .fields(db) | ||
64 | .into_iter() | ||
65 | .map(|field: hir::StructField| { | ||
66 | let ty = field.ty(db); | ||
67 | format!("{}", ty.display(db)) | ||
68 | }) | ||
69 | .collect(); | ||
70 | |||
71 | Some( | ||
72 | FunctionSignature { | ||
73 | kind: CallableKind::StructConstructor, | ||
74 | visibility: node.visibility().map(|n| n.syntax().text().to_string()), | ||
75 | name: node.name().map(|n| n.text().to_string()), | ||
76 | ret_type: node.name().map(|n| n.text().to_string()), | ||
77 | parameters: params, | ||
78 | generic_parameters: generic_parameters(&node), | ||
79 | where_predicates: where_predicates(&node), | ||
80 | doc: None, | ||
81 | } | ||
82 | .with_doc_opt(st.docs(db)), | ||
83 | ) | ||
84 | } | ||
85 | |||
86 | pub(crate) fn from_enum_variant( | ||
87 | db: &db::RootDatabase, | ||
88 | variant: hir::EnumVariant, | ||
89 | ) -> Option<Self> { | ||
90 | let node: ast::EnumVariant = variant.source(db).ast; | ||
91 | match node.kind() { | ||
92 | ast::StructKind::Named(_) | ast::StructKind::Unit => return None, | ||
93 | _ => (), | ||
94 | }; | ||
95 | |||
96 | let parent_name = match variant.parent_enum(db).name(db) { | ||
97 | Some(name) => name.to_string(), | ||
98 | None => "missing".into(), | ||
99 | }; | ||
100 | |||
101 | let name = format!("{}::{}", parent_name, variant.name(db).unwrap()); | ||
102 | |||
103 | let params = variant | ||
104 | .fields(db) | ||
105 | .into_iter() | ||
106 | .map(|field: hir::StructField| { | ||
107 | let name = field.name(db); | ||
108 | let ty = field.ty(db); | ||
109 | format!("{}: {}", name, ty.display(db)) | ||
110 | }) | ||
111 | .collect(); | ||
112 | |||
113 | Some( | ||
114 | FunctionSignature { | ||
115 | kind: CallableKind::VariantConstructor, | ||
116 | visibility: None, | ||
117 | name: Some(name), | ||
118 | ret_type: None, | ||
119 | parameters: params, | ||
120 | generic_parameters: vec![], | ||
121 | where_predicates: vec![], | ||
122 | doc: None, | ||
123 | } | ||
124 | .with_doc_opt(variant.docs(db)), | ||
125 | ) | ||
126 | } | ||
127 | |||
128 | pub(crate) fn from_macro(db: &db::RootDatabase, macro_def: hir::MacroDef) -> Option<Self> { | ||
129 | let node: ast::MacroCall = macro_def.source(db).ast; | ||
130 | |||
131 | let params = vec![]; | ||
132 | |||
133 | Some( | ||
134 | FunctionSignature { | ||
135 | kind: CallableKind::Macro, | ||
136 | visibility: None, | ||
137 | name: node.name().map(|n| n.text().to_string()), | ||
138 | ret_type: None, | ||
139 | parameters: params, | ||
140 | generic_parameters: vec![], | ||
141 | where_predicates: vec![], | ||
142 | doc: None, | ||
143 | } | ||
144 | .with_doc_opt(macro_def.docs(db)), | ||
145 | ) | ||
146 | } | ||
45 | } | 147 | } |
46 | 148 | ||
47 | impl From<&'_ ast::FnDef> for FunctionSignature { | 149 | impl From<&'_ ast::FnDef> for FunctionSignature { |
@@ -59,6 +161,7 @@ impl From<&'_ ast::FnDef> for FunctionSignature { | |||
59 | } | 161 | } |
60 | 162 | ||
61 | FunctionSignature { | 163 | FunctionSignature { |
164 | kind: CallableKind::Function, | ||
62 | visibility: node.visibility().map(|n| n.syntax().text().to_string()), | 165 | visibility: node.visibility().map(|n| n.syntax().text().to_string()), |
63 | name: node.name().map(|n| n.text().to_string()), | 166 | name: node.name().map(|n| n.text().to_string()), |
64 | ret_type: node | 167 | ret_type: node |
@@ -81,7 +184,12 @@ impl Display for FunctionSignature { | |||
81 | } | 184 | } |
82 | 185 | ||
83 | if let Some(name) = &self.name { | 186 | if let Some(name) = &self.name { |
84 | write!(f, "fn {}", name)?; | 187 | match self.kind { |
188 | CallableKind::Function => write!(f, "fn {}", name)?, | ||
189 | CallableKind::StructConstructor => write!(f, "struct {}", name)?, | ||
190 | CallableKind::VariantConstructor => write!(f, "{}", name)?, | ||
191 | CallableKind::Macro => write!(f, "{}!", name)?, | ||
192 | } | ||
85 | } | 193 | } |
86 | 194 | ||
87 | if !self.generic_parameters.is_empty() { | 195 | if !self.generic_parameters.is_empty() { |
diff --git a/crates/ra_ide_api/src/extend_selection.rs b/crates/ra_ide_api/src/extend_selection.rs index 602757e92..4b7bfc0b1 100644 --- a/crates/ra_ide_api/src/extend_selection.rs +++ b/crates/ra_ide_api/src/extend_selection.rs | |||
@@ -5,7 +5,7 @@ use ra_syntax::{ | |||
5 | algo::find_covering_element, | 5 | algo::find_covering_element, |
6 | ast::{self, AstNode, AstToken}, | 6 | ast::{self, AstNode, AstToken}, |
7 | Direction, NodeOrToken, | 7 | Direction, NodeOrToken, |
8 | SyntaxKind::*, | 8 | SyntaxKind::{self, *}, |
9 | SyntaxNode, SyntaxToken, TextRange, TextUnit, TokenAtOffset, T, | 9 | SyntaxNode, SyntaxToken, TextRange, TextUnit, TokenAtOffset, T, |
10 | }; | 10 | }; |
11 | 11 | ||
@@ -29,10 +29,12 @@ fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange | |||
29 | USE_TREE_LIST, | 29 | USE_TREE_LIST, |
30 | TYPE_PARAM_LIST, | 30 | TYPE_PARAM_LIST, |
31 | TYPE_ARG_LIST, | 31 | TYPE_ARG_LIST, |
32 | TYPE_BOUND_LIST, | ||
32 | PARAM_LIST, | 33 | PARAM_LIST, |
33 | ARG_LIST, | 34 | ARG_LIST, |
34 | ARRAY_EXPR, | 35 | ARRAY_EXPR, |
35 | TUPLE_EXPR, | 36 | TUPLE_EXPR, |
37 | WHERE_CLAUSE, | ||
36 | ]; | 38 | ]; |
37 | 39 | ||
38 | if range.is_empty() { | 40 | if range.is_empty() { |
@@ -146,13 +148,17 @@ fn pick_best<'a>(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken { | |||
146 | } | 148 | } |
147 | } | 149 | } |
148 | 150 | ||
149 | /// Extend list item selection to include nearby comma and whitespace. | 151 | /// Extend list item selection to include nearby delimiter and whitespace. |
150 | fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> { | 152 | fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> { |
151 | fn is_single_line_ws(node: &SyntaxToken) -> bool { | 153 | fn is_single_line_ws(node: &SyntaxToken) -> bool { |
152 | node.kind() == WHITESPACE && !node.text().contains('\n') | 154 | node.kind() == WHITESPACE && !node.text().contains('\n') |
153 | } | 155 | } |
154 | 156 | ||
155 | fn nearby_comma(node: &SyntaxNode, dir: Direction) -> Option<SyntaxToken> { | 157 | fn nearby_delimiter( |
158 | delimiter_kind: SyntaxKind, | ||
159 | node: &SyntaxNode, | ||
160 | dir: Direction, | ||
161 | ) -> Option<SyntaxToken> { | ||
156 | node.siblings_with_tokens(dir) | 162 | node.siblings_with_tokens(dir) |
157 | .skip(1) | 163 | .skip(1) |
158 | .skip_while(|node| match node { | 164 | .skip_while(|node| match node { |
@@ -161,19 +167,26 @@ fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> { | |||
161 | }) | 167 | }) |
162 | .next() | 168 | .next() |
163 | .and_then(|it| it.into_token()) | 169 | .and_then(|it| it.into_token()) |
164 | .filter(|node| node.kind() == T![,]) | 170 | .filter(|node| node.kind() == delimiter_kind) |
165 | } | 171 | } |
166 | 172 | ||
167 | if let Some(comma_node) = nearby_comma(node, Direction::Prev) { | 173 | let delimiter = match node.kind() { |
168 | return Some(TextRange::from_to(comma_node.text_range().start(), node.text_range().end())); | 174 | TYPE_BOUND => T![+], |
175 | _ => T![,], | ||
176 | }; | ||
177 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) { | ||
178 | return Some(TextRange::from_to( | ||
179 | delimiter_node.text_range().start(), | ||
180 | node.text_range().end(), | ||
181 | )); | ||
169 | } | 182 | } |
170 | if let Some(comma_node) = nearby_comma(node, Direction::Next) { | 183 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) { |
171 | // Include any following whitespace when comma if after list item. | 184 | // Include any following whitespace when delimiter is after list item. |
172 | let final_node = comma_node | 185 | let final_node = delimiter_node |
173 | .next_sibling_or_token() | 186 | .next_sibling_or_token() |
174 | .and_then(|it| it.into_token()) | 187 | .and_then(|it| it.into_token()) |
175 | .filter(|node| is_single_line_ws(node)) | 188 | .filter(|node| is_single_line_ws(node)) |
176 | .unwrap_or(comma_node); | 189 | .unwrap_or(delimiter_node); |
177 | 190 | ||
178 | return Some(TextRange::from_to(node.text_range().start(), final_node.text_range().end())); | 191 | return Some(TextRange::from_to(node.text_range().start(), final_node.text_range().end())); |
179 | } | 192 | } |
@@ -387,4 +400,53 @@ fn bar(){} | |||
387 | &["foo", "\" fn foo() {\""], | 400 | &["foo", "\" fn foo() {\""], |
388 | ); | 401 | ); |
389 | } | 402 | } |
403 | |||
404 | #[test] | ||
405 | fn test_extend_trait_bounds_list_in_where_clause() { | ||
406 | do_check( | ||
407 | r#" | ||
408 | fn foo<R>() | ||
409 | where | ||
410 | R: req::Request + 'static, | ||
411 | R::Params: DeserializeOwned<|> + panic::UnwindSafe + 'static, | ||
412 | R::Result: Serialize + 'static, | ||
413 | "#, | ||
414 | &[ | ||
415 | "DeserializeOwned", | ||
416 | "DeserializeOwned + ", | ||
417 | "DeserializeOwned + panic::UnwindSafe + 'static", | ||
418 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static", | ||
419 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static,", | ||
420 | ], | ||
421 | ); | ||
422 | do_check(r#"fn foo<T>() where T: <|>Copy"#, &["Copy"]); | ||
423 | do_check(r#"fn foo<T>() where T: <|>Copy + Display"#, &["Copy", "Copy + "]); | ||
424 | do_check(r#"fn foo<T>() where T: <|>Copy +Display"#, &["Copy", "Copy +"]); | ||
425 | do_check(r#"fn foo<T>() where T: <|>Copy+Display"#, &["Copy", "Copy+"]); | ||
426 | do_check(r#"fn foo<T>() where T: Copy + <|>Display"#, &["Display", "+ Display"]); | ||
427 | do_check(r#"fn foo<T>() where T: Copy + <|>Display + Sync"#, &["Display", "+ Display"]); | ||
428 | do_check(r#"fn foo<T>() where T: Copy +<|>Display"#, &["Display", "+Display"]); | ||
429 | } | ||
430 | |||
431 | #[test] | ||
432 | fn test_extend_trait_bounds_list_inline() { | ||
433 | do_check(r#"fn foo<T: <|>Copy>() {}"#, &["Copy"]); | ||
434 | do_check(r#"fn foo<T: <|>Copy + Display>() {}"#, &["Copy", "Copy + "]); | ||
435 | do_check(r#"fn foo<T: <|>Copy +Display>() {}"#, &["Copy", "Copy +"]); | ||
436 | do_check(r#"fn foo<T: <|>Copy+Display>() {}"#, &["Copy", "Copy+"]); | ||
437 | do_check(r#"fn foo<T: Copy + <|>Display>() {}"#, &["Display", "+ Display"]); | ||
438 | do_check(r#"fn foo<T: Copy + <|>Display + Sync>() {}"#, &["Display", "+ Display"]); | ||
439 | do_check(r#"fn foo<T: Copy +<|>Display>() {}"#, &["Display", "+Display"]); | ||
440 | do_check( | ||
441 | r#"fn foo<T: Copy<|> + Display, U: Copy>() {}"#, | ||
442 | &[ | ||
443 | "Copy", | ||
444 | "Copy + ", | ||
445 | "Copy + Display", | ||
446 | "T: Copy + Display", | ||
447 | "T: Copy + Display, ", | ||
448 | "<T: Copy + Display, U: Copy>", | ||
449 | ], | ||
450 | ); | ||
451 | } | ||
390 | } | 452 | } |
diff --git a/crates/ra_ide_api/src/impls.rs b/crates/ra_ide_api/src/impls.rs index 7fc1b1efa..b899ed3a5 100644 --- a/crates/ra_ide_api/src/impls.rs +++ b/crates/ra_ide_api/src/impls.rs | |||
@@ -51,7 +51,7 @@ fn impls_for_def( | |||
51 | } | 51 | } |
52 | }; | 52 | }; |
53 | 53 | ||
54 | let krate = module.krate(db)?; | 54 | let krate = module.krate(); |
55 | let impls = db.impls_in_crate(krate); | 55 | let impls = db.impls_in_crate(krate); |
56 | 56 | ||
57 | Some( | 57 | Some( |
@@ -72,7 +72,7 @@ fn impls_for_trait( | |||
72 | let src = hir::Source { file_id: position.file_id.into(), ast: node.clone() }; | 72 | let src = hir::Source { file_id: position.file_id.into(), ast: node.clone() }; |
73 | let tr = hir::Trait::from_source(db, src)?; | 73 | let tr = hir::Trait::from_source(db, src)?; |
74 | 74 | ||
75 | let krate = module.krate(db)?; | 75 | let krate = module.krate(); |
76 | let impls = db.impls_in_crate(krate); | 76 | let impls = db.impls_in_crate(krate); |
77 | 77 | ||
78 | Some( | 78 | Some( |
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 0832229fd..d0188da44 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs | |||
@@ -14,6 +14,7 @@ mod db; | |||
14 | pub mod mock_analysis; | 14 | pub mod mock_analysis; |
15 | mod symbol_index; | 15 | mod symbol_index; |
16 | mod change; | 16 | mod change; |
17 | mod source_change; | ||
17 | mod feature_flags; | 18 | mod feature_flags; |
18 | 19 | ||
19 | mod status; | 20 | mod status; |
@@ -54,8 +55,6 @@ use ra_db::{ | |||
54 | CheckCanceled, FileLoader, SourceDatabase, | 55 | CheckCanceled, FileLoader, SourceDatabase, |
55 | }; | 56 | }; |
56 | use ra_syntax::{SourceFile, TextRange, TextUnit}; | 57 | use ra_syntax::{SourceFile, TextRange, TextUnit}; |
57 | use ra_text_edit::TextEdit; | ||
58 | use relative_path::RelativePathBuf; | ||
59 | 58 | ||
60 | use crate::{db::LineIndexDatabase, symbol_index::FileSymbol}; | 59 | use crate::{db::LineIndexDatabase, symbol_index::FileSymbol}; |
61 | 60 | ||
@@ -73,6 +72,7 @@ pub use crate::{ | |||
73 | line_index_utils::translate_offset_with_edit, | 72 | line_index_utils::translate_offset_with_edit, |
74 | references::{ReferenceSearchResult, SearchScope}, | 73 | references::{ReferenceSearchResult, SearchScope}, |
75 | runnables::{Runnable, RunnableKind}, | 74 | runnables::{Runnable, RunnableKind}, |
75 | source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, | ||
76 | syntax_highlighting::HighlightedRange, | 76 | syntax_highlighting::HighlightedRange, |
77 | }; | 77 | }; |
78 | 78 | ||
@@ -84,99 +84,6 @@ pub use ra_db::{ | |||
84 | pub type Cancelable<T> = Result<T, Canceled>; | 84 | pub type Cancelable<T> = Result<T, Canceled>; |
85 | 85 | ||
86 | #[derive(Debug)] | 86 | #[derive(Debug)] |
87 | pub struct SourceChange { | ||
88 | pub label: String, | ||
89 | pub source_file_edits: Vec<SourceFileEdit>, | ||
90 | pub file_system_edits: Vec<FileSystemEdit>, | ||
91 | pub cursor_position: Option<FilePosition>, | ||
92 | } | ||
93 | |||
94 | impl SourceChange { | ||
95 | /// Creates a new SourceChange with the given label | ||
96 | /// from the edits. | ||
97 | pub(crate) fn from_edits<L: Into<String>>( | ||
98 | label: L, | ||
99 | source_file_edits: Vec<SourceFileEdit>, | ||
100 | file_system_edits: Vec<FileSystemEdit>, | ||
101 | ) -> Self { | ||
102 | SourceChange { | ||
103 | label: label.into(), | ||
104 | source_file_edits, | ||
105 | file_system_edits, | ||
106 | cursor_position: None, | ||
107 | } | ||
108 | } | ||
109 | |||
110 | /// Creates a new SourceChange with the given label, | ||
111 | /// containing only the given `SourceFileEdits`. | ||
112 | pub(crate) fn source_file_edits<L: Into<String>>(label: L, edits: Vec<SourceFileEdit>) -> Self { | ||
113 | SourceChange { | ||
114 | label: label.into(), | ||
115 | source_file_edits: edits, | ||
116 | file_system_edits: vec![], | ||
117 | cursor_position: None, | ||
118 | } | ||
119 | } | ||
120 | |||
121 | /// Creates a new SourceChange with the given label, | ||
122 | /// containing only the given `FileSystemEdits`. | ||
123 | pub(crate) fn file_system_edits<L: Into<String>>(label: L, edits: Vec<FileSystemEdit>) -> Self { | ||
124 | SourceChange { | ||
125 | label: label.into(), | ||
126 | source_file_edits: vec![], | ||
127 | file_system_edits: edits, | ||
128 | cursor_position: None, | ||
129 | } | ||
130 | } | ||
131 | |||
132 | /// Creates a new SourceChange with the given label, | ||
133 | /// containing only a single `SourceFileEdit`. | ||
134 | pub(crate) fn source_file_edit<L: Into<String>>(label: L, edit: SourceFileEdit) -> Self { | ||
135 | SourceChange::source_file_edits(label, vec![edit]) | ||
136 | } | ||
137 | |||
138 | /// Creates a new SourceChange with the given label | ||
139 | /// from the given `FileId` and `TextEdit` | ||
140 | pub(crate) fn source_file_edit_from<L: Into<String>>( | ||
141 | label: L, | ||
142 | file_id: FileId, | ||
143 | edit: TextEdit, | ||
144 | ) -> Self { | ||
145 | SourceChange::source_file_edit(label, SourceFileEdit { file_id, edit }) | ||
146 | } | ||
147 | |||
148 | /// Creates a new SourceChange with the given label | ||
149 | /// from the given `FileId` and `TextEdit` | ||
150 | pub(crate) fn file_system_edit<L: Into<String>>(label: L, edit: FileSystemEdit) -> Self { | ||
151 | SourceChange::file_system_edits(label, vec![edit]) | ||
152 | } | ||
153 | |||
154 | /// Sets the cursor position to the given `FilePosition` | ||
155 | pub(crate) fn with_cursor(mut self, cursor_position: FilePosition) -> Self { | ||
156 | self.cursor_position = Some(cursor_position); | ||
157 | self | ||
158 | } | ||
159 | |||
160 | /// Sets the cursor position to the given `FilePosition` | ||
161 | pub(crate) fn with_cursor_opt(mut self, cursor_position: Option<FilePosition>) -> Self { | ||
162 | self.cursor_position = cursor_position; | ||
163 | self | ||
164 | } | ||
165 | } | ||
166 | |||
167 | #[derive(Debug)] | ||
168 | pub struct SourceFileEdit { | ||
169 | pub file_id: FileId, | ||
170 | pub edit: TextEdit, | ||
171 | } | ||
172 | |||
173 | #[derive(Debug)] | ||
174 | pub enum FileSystemEdit { | ||
175 | CreateFile { source_root: SourceRootId, path: RelativePathBuf }, | ||
176 | MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf }, | ||
177 | } | ||
178 | |||
179 | #[derive(Debug)] | ||
180 | pub struct Diagnostic { | 87 | pub struct Diagnostic { |
181 | pub message: String, | 88 | pub message: String, |
182 | pub range: TextRange, | 89 | pub range: TextRange, |
@@ -407,24 +314,20 @@ impl Analysis { | |||
407 | self.with_db(|db| typing::on_enter(&db, position)) | 314 | self.with_db(|db| typing::on_enter(&db, position)) |
408 | } | 315 | } |
409 | 316 | ||
410 | /// Returns an edit which should be applied after `=` was typed. Primarily, | 317 | /// Returns an edit which should be applied after a character was typed. |
411 | /// this works when adding `let =`. | 318 | /// |
412 | // FIXME: use a snippet completion instead of this hack here. | 319 | /// This is useful for some on-the-fly fixups, like adding `;` to `let =` |
413 | pub fn on_eq_typed(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { | 320 | /// automatically. |
414 | self.with_db(|db| { | 321 | pub fn on_char_typed( |
415 | let parse = db.parse(position.file_id); | 322 | &self, |
416 | let file = parse.tree(); | 323 | position: FilePosition, |
417 | let edit = typing::on_eq_typed(&file, position.offset)?; | 324 | char_typed: char, |
418 | Some(SourceChange::source_file_edit( | 325 | ) -> Cancelable<Option<SourceChange>> { |
419 | "add semicolon", | 326 | // Fast path to not even parse the file. |
420 | SourceFileEdit { edit, file_id: position.file_id }, | 327 | if !typing::TRIGGER_CHARS.contains(char_typed) { |
421 | )) | 328 | return Ok(None); |
422 | }) | 329 | } |
423 | } | 330 | self.with_db(|db| typing::on_char_typed(&db, position, char_typed)) |
424 | |||
425 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | ||
426 | pub fn on_dot_typed(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { | ||
427 | self.with_db(|db| typing::on_dot_typed(&db, position)) | ||
428 | } | 331 | } |
429 | 332 | ||
430 | /// Returns a tree representation of symbols in the file. Useful to draw a | 333 | /// Returns a tree representation of symbols in the file. Useful to draw a |
diff --git a/crates/ra_ide_api/src/parent_module.rs b/crates/ra_ide_api/src/parent_module.rs index 566509849..4c57566e2 100644 --- a/crates/ra_ide_api/src/parent_module.rs +++ b/crates/ra_ide_api/src/parent_module.rs | |||
@@ -27,10 +27,7 @@ pub(crate) fn crate_for(db: &RootDatabase, file_id: FileId) -> Vec<CrateId> { | |||
27 | Some(it) => it, | 27 | Some(it) => it, |
28 | None => return Vec::new(), | 28 | None => return Vec::new(), |
29 | }; | 29 | }; |
30 | let krate = match module.krate(db) { | 30 | let krate = module.krate(); |
31 | Some(it) => it, | ||
32 | None => return Vec::new(), | ||
33 | }; | ||
34 | vec![krate.crate_id()] | 31 | vec![krate.crate_id()] |
35 | } | 32 | } |
36 | 33 | ||
diff --git a/crates/ra_ide_api/src/references/rename.rs b/crates/ra_ide_api/src/references/rename.rs index ee6e73e1b..a8783d7a0 100644 --- a/crates/ra_ide_api/src/references/rename.rs +++ b/crates/ra_ide_api/src/references/rename.rs | |||
@@ -3,6 +3,7 @@ | |||
3 | use hir::ModuleSource; | 3 | use hir::ModuleSource; |
4 | use ra_db::{SourceDatabase, SourceDatabaseExt}; | 4 | use ra_db::{SourceDatabase, SourceDatabaseExt}; |
5 | use ra_syntax::{algo::find_node_at_offset, ast, AstNode, SyntaxNode}; | 5 | use ra_syntax::{algo::find_node_at_offset, ast, AstNode, SyntaxNode}; |
6 | use ra_text_edit::TextEdit; | ||
6 | use relative_path::{RelativePath, RelativePathBuf}; | 7 | use relative_path::{RelativePath, RelativePathBuf}; |
7 | 8 | ||
8 | use crate::{ | 9 | use crate::{ |
@@ -43,14 +44,7 @@ fn source_edit_from_file_id_range( | |||
43 | range: TextRange, | 44 | range: TextRange, |
44 | new_name: &str, | 45 | new_name: &str, |
45 | ) -> SourceFileEdit { | 46 | ) -> SourceFileEdit { |
46 | SourceFileEdit { | 47 | SourceFileEdit { file_id, edit: TextEdit::replace(range, new_name.into()) } |
47 | file_id, | ||
48 | edit: { | ||
49 | let mut builder = ra_text_edit::TextEditBuilder::default(); | ||
50 | builder.replace(range, new_name.into()); | ||
51 | builder.finish() | ||
52 | }, | ||
53 | } | ||
54 | } | 48 | } |
55 | 49 | ||
56 | fn rename_mod( | 50 | fn rename_mod( |
@@ -94,11 +88,7 @@ fn rename_mod( | |||
94 | 88 | ||
95 | let edit = SourceFileEdit { | 89 | let edit = SourceFileEdit { |
96 | file_id: position.file_id, | 90 | file_id: position.file_id, |
97 | edit: { | 91 | edit: TextEdit::replace(ast_name.syntax().text_range(), new_name.into()), |
98 | let mut builder = ra_text_edit::TextEditBuilder::default(); | ||
99 | builder.replace(ast_name.syntax().text_range(), new_name.into()); | ||
100 | builder.finish() | ||
101 | }, | ||
102 | }; | 92 | }; |
103 | source_file_edits.push(edit); | 93 | source_file_edits.push(edit); |
104 | 94 | ||
@@ -126,12 +116,14 @@ fn rename_reference( | |||
126 | 116 | ||
127 | #[cfg(test)] | 117 | #[cfg(test)] |
128 | mod tests { | 118 | mod tests { |
119 | use insta::assert_debug_snapshot; | ||
120 | use ra_text_edit::TextEditBuilder; | ||
121 | use test_utils::assert_eq_text; | ||
122 | |||
129 | use crate::{ | 123 | use crate::{ |
130 | mock_analysis::analysis_and_position, mock_analysis::single_file_with_position, FileId, | 124 | mock_analysis::analysis_and_position, mock_analysis::single_file_with_position, FileId, |
131 | ReferenceSearchResult, | 125 | ReferenceSearchResult, |
132 | }; | 126 | }; |
133 | use insta::assert_debug_snapshot; | ||
134 | use test_utils::assert_eq_text; | ||
135 | 127 | ||
136 | #[test] | 128 | #[test] |
137 | fn test_find_all_refs_for_local() { | 129 | fn test_find_all_refs_for_local() { |
@@ -452,7 +444,7 @@ mod tests { | |||
452 | fn test_rename(text: &str, new_name: &str, expected: &str) { | 444 | fn test_rename(text: &str, new_name: &str, expected: &str) { |
453 | let (analysis, position) = single_file_with_position(text); | 445 | let (analysis, position) = single_file_with_position(text); |
454 | let source_change = analysis.rename(position, new_name).unwrap(); | 446 | let source_change = analysis.rename(position, new_name).unwrap(); |
455 | let mut text_edit_builder = ra_text_edit::TextEditBuilder::default(); | 447 | let mut text_edit_builder = TextEditBuilder::default(); |
456 | let mut file_id: Option<FileId> = None; | 448 | let mut file_id: Option<FileId> = None; |
457 | if let Some(change) = source_change { | 449 | if let Some(change) = source_change { |
458 | for edit in change.info.source_file_edits { | 450 | for edit in change.info.source_file_edits { |
diff --git a/crates/ra_ide_api/src/references/search_scope.rs b/crates/ra_ide_api/src/references/search_scope.rs index b6eb248b7..f2789e0b2 100644 --- a/crates/ra_ide_api/src/references/search_scope.rs +++ b/crates/ra_ide_api/src/references/search_scope.rs | |||
@@ -111,8 +111,7 @@ impl NameDefinition { | |||
111 | if vis.as_str() != "" { | 111 | if vis.as_str() != "" { |
112 | let source_root_id = db.file_source_root(file_id); | 112 | let source_root_id = db.file_source_root(file_id); |
113 | let source_root = db.source_root(source_root_id); | 113 | let source_root = db.source_root(source_root_id); |
114 | let mut res = | 114 | let mut res = source_root.walk().map(|id| (id, None)).collect::<FxHashMap<_, _>>(); |
115 | source_root.walk().map(|id| (id.into(), None)).collect::<FxHashMap<_, _>>(); | ||
116 | 115 | ||
117 | // FIXME: add "pub(in path)" | 116 | // FIXME: add "pub(in path)" |
118 | 117 | ||
@@ -120,7 +119,7 @@ impl NameDefinition { | |||
120 | return SearchScope::new(res); | 119 | return SearchScope::new(res); |
121 | } | 120 | } |
122 | if vis.as_str() == "pub" { | 121 | if vis.as_str() == "pub" { |
123 | let krate = self.container.krate(db).unwrap(); | 122 | let krate = self.container.krate(); |
124 | let crate_graph = db.crate_graph(); | 123 | let crate_graph = db.crate_graph(); |
125 | for crate_id in crate_graph.iter() { | 124 | for crate_id in crate_graph.iter() { |
126 | let mut crate_deps = crate_graph.dependencies(crate_id); | 125 | let mut crate_deps = crate_graph.dependencies(crate_id); |
@@ -128,7 +127,7 @@ impl NameDefinition { | |||
128 | let root_file = crate_graph.crate_root(crate_id); | 127 | let root_file = crate_graph.crate_root(crate_id); |
129 | let source_root_id = db.file_source_root(root_file); | 128 | let source_root_id = db.file_source_root(root_file); |
130 | let source_root = db.source_root(source_root_id); | 129 | let source_root = db.source_root(source_root_id); |
131 | res.extend(source_root.walk().map(|id| (id.into(), None))); | 130 | res.extend(source_root.walk().map(|id| (id, None))); |
132 | } | 131 | } |
133 | } | 132 | } |
134 | return SearchScope::new(res); | 133 | return SearchScope::new(res); |
diff --git a/crates/ra_ide_api/src/runnables.rs b/crates/ra_ide_api/src/runnables.rs index 910883da7..1b5c8deea 100644 --- a/crates/ra_ide_api/src/runnables.rs +++ b/crates/ra_ide_api/src/runnables.rs | |||
@@ -4,7 +4,7 @@ use itertools::Itertools; | |||
4 | use ra_db::SourceDatabase; | 4 | use ra_db::SourceDatabase; |
5 | use ra_syntax::{ | 5 | use ra_syntax::{ |
6 | ast::{self, AstNode, AttrsOwner, ModuleItemOwner, NameOwner}, | 6 | ast::{self, AstNode, AttrsOwner, ModuleItemOwner, NameOwner}, |
7 | SyntaxNode, TextRange, | 7 | match_ast, SyntaxNode, TextRange, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | use crate::{db::RootDatabase, FileId}; | 10 | use crate::{db::RootDatabase, FileId}; |
@@ -29,12 +29,12 @@ pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { | |||
29 | } | 29 | } |
30 | 30 | ||
31 | fn runnable(db: &RootDatabase, file_id: FileId, item: SyntaxNode) -> Option<Runnable> { | 31 | fn runnable(db: &RootDatabase, file_id: FileId, item: SyntaxNode) -> Option<Runnable> { |
32 | if let Some(fn_def) = ast::FnDef::cast(item.clone()) { | 32 | match_ast! { |
33 | runnable_fn(fn_def) | 33 | match item { |
34 | } else if let Some(m) = ast::Module::cast(item) { | 34 | ast::FnDef(it) => { runnable_fn(it) }, |
35 | runnable_mod(db, file_id, m) | 35 | ast::Module(it) => { runnable_mod(db, file_id, it) }, |
36 | } else { | 36 | _ => { None }, |
37 | None | 37 | } |
38 | } | 38 | } |
39 | } | 39 | } |
40 | 40 | ||
diff --git a/crates/ra_ide_api/src/source_change.rs b/crates/ra_ide_api/src/source_change.rs new file mode 100644 index 000000000..4e63bbf6f --- /dev/null +++ b/crates/ra_ide_api/src/source_change.rs | |||
@@ -0,0 +1,119 @@ | |||
1 | //! This modules defines type to represent changes to the source code, that flow | ||
2 | //! from the server to the client. | ||
3 | //! | ||
4 | //! It can be viewed as a dual for `AnalysisChange`. | ||
5 | |||
6 | use ra_text_edit::TextEdit; | ||
7 | use relative_path::RelativePathBuf; | ||
8 | |||
9 | use crate::{FileId, FilePosition, SourceRootId, TextUnit}; | ||
10 | |||
11 | #[derive(Debug)] | ||
12 | pub struct SourceChange { | ||
13 | pub label: String, | ||
14 | pub source_file_edits: Vec<SourceFileEdit>, | ||
15 | pub file_system_edits: Vec<FileSystemEdit>, | ||
16 | pub cursor_position: Option<FilePosition>, | ||
17 | } | ||
18 | |||
19 | impl SourceChange { | ||
20 | /// Creates a new SourceChange with the given label | ||
21 | /// from the edits. | ||
22 | pub(crate) fn from_edits<L: Into<String>>( | ||
23 | label: L, | ||
24 | source_file_edits: Vec<SourceFileEdit>, | ||
25 | file_system_edits: Vec<FileSystemEdit>, | ||
26 | ) -> Self { | ||
27 | SourceChange { | ||
28 | label: label.into(), | ||
29 | source_file_edits, | ||
30 | file_system_edits, | ||
31 | cursor_position: None, | ||
32 | } | ||
33 | } | ||
34 | |||
35 | /// Creates a new SourceChange with the given label, | ||
36 | /// containing only the given `SourceFileEdits`. | ||
37 | pub(crate) fn source_file_edits<L: Into<String>>(label: L, edits: Vec<SourceFileEdit>) -> Self { | ||
38 | SourceChange { | ||
39 | label: label.into(), | ||
40 | source_file_edits: edits, | ||
41 | file_system_edits: vec![], | ||
42 | cursor_position: None, | ||
43 | } | ||
44 | } | ||
45 | |||
46 | /// Creates a new SourceChange with the given label, | ||
47 | /// containing only the given `FileSystemEdits`. | ||
48 | pub(crate) fn file_system_edits<L: Into<String>>(label: L, edits: Vec<FileSystemEdit>) -> Self { | ||
49 | SourceChange { | ||
50 | label: label.into(), | ||
51 | source_file_edits: vec![], | ||
52 | file_system_edits: edits, | ||
53 | cursor_position: None, | ||
54 | } | ||
55 | } | ||
56 | |||
57 | /// Creates a new SourceChange with the given label, | ||
58 | /// containing only a single `SourceFileEdit`. | ||
59 | pub(crate) fn source_file_edit<L: Into<String>>(label: L, edit: SourceFileEdit) -> Self { | ||
60 | SourceChange::source_file_edits(label, vec![edit]) | ||
61 | } | ||
62 | |||
63 | /// Creates a new SourceChange with the given label | ||
64 | /// from the given `FileId` and `TextEdit` | ||
65 | pub(crate) fn source_file_edit_from<L: Into<String>>( | ||
66 | label: L, | ||
67 | file_id: FileId, | ||
68 | edit: TextEdit, | ||
69 | ) -> Self { | ||
70 | SourceChange::source_file_edit(label, SourceFileEdit { file_id, edit }) | ||
71 | } | ||
72 | |||
73 | /// Creates a new SourceChange with the given label | ||
74 | /// from the given `FileId` and `TextEdit` | ||
75 | pub(crate) fn file_system_edit<L: Into<String>>(label: L, edit: FileSystemEdit) -> Self { | ||
76 | SourceChange::file_system_edits(label, vec![edit]) | ||
77 | } | ||
78 | |||
79 | /// Sets the cursor position to the given `FilePosition` | ||
80 | pub(crate) fn with_cursor(mut self, cursor_position: FilePosition) -> Self { | ||
81 | self.cursor_position = Some(cursor_position); | ||
82 | self | ||
83 | } | ||
84 | |||
85 | /// Sets the cursor position to the given `FilePosition` | ||
86 | pub(crate) fn with_cursor_opt(mut self, cursor_position: Option<FilePosition>) -> Self { | ||
87 | self.cursor_position = cursor_position; | ||
88 | self | ||
89 | } | ||
90 | } | ||
91 | |||
92 | #[derive(Debug)] | ||
93 | pub struct SourceFileEdit { | ||
94 | pub file_id: FileId, | ||
95 | pub edit: TextEdit, | ||
96 | } | ||
97 | |||
98 | #[derive(Debug)] | ||
99 | pub enum FileSystemEdit { | ||
100 | CreateFile { source_root: SourceRootId, path: RelativePathBuf }, | ||
101 | MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf }, | ||
102 | } | ||
103 | |||
104 | pub(crate) struct SingleFileChange { | ||
105 | pub label: String, | ||
106 | pub edit: TextEdit, | ||
107 | pub cursor_position: Option<TextUnit>, | ||
108 | } | ||
109 | |||
110 | impl SingleFileChange { | ||
111 | pub(crate) fn into_source_change(self, file_id: FileId) -> SourceChange { | ||
112 | SourceChange { | ||
113 | label: self.label, | ||
114 | source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }], | ||
115 | file_system_edits: Vec::new(), | ||
116 | cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }), | ||
117 | } | ||
118 | } | ||
119 | } | ||
diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index 2f5782012..d51132f73 100644 --- a/crates/ra_ide_api/src/typing.rs +++ b/crates/ra_ide_api/src/typing.rs | |||
@@ -1,4 +1,17 @@ | |||
1 | //! FIXME: write short doc here | 1 | //! This module handles auto-magic editing actions applied together with users |
2 | //! edits. For example, if the user typed | ||
3 | //! | ||
4 | //! ```text | ||
5 | //! foo | ||
6 | //! .bar() | ||
7 | //! .baz() | ||
8 | //! | // <- cursor is here | ||
9 | //! ``` | ||
10 | //! | ||
11 | //! and types `.` next, we want to indent the dot. | ||
12 | //! | ||
13 | //! Language server executes such typing assists synchronously. That is, they | ||
14 | //! block user's typing and should be pretty fast for this reason! | ||
2 | 15 | ||
3 | use ra_db::{FilePosition, SourceDatabase}; | 16 | use ra_db::{FilePosition, SourceDatabase}; |
4 | use ra_fmt::leading_indent; | 17 | use ra_fmt::leading_indent; |
@@ -9,9 +22,9 @@ use ra_syntax::{ | |||
9 | SyntaxKind::*, | 22 | SyntaxKind::*, |
10 | SyntaxToken, TextRange, TextUnit, TokenAtOffset, | 23 | SyntaxToken, TextRange, TextUnit, TokenAtOffset, |
11 | }; | 24 | }; |
12 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 25 | use ra_text_edit::TextEdit; |
13 | 26 | ||
14 | use crate::{db::RootDatabase, SourceChange, SourceFileEdit}; | 27 | use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit}; |
15 | 28 | ||
16 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | 29 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { |
17 | let parse = db.parse(position.file_id); | 30 | let parse = db.parse(position.file_id); |
@@ -36,13 +49,12 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Sour | |||
36 | let indent = node_indent(&file, comment.syntax())?; | 49 | let indent = node_indent(&file, comment.syntax())?; |
37 | let inserted = format!("\n{}{} ", indent, prefix); | 50 | let inserted = format!("\n{}{} ", indent, prefix); |
38 | let cursor_position = position.offset + TextUnit::of_str(&inserted); | 51 | let cursor_position = position.offset + TextUnit::of_str(&inserted); |
39 | let mut edit = TextEditBuilder::default(); | 52 | let edit = TextEdit::insert(position.offset, inserted); |
40 | edit.insert(position.offset, inserted); | ||
41 | 53 | ||
42 | Some( | 54 | Some( |
43 | SourceChange::source_file_edit( | 55 | SourceChange::source_file_edit( |
44 | "on enter", | 56 | "on enter", |
45 | SourceFileEdit { edit: edit.finish(), file_id: position.file_id }, | 57 | SourceFileEdit { edit, file_id: position.file_id }, |
46 | ) | 58 | ) |
47 | .with_cursor(FilePosition { offset: cursor_position, file_id: position.file_id }), | 59 | .with_cursor(FilePosition { offset: cursor_position, file_id: position.file_id }), |
48 | ) | 60 | ) |
@@ -68,39 +80,67 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { | |||
68 | Some(text[pos..].into()) | 80 | Some(text[pos..].into()) |
69 | } | 81 | } |
70 | 82 | ||
71 | pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> { | 83 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; |
72 | assert_eq!(file.syntax().text().char_at(eq_offset), Some('=')); | 84 | |
73 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?; | 85 | pub(crate) fn on_char_typed( |
86 | db: &RootDatabase, | ||
87 | position: FilePosition, | ||
88 | char_typed: char, | ||
89 | ) -> Option<SourceChange> { | ||
90 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
91 | let file = &db.parse(position.file_id).tree(); | ||
92 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | ||
93 | let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?; | ||
94 | Some(single_file_change.into_source_change(position.file_id)) | ||
95 | } | ||
96 | |||
97 | fn on_char_typed_inner( | ||
98 | file: &SourceFile, | ||
99 | offset: TextUnit, | ||
100 | char_typed: char, | ||
101 | ) -> Option<SingleFileChange> { | ||
102 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
103 | match char_typed { | ||
104 | '.' => on_dot_typed(file, offset), | ||
105 | '=' => on_eq_typed(file, offset), | ||
106 | '>' => on_arrow_typed(file, offset), | ||
107 | _ => unreachable!(), | ||
108 | } | ||
109 | } | ||
110 | |||
111 | /// Returns an edit which should be applied after `=` was typed. Primarily, | ||
112 | /// this works when adding `let =`. | ||
113 | // FIXME: use a snippet completion instead of this hack here. | ||
114 | fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
115 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | ||
116 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
74 | if let_stmt.has_semi() { | 117 | if let_stmt.has_semi() { |
75 | return None; | 118 | return None; |
76 | } | 119 | } |
77 | if let Some(expr) = let_stmt.initializer() { | 120 | if let Some(expr) = let_stmt.initializer() { |
78 | let expr_range = expr.syntax().text_range(); | 121 | let expr_range = expr.syntax().text_range(); |
79 | if expr_range.contains(eq_offset) && eq_offset != expr_range.start() { | 122 | if expr_range.contains(offset) && offset != expr_range.start() { |
80 | return None; | 123 | return None; |
81 | } | 124 | } |
82 | if file.syntax().text().slice(eq_offset..expr_range.start()).contains_char('\n') { | 125 | if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { |
83 | return None; | 126 | return None; |
84 | } | 127 | } |
85 | } else { | 128 | } else { |
86 | return None; | 129 | return None; |
87 | } | 130 | } |
88 | let offset = let_stmt.syntax().text_range().end(); | 131 | let offset = let_stmt.syntax().text_range().end(); |
89 | let mut edit = TextEditBuilder::default(); | 132 | Some(SingleFileChange { |
90 | edit.insert(offset, ";".to_string()); | 133 | label: "add semicolon".to_string(), |
91 | Some(edit.finish()) | 134 | edit: TextEdit::insert(offset, ";".to_string()), |
135 | cursor_position: None, | ||
136 | }) | ||
92 | } | 137 | } |
93 | 138 | ||
94 | pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | 139 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. |
95 | let parse = db.parse(position.file_id); | 140 | fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { |
96 | assert_eq!(parse.tree().syntax().text().char_at(position.offset), Some('.')); | 141 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); |
97 | 142 | let whitespace = | |
98 | let whitespace = parse | 143 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; |
99 | .tree() | ||
100 | .syntax() | ||
101 | .token_at_offset(position.offset) | ||
102 | .left_biased() | ||
103 | .and_then(ast::Whitespace::cast)?; | ||
104 | 144 | ||
105 | let current_indent = { | 145 | let current_indent = { |
106 | let text = whitespace.text(); | 146 | let text = whitespace.text(); |
@@ -117,20 +157,36 @@ pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option< | |||
117 | if current_indent_len == target_indent_len { | 157 | if current_indent_len == target_indent_len { |
118 | return None; | 158 | return None; |
119 | } | 159 | } |
120 | let mut edit = TextEditBuilder::default(); | 160 | |
121 | edit.replace( | 161 | Some(SingleFileChange { |
122 | TextRange::from_to(position.offset - current_indent_len, position.offset), | 162 | label: "reindent dot".to_string(), |
123 | target_indent, | 163 | edit: TextEdit::replace( |
124 | ); | 164 | TextRange::from_to(offset - current_indent_len, offset), |
125 | 165 | target_indent, | |
126 | let res = SourceChange::source_file_edit_from("reindent dot", position.file_id, edit.finish()) | 166 | ), |
127 | .with_cursor(FilePosition { | 167 | cursor_position: Some( |
128 | offset: position.offset + target_indent_len - current_indent_len | 168 | offset + target_indent_len - current_indent_len + TextUnit::of_char('.'), |
129 | + TextUnit::of_char('.'), | 169 | ), |
130 | file_id: position.file_id, | 170 | }) |
131 | }); | 171 | } |
132 | 172 | ||
133 | Some(res) | 173 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` |
174 | fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
175 | let file_text = file.syntax().text(); | ||
176 | assert_eq!(file_text.char_at(offset), Some('>')); | ||
177 | let after_arrow = offset + TextUnit::of_char('>'); | ||
178 | if file_text.char_at(after_arrow) != Some('{') { | ||
179 | return None; | ||
180 | } | ||
181 | if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() { | ||
182 | return None; | ||
183 | } | ||
184 | |||
185 | Some(SingleFileChange { | ||
186 | label: "add space after return type".to_string(), | ||
187 | edit: TextEdit::insert(after_arrow, " ".to_string()), | ||
188 | cursor_position: Some(after_arrow), | ||
189 | }) | ||
134 | } | 190 | } |
135 | 191 | ||
136 | #[cfg(test)] | 192 | #[cfg(test)] |
@@ -142,21 +198,87 @@ mod tests { | |||
142 | use super::*; | 198 | use super::*; |
143 | 199 | ||
144 | #[test] | 200 | #[test] |
145 | fn test_on_eq_typed() { | 201 | fn test_on_enter() { |
146 | fn type_eq(before: &str, after: &str) { | 202 | fn apply_on_enter(before: &str) -> Option<String> { |
147 | let (offset, before) = extract_offset(before); | 203 | let (offset, before) = extract_offset(before); |
148 | let mut edit = TextEditBuilder::default(); | 204 | let (analysis, file_id) = single_file(&before); |
149 | edit.insert(offset, "=".to_string()); | 205 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; |
150 | let before = edit.finish().apply(&before); | 206 | |
151 | let parse = SourceFile::parse(&before); | 207 | assert_eq!(result.source_file_edits.len(), 1); |
152 | if let Some(result) = on_eq_typed(&parse.tree(), offset) { | 208 | let actual = result.source_file_edits[0].edit.apply(&before); |
153 | let actual = result.apply(&before); | 209 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); |
154 | assert_eq_text!(after, &actual); | 210 | Some(actual) |
155 | } else { | 211 | } |
156 | assert_eq_text!(&before, after) | 212 | |
157 | }; | 213 | fn do_check(before: &str, after: &str) { |
214 | let actual = apply_on_enter(before).unwrap(); | ||
215 | assert_eq_text!(after, &actual); | ||
216 | } | ||
217 | |||
218 | fn do_check_noop(text: &str) { | ||
219 | assert!(apply_on_enter(text).is_none()) | ||
158 | } | 220 | } |
159 | 221 | ||
222 | do_check( | ||
223 | r" | ||
224 | /// Some docs<|> | ||
225 | fn foo() { | ||
226 | } | ||
227 | ", | ||
228 | r" | ||
229 | /// Some docs | ||
230 | /// <|> | ||
231 | fn foo() { | ||
232 | } | ||
233 | ", | ||
234 | ); | ||
235 | do_check( | ||
236 | r" | ||
237 | impl S { | ||
238 | /// Some<|> docs. | ||
239 | fn foo() {} | ||
240 | } | ||
241 | ", | ||
242 | r" | ||
243 | impl S { | ||
244 | /// Some | ||
245 | /// <|> docs. | ||
246 | fn foo() {} | ||
247 | } | ||
248 | ", | ||
249 | ); | ||
250 | do_check_noop(r"<|>//! docz"); | ||
251 | } | ||
252 | |||
253 | fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> { | ||
254 | let (offset, before) = extract_offset(before); | ||
255 | let edit = TextEdit::insert(offset, char_typed.to_string()); | ||
256 | let before = edit.apply(&before); | ||
257 | let parse = SourceFile::parse(&before); | ||
258 | on_char_typed_inner(&parse.tree(), offset, char_typed) | ||
259 | .map(|it| (it.edit.apply(&before), it)) | ||
260 | } | ||
261 | |||
262 | fn type_char(char_typed: char, before: &str, after: &str) { | ||
263 | let (actual, file_change) = do_type_char(char_typed, before) | ||
264 | .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed)); | ||
265 | |||
266 | if after.contains("<|>") { | ||
267 | let (offset, after) = extract_offset(after); | ||
268 | assert_eq_text!(&after, &actual); | ||
269 | assert_eq!(file_change.cursor_position, Some(offset)) | ||
270 | } else { | ||
271 | assert_eq_text!(after, &actual); | ||
272 | } | ||
273 | } | ||
274 | |||
275 | fn type_char_noop(char_typed: char, before: &str) { | ||
276 | let file_change = do_type_char(char_typed, before); | ||
277 | assert!(file_change.is_none()) | ||
278 | } | ||
279 | |||
280 | #[test] | ||
281 | fn test_on_eq_typed() { | ||
160 | // do_check(r" | 282 | // do_check(r" |
161 | // fn foo() { | 283 | // fn foo() { |
162 | // let foo =<|> | 284 | // let foo =<|> |
@@ -166,7 +288,8 @@ mod tests { | |||
166 | // let foo =; | 288 | // let foo =; |
167 | // } | 289 | // } |
168 | // "); | 290 | // "); |
169 | type_eq( | 291 | type_char( |
292 | '=', | ||
170 | r" | 293 | r" |
171 | fn foo() { | 294 | fn foo() { |
172 | let foo <|> 1 + 1 | 295 | let foo <|> 1 + 1 |
@@ -191,24 +314,10 @@ fn foo() { | |||
191 | // "); | 314 | // "); |
192 | } | 315 | } |
193 | 316 | ||
194 | fn type_dot(before: &str, after: &str) { | ||
195 | let (offset, before) = extract_offset(before); | ||
196 | let mut edit = TextEditBuilder::default(); | ||
197 | edit.insert(offset, ".".to_string()); | ||
198 | let before = edit.finish().apply(&before); | ||
199 | let (analysis, file_id) = single_file(&before); | ||
200 | if let Some(result) = analysis.on_dot_typed(FilePosition { offset, file_id }).unwrap() { | ||
201 | assert_eq!(result.source_file_edits.len(), 1); | ||
202 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
203 | assert_eq_text!(after, &actual); | ||
204 | } else { | ||
205 | assert_eq_text!(&before, after) | ||
206 | }; | ||
207 | } | ||
208 | |||
209 | #[test] | 317 | #[test] |
210 | fn indents_new_chain_call() { | 318 | fn indents_new_chain_call() { |
211 | type_dot( | 319 | type_char( |
320 | '.', | ||
212 | r" | 321 | r" |
213 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 322 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
214 | self.child_impl(db, name) | 323 | self.child_impl(db, name) |
@@ -222,25 +331,21 @@ fn foo() { | |||
222 | } | 331 | } |
223 | ", | 332 | ", |
224 | ); | 333 | ); |
225 | type_dot( | 334 | type_char_noop( |
335 | '.', | ||
226 | r" | 336 | r" |
227 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 337 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
228 | self.child_impl(db, name) | 338 | self.child_impl(db, name) |
229 | <|> | 339 | <|> |
230 | } | 340 | } |
231 | ", | 341 | ", |
232 | r" | ||
233 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
234 | self.child_impl(db, name) | ||
235 | . | ||
236 | } | ||
237 | ", | ||
238 | ) | 342 | ) |
239 | } | 343 | } |
240 | 344 | ||
241 | #[test] | 345 | #[test] |
242 | fn indents_new_chain_call_with_semi() { | 346 | fn indents_new_chain_call_with_semi() { |
243 | type_dot( | 347 | type_char( |
348 | '.', | ||
244 | r" | 349 | r" |
245 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 350 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
246 | self.child_impl(db, name) | 351 | self.child_impl(db, name) |
@@ -254,25 +359,21 @@ fn foo() { | |||
254 | } | 359 | } |
255 | ", | 360 | ", |
256 | ); | 361 | ); |
257 | type_dot( | 362 | type_char_noop( |
363 | '.', | ||
258 | r" | 364 | r" |
259 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 365 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
260 | self.child_impl(db, name) | 366 | self.child_impl(db, name) |
261 | <|>; | 367 | <|>; |
262 | } | 368 | } |
263 | ", | 369 | ", |
264 | r" | ||
265 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
266 | self.child_impl(db, name) | ||
267 | .; | ||
268 | } | ||
269 | ", | ||
270 | ) | 370 | ) |
271 | } | 371 | } |
272 | 372 | ||
273 | #[test] | 373 | #[test] |
274 | fn indents_continued_chain_call() { | 374 | fn indents_continued_chain_call() { |
275 | type_dot( | 375 | type_char( |
376 | '.', | ||
276 | r" | 377 | r" |
277 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 378 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
278 | self.child_impl(db, name) | 379 | self.child_impl(db, name) |
@@ -288,7 +389,8 @@ fn foo() { | |||
288 | } | 389 | } |
289 | ", | 390 | ", |
290 | ); | 391 | ); |
291 | type_dot( | 392 | type_char_noop( |
393 | '.', | ||
292 | r" | 394 | r" |
293 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 395 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
294 | self.child_impl(db, name) | 396 | self.child_impl(db, name) |
@@ -296,19 +398,13 @@ fn foo() { | |||
296 | <|> | 398 | <|> |
297 | } | 399 | } |
298 | ", | 400 | ", |
299 | r" | ||
300 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
301 | self.child_impl(db, name) | ||
302 | .first() | ||
303 | . | ||
304 | } | ||
305 | ", | ||
306 | ); | 401 | ); |
307 | } | 402 | } |
308 | 403 | ||
309 | #[test] | 404 | #[test] |
310 | fn indents_middle_of_chain_call() { | 405 | fn indents_middle_of_chain_call() { |
311 | type_dot( | 406 | type_char( |
407 | '.', | ||
312 | r" | 408 | r" |
313 | fn source_impl() { | 409 | fn source_impl() { |
314 | let var = enum_defvariant_list().unwrap() | 410 | let var = enum_defvariant_list().unwrap() |
@@ -326,7 +422,8 @@ fn foo() { | |||
326 | } | 422 | } |
327 | ", | 423 | ", |
328 | ); | 424 | ); |
329 | type_dot( | 425 | type_char_noop( |
426 | '.', | ||
330 | r" | 427 | r" |
331 | fn source_impl() { | 428 | fn source_impl() { |
332 | let var = enum_defvariant_list().unwrap() | 429 | let var = enum_defvariant_list().unwrap() |
@@ -335,95 +432,31 @@ fn foo() { | |||
335 | .unwrap(); | 432 | .unwrap(); |
336 | } | 433 | } |
337 | ", | 434 | ", |
338 | r" | ||
339 | fn source_impl() { | ||
340 | let var = enum_defvariant_list().unwrap() | ||
341 | . | ||
342 | .nth(92) | ||
343 | .unwrap(); | ||
344 | } | ||
345 | ", | ||
346 | ); | 435 | ); |
347 | } | 436 | } |
348 | 437 | ||
349 | #[test] | 438 | #[test] |
350 | fn dont_indent_freestanding_dot() { | 439 | fn dont_indent_freestanding_dot() { |
351 | type_dot( | 440 | type_char_noop( |
441 | '.', | ||
352 | r" | 442 | r" |
353 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 443 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
354 | <|> | 444 | <|> |
355 | } | 445 | } |
356 | ", | 446 | ", |
357 | r" | ||
358 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
359 | . | ||
360 | } | ||
361 | ", | ||
362 | ); | 447 | ); |
363 | type_dot( | 448 | type_char_noop( |
449 | '.', | ||
364 | r" | 450 | r" |
365 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 451 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
366 | <|> | 452 | <|> |
367 | } | 453 | } |
368 | ", | 454 | ", |
369 | r" | ||
370 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
371 | . | ||
372 | } | ||
373 | ", | ||
374 | ); | 455 | ); |
375 | } | 456 | } |
376 | 457 | ||
377 | #[test] | 458 | #[test] |
378 | fn test_on_enter() { | 459 | fn adds_space_after_return_type() { |
379 | fn apply_on_enter(before: &str) -> Option<String> { | 460 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") |
380 | let (offset, before) = extract_offset(before); | ||
381 | let (analysis, file_id) = single_file(&before); | ||
382 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; | ||
383 | |||
384 | assert_eq!(result.source_file_edits.len(), 1); | ||
385 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
386 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); | ||
387 | Some(actual) | ||
388 | } | ||
389 | |||
390 | fn do_check(before: &str, after: &str) { | ||
391 | let actual = apply_on_enter(before).unwrap(); | ||
392 | assert_eq_text!(after, &actual); | ||
393 | } | ||
394 | |||
395 | fn do_check_noop(text: &str) { | ||
396 | assert!(apply_on_enter(text).is_none()) | ||
397 | } | ||
398 | |||
399 | do_check( | ||
400 | r" | ||
401 | /// Some docs<|> | ||
402 | fn foo() { | ||
403 | } | ||
404 | ", | ||
405 | r" | ||
406 | /// Some docs | ||
407 | /// <|> | ||
408 | fn foo() { | ||
409 | } | ||
410 | ", | ||
411 | ); | ||
412 | do_check( | ||
413 | r" | ||
414 | impl S { | ||
415 | /// Some<|> docs. | ||
416 | fn foo() {} | ||
417 | } | ||
418 | ", | ||
419 | r" | ||
420 | impl S { | ||
421 | /// Some | ||
422 | /// <|> docs. | ||
423 | fn foo() {} | ||
424 | } | ||
425 | ", | ||
426 | ); | ||
427 | do_check_noop(r"<|>//! docz"); | ||
428 | } | 461 | } |
429 | } | 462 | } |