Skip to content

Commit 700cd21

Browse files
committed
Init implementation of structural search replace
1 parent f8d6d6f commit 700cd21

File tree

10 files changed

+370
-6
lines changed

10 files changed

+370
-6
lines changed

crates/ra_ide/src/lib.rs

+32-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ mod display;
3737
mod inlay_hints;
3838
mod expand;
3939
mod expand_macro;
40+
mod ssr;
4041

4142
#[cfg(test)]
4243
mod marks;
@@ -48,13 +49,13 @@ use std::sync::Arc;
4849
use ra_cfg::CfgOptions;
4950
use ra_db::{
5051
salsa::{self, ParallelDatabase},
51-
CheckCanceled, Env, FileLoader, SourceDatabase,
52+
CheckCanceled, Env, SourceDatabase,
5253
};
5354
use ra_ide_db::{
5455
symbol_index::{self, FileSymbol},
5556
LineIndexDatabase,
5657
};
57-
use ra_syntax::{SourceFile, TextRange, TextUnit};
58+
use ra_syntax::{ast::AstNode, SourceFile, TextRange, TextUnit};
5859

5960
use crate::display::ToNav;
6061

@@ -73,19 +74,21 @@ pub use crate::{
7374
},
7475
runnables::{Runnable, RunnableKind},
7576
source_change::{FileSystemEdit, SourceChange, SourceFileEdit},
77+
ssr::SsrError,
7678
syntax_highlighting::HighlightedRange,
7779
};
7880

79-
pub use hir::Documentation;
81+
pub use hir::{Crate, Documentation};
8082
pub use ra_db::{
81-
Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId,
83+
Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceDatabaseExt,
84+
SourceRootId,
8285
};
8386
pub use ra_ide_db::{
8487
change::{AnalysisChange, LibraryData},
8588
feature_flags::FeatureFlags,
8689
line_index::{LineCol, LineIndex},
8790
line_index_utils::translate_offset_with_edit,
88-
symbol_index::Query,
91+
symbol_index::{Query, SymbolsDatabase},
8992
RootDatabase,
9093
};
9194

@@ -464,6 +467,30 @@ impl Analysis {
464467
self.with_db(|db| references::rename(db, position, new_name))
465468
}
466469

