Skip to content

Commit 9a9ddcc

Browse files
Merge #5564
5564: SSR: Restrict to current selection if any r=davidlattimore a=davidlattimore The selection is also used to avoid unnecessary work, but only to the file level. Further restricting unnecessary work is left for later. Co-authored-by: David Lattimore <[email protected]>
2 parents 04d2b7b + fcb6b16 commit 9a9ddcc

File tree

11 files changed

+186
-55
lines changed

11 files changed

+186
-55
lines changed

crates/ra_ide/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,9 +510,10 @@ impl Analysis {
510510
query: &str,
511511
parse_only: bool,
512512
position: FilePosition,
513+
selections: Vec<FileRange>,
513514
) -> Cancelable<Result<SourceChange, SsrError>> {
514515
self.with_db(|db| {
515-
let edits = ssr::parse_search_replace(query, parse_only, db, position)?;
516+
let edits = ssr::parse_search_replace(query, parse_only, db, position, selections)?;
516517
Ok(SourceChange::from(edits))
517518
})
518519
}

crates/ra_ide/src/ssr.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ra_db::FilePosition;
1+
use ra_db::{FilePosition, FileRange};
22
use ra_ide_db::RootDatabase;
33

44
use crate::SourceFileEdit;
@@ -24,6 +24,9 @@ use ra_ssr::{MatchFinder, SsrError, SsrRule};
2424
// Method calls should generally be written in UFCS form. e.g. `foo::Bar::baz($s, $a)` will match
2525
// `$s.baz($a)`, provided the method call `baz` resolves to the method `foo::Bar::baz`.
2626
//
27+
// The scope of the search / replace will be restricted to the current selection if any, otherwise
28+
// it will apply to the whole workspace.
29+
//
2730
// Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`.
2831
//
2932
// Supported constraints:
@@ -56,10 +59,11 @@ pub fn parse_search_replace(
5659
rule: &str,
5760
parse_only: bool,
5861
db: &RootDatabase,
59-
position: FilePosition,
62+
resolve_context: FilePosition,
63+
selections: Vec<FileRange>,
6064
) -> Result<Vec<SourceFileEdit>, SsrError> {
6165
let rule: SsrRule = rule.parse()?;
62-
let mut match_finder = MatchFinder::in_context(db, position);
66+
let mut match_finder = MatchFinder::in_context(db, resolve_context, selections);
6367
match_finder.add_rule(rule)?;
6468
if parse_only {
6569
return Ok(Vec::new());

crates/ra_ssr/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub struct MatchFinder<'db> {
5252
sema: Semantics<'db, ra_ide_db::RootDatabase>,
5353
rules: Vec<ResolvedRule>,
5454
resolution_scope: resolving::ResolutionScope<'db>,
55+
restrict_ranges: Vec<FileRange>,
5556
}
5657

5758
impl<'db> MatchFinder<'db> {
@@ -60,10 +61,17 @@ impl<'db> MatchFinder<'db> {
6061
pub fn in_context(
6162
db: &'db ra_ide_db::RootDatabase,
6263
lookup_context: FilePosition,
64+
mut restrict_ranges: Vec<FileRange>,
6365
) -> MatchFinder<'db> {
66+
restrict_ranges.retain(|range| !range.range.is_empty());
6467
let sema = Semantics::new(db);
6568
let resolution_scope = resolving::ResolutionScope::new(&sema, lookup_context);
66-
MatchFinder { sema: Semantics::new(db), rules: Vec::new(), resolution_scope }
69+
MatchFinder {
70+
sema: Semantics::new(db),
71+
rules: Vec::new(),
72+
resolution_scope,
73+
restrict_ranges,
74+
}
6775
}
6876

6977
/// Constructs an instance using the start of the first file in `db` as the lookup context.
@@ -79,6 +87,7 @@ impl<'db> MatchFinder<'db> {
7987
Ok(MatchFinder::in_context(
8088
db,
8189
FilePosition { file_id: first_file_id, offset: 0.into() },
90+
vec![],
8291
))
8392
} else {
8493
bail!("No files to search");

crates/ra_ssr/src/matching.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -706,8 +706,8 @@ mod tests {
706706
let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap();
707707
let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }";
708708

709-
let (db, position) = crate::tests::single_file(input);
710-
let mut match_finder = MatchFinder::in_context(&db, position);
709+
let (db, position, selections) = crate::tests::single_file(input);
710+
let mut match_finder = MatchFinder::in_context(&db, position, selections);
711711
match_finder.add_rule(rule).unwrap();
712712
let matches = match_finder.matches();
713713
assert_eq!(matches.matches.len(), 1);

crates/ra_ssr/src/resolving.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,22 @@ impl Resolver<'_, '_> {
141141
impl<'db> ResolutionScope<'db> {
142142
pub(crate) fn new(
143143
sema: &hir::Semantics<'db, ra_ide_db::RootDatabase>,
144-
lookup_context: FilePosition,
144+
resolve_context: FilePosition,
145145
) -> ResolutionScope<'db> {
146146
use ra_syntax::ast::AstNode;
147-
let file = sema.parse(lookup_context.file_id);
147+
let file = sema.parse(resolve_context.file_id);
148148
// Find a node at the requested position, falling back to the whole file.
149149
let node = file
150150
.syntax()
151-
.token_at_offset(lookup_context.offset)
151+
.token_at_offset(resolve_context.offset)
152152
.left_biased()
153153
.map(|token| token.parent())
154154
.unwrap_or_else(|| file.syntax().clone());
155155
let node = pick_node_for_resolution(node);
156156
let scope = sema.scope(&node);
157157
ResolutionScope {
158158
scope,
159-
hygiene: hir::Hygiene::new(sema.db, lookup_context.file_id.into()),
159+
hygiene: hir::Hygiene::new(sema.db, resolve_context.file_id.into()),
160160
}
161161
}
162162

crates/ra_ssr/src/search.rs

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ use crate::{
55
resolving::{ResolvedPath, ResolvedPattern, ResolvedRule},
66
Match, MatchFinder,
77
};
8-
use ra_db::FileRange;
8+
use ra_db::{FileId, FileRange};
99
use ra_ide_db::{
1010
defs::Definition,
1111
search::{Reference, SearchScope},
1212
};
1313
use ra_syntax::{ast, AstNode, SyntaxKind, SyntaxNode};
14+
use rustc_hash::FxHashSet;
1415
use test_utils::mark;
1516

1617
/// A cache for the results of find_usages. This is for when we have multiple patterns that have the
@@ -54,11 +55,7 @@ impl<'db> MatchFinder<'db> {
5455
mark::hit!(use_declaration_with_braces);
5556
continue;
5657
}
57-
if let Ok(m) =
58-
matching::get_match(false, rule, &node_to_match, &None, &self.sema)
59-
{
60-
matches_out.push(m);
61-
}
58+
self.try_add_match(rule, &node_to_match, &None, matches_out);
6259
}
6360
}
6461
}
@@ -121,25 +118,39 @@ impl<'db> MatchFinder<'db> {
121118
// FIXME: We should ideally have a test that checks that we edit local roots and not library
122119
// roots. This probably would require some changes to fixtures, since currently everything
123120
// seems to get put into a single source root.
124-
use ra_db::SourceDatabaseExt;
125-
use ra_ide_db::symbol_index::SymbolsDatabase;
126121
let mut files = Vec::new();
127-
for &root in self.sema.db.local_roots().iter() {
128-
let sr = self.sema.db.source_root(root);
129-
files.extend(sr.iter());
130-
}
122+
self.search_files_do(|file_id| {
123+
files.push(file_id);
124+
});
131125
SearchScope::files(&files)
132126
}
133127

134128
fn slow_scan(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) {
135-
use ra_db::SourceDatabaseExt;
136-
use ra_ide_db::symbol_index::SymbolsDatabase;
137-
for &root in self.sema.db.local_roots().iter() {
138-
let sr = self.sema.db.source_root(root);
139-
for file_id in sr.iter() {
140-
let file = self.sema.parse(file_id);
141-
let code = file.syntax();
142-
self.slow_scan_node(code, rule, &None, matches_out);
129+
self.search_files_do(|file_id| {
130+
let file = self.sema.parse(file_id);
131+
let code = file.syntax();
132+
self.slow_scan_node(code, rule, &None, matches_out);
133+
})
134+
}
135+
136+
fn search_files_do(&self, mut callback: impl FnMut(FileId)) {
137+
if self.restrict_ranges.is_empty() {
138+
// Unrestricted search.
139+
use ra_db::SourceDatabaseExt;
140+
use ra_ide_db::symbol_index::SymbolsDatabase;
141+
for &root in self.sema.db.local_roots().iter() {
142+
let sr = self.sema.db.source_root(root);
143+
for file_id in sr.iter() {
144+
callback(file_id);
145+
}
146+
}
147+
} else {
148+
// Search is restricted, deduplicate file IDs (generally only one).
149+
let mut files = FxHashSet::default();
150+
for range in &self.restrict_ranges {
151+
if files.insert(range.file_id) {
152+
callback(range.file_id);
153+
}
143154
}
144155
}
145156
}
@@ -154,9 +165,7 @@ impl<'db> MatchFinder<'db> {
154165
if !is_search_permitted(code) {
155166
return;
156167
}
157-
if let Ok(m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) {
158-
matches_out.push(m);
159-
}
168+
self.try_add_match(rule, &code, restrict_range, matches_out);
160169
// If we've got a macro call, we already tried matching it pre-expansion, which is the only
161170
// way to match the whole macro, now try expanding it and matching the expansion.
162171
if let Some(macro_call) = ast::MacroCall::cast(code.clone()) {
@@ -178,6 +187,38 @@ impl<'db> MatchFinder<'db> {
178187
self.slow_scan_node(&child, rule, restrict_range, matches_out);
179188
}
180189
}
190+
191+
fn try_add_match(
192+
&self,
193+
rule: &ResolvedRule,
194+
code: &SyntaxNode,
195+
restrict_range: &Option<FileRange>,
196+
matches_out: &mut Vec<Match>,
197+
) {
198+
if !self.within_range_restrictions(code) {
199+
mark::hit!(replace_nonpath_within_selection);
200+
return;
201+
}
202+
if let Ok(m) = matching::get_match(false, rule, code, restrict_range, &self.sema) {
203+
matches_out.push(m);
204+
}
205+
}
206+
207+
/// Returns whether `code` is within one of our range restrictions if we have any. No range
208+
/// restrictions is considered unrestricted and always returns true.
209+
fn within_range_restrictions(&self, code: &SyntaxNode) -> bool {
210+
if self.restrict_ranges.is_empty() {
211+
// There is no range restriction.
212+
return true;
213+
}
214+
let node_range = self.sema.original_range(code);
215+
for range in &self.restrict_ranges {
216+
if range.file_id == node_range.file_id && range.range.contains_range(node_range.range) {
217+
return true;
218+
}
219+
}
220+
false
221+
}
181222
}
182223

183224
/// Returns whether we support matching within `node` and all of its ancestors.

0 commit comments

Comments
 (0)