aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ra_ide/src/lib.rs3
-rw-r--r--crates/ra_ide/src/ssr.rs6
-rw-r--r--crates/ra_ssr/src/lib.rs30
-rw-r--r--crates/ra_ssr/src/matching.rs4
-rw-r--r--crates/ra_ssr/src/tests.rs42
-rw-r--r--crates/rust-analyzer/src/cli/ssr.rs4
-rw-r--r--crates/rust-analyzer/src/handlers.rs3
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs5
-rw-r--r--docs/dev/lsp-extensions.md7
-rw-r--r--editors/code/src/commands.ts14
-rw-r--r--editors/code/src/lsp_ext.ts2
11 files changed, 88 insertions, 32 deletions
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index dc9192d42..7356e947b 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -505,9 +505,10 @@ impl Analysis {
505 &self, 505 &self,
506 query: &str, 506 query: &str,
507 parse_only: bool, 507 parse_only: bool,
508 position: FilePosition,
508 ) -> Cancelable<Result<SourceChange, SsrError>> { 509 ) -> Cancelable<Result<SourceChange, SsrError>> {
509 self.with_db(|db| { 510 self.with_db(|db| {
510 let edits = ssr::parse_search_replace(query, parse_only, db)?; 511 let edits = ssr::parse_search_replace(query, parse_only, db, position)?;
511 Ok(SourceChange::from(edits)) 512 Ok(SourceChange::from(edits))
512 }) 513 })
513 } 514 }
diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs
index ca7e0ad86..3e2705d62 100644
--- a/crates/ra_ide/src/ssr.rs
+++ b/crates/ra_ide/src/ssr.rs
@@ -1,3 +1,4 @@
1use ra_db::FilePosition;
1use ra_ide_db::RootDatabase; 2use ra_ide_db::RootDatabase;
2 3
3use crate::SourceFileEdit; 4use crate::SourceFileEdit;
@@ -42,12 +43,13 @@ pub fn parse_search_replace(
42 rule: &str, 43 rule: &str,
43 parse_only: bool, 44 parse_only: bool,
44 db: &RootDatabase, 45 db: &RootDatabase,
46 position: FilePosition,
45) -> Result<Vec<SourceFileEdit>, SsrError> { 47) -> Result<Vec<SourceFileEdit>, SsrError> {
46 let rule: SsrRule = rule.parse()?; 48 let rule: SsrRule = rule.parse()?;
49 let mut match_finder = MatchFinder::in_context(db, position);
50 match_finder.add_rule(rule);
47 if parse_only { 51 if parse_only {
48 return Ok(Vec::new()); 52 return Ok(Vec::new());
49 } 53 }
50 let mut match_finder = MatchFinder::new(db);
51 match_finder.add_rule(rule);
52 Ok(match_finder.edits()) 54 Ok(match_finder.edits())
53} 55}
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
index 6d578610b..a0a5c9762 100644
--- a/crates/ra_ssr/src/lib.rs
+++ b/crates/ra_ssr/src/lib.rs
@@ -13,11 +13,12 @@ mod errors;
13#[cfg(test)] 13#[cfg(test)]
14mod tests; 14mod tests;
15 15
16use crate::errors::bail;
16pub use crate::errors::SsrError; 17pub use crate::errors::SsrError;
17pub use crate::matching::Match; 18pub use crate::matching::Match;
18use crate::matching::MatchFailureReason; 19use crate::matching::MatchFailureReason;
19use hir::Semantics; 20use hir::Semantics;
20use ra_db::{FileId, FileRange}; 21use ra_db::{FileId, FilePosition, FileRange};
21use ra_ide_db::source_change::SourceFileEdit; 22use ra_ide_db::source_change::SourceFileEdit;
22use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; 23use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
23use rustc_hash::FxHashMap; 24use rustc_hash::FxHashMap;
@@ -51,10 +52,35 @@ pub struct MatchFinder<'db> {
51} 52}
52 53
53impl<'db> MatchFinder<'db> { 54impl<'db> MatchFinder<'db> {
54 pub fn new(db: &'db ra_ide_db::RootDatabase) -> MatchFinder<'db> { 55 /// Constructs a new instance where names will be looked up as if they appeared at
56 /// `lookup_context`.
57 pub fn in_context(
58 db: &'db ra_ide_db::RootDatabase,
59 _lookup_context: FilePosition,
60 ) -> MatchFinder<'db> {
61 // FIXME: Use lookup_context
55 MatchFinder { sema: Semantics::new(db), rules: Vec::new() } 62 MatchFinder { sema: Semantics::new(db), rules: Vec::new() }
56 } 63 }
57 64
65 /// Constructs an instance using the start of the first file in `db` as the lookup context.
66 pub fn at_first_file(db: &'db ra_ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
67 use ra_db::SourceDatabaseExt;
68 use ra_ide_db::symbol_index::SymbolsDatabase;
69 if let Some(first_file_id) = db
70 .local_roots()
71 .iter()
72 .next()
73 .and_then(|root| db.source_root(root.clone()).iter().next())
74 {
75 Ok(MatchFinder::in_context(
76 db,
77 FilePosition { file_id: first_file_id, offset: 0.into() },
78 ))
79 } else {
80 bail!("No files to search");
81 }
82 }
83
58 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take 84 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
59 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to 85 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
60 /// match to it. 86 /// match to it.
diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs
index 005569f6f..a43d57c34 100644
--- a/crates/ra_ssr/src/matching.rs
+++ b/crates/ra_ssr/src/matching.rs
@@ -576,8 +576,8 @@ mod tests {
576 let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap(); 576 let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap();
577 let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }"; 577 let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }";
578 578
579 let (db, _) = crate::tests::single_file(input); 579 let (db, position) = crate::tests::single_file(input);
580 let mut match_finder = MatchFinder::new(&db); 580 let mut match_finder = MatchFinder::in_context(&db, position);
581 match_finder.add_rule(rule); 581 match_finder.add_rule(rule);
582 let matches = match_finder.matches(); 582 let matches = match_finder.matches();
583 assert_eq!(matches.matches.len(), 1); 583 assert_eq!(matches.matches.len(), 1);
diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs
index 523035b31..63d527894 100644
--- a/crates/ra_ssr/src/tests.rs
+++ b/crates/ra_ssr/src/tests.rs
@@ -1,6 +1,6 @@
1use crate::{MatchFinder, SsrRule}; 1use crate::{MatchFinder, SsrRule};
2use expect::{expect, Expect}; 2use expect::{expect, Expect};
3use ra_db::{salsa::Durability, FileId, SourceDatabaseExt}; 3use ra_db::{salsa::Durability, FileId, FilePosition, SourceDatabaseExt};
4use rustc_hash::FxHashSet; 4use rustc_hash::FxHashSet;
5use std::sync::Arc; 5use std::sync::Arc;
6use test_utils::mark; 6use test_utils::mark;
@@ -59,15 +59,21 @@ fn parser_undefined_placeholder_in_replacement() {
59 ); 59 );
60} 60}
61 61
62pub(crate) fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FileId) { 62/// `code` may optionally contain a cursor marker `<|>`. If it doesn't, then the position will be
63/// the start of the file.
64pub(crate) fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FilePosition) {
63 use ra_db::fixture::WithFixture; 65 use ra_db::fixture::WithFixture;
64 use ra_ide_db::symbol_index::SymbolsDatabase; 66 use ra_ide_db::symbol_index::SymbolsDatabase;
65 let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(code); 67 let (mut db, position) = if code.contains(test_utils::CURSOR_MARKER) {
66 let mut db = db; 68 ra_ide_db::RootDatabase::with_position(code)
69 } else {
70 let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(code);
71 (db, FilePosition { file_id, offset: 0.into() })
72 };
67 let mut local_roots = FxHashSet::default(); 73 let mut local_roots = FxHashSet::default();
68 local_roots.insert(ra_db::fixture::WORKSPACE); 74 local_roots.insert(ra_db::fixture::WORKSPACE);
69 db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH); 75 db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH);
70 (db, file_id) 76 (db, position)
71} 77}
72 78
73fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) { 79fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) {
@@ -75,8 +81,8 @@ fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) {
75} 81}
76 82
77fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) { 83fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
78 let (db, file_id) = single_file(input); 84 let (db, position) = single_file(input);
79 let mut match_finder = MatchFinder::new(&db); 85 let mut match_finder = MatchFinder::in_context(&db, position);
80 for rule in rules { 86 for rule in rules {
81 let rule: SsrRule = rule.parse().unwrap(); 87 let rule: SsrRule = rule.parse().unwrap();
82 match_finder.add_rule(rule); 88 match_finder.add_rule(rule);
@@ -85,10 +91,10 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
85 if edits.is_empty() { 91 if edits.is_empty() {
86 panic!("No edits were made"); 92 panic!("No edits were made");
87 } 93 }
88 assert_eq!(edits[0].file_id, file_id); 94 assert_eq!(edits[0].file_id, position.file_id);
89 // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters 95 // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters
90 // stuff. 96 // stuff.
91 let mut actual = db.file_text(file_id).to_string(); 97 let mut actual = db.file_text(position.file_id).to_string();
92 edits[0].edit.apply(&mut actual); 98 edits[0].edit.apply(&mut actual);
93 expected.assert_eq(&actual); 99 expected.assert_eq(&actual);
94} 100}
@@ -106,34 +112,34 @@ fn print_match_debug_info(match_finder: &MatchFinder, file_id: FileId, snippet:
106} 112}
107 113
108fn assert_matches(pattern: &str, code: &str, expected: &[&str]) { 114fn assert_matches(pattern: &str, code: &str, expected: &[&str]) {
109 let (db, file_id) = single_file(code); 115 let (db, position) = single_file(code);
110 let mut match_finder = MatchFinder::new(&db); 116 let mut match_finder = MatchFinder::in_context(&db, position);
111 match_finder.add_search_pattern(pattern.parse().unwrap()); 117 match_finder.add_search_pattern(pattern.parse().unwrap());
112 let matched_strings: Vec<String> = 118 let matched_strings: Vec<String> =
113 match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect(); 119 match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect();
114 if matched_strings != expected && !expected.is_empty() { 120 if matched_strings != expected && !expected.is_empty() {
115 print_match_debug_info(&match_finder, file_id, &expected[0]); 121 print_match_debug_info(&match_finder, position.file_id, &expected[0]);
116 } 122 }
117 assert_eq!(matched_strings, expected); 123 assert_eq!(matched_strings, expected);
118} 124}
119 125
120fn assert_no_match(pattern: &str, code: &str) { 126fn assert_no_match(pattern: &str, code: &str) {
121 let (db, file_id) = single_file(code); 127 let (db, position) = single_file(code);
122 let mut match_finder = MatchFinder::new(&db); 128 let mut match_finder = MatchFinder::in_context(&db, position);
123 match_finder.add_search_pattern(pattern.parse().unwrap()); 129 match_finder.add_search_pattern(pattern.parse().unwrap());
124 let matches = match_finder.matches().flattened().matches; 130 let matches = match_finder.matches().flattened().matches;
125 if !matches.is_empty() { 131 if !matches.is_empty() {
126 print_match_debug_info(&match_finder, file_id, &matches[0].matched_text()); 132 print_match_debug_info(&match_finder, position.file_id, &matches[0].matched_text());
127 panic!("Got {} matches when we expected none: {:#?}", matches.len(), matches); 133 panic!("Got {} matches when we expected none: {:#?}", matches.len(), matches);
128 } 134 }
129} 135}
130 136
131fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { 137fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) {
132 let (db, file_id) = single_file(code); 138 let (db, position) = single_file(code);
133 let mut match_finder = MatchFinder::new(&db); 139 let mut match_finder = MatchFinder::in_context(&db, position);
134 match_finder.add_search_pattern(pattern.parse().unwrap()); 140 match_finder.add_search_pattern(pattern.parse().unwrap());
135 let mut reasons = Vec::new(); 141 let mut reasons = Vec::new();
136 for d in match_finder.debug_where_text_equal(file_id, snippet) { 142 for d in match_finder.debug_where_text_equal(position.file_id, snippet) {
137 if let Some(reason) = d.match_failure_reason() { 143 if let Some(reason) = d.match_failure_reason() {
138 reasons.push(reason.to_owned()); 144 reasons.push(reason.to_owned());
139 } 145 }
diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs
index 014bc70a4..22f5b4be0 100644
--- a/crates/rust-analyzer/src/cli/ssr.rs
+++ b/crates/rust-analyzer/src/cli/ssr.rs
@@ -7,7 +7,7 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> {
7 use ra_db::SourceDatabaseExt; 7 use ra_db::SourceDatabaseExt;
8 let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?; 8 let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?;
9 let db = host.raw_database(); 9 let db = host.raw_database();
10 let mut match_finder = MatchFinder::new(db); 10 let mut match_finder = MatchFinder::at_first_file(db)?;
11 for rule in rules { 11 for rule in rules {
12 match_finder.add_rule(rule); 12 match_finder.add_rule(rule);
13 } 13 }
@@ -30,7 +30,7 @@ pub fn search_for_patterns(patterns: Vec<SsrPattern>, debug_snippet: Option<Stri
30 use ra_ide_db::symbol_index::SymbolsDatabase; 30 use ra_ide_db::symbol_index::SymbolsDatabase;
31 let (host, _vfs) = load_cargo(&std::env::current_dir()?, true, true)?; 31 let (host, _vfs) = load_cargo(&std::env::current_dir()?, true, true)?;
32 let db = host.raw_database(); 32 let db = host.raw_database();
33 let mut match_finder = MatchFinder::new(db); 33 let mut match_finder = MatchFinder::at_first_file(db)?;
34 for pattern in patterns { 34 for pattern in patterns {
35 match_finder.add_search_pattern(pattern); 35 match_finder.add_search_pattern(pattern);
36 } 36 }
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 8d8c9442b..cad92c444 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -1026,8 +1026,9 @@ pub(crate) fn handle_ssr(
1026 params: lsp_ext::SsrParams, 1026 params: lsp_ext::SsrParams,
1027) -> Result<lsp_types::WorkspaceEdit> { 1027) -> Result<lsp_types::WorkspaceEdit> {
1028 let _p = profile("handle_ssr"); 1028 let _p = profile("handle_ssr");
1029 let position = from_proto::file_position(&snap, params.position)?;
1029 let source_change = 1030 let source_change =
1030 snap.analysis.structural_search_replace(&params.query, params.parse_only)??; 1031 snap.analysis.structural_search_replace(&params.query, params.parse_only, position)??;
1031 to_proto::workspace_edit(&snap, source_change) 1032 to_proto::workspace_edit(&snap, source_change)
1032} 1033}
1033 1034
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 13ebb18fb..113e0e070 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -216,6 +216,11 @@ impl Request for Ssr {
216pub struct SsrParams { 216pub struct SsrParams {
217 pub query: String, 217 pub query: String,
218 pub parse_only: bool, 218 pub parse_only: bool,
219
220 /// File position where SSR was invoked. Paths in `query` will be resolved relative to this
221 /// position.
222 #[serde(flatten)]
223 pub position: lsp_types::TextDocumentPositionParams,
219} 224}
220 225
221pub enum StatusNotification {} 226pub enum StatusNotification {}
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 98d14450b..1be01fd88 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -274,6 +274,11 @@ interface SsrParams {
274 query: string, 274 query: string,
275 /// If true, only check the syntax of the query and don't compute the actual edit. 275 /// If true, only check the syntax of the query and don't compute the actual edit.
276 parseOnly: bool, 276 parseOnly: bool,
277 /// The current text document. This and `position` will be used to determine in what scope
278 /// paths in `query` should be resolved.
279 textDocument: lc.TextDocumentIdentifier;
280 /// Position where SSR was invoked.
281 position: lc.Position;
277} 282}
278``` 283```
279 284
@@ -285,7 +290,7 @@ WorkspaceEdit
285 290
286### Example 291### Example
287 292
288SSR with query `foo($a:expr, $b:expr) ==>> ($a).foo($b)` will transform, eg `foo(y + 5, z)` into `(y + 5).foo(z)`. 293SSR with query `foo($a, $b) ==>> ($a).foo($b)` will transform, eg `foo(y + 5, z)` into `(y + 5).foo(z)`.
289 294
290### Unresolved Question 295### Unresolved Question
291 296
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 1f3a7cf7e..3ae995705 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -185,15 +185,21 @@ export function parentModule(ctx: Ctx): Cmd {
185 185
186export function ssr(ctx: Ctx): Cmd { 186export function ssr(ctx: Ctx): Cmd {
187 return async () => { 187 return async () => {
188 const editor = vscode.window.activeTextEditor;
188 const client = ctx.client; 189 const client = ctx.client;
189 if (!client) return; 190 if (!editor || !client) return;
191
192 const position = editor.selection.active;
193 let textDocument = { uri: editor.document.uri.toString() };
190 194
191 const options: vscode.InputBoxOptions = { 195 const options: vscode.InputBoxOptions = {
192 value: "() ==>> ()", 196 value: "() ==>> ()",
193 prompt: "Enter request, for example 'Foo($a) ==> Foo::new($a)' ", 197 prompt: "Enter request, for example 'Foo($a) ==> Foo::new($a)' ",
194 validateInput: async (x: string) => { 198 validateInput: async (x: string) => {
195 try { 199 try {
196 await client.sendRequest(ra.ssr, { query: x, parseOnly: true }); 200 await client.sendRequest(ra.ssr, {
201 query: x, parseOnly: true, textDocument, position,
202 });
197 } catch (e) { 203 } catch (e) {
198 return e.toString(); 204 return e.toString();
199 } 205 }
@@ -208,7 +214,9 @@ export function ssr(ctx: Ctx): Cmd {
208 title: "Structured search replace in progress...", 214 title: "Structured search replace in progress...",
209 cancellable: false, 215 cancellable: false,
210 }, async (_progress, _token) => { 216 }, async (_progress, _token) => {
211 const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false }); 217 const edit = await client.sendRequest(ra.ssr, {
218 query: request, parseOnly: false, textDocument, position
219 });
212 220
213 await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit)); 221 await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
214 }); 222 });
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 5f32cb40e..149f9a0d6 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -93,6 +93,8 @@ export const inlayHints = new lc.RequestType<InlayHintsParams, InlayHint[], void
93export interface SsrParams { 93export interface SsrParams {
94 query: string; 94 query: string;
95 parseOnly: boolean; 95 parseOnly: boolean;
96 textDocument: lc.TextDocumentIdentifier;
97 position: lc.Position;
96} 98}
97export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr'); 99export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr');
98 100