470+
pub fn structural_search_replace(
471+
&self,
472+
template: &str,
473+
) -> Cancelable<Result<SourceChange, SsrError>> {
474+
self.with_db(|db| {
475+
let mut edits = vec![];
476+
let (pattern, template) = ssr::parse(template)?;
477+
for &root in db.local_roots().iter() {
478+
let sr = db.source_root(root);
479+
for file_id in sr.walk() {
480+
dbg!(db.file_relative_path(file_id));
481+
let matches = ssr::find(&pattern, db.parse(file_id).tree().syntax());
482+
if !matches.is_empty() {
483+
edits.push(SourceFileEdit {
484+
file_id,
485+
edit: ssr::replace(&matches, &template),
486+
});
487+
}
488+
}
489+
}
490+
Ok(SourceChange::source_file_edits("ssr", edits))
491+
})
492+
}
493+
467494
/// Performs an operation on that may be Canceled.
468495
fn with_db<F: FnOnce(&RootDatabase) -> T + std::panic::UnwindSafe, T>(
469496
&self,

crates/ra_ide/src/ssr.rs

+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
//! structural search replace
2+
3+
use ra_syntax::ast::make::expr_from_text;
4+
use ra_syntax::AstNode;
5+
use ra_syntax::SyntaxElement;
6+
use ra_syntax::SyntaxNode;
7+
use ra_text_edit::{TextEdit, TextEditBuilder};
8+
use std::collections::HashMap;
9+
10+
#[derive(Debug, PartialEq)]
11+
pub enum SsrError {
12+
ParseError(String),
13+
}
14+
15+
impl std::fmt::Display for SsrError {
16+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
17+
match self {
18+
SsrError::ParseError(s) => write!(f, "Parse error: {}", s),
19+
}
20+
}
21+
}
22+
23+
impl std::error::Error for SsrError {}
24+
25+
#[derive(Debug, PartialEq)]
26+
pub struct SsrPattern {
27+
pattern: SyntaxNode,
28+
names: Vec<String>,
29+
}
30+
31+
#[derive(Debug, PartialEq)]
32+
pub struct SsrTemplate {
33+
template: SyntaxNode,
34+
}
35+
36+
type Binding = HashMap<String, SyntaxNode>;
37+
38+
#[derive(Debug)]
39+
pub struct Match {
40+
place: SyntaxNode,
41+
binding: Binding,
42+
}
43+
44+
#[derive(Debug)]
45+
pub struct SsrMatches {
46+
matches: Vec<Match>,
47+
}
48+
49+
impl SsrMatches {
50+
pub(crate) fn is_empty(&self) -> bool {
51+
self.matches.is_empty()
52+
}
53+
}
54+
55+
pub(crate) fn parse(query: &str) -> Result<(SsrPattern, SsrTemplate), SsrError> {
56+
let mut it = query.split("==>>");
57+
let pattern = it.next().expect("at least empty string").trim();
58+
let mut template = it
59+
.next()
60+
.ok_or(SsrError::ParseError("Cannot find delemiter `==>>`".into()))?
61+
.trim()
62+
.to_string();
63+
if it.next().is_some() {
64+
return Err(SsrError::ParseError("More than one delimiter found".into()));
65+
}
66+
let mut names = vec![];
67+
let mut it = pattern.split('$');
68+
let mut pattern = it.next().expect("something").to_string();
69+
70+
for part in it.map(split_by_binding) {
71+
let (binding_name, binding_type, remainder) = part?;
72+
is_expr(binding_type)?;
73+
let new_binding_name = create_name(binding_name, &mut names)?;
74+
pattern.push_str(new_binding_name);
75+
pattern.push_str(remainder);
76+
template = replace_in_template(template, binding_name, new_binding_name);
77+
}
78+
Ok((
79+
SsrPattern { pattern: expr_from_text(&pattern).syntax().clone(), names },
80+
SsrTemplate { template: expr_from_text(&template).syntax().clone() },
81+
))
82+
}
83+
84+
fn split_by_binding(s: &str) -> Result<(&str, &str, &str), SsrError> {
85+
let end_of_name = s.find(":").ok_or(SsrError::ParseError("Use $<name>:expr".into()))?;
86+
let name = &s[0..end_of_name];
87+
is_name(name)?;
88+
let type_begin = end_of_name + 1;
89+
let type_length = s[type_begin..].find(|c| !char::is_ascii_alphanumeric(&c)).unwrap_or(s.len());
90+
let type_name = &s[type_begin..type_begin + type_length];
91+
Ok((name, type_name, &s[type_begin + type_length..]))
92+
}
93+
94+
fn is_name(s: &str) -> Result<(), SsrError> {
95+
if s.chars().all(|c| char::is_ascii_alphanumeric(&c) || c == '_') {
96+
Ok(())
97+
} else {
98+
Err(SsrError::ParseError("Name can contain only alphanumerics and _".into()))
99+
}
100+
}
101+
102+
fn is_expr(s: &str) -> Result<(), SsrError> {
103+
if s == "expr" {
104+
Ok(())
105+
} else {
106+
Err(SsrError::ParseError("Only $<name>:expr is supported".into()))
107+
}
108+
}
109+
110+
fn replace_in_template(template: String, name: &str, new_name: &str) -> String {
111+
let name = format!("${}", name);
112+
template.replace(&name, new_name)
113+
}
114+
115+
fn create_name<'a>(name: &str, binding_names: &'a mut Vec<String>) -> Result<&'a str, SsrError> {
116+
let sanitized_name = format!("__search_pattern_{}", name);
117+
if binding_names.iter().any(|a| a == &sanitized_name) {
118+
return Err(SsrError::ParseError(format!("Name `{}` repeats more than once", name)));
119+
}
120+
binding_names.push(sanitized_name);
121+
Ok(binding_names.last().unwrap())
122+
}
123+
124+
pub(crate) fn find(pattern: &SsrPattern, code: &SyntaxNode) -> SsrMatches {
125+
fn check(
126+
pattern: &SyntaxElement,
127+
code: &SyntaxElement,
128+
placeholders: &[String],
129+
m: &mut Match,
130+
) -> bool {
131+
match (pattern, code) {
132+
(SyntaxElement::Token(ref pattern), SyntaxElement::Token(ref code)) => {
133+
pattern.text() == code.text()
134+
}
135+
(SyntaxElement::Node(ref pattern), SyntaxElement::Node(ref code)) => {
136+
if placeholders.iter().find(|&n| n.as_str() == pattern.text()).is_some() {
137+
m.binding.insert(pattern.text().to_string(), code.clone());
138+
true
139+
} else {
140+
pattern.green().children().count() == code.green().children().count()
141+
&& pattern
142+
.children_with_tokens()
143+
.zip(code.children_with_tokens())
144+
.all(|(a, b)| check(&a, &b, placeholders, m))
145+
}
146+
}
147+
_ => false,
148+
}
149+
}
150+
let kind = pattern.pattern.kind();
151+
let matches = code
152+
.descendants_with_tokens()
153+
.filter(|n| n.kind() == kind)
154+
.filter_map(|code| {
155+
let mut m = Match { place: code.as_node().unwrap().clone(), binding: HashMap::new() };
156+
if check(&SyntaxElement::from(pattern.pattern.clone()), &code, &pattern.names, &mut m) {
157+
Some(m)
158+
} else {
159+
None
160+
}
161+
})
162+
.collect();
163+
SsrMatches { matches }
164+
}
165+
166+
pub(crate) fn replace(matches: &SsrMatches, template: &SsrTemplate) -> TextEdit {
167+
let mut builder = TextEditBuilder::default();
168+
for m in &matches.matches {
169+
builder.replace(m.place.text_range(), render(&m.binding, template));
170+
}
171+
builder.finish()
172+
}
173+
174+
fn render(binding: &Binding, template: &SsrTemplate) -> String {
175+
fn replace(p: &SyntaxNode, binding: &Binding, builder: &mut TextEditBuilder) {
176+
if let Some(name) = binding.keys().find(|&n| n.as_str() == p.text()) {
177+
builder.replace(p.text_range(), binding[name].text().to_string())
178+
} else {
179+
for ref child in p.children() {
180+
replace(child, binding, builder);
181+
}
182+
}
183+
}
184+
let mut builder = TextEditBuilder::default();
185+
replace(&template.template, binding, &mut builder);
186+
builder.finish().apply(&template.template.text().to_string()) //FIXME
187+
}
188+
189+
#[cfg(test)]
190+
mod tests {
191+
use super::*;
192+
use ra_syntax::SourceFile;
193+
#[test]
194+
fn parser_happy_case() {
195+
let result = parse("foo($a:expr, $b:expr) ==>> bar($b, $a)").unwrap();
196+
assert_eq!(&result.0.pattern.text(), "foo(__search_pattern_a, __search_pattern_b)");
197+
assert_eq!(
198+
result.0.names,
199+
vec!["__search_pattern_a".to_string(), "__search_pattern_b".to_string()]
200+
);
201+
assert_eq!(&result.1.template.text(), "bar(__search_pattern_b, __search_pattern_a)");
202+
}
203+
204+
#[test]
205+
fn parser_empty_query() {
206+
assert_eq!(
207+
parse("").unwrap_err(),
208+
SsrError::ParseError("Cannot find delemiter `==>>`".into())
209+
);
210+
}
211+
212+
#[test]
213+
fn parser_no_delimiter() {
214+
assert_eq!(
215+
parse("foo()").unwrap_err(),
216+
SsrError::ParseError("Cannot find delemiter `==>>`".into())
217+
);
218+
}
219+
220+
#[test]
221+
fn parser_two_delimiters() {
222+
assert_eq!(
223+
parse("foo() ==>> a ==>> b ").unwrap_err(),
224+
SsrError::ParseError("More than one delimiter found".into())
225+
);
226+
}
227+
228+
#[test]
229+
fn parser_no_pattern_type() {
230+
assert_eq!(
231+
parse("foo($a) ==>>").unwrap_err(),
232+
SsrError::ParseError("Use $<name>:expr".into())
233+
);
234+
}
235+
236+
#[test]
237+
fn parser_invalid_name() {
238+
assert_eq!(
239+
parse("foo($a+:expr) ==>>").unwrap_err(),
240+
SsrError::ParseError("Name can contain only alphanumerics and _".into())
241+
);
242+
}
243+
244+
#[test]
245+
fn parser_invalid_type() {
246+
assert_eq!(
247+
parse("foo($a:ident) ==>>").unwrap_err(),
248+
SsrError::ParseError("Only $<name>:expr is supported".into())
249+
);
250+
}
251+
252+
#[test]
253+
fn parser_repeated_name() {
254+
assert_eq!(
255+
parse("foo($a:expr, $a:expr) ==>>").unwrap_err(),
256+
SsrError::ParseError("Name `a` repeats more than once".into())
257+
);
258+
}
259+
260+
#[test]
261+
fn parse_match_replace() {
262+
let (p, template) = parse("foo($x:expr) ==>> bar($x)").unwrap();
263+
let input = &"fn main() { foo(1+2); }";
264+
265+
let code = SourceFile::parse(input).tree();
266+
let matches = find(&p, code.syntax());
267+
assert_eq!(matches.matches.len(), 1);
268+
assert_eq!(matches.matches[0].place.text(), "foo(1+2)");
269+
assert_eq!(matches.matches[0].binding.len(), 1);
270+
assert_eq!(matches.matches[0].binding["__search_pattern_x"].text(), "1+2");
271+
272+
let edit = replace(&matches, &template);
273+
assert_eq!(edit.apply(input), "fn main() { bar(1+2); }");
274+
}
275+
}

