Skip to content

Commit cf55806

Browse files
SSR: Restrict to current selection if any
The selection is also used to avoid unnecessary work, but only to the file level. Further restricting unnecessary work is left for later.
1 parent 5a81242 commit cf55806

File tree

10 files changed

+181
-50
lines changed

10 files changed

+181
-50
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: 6 additions & 2 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:
@@ -57,9 +60,10 @@ pub fn parse_search_replace(
5760
parse_only: bool,
5861
db: &RootDatabase,
5962
position: 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, position, 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/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.

crates/ra_ssr/src/tests.rs

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use crate::{MatchFinder, SsrRule};
22
use expect::{expect, Expect};
3-
use ra_db::{salsa::Durability, FileId, FilePosition, SourceDatabaseExt};
3+
use ra_db::{salsa::Durability, FileId, FilePosition, FileRange, SourceDatabaseExt};
44
use rustc_hash::FxHashSet;
55
use std::sync::Arc;
6-
use test_utils::mark;
6+
use test_utils::{mark, RangeOrOffset};
77

88
fn parse_error_text(query: &str) -> String {
99
format!("{}", query.parse::<SsrRule>().unwrap_err())
@@ -60,29 +60,41 @@ fn parser_undefined_placeholder_in_replacement() {
6060
}
6161

6262
/// `code` may optionally contain a cursor marker `<|>`. If it doesn't, then the position will be
63-
/// the start of the file.
64-
pub(crate) fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FilePosition) {
63+
/// the start of the file. If there's a second cursor marker, then we'll return a single range.
64+
pub(crate) fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FilePosition, Vec<FileRange>) {
6565
use ra_db::fixture::WithFixture;
6666
use ra_ide_db::symbol_index::SymbolsDatabase;
67-
let (mut db, position) = if code.contains(test_utils::CURSOR_MARKER) {
68-
ra_ide_db::RootDatabase::with_position(code)
67+
let (mut db, file_id, range_or_offset) = if code.contains(test_utils::CURSOR_MARKER) {
68+
ra_ide_db::RootDatabase::with_range_or_offset(code)
6969
} else {
7070
let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(code);
71-
(db, FilePosition { file_id, offset: 0.into() })
71+
(db, file_id, RangeOrOffset::Offset(0.into()))
7272
};
73+
let selections;
74+
let position;
75+
match range_or_offset {
76+
RangeOrOffset::Range(range) => {
77+
position = FilePosition { file_id, offset: range.start() };
78+
selections = vec![FileRange { file_id, range: range }];
79+
}
80+
RangeOrOffset::Offset(offset) => {
81+
position = FilePosition { file_id, offset };
82+
selections = vec![];
83+
}
84+
}
7385
let mut local_roots = FxHashSet::default();
7486
local_roots.insert(ra_db::fixture::WORKSPACE);
7587
db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH);
76-
(db, position)
88+
(db, position, selections)
7789
}
7890

7991
fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) {
8092
assert_ssr_transforms(&[rule], input, expected);
8193
}
8294

