aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ra_assists/src/assist_config.rs27
-rw-r--r--crates/ra_assists/src/assist_context.rs41
-rw-r--r--crates/ra_assists/src/handlers/add_custom_impl.rs51
-rw-r--r--crates/ra_assists/src/handlers/add_derive.rs28
-rw-r--r--crates/ra_assists/src/handlers/add_impl.rs34
-rw-r--r--crates/ra_assists/src/lib.rs15
-rw-r--r--crates/ra_assists/src/tests.rs38
-rw-r--r--crates/ra_assists/src/tests/generated.rs10
-rw-r--r--crates/ra_ide/src/completion.rs2
-rw-r--r--crates/ra_ide/src/completion/test_utils.rs2
-rw-r--r--crates/ra_ide/src/diagnostics.rs2
-rw-r--r--crates/ra_ide/src/lib.rs10
-rw-r--r--crates/ra_ide/src/references/rename.rs3
-rw-r--r--crates/ra_ide_db/src/source_change.rs5
-rw-r--r--crates/rust-analyzer/src/cli/analysis_bench.rs2
-rw-r--r--crates/rust-analyzer/src/config.rs12
-rw-r--r--crates/rust-analyzer/src/diagnostics.rs10
-rw-r--r--crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap6
-rw-r--r--crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap6
-rw-r--r--crates/rust-analyzer/src/diagnostics/to_proto.rs20
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs55
-rw-r--r--crates/rust-analyzer/src/main_loop.rs2
-rw-r--r--crates/rust-analyzer/src/main_loop/handlers.rs88
-rw-r--r--crates/rust-analyzer/src/to_proto.rs130
-rw-r--r--crates/rust-analyzer/tests/heavy_tests/main.rs52
-rw-r--r--docs/dev/lsp-extensions.md34
-rw-r--r--docs/user/assists.md10
-rw-r--r--editors/code/src/client.ts65
-rw-r--r--editors/code/src/commands/index.ts34
-rw-r--r--editors/code/src/main.ts1
30 files changed, 543 insertions, 252 deletions
diff --git a/crates/ra_assists/src/assist_config.rs b/crates/ra_assists/src/assist_config.rs
new file mode 100644
index 000000000..c0a0226fb
--- /dev/null
+++ b/crates/ra_assists/src/assist_config.rs
@@ -0,0 +1,27 @@
1//! Settings for tweaking assists.
2//!
3//! The fun thing here is `SnippetCap` -- this type can only be created in this
4//! module, and we use to statically check that we only produce snippet
5//! assists if we are allowed to.
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct AssistConfig {
9 pub snippet_cap: Option<SnippetCap>,
10}
11
12impl AssistConfig {
13 pub fn allow_snippets(&mut self, yes: bool) {
14 self.snippet_cap = if yes { Some(SnippetCap { _private: () }) } else { None }
15 }
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct SnippetCap {
20 _private: (),
21}
22
23impl Default for AssistConfig {
24 fn default() -> Self {
25 AssistConfig { snippet_cap: Some(SnippetCap { _private: () }) }
26 }
27}
diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs
index a680f752b..0dcd9df61 100644
--- a/crates/ra_assists/src/assist_context.rs
+++ b/crates/ra_assists/src/assist_context.rs
@@ -15,7 +15,10 @@ use ra_syntax::{
15}; 15};
16use ra_text_edit::TextEditBuilder; 16use ra_text_edit::TextEditBuilder;
17 17
18use crate::{Assist, AssistId, GroupLabel, ResolvedAssist}; 18use crate::{
19 assist_config::{AssistConfig, SnippetCap},
20 Assist, AssistId, GroupLabel, ResolvedAssist,
21};
19 22
20/// `AssistContext` allows to apply an assist or check if it could be applied. 23/// `AssistContext` allows to apply an assist or check if it could be applied.
21/// 24///
@@ -48,6 +51,7 @@ use crate::{Assist, AssistId, GroupLabel, ResolvedAssist};
48/// moment, because the LSP API is pretty awkward in this place, and it's much 51/// moment, because the LSP API is pretty awkward in this place, and it's much
49/// easier to just compute the edit eagerly :-) 52/// easier to just compute the edit eagerly :-)
50pub(crate) struct AssistContext<'a> { 53pub(crate) struct AssistContext<'a> {
54 pub(crate) config: &'a AssistConfig,
51 pub(crate) sema: Semantics<'a, RootDatabase>, 55 pub(crate) sema: Semantics<'a, RootDatabase>,
52 pub(crate) db: &'a RootDatabase, 56 pub(crate) db: &'a RootDatabase,
53 pub(crate) frange: FileRange, 57 pub(crate) frange: FileRange,
@@ -55,10 +59,14 @@ pub(crate) struct AssistContext<'a> {
55} 59}
56 60
57impl<'a> AssistContext<'a> { 61impl<'a> AssistContext<'a> {
58 pub fn new(sema: Semantics<'a, RootDatabase>, frange: FileRange) -> AssistContext<'a> { 62 pub(crate) fn new(
63 sema: Semantics<'a, RootDatabase>,
64 config: &'a AssistConfig,
65 frange: FileRange,
66 ) -> AssistContext<'a> {
59 let source_file = sema.parse(frange.file_id); 67 let source_file = sema.parse(frange.file_id);
60 let db = sema.db; 68 let db = sema.db;
61 AssistContext { sema, db, frange, source_file } 69 AssistContext { config, sema, db, frange, source_file }
62 } 70 }
63 71
64 // NB, this ignores active selection. 72 // NB, this ignores active selection.
@@ -165,11 +173,17 @@ pub(crate) struct AssistBuilder {
165 edit: TextEditBuilder, 173 edit: TextEditBuilder,
166 cursor_position: Option<TextSize>, 174 cursor_position: Option<TextSize>,
167 file: FileId, 175 file: FileId,
176 is_snippet: bool,
168} 177}
169 178
170impl AssistBuilder { 179impl AssistBuilder {
171 pub(crate) fn new(file: FileId) -> AssistBuilder { 180 pub(crate) fn new(file: FileId) -> AssistBuilder {
172 AssistBuilder { edit: TextEditBuilder::default(), cursor_position: None, file } 181 AssistBuilder {
182 edit: TextEditBuilder::default(),
183 cursor_position: None,
184 file,
185 is_snippet: false,
186 }
173 } 187 }
174 188
175 /// Remove specified `range` of text. 189 /// Remove specified `range` of text.
@@ -180,6 +194,16 @@ impl AssistBuilder {
180 pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into<String>) { 194 pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into<String>) {
181 self.edit.insert(offset, text.into()) 195 self.edit.insert(offset, text.into())
182 } 196 }
197 /// Append specified `text` at the given `offset`
198 pub(crate) fn insert_snippet(
199 &mut self,
200 _cap: SnippetCap,
201 offset: TextSize,
202 text: impl Into<String>,
203 ) {
204 self.is_snippet = true;
205 self.edit.insert(offset, text.into())
206 }
183 /// Replaces specified `range` of text with a given string. 207 /// Replaces specified `range` of text with a given string.
184 pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { 208 pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
185 self.edit.replace(range, replace_with.into()) 209 self.edit.replace(range, replace_with.into())
@@ -227,7 +251,12 @@ impl AssistBuilder {
227 if edit.is_empty() && self.cursor_position.is_none() { 251 if edit.is_empty() && self.cursor_position.is_none() {
228 panic!("Only call `add_assist` if the assist can be applied") 252 panic!("Only call `add_assist` if the assist can be applied")
229 } 253 }
230 SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position } 254 let mut res =
231 .into_source_change(self.file) 255 SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position }
256 .into_source_change(self.file);
257 if self.is_snippet {
258 res.is_snippet = true;
259 }
260 res
232 } 261 }
233} 262}
diff --git a/crates/ra_assists/src/handlers/add_custom_impl.rs b/crates/ra_assists/src/handlers/add_custom_impl.rs
index 2baeb8607..fa70c8496 100644
--- a/crates/ra_assists/src/handlers/add_custom_impl.rs
+++ b/crates/ra_assists/src/handlers/add_custom_impl.rs
@@ -25,7 +25,7 @@ use crate::{
25// struct S; 25// struct S;
26// 26//
27// impl Debug for S { 27// impl Debug for S {
28// 28// $0
29// } 29// }
30// ``` 30// ```
31pub(crate) fn add_custom_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { 31pub(crate) fn add_custom_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
@@ -52,7 +52,7 @@ pub(crate) fn add_custom_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<
52 format!("Add custom impl `{}` for `{}`", trait_token.text().as_str(), annotated_name); 52 format!("Add custom impl `{}` for `{}`", trait_token.text().as_str(), annotated_name);
53 53
54 let target = attr.syntax().text_range(); 54 let target = attr.syntax().text_range();
55 acc.add(AssistId("add_custom_impl"), label, target, |edit| { 55 acc.add(AssistId("add_custom_impl"), label, target, |builder| {
56 let new_attr_input = input 56 let new_attr_input = input
57 .syntax() 57 .syntax()
58 .descendants_with_tokens() 58 .descendants_with_tokens()
@@ -63,20 +63,11 @@ pub(crate) fn add_custom_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<
63 let has_more_derives = !new_attr_input.is_empty(); 63 let has_more_derives = !new_attr_input.is_empty();
64 let new_attr_input = new_attr_input.iter().sep_by(", ").surround_with("(", ")").to_string(); 64 let new_attr_input = new_attr_input.iter().sep_by(", ").surround_with("(", ")").to_string();
65 65
66 let mut buf = String::new(); 66 if has_more_derives {
67 buf.push_str("\n\nimpl "); 67 builder.replace(input.syntax().text_range(), new_attr_input);
68 buf.push_str(trait_token.text().as_str());
69 buf.push_str(" for ");
70 buf.push_str(annotated_name.as_str());
71 buf.push_str(" {\n");
72
73 let cursor_delta = if has_more_derives {
74 let delta = input.syntax().text_range().len() - TextSize::of(&new_attr_input);
75 edit.replace(input.syntax().text_range(), new_attr_input);
76 delta
77 } else { 68 } else {
78 let attr_range = attr.syntax().text_range(); 69 let attr_range = attr.syntax().text_range();
79 edit.delete(attr_range); 70 builder.delete(attr_range);
80 71
81 let line_break_range = attr 72 let line_break_range = attr
82 .syntax() 73 .syntax()
@@ -84,14 +75,24 @@ pub(crate) fn add_custom_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<
84 .filter(|t| t.kind() == WHITESPACE) 75 .filter(|t| t.kind() == WHITESPACE)
85 .map(|t| t.text_range()) 76 .map(|t| t.text_range())
86 .unwrap_or_else(|| TextRange::new(TextSize::from(0), TextSize::from(0))); 77 .unwrap_or_else(|| TextRange::new(TextSize::from(0), TextSize::from(0)));
87 edit.delete(line_break_range); 78 builder.delete(line_break_range);
88 79 }
89 attr_range.len() + line_break_range.len() 80
90 }; 81 match ctx.config.snippet_cap {
91 82 Some(cap) => {
92 edit.set_cursor(start_offset + TextSize::of(&buf) - cursor_delta); 83 builder.insert_snippet(
93 buf.push_str("\n}"); 84 cap,
94 edit.insert(start_offset, buf); 85 start_offset,
86 format!("\n\nimpl {} for {} {{\n $0\n}}", trait_token, annotated_name),
87 );
88 }
89 None => {
90 builder.insert(
91 start_offset,
92 format!("\n\nimpl {} for {} {{\n\n}}", trait_token, annotated_name),
93 );
94 }
95 }
95 }) 96 })
96} 97}
97 98
@@ -117,7 +118,7 @@ struct Foo {
117} 118}
118 119
119impl Debug for Foo { 120impl Debug for Foo {
120<|> 121 $0
121} 122}
122 ", 123 ",
123 ) 124 )
@@ -139,7 +140,7 @@ pub struct Foo {
139} 140}
140 141
141impl Debug for Foo { 142impl Debug for Foo {
142<|> 143 $0
143} 144}
144 ", 145 ",
145 ) 146 )
@@ -158,7 +159,7 @@ struct Foo {}
158struct Foo {} 159struct Foo {}
159 160
160impl Debug for Foo { 161impl Debug for Foo {
161<|> 162 $0
162} 163}
163 ", 164 ",
164 ) 165 )
diff --git a/crates/ra_assists/src/handlers/add_derive.rs b/crates/ra_assists/src/handlers/add_derive.rs
index fb08c19e9..b123b8498 100644
--- a/crates/ra_assists/src/handlers/add_derive.rs
+++ b/crates/ra_assists/src/handlers/add_derive.rs
@@ -18,31 +18,37 @@ use crate::{AssistContext, AssistId, Assists};
18// ``` 18// ```
19// -> 19// ->
20// ``` 20// ```
21// #[derive()] 21// #[derive($0)]
22// struct Point { 22// struct Point {
23// x: u32, 23// x: u32,
24// y: u32, 24// y: u32,
25// } 25// }
26// ``` 26// ```
27pub(crate) fn add_derive(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { 27pub(crate) fn add_derive(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
28 let cap = ctx.config.snippet_cap?;
28 let nominal = ctx.find_node_at_offset::<ast::NominalDef>()?; 29 let nominal = ctx.find_node_at_offset::<ast::NominalDef>()?;
29 let node_start = derive_insertion_offset(&nominal)?; 30 let node_start = derive_insertion_offset(&nominal)?;
30 let target = nominal.syntax().text_range(); 31 let target = nominal.syntax().text_range();
31 acc.add(AssistId("add_derive"), "Add `#[derive]`", target, |edit| { 32 acc.add(AssistId("add_derive"), "Add `#[derive]`", target, |builder| {
32 let derive_attr = nominal 33 let derive_attr = nominal
33 .attrs() 34 .attrs()
34 .filter_map(|x| x.as_simple_call()) 35 .filter_map(|x| x.as_simple_call())
35 .filter(|(name, _arg)| name == "derive") 36 .filter(|(name, _arg)| name == "derive")
36 .map(|(_name, arg)| arg) 37 .map(|(_name, arg)| arg)
37 .next(); 38 .next();
38 let offset = match derive_attr { 39 match derive_attr {
39 None => { 40 None => {
40 edit.insert(node_start, "#[derive()]\n"); 41 builder.insert_snippet(cap, node_start, "#[derive($0)]\n");
41 node_start + TextSize::of("#[derive(") 42 }
43 Some(tt) => {
44 // Just move the cursor.
45 builder.insert_snippet(
46 cap,
47 tt.syntax().text_range().end() - TextSize::of(')'),
48 "$0",
49 )
42 } 50 }
43 Some(tt) => tt.syntax().text_range().end() - TextSize::of(')'),
44 }; 51 };
45 edit.set_cursor(offset)
46 }) 52 })
47} 53}
48 54
@@ -66,12 +72,12 @@ mod tests {
66 check_assist( 72 check_assist(
67 add_derive, 73 add_derive,
68 "struct Foo { a: i32, <|>}", 74 "struct Foo { a: i32, <|>}",
69 "#[derive(<|>)]\nstruct Foo { a: i32, }", 75 "#[derive($0)]\nstruct Foo { a: i32, }",
70 ); 76 );
71 check_assist( 77 check_assist(
72 add_derive, 78 add_derive,
73 "struct Foo { <|> a: i32, }", 79 "struct Foo { <|> a: i32, }",
74 "#[derive(<|>)]\nstruct Foo { a: i32, }", 80 "#[derive($0)]\nstruct Foo { a: i32, }",
75 ); 81 );
76 } 82 }
77 83
@@ -80,7 +86,7 @@ mod tests {
80 check_assist( 86 check_assist(
81 add_derive, 87 add_derive,
82 "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", 88 "#[derive(Clone)]\nstruct Foo { a: i32<|>, }",
83 "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", 89 "#[derive(Clone$0)]\nstruct Foo { a: i32, }",
84 ); 90 );
85 } 91 }
86 92
@@ -96,7 +102,7 @@ struct Foo { a: i32<|>, }
96 " 102 "
97/// `Foo` is a pretty important struct. 103/// `Foo` is a pretty important struct.
98/// It does stuff. 104/// It does stuff.
99#[derive(<|>)] 105#[derive($0)]
100struct Foo { a: i32, } 106struct Foo { a: i32, }
101 ", 107 ",
102 ); 108 );
diff --git a/crates/ra_assists/src/handlers/add_impl.rs b/crates/ra_assists/src/handlers/add_impl.rs
index df114a0d8..eceba7d0a 100644
--- a/crates/ra_assists/src/handlers/add_impl.rs
+++ b/crates/ra_assists/src/handlers/add_impl.rs
@@ -1,7 +1,4 @@
1use ra_syntax::{ 1use ra_syntax::ast::{self, AstNode, NameOwner, TypeParamsOwner};
2 ast::{self, AstNode, NameOwner, TypeParamsOwner},
3 TextSize,
4};
5use stdx::{format_to, SepBy}; 2use stdx::{format_to, SepBy};
6 3
7use crate::{AssistContext, AssistId, Assists}; 4use crate::{AssistContext, AssistId, Assists};
@@ -12,17 +9,17 @@ use crate::{AssistContext, AssistId, Assists};
12// 9//
13// ``` 10// ```
14// struct Ctx<T: Clone> { 11// struct Ctx<T: Clone> {
15// data: T,<|> 12// data: T,<|>
16// } 13// }
17// ``` 14// ```
18// -> 15// ->
19// ``` 16// ```
20// struct Ctx<T: Clone> { 17// struct Ctx<T: Clone> {
21// data: T, 18// data: T,
22// } 19// }
23// 20//
24// impl<T: Clone> Ctx<T> { 21// impl<T: Clone> Ctx<T> {
25// 22// $0
26// } 23// }
27// ``` 24// ```
28pub(crate) fn add_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { 25pub(crate) fn add_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
@@ -50,30 +47,37 @@ pub(crate) fn add_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
50 let generic_params = lifetime_params.chain(type_params).sep_by(", "); 47 let generic_params = lifetime_params.chain(type_params).sep_by(", ");
51 format_to!(buf, "<{}>", generic_params) 48 format_to!(buf, "<{}>", generic_params)
52 } 49 }
53 buf.push_str(" {\n"); 50 match ctx.config.snippet_cap {
54 edit.set_cursor(start_offset + TextSize::of(&buf)); 51 Some(cap) => {
55 buf.push_str("\n}"); 52 buf.push_str(" {\n $0\n}");
56 edit.insert(start_offset, buf); 53 edit.insert_snippet(cap, start_offset, buf);
54 }
55 None => {
56 buf.push_str(" {\n}");
57 edit.insert(start_offset, buf);
58 }
59 }
57 }) 60 })
58} 61}
59 62
60#[cfg(test)] 63#[cfg(test)]
61mod tests { 64mod tests {
62 use super::*;
63 use crate::tests::{check_assist, check_assist_target}; 65 use crate::tests::{check_assist, check_assist_target};
64 66
67 use super::*;
68
65 #[test] 69 #[test]
66 fn test_add_impl() { 70 fn test_add_impl() {
67 check_assist(add_impl, "struct Foo {<|>}\n", "struct Foo {}\n\nimpl Foo {\n<|>\n}\n"); 71 check_assist(add_impl, "struct Foo {<|>}\n", "struct Foo {}\n\nimpl Foo {\n $0\n}\n");
68 check_assist( 72 check_assist(
69 add_impl, 73 add_impl,
70 "struct Foo<T: Clone> {<|>}", 74 "struct Foo<T: Clone> {<|>}",
71 "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}", 75 "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n $0\n}",
72 ); 76 );
73 check_assist( 77 check_assist(
74 add_impl, 78 add_impl,
75 "struct Foo<'a, T: Foo<'a>> {<|>}", 79 "struct Foo<'a, T: Foo<'a>> {<|>}",
76 "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", 80 "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n $0\n}",
77 ); 81 );
78 } 82 }
79 83
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs
index b6dc7cb1b..7f0a723c9 100644
--- a/crates/ra_assists/src/lib.rs
+++ b/crates/ra_assists/src/lib.rs
@@ -10,6 +10,7 @@ macro_rules! eprintln {
10 ($($tt:tt)*) => { stdx::eprintln!($($tt)*) }; 10 ($($tt:tt)*) => { stdx::eprintln!($($tt)*) };
11} 11}
12 12
13mod assist_config;
13mod assist_context; 14mod assist_context;
14mod marks; 15mod marks;
15#[cfg(test)] 16#[cfg(test)]
@@ -24,6 +25,8 @@ use ra_syntax::TextRange;
24 25
25pub(crate) use crate::assist_context::{AssistContext, Assists}; 26pub(crate) use crate::assist_context::{AssistContext, Assists};
26 27
28pub use assist_config::AssistConfig;
29
27/// Unique identifier of the assist, should not be shown to the user 30/// Unique identifier of the assist, should not be shown to the user
28/// directly. 31/// directly.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)] 32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -54,9 +57,9 @@ impl Assist {
54 /// 57 ///
55 /// Assists are returned in the "unresolved" state, that is only labels are 58 /// Assists are returned in the "unresolved" state, that is only labels are
56 /// returned, without actual edits. 59 /// returned, without actual edits.
57 pub fn unresolved(db: &RootDatabase, range: FileRange) -> Vec<Assist> { 60 pub fn unresolved(db: &RootDatabase, config: &AssistConfig, range: FileRange) -> Vec<Assist> {
58 let sema = Semantics::new(db); 61 let sema = Semantics::new(db);
59 let ctx = AssistContext::new(sema, range); 62 let ctx = AssistContext::new(sema, config, range);
60 let mut acc = Assists::new_unresolved(&ctx); 63 let mut acc = Assists::new_unresolved(&ctx);
61 handlers::all().iter().for_each(|handler| { 64 handlers::all().iter().for_each(|handler| {
62 handler(&mut acc, &ctx); 65 handler(&mut acc, &ctx);
@@ -68,9 +71,13 @@ impl Assist {
68 /// 71 ///
69 /// Assists are returned in the "resolved" state, that is with edit fully 72 /// Assists are returned in the "resolved" state, that is with edit fully
70 /// computed. 73 /// computed.
71 pub fn resolved(db: &RootDatabase, range: FileRange) -> Vec<ResolvedAssist> { 74 pub fn resolved(
75 db: &RootDatabase,
76 config: &AssistConfig,
77 range: FileRange,
78 ) -> Vec<ResolvedAssist> {
72 let sema = Semantics::new(db); 79 let sema = Semantics::new(db);
73 let ctx = AssistContext::new(sema, range); 80 let ctx = AssistContext::new(sema, config, range);
74 let mut acc = Assists::new_resolved(&ctx); 81 let mut acc = Assists::new_resolved(&ctx);
75 handlers::all().iter().for_each(|handler| { 82 handlers::all().iter().for_each(|handler| {
76 handler(&mut acc, &ctx); 83 handler(&mut acc, &ctx);
diff --git a/crates/ra_assists/src/tests.rs b/crates/ra_assists/src/tests.rs
index a3eacb8f1..9ba3da786 100644
--- a/crates/ra_assists/src/tests.rs
+++ b/crates/ra_assists/src/tests.rs
@@ -11,7 +11,7 @@ use test_utils::{
11 RangeOrOffset, 11 RangeOrOffset,
12}; 12};
13 13
14use crate::{handlers::Handler, Assist, AssistContext, Assists}; 14use crate::{handlers::Handler, Assist, AssistConfig, AssistContext, Assists};
15 15
16pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { 16pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
17 let (mut db, file_id) = RootDatabase::with_single_file(text); 17 let (mut db, file_id) = RootDatabase::with_single_file(text);
@@ -41,14 +41,14 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
41 let (db, file_id) = crate::tests::with_single_file(&before); 41 let (db, file_id) = crate::tests::with_single_file(&before);
42 let frange = FileRange { file_id, range: selection.into() }; 42 let frange = FileRange { file_id, range: selection.into() };
43 43
44 let mut assist = Assist::resolved(&db, frange) 44 let mut assist = Assist::resolved(&db, &AssistConfig::default(), frange)
45 .into_iter() 45 .into_iter()
46 .find(|assist| assist.assist.id.0 == assist_id) 46 .find(|assist| assist.assist.id.0 == assist_id)
47 .unwrap_or_else(|| { 47 .unwrap_or_else(|| {
48 panic!( 48 panic!(
49 "\n\nAssist is not applicable: {}\nAvailable assists: {}", 49 "\n\nAssist is not applicable: {}\nAvailable assists: {}",
50 assist_id, 50 assist_id,
51 Assist::resolved(&db, frange) 51 Assist::resolved(&db, &AssistConfig::default(), frange)
52 .into_iter() 52 .into_iter()
53 .map(|assist| assist.assist.id.0) 53 .map(|assist| assist.assist.id.0)
54 .collect::<Vec<_>>() 54 .collect::<Vec<_>>()
@@ -90,7 +90,8 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult) {
90 let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() }; 90 let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() };
91 91
92 let sema = Semantics::new(&db); 92 let sema = Semantics::new(&db);
93 let ctx = AssistContext::new(sema, frange); 93 let config = AssistConfig::default();
94 let ctx = AssistContext::new(sema, &config, frange);
94 let mut acc = Assists::new_resolved(&ctx); 95 let mut acc = Assists::new_resolved(&ctx);
95 handler(&mut acc, &ctx); 96 handler(&mut acc, &ctx);
96 let mut res = acc.finish_resolved(); 97 let mut res = acc.finish_resolved();
@@ -103,19 +104,20 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult) {
103 let mut actual = db.file_text(change.file_id).as_ref().to_owned(); 104 let mut actual = db.file_text(change.file_id).as_ref().to_owned();
104 change.edit.apply(&mut actual); 105 change.edit.apply(&mut actual);
105 106
106 match source_change.cursor_position { 107 if !source_change.is_snippet {
107 None => { 108 match source_change.cursor_position {
108 if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset { 109 None => {
109 let off = change 110 if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset {
110 .edit 111 let off = change
111 .apply_to_offset(before_cursor_pos) 112 .edit
112 .expect("cursor position is affected by the edit"); 113 .apply_to_offset(before_cursor_pos)
113 actual = add_cursor(&actual, off) 114 .expect("cursor position is affected by the edit");
115 actual = add_cursor(&actual, off)
116 }
114 } 117 }
115 } 118 Some(off) => actual = add_cursor(&actual, off.offset),
116 Some(off) => actual = add_cursor(&actual, off.offset), 119 };
117 }; 120 }
118
119 assert_eq_text!(after, &actual); 121 assert_eq_text!(after, &actual);
120 } 122 }
121 (Some(assist), ExpectedResult::Target(target)) => { 123 (Some(assist), ExpectedResult::Target(target)) => {
@@ -136,7 +138,7 @@ fn assist_order_field_struct() {
136 let (before_cursor_pos, before) = extract_offset(before); 138 let (before_cursor_pos, before) = extract_offset(before);
137 let (db, file_id) = with_single_file(&before); 139 let (db, file_id) = with_single_file(&before);
138 let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) }; 140 let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) };
139 let assists = Assist::resolved(&db, frange); 141 let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
140 let mut assists = assists.iter(); 142 let mut assists = assists.iter();
141 143
142 assert_eq!( 144 assert_eq!(
@@ -159,7 +161,7 @@ fn assist_order_if_expr() {
159 let (range, before) = extract_range(before); 161 let (range, before) = extract_range(before);
160 let (db, file_id) = with_single_file(&before); 162 let (db, file_id) = with_single_file(&before);
161 let frange = FileRange { file_id, range }; 163 let frange = FileRange { file_id, range };
162 let assists = Assist::resolved(&db, frange); 164 let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
163 let mut assists = assists.iter(); 165 let mut assists = assists.iter();
164 166
165 assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable"); 167 assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable");
diff --git a/crates/ra_assists/src/tests/generated.rs b/crates/ra_assists/src/tests/generated.rs
index 972dbd251..9487c9239 100644
--- a/crates/ra_assists/src/tests/generated.rs
+++ b/crates/ra_assists/src/tests/generated.rs
@@ -15,7 +15,7 @@ struct S;
15struct S; 15struct S;
16 16
17impl Debug for S { 17impl Debug for S {
18 18 $0
19} 19}
20"#####, 20"#####,
21 ) 21 )
@@ -32,7 +32,7 @@ struct Point {
32} 32}
33"#####, 33"#####,
34 r#####" 34 r#####"
35#[derive()] 35#[derive($0)]
36struct Point { 36struct Point {
37 x: u32, 37 x: u32,
38 y: u32, 38 y: u32,
@@ -108,16 +108,16 @@ fn doctest_add_impl() {
108 "add_impl", 108 "add_impl",
109 r#####" 109 r#####"
110struct Ctx<T: Clone> { 110struct Ctx<T: Clone> {
111 data: T,<|> 111 data: T,<|>
112} 112}
113"#####, 113"#####,
114 r#####" 114 r#####"
115struct Ctx<T: Clone> { 115struct Ctx<T: Clone> {
116 data: T, 116 data: T,
117} 117}
118 118
119impl<T: Clone> Ctx<T> { 119impl<T: Clone> Ctx<T> {
120 120 $0
121} 121}
122"#####, 122"#####,
123 ) 123 )
diff --git a/crates/ra_ide/src/completion.rs b/crates/ra_ide/src/completion.rs
index 8bdc43b1a..191300704 100644
--- a/crates/ra_ide/src/completion.rs
+++ b/crates/ra_ide/src/completion.rs
@@ -59,8 +59,8 @@ pub use crate::completion::{
59/// with ordering of completions (currently this is done by the client). 59/// with ordering of completions (currently this is done by the client).
60pub(crate) fn completions( 60pub(crate) fn completions(
61 db: &RootDatabase, 61 db: &RootDatabase,
62 position: FilePosition,
63 config: &CompletionConfig, 62 config: &CompletionConfig,
63 position: FilePosition,
64) -> Option<Completions> { 64) -> Option<Completions> {
65 let ctx = CompletionContext::new(db, position, config)?; 65 let ctx = CompletionContext::new(db, position, config)?;
66 66
diff --git a/crates/ra_ide/src/completion/test_utils.rs b/crates/ra_ide/src/completion/test_utils.rs
index eb90b5279..bf22452a2 100644
--- a/crates/ra_ide/src/completion/test_utils.rs
+++ b/crates/ra_ide/src/completion/test_utils.rs
@@ -20,7 +20,7 @@ pub(crate) fn do_completion_with_options(
20 } else { 20 } else {
21 single_file_with_position(code) 21 single_file_with_position(code)
22 }; 22 };
23 let completions = analysis.completions(position, options).unwrap().unwrap(); 23 let completions = analysis.completions(options, position).unwrap().unwrap();
24 let completion_items: Vec<CompletionItem> = completions.into(); 24 let completion_items: Vec<CompletionItem> = completions.into();
25 let mut kind_completions: Vec<CompletionItem> = 25 let mut kind_completions: Vec<CompletionItem> =
26 completion_items.into_iter().filter(|c| c.completion_kind == kind).collect(); 26 completion_items.into_iter().filter(|c| c.completion_kind == kind).collect();
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs
index 87a0b80f1..54c2bcc09 100644
--- a/crates/ra_ide/src/diagnostics.rs
+++ b/crates/ra_ide/src/diagnostics.rs
@@ -629,6 +629,7 @@ mod tests {
629 }, 629 },
630 ], 630 ],
631 cursor_position: None, 631 cursor_position: None,
632 is_snippet: false,
632 }, 633 },
633 ), 634 ),
634 severity: Error, 635 severity: Error,
@@ -685,6 +686,7 @@ mod tests {
685 ], 686 ],
686 file_system_edits: [], 687 file_system_edits: [],
687 cursor_position: None, 688 cursor_position: None,
689 is_snippet: false,
688 }, 690 },
689 ), 691 ),
690 severity: Error, 692 severity: Error,
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index 78149ddfc..66125f2f5 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -82,7 +82,7 @@ pub use crate::{
82}; 82};
83 83
84pub use hir::Documentation; 84pub use hir::Documentation;
85pub use ra_assists::AssistId; 85pub use ra_assists::{AssistConfig, AssistId};
86pub use ra_db::{ 86pub use ra_db::{
87 Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId, 87 Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId,
88}; 88};
@@ -458,17 +458,17 @@ impl Analysis {
458 /// Computes completions at the given position. 458 /// Computes completions at the given position.
459 pub fn completions( 459 pub fn completions(
460 &self, 460 &self,
461 position: FilePosition,
462 config: &CompletionConfig, 461 config: &CompletionConfig,
462 position: FilePosition,
463 ) -> Cancelable<Option<Vec<CompletionItem>>> { 463 ) -> Cancelable<Option<Vec<CompletionItem>>> {
464 self.with_db(|db| completion::completions(db, position, config).map(Into::into)) 464 self.with_db(|db| completion::completions(db, config, position).map(Into::into))
465 } 465 }
466 466
467 /// Computes assists (aka code actions aka intentions) for the given 467 /// Computes assists (aka code actions aka intentions) for the given
468 /// position. 468 /// position.
469 pub fn assists(&self, frange: FileRange) -> Cancelable<Vec<Assist>> { 469 pub fn assists(&self, config: &AssistConfig, frange: FileRange) -> Cancelable<Vec<Assist>> {
470 self.with_db(|db| { 470 self.with_db(|db| {
471 ra_assists::Assist::resolved(db, frange) 471 ra_assists::Assist::resolved(db, config, frange)
472 .into_iter() 472 .into_iter()
473 .map(|assist| Assist { 473 .map(|assist| Assist {
474 id: assist.assist.id, 474 id: assist.assist.id,
diff --git a/crates/ra_ide/src/references/rename.rs b/crates/ra_ide/src/references/rename.rs
index 410dae75c..68a53ad4b 100644
--- a/crates/ra_ide/src/references/rename.rs
+++ b/crates/ra_ide/src/references/rename.rs
@@ -670,6 +670,7 @@ mod tests {
670 }, 670 },
671 ], 671 ],
672 cursor_position: None, 672 cursor_position: None,
673 is_snippet: false,
673 }, 674 },
674 }, 675 },
675 ) 676 )
@@ -722,6 +723,7 @@ mod tests {
722 }, 723 },
723 ], 724 ],
724 cursor_position: None, 725 cursor_position: None,
726 is_snippet: false,
725 }, 727 },
726 }, 728 },
727 ) 729 )
@@ -818,6 +820,7 @@ mod tests {
818 }, 820 },
819 ], 821 ],
820 cursor_position: None, 822 cursor_position: None,
823 is_snippet: false,
821 }, 824 },
822 }, 825 },
823 ) 826 )
diff --git a/crates/ra_ide_db/src/source_change.rs b/crates/ra_ide_db/src/source_change.rs
index af81a91a4..c64165f3a 100644
--- a/crates/ra_ide_db/src/source_change.rs
+++ b/crates/ra_ide_db/src/source_change.rs
@@ -13,6 +13,7 @@ pub struct SourceChange {
13 pub source_file_edits: Vec<SourceFileEdit>, 13 pub source_file_edits: Vec<SourceFileEdit>,
14 pub file_system_edits: Vec<FileSystemEdit>, 14 pub file_system_edits: Vec<FileSystemEdit>,
15 pub cursor_position: Option<FilePosition>, 15 pub cursor_position: Option<FilePosition>,
16 pub is_snippet: bool,
16} 17}
17 18
18impl SourceChange { 19impl SourceChange {
@@ -28,6 +29,7 @@ impl SourceChange {
28 source_file_edits, 29 source_file_edits,
29 file_system_edits, 30 file_system_edits,
30 cursor_position: None, 31 cursor_position: None,
32 is_snippet: false,
31 } 33 }
32 } 34 }
33 35
@@ -41,6 +43,7 @@ impl SourceChange {
41 source_file_edits: edits, 43 source_file_edits: edits,
42 file_system_edits: vec![], 44 file_system_edits: vec![],
43 cursor_position: None, 45 cursor_position: None,
46 is_snippet: false,
44 } 47 }
45 } 48 }
46 49
@@ -52,6 +55,7 @@ impl SourceChange {
52 source_file_edits: vec![], 55 source_file_edits: vec![],
53 file_system_edits: edits, 56 file_system_edits: edits,
54 cursor_position: None, 57 cursor_position: None,
58 is_snippet: false,
55 } 59 }
56 } 60 }
57 61
@@ -115,6 +119,7 @@ impl SingleFileChange {
115 source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }], 119 source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }],
116 file_system_edits: Vec::new(), 120 file_system_edits: Vec::new(),
117 cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }), 121 cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }),
122 is_snippet: false,
118 } 123 }
119 } 124 }
120} 125}
diff --git a/crates/rust-analyzer/src/cli/analysis_bench.rs b/crates/rust-analyzer/src/cli/analysis_bench.rs
index 6147ae207..b20efe98d 100644
--- a/crates/rust-analyzer/src/cli/analysis_bench.rs
+++ b/crates/rust-analyzer/src/cli/analysis_bench.rs
@@ -105,7 +105,7 @@ pub fn analysis_bench(
105 if is_completion { 105 if is_completion {
106 let options = CompletionConfig::default(); 106 let options = CompletionConfig::default();
107 let res = do_work(&mut host, file_id, |analysis| { 107 let res = do_work(&mut host, file_id, |analysis| {
108 analysis.completions(file_position, &options) 108 analysis.completions(&options, file_position)
109 }); 109 });
110 if verbosity.is_verbose() { 110 if verbosity.is_verbose() {
111 println!("\n{:#?}", res); 111 println!("\n{:#?}", res);
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index b5dc6f0fa..d75c48597 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -11,7 +11,7 @@ use std::{ffi::OsString, path::PathBuf};
11 11
12use lsp_types::ClientCapabilities; 12use lsp_types::ClientCapabilities;
13use ra_flycheck::FlycheckConfig; 13use ra_flycheck::FlycheckConfig;
14use ra_ide::{CompletionConfig, InlayHintsConfig}; 14use ra_ide::{AssistConfig, CompletionConfig, InlayHintsConfig};
15use ra_project_model::CargoConfig; 15use ra_project_model::CargoConfig;
16use serde::Deserialize; 16use serde::Deserialize;
17 17
@@ -32,6 +32,7 @@ pub struct Config {
32 32
33 pub inlay_hints: InlayHintsConfig, 33 pub inlay_hints: InlayHintsConfig,
34 pub completion: CompletionConfig, 34 pub completion: CompletionConfig,
35 pub assist: AssistConfig,
35 pub call_info_full: bool, 36 pub call_info_full: bool,
36 pub lens: LensConfig, 37 pub lens: LensConfig,
37} 38}
@@ -136,6 +137,7 @@ impl Default for Config {
136 add_call_argument_snippets: true, 137 add_call_argument_snippets: true,
137 ..CompletionConfig::default() 138 ..CompletionConfig::default()
138 }, 139 },
140 assist: AssistConfig::default(),
139 call_info_full: true, 141 call_info_full: true,
140 lens: LensConfig::default(), 142 lens: LensConfig::default(),
141 } 143 }
@@ -273,6 +275,7 @@ impl Config {
273 { 275 {
274 self.client_caps.code_action_literals = value; 276 self.client_caps.code_action_literals = value;
275 } 277 }
278
276 self.completion.allow_snippets(false); 279 self.completion.allow_snippets(false);
277 if let Some(completion) = &doc_caps.completion { 280 if let Some(completion) = &doc_caps.completion {
278 if let Some(completion_item) = &completion.completion_item { 281 if let Some(completion_item) = &completion.completion_item {
@@ -288,5 +291,12 @@ impl Config {
288 self.client_caps.work_done_progress = value; 291 self.client_caps.work_done_progress = value;
289 } 292 }
290 } 293 }
294
295 self.assist.allow_snippets(false);
296 if let Some(experimental) = &caps.experimental {
297 let enable =
298 experimental.get("snippetTextEdit").and_then(|it| it.as_bool()) == Some(true);
299 self.assist.allow_snippets(enable);
300 }
291 } 301 }
292} 302}
diff --git a/crates/rust-analyzer/src/diagnostics.rs b/crates/rust-analyzer/src/diagnostics.rs
index 4bdd45a7d..25856c543 100644
--- a/crates/rust-analyzer/src/diagnostics.rs
+++ b/crates/rust-analyzer/src/diagnostics.rs
@@ -3,9 +3,11 @@ pub(crate) mod to_proto;
3 3
4use std::{collections::HashMap, sync::Arc}; 4use std::{collections::HashMap, sync::Arc};
5 5
6use lsp_types::{CodeActionOrCommand, Diagnostic, Range}; 6use lsp_types::{Diagnostic, Range};
7use ra_ide::FileId; 7use ra_ide::FileId;
8 8
9use crate::lsp_ext;
10
9pub type CheckFixes = Arc<HashMap<FileId, Vec<Fix>>>; 11pub type CheckFixes = Arc<HashMap<FileId, Vec<Fix>>>;
10 12
11#[derive(Debug, Default, Clone)] 13#[derive(Debug, Default, Clone)]
@@ -18,13 +20,13 @@ pub struct DiagnosticCollection {
18#[derive(Debug, Clone)] 20#[derive(Debug, Clone)]
19pub struct Fix { 21pub struct Fix {
20 pub range: Range, 22 pub range: Range,
21 pub action: CodeActionOrCommand, 23 pub action: lsp_ext::CodeAction,
22} 24}
23 25
24#[derive(Debug)] 26#[derive(Debug)]
25pub enum DiagnosticTask { 27pub enum DiagnosticTask {
26 ClearCheck, 28 ClearCheck,
27 AddCheck(FileId, Diagnostic, Vec<CodeActionOrCommand>), 29 AddCheck(FileId, Diagnostic, Vec<lsp_ext::CodeAction>),
28 SetNative(FileId, Vec<Diagnostic>), 30 SetNative(FileId, Vec<Diagnostic>),
29} 31}
30 32
@@ -38,7 +40,7 @@ impl DiagnosticCollection {
38 &mut self, 40 &mut self,
39 file_id: FileId, 41 file_id: FileId,
40 diagnostic: Diagnostic, 42 diagnostic: Diagnostic,
41 fixes: Vec<CodeActionOrCommand>, 43 fixes: Vec<lsp_ext::CodeAction>,
42 ) { 44 ) {
43 let diagnostics = self.check.entry(file_id).or_default(); 45 let diagnostics = self.check.entry(file_id).or_default();
44 for existing_diagnostic in diagnostics.iter() { 46 for existing_diagnostic in diagnostics.iter() {
diff --git a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap
index 076b3cf27..96466b5c9 100644
--- a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap
+++ b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap
@@ -68,9 +68,9 @@ expression: diag
68 kind: Some( 68 kind: Some(
69 "quickfix", 69 "quickfix",
70 ), 70 ),
71 diagnostics: None, 71 command: None,
72 edit: Some( 72 edit: Some(
73 WorkspaceEdit { 73 SnippetWorkspaceEdit {
74 changes: Some( 74 changes: Some(
75 { 75 {
76 "file:///test/src/main.rs": [ 76 "file:///test/src/main.rs": [
@@ -106,8 +106,6 @@ expression: diag
106 document_changes: None, 106 document_changes: None,
107 }, 107 },
108 ), 108 ),
109 command: None,
110 is_preferred: None,
111 }, 109 },
112 ], 110 ],
113 }, 111 },
diff --git a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap
index 69138c15b..8f962277f 100644
--- a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap
+++ b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap
@@ -53,9 +53,9 @@ expression: diag
53 kind: Some( 53 kind: Some(
54 "quickfix", 54 "quickfix",
55 ), 55 ),
56 diagnostics: None, 56 command: None,
57 edit: Some( 57 edit: Some(
58 WorkspaceEdit { 58 SnippetWorkspaceEdit {
59 changes: Some( 59 changes: Some(
60 { 60 {
61 "file:///test/driver/subcommand/repl.rs": [ 61 "file:///test/driver/subcommand/repl.rs": [
@@ -78,8 +78,6 @@ expression: diag
78 document_changes: None, 78 document_changes: None,
79 }, 79 },
80 ), 80 ),
81 command: None,
82 is_preferred: None,
83 }, 81 },
84 ], 82 ],
85 }, 83 },
diff --git a/crates/rust-analyzer/src/diagnostics/to_proto.rs b/crates/rust-analyzer/src/diagnostics/to_proto.rs
index eabf4908f..afea59525 100644
--- a/crates/rust-analyzer/src/diagnostics/to_proto.rs
+++ b/crates/rust-analyzer/src/diagnostics/to_proto.rs
@@ -7,13 +7,13 @@ use std::{
7}; 7};
8 8
9use lsp_types::{ 9use lsp_types::{
10 CodeAction, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, 10 Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location,
11 Location, NumberOrString, Position, Range, TextEdit, Url, WorkspaceEdit, 11 NumberOrString, Position, Range, TextEdit, Url,
12}; 12};
13use ra_flycheck::{Applicability, DiagnosticLevel, DiagnosticSpan, DiagnosticSpanMacroExpansion}; 13use ra_flycheck::{Applicability, DiagnosticLevel, DiagnosticSpan, DiagnosticSpanMacroExpansion};
14use stdx::format_to; 14use stdx::format_to;
15 15
16use crate::Result; 16use crate::{lsp_ext, Result};
17 17
18/// Converts a Rust level string to a LSP severity 18/// Converts a Rust level string to a LSP severity
19fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> { 19fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> {
@@ -110,7 +110,7 @@ fn is_deprecated(rd: &ra_flycheck::Diagnostic) -> bool {
110 110
111enum MappedRustChildDiagnostic { 111enum MappedRustChildDiagnostic {
112 Related(DiagnosticRelatedInformation), 112 Related(DiagnosticRelatedInformation),
113 SuggestedFix(CodeAction), 113 SuggestedFix(lsp_ext::CodeAction),
114 MessageLine(String), 114 MessageLine(String),
115} 115}
116 116
@@ -143,13 +143,15 @@ fn map_rust_child_diagnostic(
143 message: rd.message.clone(), 143 message: rd.message.clone(),
144 }) 144 })
145 } else { 145 } else {
146 MappedRustChildDiagnostic::SuggestedFix(CodeAction { 146 MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction {
147 title: rd.message.clone(), 147 title: rd.message.clone(),
148 kind: Some("quickfix".to_string()), 148 kind: Some("quickfix".to_string()),
149 diagnostics: None, 149 edit: Some(lsp_ext::SnippetWorkspaceEdit {
150 edit: Some(WorkspaceEdit::new(edit_map)), 150 // FIXME: there's no good reason to use edit_map here....
151 changes: Some(edit_map),
152 document_changes: None,
153 }),
151 command: None, 154 command: None,
152 is_preferred: None,
153 }) 155 })
154 } 156 }
155} 157}
@@ -158,7 +160,7 @@ fn map_rust_child_diagnostic(
158pub(crate) struct MappedRustDiagnostic { 160pub(crate) struct MappedRustDiagnostic {
159 pub location: Location, 161 pub location: Location,
160 pub diagnostic: Diagnostic, 162 pub diagnostic: Diagnostic,
161 pub fixes: Vec<CodeAction>, 163 pub fixes: Vec<lsp_ext::CodeAction>,
162} 164}
163 165
164/// Converts a Rust root diagnostic to LSP form 166/// Converts a Rust root diagnostic to LSP form
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 313a8c769..f75a26eb7 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -1,6 +1,6 @@
1//! rust-analyzer extensions to the LSP. 1//! rust-analyzer extensions to the LSP.
2 2
3use std::path::PathBuf; 3use std::{collections::HashMap, path::PathBuf};
4 4
5use lsp_types::request::Request; 5use lsp_types::request::Request;
6use lsp_types::{Location, Position, Range, TextDocumentIdentifier}; 6use lsp_types::{Location, Position, Range, TextDocumentIdentifier};
@@ -137,7 +137,7 @@ pub struct Runnable {
137#[serde(rename_all = "camelCase")] 137#[serde(rename_all = "camelCase")]
138pub struct SourceChange { 138pub struct SourceChange {
139 pub label: String, 139 pub label: String,
140 pub workspace_edit: lsp_types::WorkspaceEdit, 140 pub workspace_edit: SnippetWorkspaceEdit,
141 pub cursor_position: Option<lsp_types::TextDocumentPositionParams>, 141 pub cursor_position: Option<lsp_types::TextDocumentPositionParams>,
142} 142}
143 143
@@ -183,3 +183,54 @@ pub struct SsrParams {
183 pub query: String, 183 pub query: String,
184 pub parse_only: bool, 184 pub parse_only: bool,
185} 185}
186
187pub enum CodeActionRequest {}
188
189impl Request for CodeActionRequest {
190 type Params = lsp_types::CodeActionParams;
191 type Result = Option<Vec<CodeAction>>;
192 const METHOD: &'static str = "textDocument/codeAction";
193}
194
195#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
196pub struct CodeAction {
197 pub title: String,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub kind: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub command: Option<lsp_types::Command>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub edit: Option<SnippetWorkspaceEdit>,
204}
205
206#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
207#[serde(rename_all = "camelCase")]
208pub struct SnippetWorkspaceEdit {
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub changes: Option<HashMap<lsp_types::Url, Vec<lsp_types::TextEdit>>>,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub document_changes: Option<Vec<SnippetDocumentChangeOperation>>,
213}
214
215#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
216#[serde(untagged, rename_all = "lowercase")]
217pub enum SnippetDocumentChangeOperation {
218 Op(lsp_types::ResourceOp),
219 Edit(SnippetTextDocumentEdit),
220}
221
222#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
223#[serde(rename_all = "camelCase")]
224pub struct SnippetTextDocumentEdit {
225 pub text_document: lsp_types::VersionedTextDocumentIdentifier,
226 pub edits: Vec<SnippetTextEdit>,
227}
228
229#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
230#[serde(rename_all = "camelCase")]
231pub struct SnippetTextEdit {
232 pub range: Range,
233 pub new_text: String,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub insert_text_format: Option<lsp_types::InsertTextFormat>,
236}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 15e5bb354..87795fffb 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -518,6 +518,7 @@ fn on_request(
518 .on::<lsp_ext::ParentModule>(handlers::handle_parent_module)? 518 .on::<lsp_ext::ParentModule>(handlers::handle_parent_module)?
519 .on::<lsp_ext::Runnables>(handlers::handle_runnables)? 519 .on::<lsp_ext::Runnables>(handlers::handle_runnables)?
520 .on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)? 520 .on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)?
521 .on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)?
521 .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)? 522 .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)?
522 .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)? 523 .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)?
523 .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)? 524 .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
@@ -525,7 +526,6 @@ fn on_request(
525 .on::<lsp_types::request::GotoImplementation>(handlers::handle_goto_implementation)? 526 .on::<lsp_types::request::GotoImplementation>(handlers::handle_goto_implementation)?
526 .on::<lsp_types::request::GotoTypeDefinition>(handlers::handle_goto_type_definition)? 527 .on::<lsp_types::request::GotoTypeDefinition>(handlers::handle_goto_type_definition)?
527 .on::<lsp_types::request::Completion>(handlers::handle_completion)? 528 .on::<lsp_types::request::Completion>(handlers::handle_completion)?
528 .on::<lsp_types::request::CodeActionRequest>(handlers::handle_code_action)?
529 .on::<lsp_types::request::CodeLensRequest>(handlers::handle_code_lens)? 529 .on::<lsp_types::request::CodeLensRequest>(handlers::handle_code_lens)?
530 .on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)? 530 .on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)?
531 .on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)? 531 .on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)?
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index e67556752..4ff8fa69e 100644
--- a/crates/rust-analyzer/src/main_loop/handlers.rs
+++ b/crates/rust-analyzer/src/main_loop/handlers.rs
@@ -11,12 +11,11 @@ use lsp_server::ErrorCode;
11use lsp_types::{ 11use lsp_types::{
12 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem, 12 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
13 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams, 13 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
14 CodeAction, CodeActionResponse, CodeLens, Command, CompletionItem, Diagnostic, 14 CodeLens, Command, CompletionItem, Diagnostic, DocumentFormattingParams, DocumentHighlight,
15 DocumentFormattingParams, DocumentHighlight, DocumentSymbol, FoldingRange, FoldingRangeParams, 15 DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
16 Hover, HoverContents, Location, MarkupContent, MarkupKind, Position, PrepareRenameResponse, 16 MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
17 Range, RenameParams, SemanticTokensParams, SemanticTokensRangeParams, 17 SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
18 SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, 18 SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, TextEdit, Url, WorkspaceEdit,
19 TextEdit, Url, WorkspaceEdit,
20}; 19};
21use ra_ide::{ 20use ra_ide::{
22 Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope, 21 Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
@@ -476,7 +475,7 @@ pub fn handle_completion(
476 return Ok(None); 475 return Ok(None);
477 } 476 }
478 477
479 let items = match world.analysis().completions(position, &world.config.completion)? { 478 let items = match world.analysis().completions(&world.config.completion, position)? {
480 None => return Ok(None), 479 None => return Ok(None),
481 Some(items) => items, 480 Some(items) => items,
482 }; 481 };
@@ -585,9 +584,8 @@ pub fn handle_rename(world: WorldSnapshot, params: RenameParams) -> Result<Optio
585 None => return Ok(None), 584 None => return Ok(None),
586 Some(it) => it.info, 585 Some(it) => it.info,
587 }; 586 };
588 587 let workspace_edit = to_proto::workspace_edit(&world, source_change)?;
589 let source_change = to_proto::source_change(&world, source_change)?; 588 Ok(Some(workspace_edit))
590 Ok(Some(source_change.workspace_edit))
591} 589}
592 590
593pub fn handle_references( 591pub fn handle_references(
@@ -696,14 +694,21 @@ pub fn handle_formatting(
696pub fn handle_code_action( 694pub fn handle_code_action(
697 world: WorldSnapshot, 695 world: WorldSnapshot,
698 params: lsp_types::CodeActionParams, 696 params: lsp_types::CodeActionParams,
699) -> Result<Option<CodeActionResponse>> { 697) -> Result<Option<Vec<lsp_ext::CodeAction>>> {
700 let _p = profile("handle_code_action"); 698 let _p = profile("handle_code_action");
699 // We intentionally don't support command-based actions, as those either
700 // requires custom client-code anyway, or requires server-initiated edits.
701 // Server initiated edits break causality, so we avoid those as well.
702 if !world.config.client_caps.code_action_literals {
703 return Ok(None);
704 }
705
701 let file_id = from_proto::file_id(&world, &params.text_document.uri)?; 706 let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
702 let line_index = world.analysis().file_line_index(file_id)?; 707 let line_index = world.analysis().file_line_index(file_id)?;
703 let range = from_proto::text_range(&line_index, params.range); 708 let range = from_proto::text_range(&line_index, params.range);
704 709
705 let diagnostics = world.analysis().diagnostics(file_id)?; 710 let diagnostics = world.analysis().diagnostics(file_id)?;
706 let mut res = CodeActionResponse::default(); 711 let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
707 712
708 let fixes_from_diagnostics = diagnostics 713 let fixes_from_diagnostics = diagnostics
709 .into_iter() 714 .into_iter()
@@ -713,22 +718,9 @@ pub fn handle_code_action(
713 718
714 for source_edit in fixes_from_diagnostics { 719 for source_edit in fixes_from_diagnostics {
715 let title = source_edit.label.clone(); 720 let title = source_edit.label.clone();
716 let edit = to_proto::source_change(&world, source_edit)?; 721 let edit = to_proto::snippet_workspace_edit(&world, source_edit)?;
717 722 let action = lsp_ext::CodeAction { title, kind: None, edit: Some(edit), command: None };
718 let command = Command { 723 res.push(action);
719 title,
720 command: "rust-analyzer.applySourceChange".to_string(),
721 arguments: Some(vec![to_value(edit).unwrap()]),
722 };
723 let action = CodeAction {
724 title: command.title.clone(),
725 kind: None,
726 diagnostics: None,
727 edit: None,
728 command: Some(command),
729 is_preferred: None,
730 };
731 res.push(action.into());
732 } 724 }
733 725
734 for fix in world.check_fixes.get(&file_id).into_iter().flatten() { 726 for fix in world.check_fixes.get(&file_id).into_iter().flatten() {
@@ -740,14 +732,21 @@ pub fn handle_code_action(
740 } 732 }
741 733
742 let mut grouped_assists: FxHashMap<String, (usize, Vec<Assist>)> = FxHashMap::default(); 734 let mut grouped_assists: FxHashMap<String, (usize, Vec<Assist>)> = FxHashMap::default();
743 for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() { 735 for assist in
736 world.analysis().assists(&world.config.assist, FileRange { file_id, range })?.into_iter()
737 {
744 match &assist.group_label { 738 match &assist.group_label {
745 Some(label) => grouped_assists 739 Some(label) => grouped_assists
746 .entry(label.to_owned()) 740 .entry(label.to_owned())
747 .or_insert_with(|| { 741 .or_insert_with(|| {
748 let idx = res.len(); 742 let idx = res.len();
749 let dummy = Command::new(String::new(), String::new(), None); 743 let dummy = lsp_ext::CodeAction {
750 res.push(dummy.into()); 744 title: String::new(),
745 kind: None,
746 command: None,
747 edit: None,
748 };
749 res.push(dummy);
751 (idx, Vec::new()) 750 (idx, Vec::new())
752 }) 751 })
753 .1 752 .1
@@ -775,35 +774,10 @@ pub fn handle_code_action(
775 command: "rust-analyzer.selectAndApplySourceChange".to_string(), 774 command: "rust-analyzer.selectAndApplySourceChange".to_string(),
776 arguments: Some(vec![serde_json::Value::Array(arguments)]), 775 arguments: Some(vec![serde_json::Value::Array(arguments)]),
777 }); 776 });
778 res[idx] = CodeAction { 777 res[idx] = lsp_ext::CodeAction { title, kind: None, edit: None, command };
779 title,
780 kind: None,
781 diagnostics: None,
782 edit: None,
783 command,
784 is_preferred: None,
785 }
786 .into();
787 } 778 }
788 } 779 }
789 780
790 // If the client only supports commands then filter the list
791 // and remove and actions that depend on edits.
792 if !world.config.client_caps.code_action_literals {
793 // FIXME: use drain_filter once it hits stable.
794 res = res
795 .into_iter()
796 .filter_map(|it| match it {
797 cmd @ lsp_types::CodeActionOrCommand::Command(_) => Some(cmd),
798 lsp_types::CodeActionOrCommand::CodeAction(action) => match action.command {
799 Some(cmd) if action.edit.is_none() => {
800 Some(lsp_types::CodeActionOrCommand::Command(cmd))
801 }
802 _ => None,
803 },
804 })
805 .collect();
806 }
807 Ok(Some(res)) 781 Ok(Some(res))
808} 782}
809 783
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index a8e2e535f..2b1a3378f 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -112,6 +112,22 @@ pub(crate) fn text_edit(
112 lsp_types::TextEdit { range, new_text } 112 lsp_types::TextEdit { range, new_text }
113} 113}
114 114
115pub(crate) fn snippet_text_edit(
116 line_index: &LineIndex,
117 line_endings: LineEndings,
118 is_snippet: bool,
119 indel: Indel,
120) -> lsp_ext::SnippetTextEdit {
121 let text_edit = text_edit(line_index, line_endings, indel);
122 let insert_text_format =
123 if is_snippet { Some(lsp_types::InsertTextFormat::Snippet) } else { None };
124 lsp_ext::SnippetTextEdit {
125 range: text_edit.range,
126 new_text: text_edit.new_text,
127 insert_text_format,
128 }
129}
130
115pub(crate) fn text_edit_vec( 131pub(crate) fn text_edit_vec(
116 line_index: &LineIndex, 132 line_index: &LineIndex,
117 line_endings: LineEndings, 133 line_endings: LineEndings,
@@ -441,10 +457,11 @@ pub(crate) fn goto_definition_response(
441 } 457 }
442} 458}
443 459
444pub(crate) fn text_document_edit( 460pub(crate) fn snippet_text_document_edit(
445 world: &WorldSnapshot, 461 world: &WorldSnapshot,
462 is_snippet: bool,
446 source_file_edit: SourceFileEdit, 463 source_file_edit: SourceFileEdit,
447) -> Result<lsp_types::TextDocumentEdit> { 464) -> Result<lsp_ext::SnippetTextDocumentEdit> {
448 let text_document = versioned_text_document_identifier(world, source_file_edit.file_id, None)?; 465 let text_document = versioned_text_document_identifier(world, source_file_edit.file_id, None)?;
449 let line_index = world.analysis().file_line_index(source_file_edit.file_id)?; 466 let line_index = world.analysis().file_line_index(source_file_edit.file_id)?;
450 let line_endings = world.file_line_endings(source_file_edit.file_id); 467 let line_endings = world.file_line_endings(source_file_edit.file_id);
@@ -452,9 +469,9 @@ pub(crate) fn text_document_edit(
452 .edit 469 .edit
453 .as_indels() 470 .as_indels()
454 .iter() 471 .iter()
455 .map(|it| text_edit(&line_index, line_endings, it.clone())) 472 .map(|it| snippet_text_edit(&line_index, line_endings, is_snippet, it.clone()))
456 .collect(); 473 .collect();
457 Ok(lsp_types::TextDocumentEdit { text_document, edits }) 474 Ok(lsp_ext::SnippetTextDocumentEdit { text_document, edits })
458} 475}
459 476
460pub(crate) fn resource_op( 477pub(crate) fn resource_op(
@@ -500,20 +517,70 @@ pub(crate) fn source_change(
500 }) 517 })
501 } 518 }
502 }; 519 };
503 let mut document_changes: Vec<lsp_types::DocumentChangeOperation> = Vec::new(); 520 let label = source_change.label.clone();
521 let workspace_edit = self::snippet_workspace_edit(world, source_change)?;
522 Ok(lsp_ext::SourceChange { label, workspace_edit, cursor_position })
523}
524
525pub(crate) fn snippet_workspace_edit(
526 world: &WorldSnapshot,
527 source_change: SourceChange,
528) -> Result<lsp_ext::SnippetWorkspaceEdit> {
529 let mut document_changes: Vec<lsp_ext::SnippetDocumentChangeOperation> = Vec::new();
504 for op in source_change.file_system_edits { 530 for op in source_change.file_system_edits {
505 let op = resource_op(&world, op)?; 531 let op = resource_op(&world, op)?;
506 document_changes.push(lsp_types::DocumentChangeOperation::Op(op)); 532 document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Op(op));
507 } 533 }
508 for edit in source_change.source_file_edits { 534 for edit in source_change.source_file_edits {
509 let edit = text_document_edit(&world, edit)?; 535 let edit = snippet_text_document_edit(&world, source_change.is_snippet, edit)?;
510 document_changes.push(lsp_types::DocumentChangeOperation::Edit(edit)); 536 document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit));
537 }
538 let workspace_edit =
539 lsp_ext::SnippetWorkspaceEdit { changes: None, document_changes: Some(document_changes) };
540 Ok(workspace_edit)
541}
542
543pub(crate) fn workspace_edit(
544 world: &WorldSnapshot,
545 source_change: SourceChange,
546) -> Result<lsp_types::WorkspaceEdit> {
547 assert!(!source_change.is_snippet);
548 snippet_workspace_edit(world, source_change).map(|it| it.into())
549}
550
551impl From<lsp_ext::SnippetWorkspaceEdit> for lsp_types::WorkspaceEdit {
552 fn from(snippet_workspace_edit: lsp_ext::SnippetWorkspaceEdit) -> lsp_types::WorkspaceEdit {
553 lsp_types::WorkspaceEdit {
554 changes: None,
555 document_changes: snippet_workspace_edit.document_changes.map(|changes| {
556 lsp_types::DocumentChanges::Operations(
557 changes
558 .into_iter()
559 .map(|change| match change {
560 lsp_ext::SnippetDocumentChangeOperation::Op(op) => {
561 lsp_types::DocumentChangeOperation::Op(op)
562 }
563 lsp_ext::SnippetDocumentChangeOperation::Edit(edit) => {
564 lsp_types::DocumentChangeOperation::Edit(
565 lsp_types::TextDocumentEdit {
566 text_document: edit.text_document,
567 edits: edit
568 .edits
569 .into_iter()
570 .map(|edit| lsp_types::TextEdit {
571 range: edit.range,
572 new_text: edit.new_text,
573 })
574 .collect(),
575 },
576 )
577 }
578 })
579 .collect(),
580 )
581 }),
582 }
511 } 583 }
512 let workspace_edit = lsp_types::WorkspaceEdit {
513 changes: None,
514 document_changes: Some(lsp_types::DocumentChanges::Operations(document_changes)),
515 };
516 Ok(lsp_ext::SourceChange { label: source_change.label, workspace_edit, cursor_position })
517} 584}
518 585
519pub fn call_hierarchy_item( 586pub fn call_hierarchy_item(
@@ -571,22 +638,25 @@ fn main() <fold>{
571 } 638 }
572} 639}
573 640
574pub(crate) fn code_action(world: &WorldSnapshot, assist: Assist) -> Result<lsp_types::CodeAction> { 641pub(crate) fn code_action(world: &WorldSnapshot, assist: Assist) -> Result<lsp_ext::CodeAction> {
575 let source_change = source_change(&world, assist.source_change)?; 642 let res = if assist.source_change.is_snippet {
576 let arg = serde_json::to_value(source_change)?; 643 lsp_ext::CodeAction {
577 let title = assist.label; 644 title: assist.label,
578 let command = lsp_types::Command { 645 kind: Some(String::new()),
579 title: title.clone(), 646 edit: Some(snippet_workspace_edit(world, assist.source_change)?),
580 command: "rust-analyzer.applySourceChange".to_string(), 647 command: None,
581 arguments: Some(vec![arg]), 648 }
582 }; 649 } else {
650 let source_change = source_change(&world, assist.source_change)?;
651 let arg = serde_json::to_value(source_change)?;
652 let title = assist.label;
653 let command = lsp_types::Command {
654 title: title.clone(),
655 command: "rust-analyzer.applySourceChange".to_string(),
656 arguments: Some(vec![arg]),
657 };
583 658
584 Ok(lsp_types::CodeAction { 659 lsp_ext::CodeAction { title, kind: Some(String::new()), edit: None, command: Some(command) }
585 title, 660 };
586 kind: Some(String::new()), 661 Ok(res)
587 diagnostics: None,
588 edit: None,
589 command: Some(command),
590 is_preferred: None,
591 })
592} 662}
diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs
index 5011cc273..74676b3ee 100644
--- a/crates/rust-analyzer/tests/heavy_tests/main.rs
+++ b/crates/rust-analyzer/tests/heavy_tests/main.rs
@@ -333,29 +333,17 @@ fn main() {}
333 partial_result_params: PartialResultParams::default(), 333 partial_result_params: PartialResultParams::default(),
334 work_done_progress_params: WorkDoneProgressParams::default(), 334 work_done_progress_params: WorkDoneProgressParams::default(),
335 }, 335 },
336 json!([ 336 json!([{
337 { 337 "edit": {
338 "command": { 338 "documentChanges": [
339 "arguments": [
340 { 339 {
341 "cursorPosition": null, 340 "kind": "create",
342 "label": "Create module", 341 "uri": "file:///[..]/src/bar.rs"
343 "workspaceEdit": {
344 "documentChanges": [
345 {
346 "kind": "create",
347 "uri": "file:///[..]/src/bar.rs"
348 }
349 ]
350 }
351 } 342 }
352 ], 343 ]
353 "command": "rust-analyzer.applySourceChange",
354 "title": "Create module"
355 }, 344 },
356 "title": "Create module" 345 "title": "Create module"
357 } 346 }]),
358 ]),
359 ); 347 );
360 348
361 server.request::<CodeActionRequest>( 349 server.request::<CodeActionRequest>(
@@ -416,29 +404,17 @@ fn main() {{}}
416 partial_result_params: PartialResultParams::default(), 404 partial_result_params: PartialResultParams::default(),
417 work_done_progress_params: WorkDoneProgressParams::default(), 405 work_done_progress_params: WorkDoneProgressParams::default(),
418 }, 406 },
419 json!([ 407 json!([{
420 { 408 "edit": {
421 "command": { 409 "documentChanges": [
422 "arguments": [
423 { 410 {
424 "cursorPosition": null, 411 "kind": "create",
425 "label": "Create module", 412 "uri": "file://[..]/src/bar.rs"
426 "workspaceEdit": {
427 "documentChanges": [
428 {
429 "kind": "create",
430 "uri": "file:///[..]/src/bar.rs"
431 }
432 ]
433 }
434 } 413 }
435 ], 414 ]
436 "command": "rust-analyzer.applySourceChange",
437 "title": "Create module"
438 }, 415 },
439 "title": "Create module" 416 "title": "Create module"
440 } 417 }]),
441 ]),
442 ); 418 );
443 419
444 server.request::<CodeActionRequest>( 420 server.request::<CodeActionRequest>(
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
new file mode 100644
index 000000000..d2ec6c021
--- /dev/null
+++ b/docs/dev/lsp-extensions.md
@@ -0,0 +1,34 @@
1# LSP Extensions
2
3This document describes LSP extensions used by rust-analyzer.
4It's a best effort document, when in doubt, consult the source (and send a PR with clarification ;-) ).
5We aim to upstream all non Rust-specific extensions to the protocol, but this is not a top priority.
6All capabilities are enabled via `experimental` field of `ClientCapabilities`.
7
8## `SnippetTextEdit`
9
10**Capability**
11
12```typescript
13{
14 "snippetTextEdit": boolean
15}
16```
17
18If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s:
19
20```typescript
21interface SnippetTextEdit extends TextEdit {
22 insertTextFormat?: InsertTextFormat;
23}
24```
25
26```typescript
27export interface TextDocumentEdit {
28 textDocument: VersionedTextDocumentIdentifier;
29 edits: (TextEdit | SnippetTextEdit)[];
30}
31```
32
33When applying such code action, the editor should insert snippet, with tab stops and placeholder.
34At the moment, rust-analyzer guarantees that only a single edit will have `InsertTextFormat.Snippet`.
diff --git a/docs/user/assists.md b/docs/user/assists.md
index 692fd4f52..41c5df528 100644
--- a/docs/user/assists.md
+++ b/docs/user/assists.md
@@ -17,7 +17,7 @@ struct S;
17struct S; 17struct S;
18 18
19impl Debug for S { 19impl Debug for S {
20 20 $0
21} 21}
22``` 22```
23 23
@@ -33,7 +33,7 @@ struct Point {
33} 33}
34 34
35// AFTER 35// AFTER
36#[derive()] 36#[derive($0)]
37struct Point { 37struct Point {
38 x: u32, 38 x: u32,
39 y: u32, 39 y: u32,
@@ -105,16 +105,16 @@ Adds a new inherent impl for a type.
105```rust 105```rust
106// BEFORE 106// BEFORE
107struct Ctx<T: Clone> { 107struct Ctx<T: Clone> {
108 data: T,┃ 108 data: T,┃
109} 109}
110 110
111// AFTER 111// AFTER
112struct Ctx<T: Clone> { 112struct Ctx<T: Clone> {
113 data: T, 113 data: T,
114} 114}
115 115
116impl<T: Clone> Ctx<T> { 116impl<T: Clone> Ctx<T> {
117 117 $0
118} 118}
119``` 119```
120 120
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index cffdcf11a..fac1a0be3 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -31,24 +31,79 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
31 const res = await next(document, token); 31 const res = await next(document, token);
32 if (res === undefined) throw new Error('busy'); 32 if (res === undefined) throw new Error('busy');
33 return res; 33 return res;
34 },
35 async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) {
36 const params: lc.CodeActionParams = {
37 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
38 range: client.code2ProtocolConverter.asRange(range),
39 context: client.code2ProtocolConverter.asCodeActionContext(context)
40 };
41 return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => {
42 if (values === null) return undefined;
43 const result: (vscode.CodeAction | vscode.Command)[] = [];
44 for (const item of values) {
45 if (lc.CodeAction.is(item)) {
46 const action = client.protocol2CodeConverter.asCodeAction(item);
47 if (isSnippetEdit(item)) {
48 action.command = {
49 command: "rust-analyzer.applySnippetWorkspaceEdit",
50 title: "",
51 arguments: [action.edit],
52 };
53 action.edit = undefined;
54 }
55 result.push(action);
56 } else {
57 const command = client.protocol2CodeConverter.asCommand(item);
58 result.push(command);
59 }
60 }
61 return result;
62 },
63 (_error) => undefined
64 );
34 } 65 }
66
35 } as any 67 } as any
36 }; 68 };
37 69
38 const res = new lc.LanguageClient( 70 const client = new lc.LanguageClient(
39 'rust-analyzer', 71 'rust-analyzer',
40 'Rust Analyzer Language Server', 72 'Rust Analyzer Language Server',
41 serverOptions, 73 serverOptions,
42 clientOptions, 74 clientOptions,
43 ); 75 );
44 76
45 // To turn on all proposed features use: res.registerProposedFeatures(); 77 // To turn on all proposed features use: client.registerProposedFeatures();
46 // Here we want to enable CallHierarchyFeature and SemanticTokensFeature 78 // Here we want to enable CallHierarchyFeature and SemanticTokensFeature
47 // since they are available on stable. 79 // since they are available on stable.
48 // Note that while these features are stable in vscode their LSP protocol 80 // Note that while these features are stable in vscode their LSP protocol
49 // implementations are still in the "proposed" category for 3.16. 81 // implementations are still in the "proposed" category for 3.16.
50 res.registerFeature(new CallHierarchyFeature(res)); 82 client.registerFeature(new CallHierarchyFeature(client));
51 res.registerFeature(new SemanticTokensFeature(res)); 83 client.registerFeature(new SemanticTokensFeature(client));
84 client.registerFeature(new SnippetTextEditFeature());
85
86 return client;
87}
52 88
53 return res; 89class SnippetTextEditFeature implements lc.StaticFeature {
90 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
91 const caps: any = capabilities.experimental ?? {};
92 caps.snippetTextEdit = true;
93 capabilities.experimental = caps;
94 }
95 initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
96 }
97}
98
99function isSnippetEdit(action: lc.CodeAction): boolean {
100 const documentChanges = action.edit?.documentChanges ?? [];
101 for (const edit of documentChanges) {
102 if (lc.TextDocumentEdit.is(edit)) {
103 if (edit.edits.some((indel) => (indel as any).insertTextFormat === lc.InsertTextFormat.Snippet)) {
104 return true;
105 }
106 }
107 }
108 return false;
54} 109}
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
index bdb7fc3b0..770d11bd3 100644
--- a/editors/code/src/commands/index.ts
+++ b/editors/code/src/commands/index.ts
@@ -4,6 +4,7 @@ import * as ra from '../rust-analyzer-api';
4 4
5import { Ctx, Cmd } from '../ctx'; 5import { Ctx, Cmd } from '../ctx';
6import * as sourceChange from '../source_change'; 6import * as sourceChange from '../source_change';
7import { assert } from '../util';
7 8
8export * from './analyzer_status'; 9export * from './analyzer_status';
9export * from './matching_brace'; 10export * from './matching_brace';
@@ -51,3 +52,36 @@ export function selectAndApplySourceChange(ctx: Ctx): Cmd {
51 } 52 }
52 }; 53 };
53} 54}
55
56export function applySnippetWorkspaceEdit(_ctx: Ctx): Cmd {
57 return async (edit: vscode.WorkspaceEdit) => {
58 assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`);
59 const [uri, edits] = edit.entries()[0];
60
61 const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
62 if (!editor) return;
63
64 let editWithSnippet: vscode.TextEdit | undefined = undefined;
65 let lineDelta = 0;
66 await editor.edit((builder) => {
67 for (const indel of edits) {
68 if (indel.newText.indexOf('$0') !== -1) {
69 editWithSnippet = indel;
70 } else {
71 if (!editWithSnippet) {
72 lineDelta = (indel.newText.match(/\n/g) || []).length - (indel.range.end.line - indel.range.start.line);
73 }
74 builder.replace(indel.range, indel.newText);
75 }
76 }
77 });
78 if (editWithSnippet) {
79 const snip = editWithSnippet as vscode.TextEdit;
80 const range = snip.range.with(
81 snip.range.start.with(snip.range.start.line + lineDelta),
82 snip.range.end.with(snip.range.end.line + lineDelta),
83 );
84 await editor.insertSnippet(new vscode.SnippetString(snip.newText), range);
85 }
86 };
87}
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index c015460b8..ac3bb365e 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -91,6 +91,7 @@ export async function activate(context: vscode.ExtensionContext) {
91 ctx.registerCommand('debugSingle', commands.debugSingle); 91 ctx.registerCommand('debugSingle', commands.debugSingle);
92 ctx.registerCommand('showReferences', commands.showReferences); 92 ctx.registerCommand('showReferences', commands.showReferences);
93 ctx.registerCommand('applySourceChange', commands.applySourceChange); 93 ctx.registerCommand('applySourceChange', commands.applySourceChange);
94 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEdit);
94 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); 95 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
95 96
96 ctx.pushCleanup(activateTaskProvider(workspaceFolder)); 97 ctx.pushCleanup(activateTaskProvider(workspaceFolder));