crates/ra_lsp_server/src/main_loop.rs

+1
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ fn on_request(
526526
.on::<req::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare)?
527527
.on::<req::CallHierarchyIncomingCalls>(handlers::handle_call_hierarchy_incoming)?
528528
.on::<req::CallHierarchyOutgoingCalls>(handlers::handle_call_hierarchy_outgoing)?
529+
.on::<req::Ssr>(handlers::handle_ssr)?
529530
.finish();
530531
Ok(())
531532
}

crates/ra_lsp_server/src/main_loop/handlers.rs

+5
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,11 @@ pub fn handle_document_highlight(
880880
))
881881
}
882882

883+
pub fn handle_ssr(world: WorldSnapshot, params: req::SsrParams) -> Result<req::SourceChange> {
884+
let _p = profile("handle_ssr");
885+
world.analysis().structural_search_replace(&params.arg)??.try_conv_with(&world)
886+
}
887+
883888
pub fn publish_diagnostics(world: &WorldSnapshot, file_id: FileId) -> Result<DiagnosticTask> {
884889
let _p = profile("publish_diagnostics");
885890
let line_index = world.analysis().file_line_index(file_id)?;

crates/ra_lsp_server/src/req.rs

+13
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,16 @@ pub struct InlayHint {
206206
pub kind: InlayKind,
207207
pub label: String,
208208
}
209+
210+
pub enum Ssr {}
211+
212+
impl Request for Ssr {
213+
type Params = SsrParams;
214+
type Result = SourceChange;
215+
const METHOD: &'static str = "rust-analyzer/ssr";
216+
}
217+
218+
#[derive(Debug, Deserialize, Serialize)]
219+
pub struct SsrParams {
220+
pub arg: String,
221+
}

0 commit comments

Comments
 (0)