8395
fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
84-
let (db, position) = single_file(input);
85-
let mut match_finder = MatchFinder::in_context(&db, position);
96+
let (db, position, selections) = single_file(input);
97+
let mut match_finder = MatchFinder::in_context(&db, position, selections);
8698
for rule in rules {
8799
let rule: SsrRule = rule.parse().unwrap();
88100
match_finder.add_rule(rule).unwrap();
@@ -112,8 +124,8 @@ fn print_match_debug_info(match_finder: &MatchFinder, file_id: FileId, snippet:
112124
}
113125

114126
fn assert_matches(pattern: &str, code: &str, expected: &[&str]) {
115-
let (db, position) = single_file(code);
116-
let mut match_finder = MatchFinder::in_context(&db, position);
127+
let (db, position, selections) = single_file(code);
128+
let mut match_finder = MatchFinder::in_context(&db, position, selections);
117129
match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap();
118130
let matched_strings: Vec<String> =
119131
match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect();
@@ -124,8 +136,8 @@ fn assert_matches(pattern: &str, code: &str, expected: &[&str]) {
124136
}
125137

126138
fn assert_no_match(pattern: &str, code: &str) {
127-
let (db, position) = single_file(code);
128-
let mut match_finder = MatchFinder::in_context(&db, position);
139+
let (db, position, selections) = single_file(code);
140+
let mut match_finder = MatchFinder::in_context(&db, position, selections);
129141
match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap();
130142
let matches = match_finder.matches().flattened().matches;
131143
if !matches.is_empty() {
@@ -135,8 +147,8 @@ fn assert_no_match(pattern: &str, code: &str) {
135147
}
136148

137149
fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) {
138-
let (db, position) = single_file(code);
139-
let mut match_finder = MatchFinder::in_context(&db, position);
150+
let (db, position, selections) = single_file(code);
151+
let mut match_finder = MatchFinder::in_context(&db, position, selections);
140152
match_finder.add_search_pattern(pattern.parse().unwrap()).unwrap();
141153
let mut reasons = Vec::new();
142154
for d in match_finder.debug_where_text_equal(position.file_id, snippet) {
@@ -490,9 +502,10 @@ fn no_match_split_expression() {
490502

491503
#[test]
492504
fn replace_function_call() {
505+
// This test also makes sure that we ignore empty-ranges.
493506
assert_ssr_transform(
494507
"foo() ==>> bar()",
495-
"fn foo() {} fn bar() {} fn f1() {foo(); foo();}",
508+
"fn foo() {<|><|>} fn bar() {} fn f1() {foo(); foo();}",
496509
expect![["fn foo() {} fn bar() {} fn f1() {bar(); bar();}"]],
497510
);
498511
}
@@ -922,3 +935,52 @@ fn replace_local_variable_reference() {
922935
"#]],
923936
)
924937
}
938+
939+
#[test]
940+
fn replace_path_within_selection() {
941+
assert_ssr_transform(
942+
"foo ==>> bar",
943+
r#"
944+
fn main() {
945+
let foo = 41;
946+
let bar = 42;
947+
do_stuff(foo);
948+
do_stuff(foo);<|>
949+
do_stuff(foo);
950+
do_stuff(foo);<|>
951+
do_stuff(foo);
952+
}"#,
953+
expect![[r#"
954+
fn main() {
955+
let foo = 41;
956+
let bar = 42;
957+
do_stuff(foo);
958+
do_stuff(foo);
959+
do_stuff(bar);
960+
do_stuff(bar);
961+
do_stuff(foo);
962+
}"#]],
963+
);
964+
}
965+
966+
#[test]
967+
fn replace_nonpath_within_selection() {
968+
mark::check!(replace_nonpath_within_selection);
969+
assert_ssr_transform(
970+
"$a + $b ==>> $b * $a",
971+
r#"
972+
fn main() {
973+
let v = 1 + 2;<|>
974+
let v2 = 3 + 3;
975+
let v3 = 4 + 5;<|>
976+
let v4 = 6 + 7;
977+
}"#,
978+
expect![[r#"
979+
fn main() {
980+
let v = 1 + 2;
981+
let v2 = 3 * 3;
982+
let v3 = 5 * 4;
983+
let v4 = 6 + 7;
984+
}"#]],
985+
);
986+
}

crates/rust-analyzer/src/handlers.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,9 +1026,18 @@ pub(crate) fn handle_ssr(
10261026
params: lsp_ext::SsrParams,
10271027
) -> Result<lsp_types::WorkspaceEdit> {
10281028
let _p = profile("handle_ssr");
1029+
let selections = params
1030+
.selections
1031+
.iter()
1032+
.map(|range| from_proto::file_range(&snap, params.position.text_document.clone(), *range))
1033+
.collect::<Result<Vec<_>, _>>()?;
10291034
let position = from_proto::file_position(&snap, params.position)?;
1030-
let source_change =
1031-
snap.analysis.structural_search_replace(&params.query, params.parse_only, position)??;
1035+
let source_change = snap.analysis.structural_search_replace(
1036+
&params.query,
1037+
params.parse_only,
1038+
position,
1039+
selections,
1040+
)??;
10321041
to_proto::workspace_edit(&snap, source_change)
10331042
}
10341043

0 commit comments

Comments
 (0)