aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-03-10 14:34:59 +0000
committerGitHub <[email protected]>2021-03-10 14:34:59 +0000
commit9dc13408f954719ae0b06d778ce6338bfcd5a63d (patch)
tree5c1e05f3b1afa70492d37384fd94b989d49d80c5
parenta9b1e5cde1cc6e470be60cf2c230b99a62c2d2c3 (diff)
parent09307be75b452bd7a10b3855b5d43083cca140a1 (diff)
Merge #7874
7874: add apply ssr assist r=JoshMcguigan a=JoshMcguigan This PR adds an `Apply SSR` assist which was briefly mentioned in #3186. It allows writing an ssr rule as a comment, and then applying that SSR via an assist. This workflow is much nicer than the default available via `coc-rust-analyzer` when iterating to find the proper replacement. As currently implemented, this requires the ssr rule is written as a single line in the comment, and it doesn't require any kind of prefix. Anything which properly parses as a ssr rule will enable the assist. The benefit of requiring the ssr rule be on a single line is it allows for a workflow where the user has several rules written one after the other, possibly to be triggered in order, without having to try to parse multiple lines of text and determine where one rule ends and the next begins. The benefit of not requiring a prefix is less typing :laughing: - plus, I think the chance of something accidentally parsing as an ssr rule is minimal. I think a reasonable extension of this would be to allow either any ssr rule that fits on a single line, or any comment block which in its entirety makes up a single ssr rule (parsing a comment block containing multiple ssr rules and running them all would break the use case that currently works where a user writes multiple ssr rules then runs them each one by one in arbitrary order). I've marked this as a draft because for some reason I am strugging to make the unit tests pass. It does work when I compile rust-analyzer and test it in my editor though, so I'm not sure what is going on. Co-authored-by: Josh Mcguigan <[email protected]>
-rw-r--r--crates/ide/src/lib.rs8
-rw-r--r--crates/ide/src/ssr.rs259
-rw-r--r--crates/ide_ssr/src/from_comment.rs32
-rw-r--r--crates/ide_ssr/src/lib.rs2
4 files changed, 300 insertions, 1 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index f83ed65d5..d1a250d48 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -41,6 +41,7 @@ mod parent_module;
41mod references; 41mod references;
42mod fn_references; 42mod fn_references;
43mod runnables; 43mod runnables;
44mod ssr;
44mod status; 45mod status;
45mod syntax_highlighting; 46mod syntax_highlighting;
46mod syntax_tree; 47mod syntax_tree;
@@ -51,6 +52,7 @@ mod doc_links;
51use std::sync::Arc; 52use std::sync::Arc;
52 53
53use cfg::CfgOptions; 54use cfg::CfgOptions;
55
54use ide_db::base_db::{ 56use ide_db::base_db::{
55 salsa::{self, ParallelDatabase}, 57 salsa::{self, ParallelDatabase},
56 CheckCanceled, Env, FileLoader, FileSet, SourceDatabase, VfsPath, 58 CheckCanceled, Env, FileLoader, FileSet, SourceDatabase, VfsPath,
@@ -502,7 +504,11 @@ impl Analysis {
502 resolve: bool, 504 resolve: bool,
503 frange: FileRange, 505 frange: FileRange,
504 ) -> Cancelable<Vec<Assist>> { 506 ) -> Cancelable<Vec<Assist>> {
505 self.with_db(|db| Assist::get(db, config, resolve, frange)) 507 self.with_db(|db| {
508 let mut acc = Assist::get(db, config, resolve, frange);
509 ssr::add_ssr_assist(db, &mut acc, resolve, frange);
510 acc
511 })
506 } 512 }
507 513
508 /// Computes the set of diagnostics for the given file. 514 /// Computes the set of diagnostics for the given file.
diff --git a/crates/ide/src/ssr.rs b/crates/ide/src/ssr.rs
new file mode 100644
index 000000000..f3638d928
--- /dev/null
+++ b/crates/ide/src/ssr.rs
@@ -0,0 +1,259 @@
1//! This module provides an SSR assist. It is not desirable to include this
2//! assist in ide_assists because that would require the ide_assists crate
3//! depend on the ide_ssr crate.
4
5use ide_assists::{Assist, AssistId, AssistKind, GroupLabel};
6use ide_db::{base_db::FileRange, label::Label, source_change::SourceChange, RootDatabase};
7
8pub(crate) fn add_ssr_assist(
9 db: &RootDatabase,
10 base: &mut Vec<Assist>,
11 resolve: bool,
12 frange: FileRange,
13) -> Option<()> {
14 let (match_finder, comment_range) = ide_ssr::ssr_from_comment(db, frange)?;
15
16 let (source_change_for_file, source_change_for_workspace) = if resolve {
17 let edits = match_finder.edits();
18
19 let source_change_for_file = {
20 let text_edit_for_file = edits.get(&frange.file_id).cloned().unwrap_or_default();
21 SourceChange::from_text_edit(frange.file_id, text_edit_for_file)
22 };
23
24 let source_change_for_workspace = SourceChange::from(match_finder.edits());
25
26 (Some(source_change_for_file), Some(source_change_for_workspace))
27 } else {
28 (None, None)
29 };
30
31 let assists = vec![
32 ("Apply SSR in file", source_change_for_file),
33 ("Apply SSR in workspace", source_change_for_workspace),
34 ];
35
36 for (label, source_change) in assists.into_iter() {
37 let assist = Assist {
38 id: AssistId("ssr", AssistKind::RefactorRewrite),
39 label: Label::new(label),
40 group: Some(GroupLabel("Apply SSR".into())),
41 target: comment_range,
42 source_change,
43 };
44
45 base.push(assist);
46 }
47 Some(())
48}
49
50#[cfg(test)]
51mod tests {
52 use std::sync::Arc;
53
54 use expect_test::expect;
55 use ide_assists::Assist;
56 use ide_db::{
57 base_db::{fixture::WithFixture, salsa::Durability, FileRange},
58 symbol_index::SymbolsDatabase,
59 RootDatabase,
60 };
61 use rustc_hash::FxHashSet;
62
63 use super::add_ssr_assist;
64
65 fn get_assists(ra_fixture: &str, resolve: bool) -> Vec<Assist> {
66 let (mut db, file_id, range_or_offset) = RootDatabase::with_range_or_offset(ra_fixture);
67 let mut local_roots = FxHashSet::default();
68 local_roots.insert(ide_db::base_db::fixture::WORKSPACE);
69 db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH);
70
71 let mut assists = vec![];
72
73 add_ssr_assist(
74 &db,
75 &mut assists,
76 resolve,
77 FileRange { file_id, range: range_or_offset.into() },
78 );
79
80 assists
81 }
82
83 #[test]
84 fn not_applicable_comment_not_ssr() {
85 let ra_fixture = r#"
86 //- /lib.rs
87
88 // This is foo $0
89 fn foo() {}
90 "#;
91 let resolve = true;
92
93 let assists = get_assists(ra_fixture, resolve);
94
95 assert_eq!(0, assists.len());
96 }
97
98 #[test]
99 fn resolve_edits_true() {
100 let resolve = true;
101 let assists = get_assists(
102 r#"
103 //- /lib.rs
104 mod bar;
105
106 // 2 ==>> 3$0
107 fn foo() { 2 }
108
109 //- /bar.rs
110 fn bar() { 2 }
111 "#,
112 resolve,
113 );
114
115 assert_eq!(2, assists.len());
116 let mut assists = assists.into_iter();
117
118 let apply_in_file_assist = assists.next().unwrap();
119 expect![[r#"
120 Assist {
121 id: AssistId(
122 "ssr",
123 RefactorRewrite,
124 ),
125 label: "Apply SSR in file",
126 group: Some(
127 GroupLabel(
128 "Apply SSR",
129 ),
130 ),
131 target: 10..21,
132 source_change: Some(
133 SourceChange {
134 source_file_edits: {
135 FileId(
136 0,
137 ): TextEdit {
138 indels: [
139 Indel {
140 insert: "3",
141 delete: 33..34,
142 },
143 ],
144 },
145 },
146 file_system_edits: [],
147 is_snippet: false,
148 },
149 ),
150 }
151 "#]]
152 .assert_debug_eq(&apply_in_file_assist);
153
154 let apply_in_workspace_assist = assists.next().unwrap();
155 expect![[r#"
156 Assist {
157 id: AssistId(
158 "ssr",
159 RefactorRewrite,
160 ),
161 label: "Apply SSR in workspace",
162 group: Some(
163 GroupLabel(
164 "Apply SSR",
165 ),
166 ),
167 target: 10..21,
168 source_change: Some(
169 SourceChange {
170 source_file_edits: {
171 FileId(
172 0,
173 ): TextEdit {
174 indels: [
175 Indel {
176 insert: "3",
177 delete: 33..34,
178 },
179 ],
180 },
181 FileId(
182 1,
183 ): TextEdit {
184 indels: [
185 Indel {
186 insert: "3",
187 delete: 11..12,
188 },
189 ],
190 },
191 },
192 file_system_edits: [],
193 is_snippet: false,
194 },
195 ),
196 }
197 "#]]
198 .assert_debug_eq(&apply_in_workspace_assist);
199 }
200
201 #[test]
202 fn resolve_edits_false() {
203 let resolve = false;
204 let assists = get_assists(
205 r#"
206 //- /lib.rs
207 mod bar;
208
209 // 2 ==>> 3$0
210 fn foo() { 2 }
211
212 //- /bar.rs
213 fn bar() { 2 }
214 "#,
215 resolve,
216 );
217
218 assert_eq!(2, assists.len());
219 let mut assists = assists.into_iter();
220
221 let apply_in_file_assist = assists.next().unwrap();
222 expect![[r#"
223 Assist {
224 id: AssistId(
225 "ssr",
226 RefactorRewrite,
227 ),
228 label: "Apply SSR in file",
229 group: Some(
230 GroupLabel(
231 "Apply SSR",
232 ),
233 ),
234 target: 10..21,
235 source_change: None,
236 }
237 "#]]
238 .assert_debug_eq(&apply_in_file_assist);
239
240 let apply_in_workspace_assist = assists.next().unwrap();
241 expect![[r#"
242 Assist {
243 id: AssistId(
244 "ssr",
245 RefactorRewrite,
246 ),
247 label: "Apply SSR in workspace",
248 group: Some(
249 GroupLabel(
250 "Apply SSR",
251 ),
252 ),
253 target: 10..21,
254 source_change: None,
255 }
256 "#]]
257 .assert_debug_eq(&apply_in_workspace_assist);
258 }
259}
diff --git a/crates/ide_ssr/src/from_comment.rs b/crates/ide_ssr/src/from_comment.rs
new file mode 100644
index 000000000..f1b312284
--- /dev/null
+++ b/crates/ide_ssr/src/from_comment.rs
@@ -0,0 +1,32 @@
1//! This module allows building an SSR MatchFinder by parsing the SSR rule
2//! from a comment.
3
4use ide_db::{
5 base_db::{FilePosition, FileRange, SourceDatabase},
6 RootDatabase,
7};
8use syntax::{
9 ast::{self, AstNode, AstToken},
10 TextRange,
11};
12
13use crate::MatchFinder;
14
15/// Attempts to build an SSR MatchFinder from a comment at the given file
16/// range. If successful, returns the MatchFinder and a TextRange covering
17/// comment.
18pub fn ssr_from_comment(db: &RootDatabase, frange: FileRange) -> Option<(MatchFinder, TextRange)> {
19 let comment = {
20 let file = db.parse(frange.file_id);
21 file.tree().syntax().token_at_offset(frange.range.start()).find_map(ast::Comment::cast)
22 }?;
23 let comment_text_without_prefix = comment.text().strip_prefix(comment.prefix()).unwrap();
24 let ssr_rule = comment_text_without_prefix.parse().ok()?;
25
26 let lookup_context = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
27
28 let mut match_finder = MatchFinder::in_context(db, lookup_context, vec![]);
29 match_finder.add_rule(ssr_rule).ok()?;
30
31 Some((match_finder, comment.syntax().text_range()))
32}
diff --git a/crates/ide_ssr/src/lib.rs b/crates/ide_ssr/src/lib.rs
index a97fc8bca..e72c611a3 100644
--- a/crates/ide_ssr/src/lib.rs
+++ b/crates/ide_ssr/src/lib.rs
@@ -58,6 +58,7 @@
58// | VS Code | **Rust Analyzer: Structural Search Replace** 58// | VS Code | **Rust Analyzer: Structural Search Replace**
59// |=== 59// |===
60 60
61mod from_comment;
61mod matching; 62mod matching;
62mod nester; 63mod nester;
63mod parsing; 64mod parsing;
@@ -71,6 +72,7 @@ mod tests;
71 72
72use crate::errors::bail; 73use crate::errors::bail;
73pub use crate::errors::SsrError; 74pub use crate::errors::SsrError;
75pub use crate::from_comment::ssr_from_comment;
74pub use crate::matching::Match; 76pub use crate::matching::Match;
75use crate::matching::MatchFailureReason; 77use crate::matching::MatchFailureReason;
76use hir::Semantics; 78use hir::Semantics;