-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Init implementation of structural search replace #3099
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
crates/ra_ide/src/lib.rs
Outdated
@@ -464,6 +467,30 @@ impl Analysis { | |||
self.with_db(|db| references::rename(db, position, new_name)) | |||
} | |||
|
|||
pub fn structural_search_replace( | |||
&self, | |||
template: &str, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what's the best API around passing the template. I sort of wish that this was more structured, query: SsrQuery
, like we do for workspace_symbol
, but the query is not really structured yet. Still, I think I'd prefer this API
struct SsrQuery { ... }
impl FromStr for SsrQuery { ... }
pub fn structural_search_replace(&self, query: SsrQuery) { ... }
I imagine we might want to add quite a few knobs to the SsrQuey
in the future, like this:
let query = str.parse::<SsrQuery>()
.ignore_macros(true)
.strict_whitespace(true)
.crates(vec![foo, bar, baz])
Passing in a struct would be more convenient for adding those kinds of tweakables later.
This also nicely separates fast, synchronous and fail able operation of parsing the template from slow, async and infailable op of doing actual search&replace
crates/ra_ide/src/lib.rs
Outdated
}); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this to the ssr module.
crates/ra_ide/src/ssr.rs
Outdated
#[derive(Debug, PartialEq)] | ||
pub enum SsrError { | ||
ParseError(String), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably do just struct SsrError(String)
. In general, I've found that making error opaque struct (as opposed to open enums) usually works better, as you almost never actually want to match on the result anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree on that, but only under the condition that we are the only users of that error (i.e. we are not shipping a library for anyone else where we cannot be sure that the users will want to match on particular error types).
As a reference, we move to opaque error in ra_syntax
crate right now
code: &SyntaxElement, | ||
placeholders: &[String], | ||
m: &mut Match, | ||
) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A better signature might be
fn check(
pattern: &SyntaxElement,
code: &SyntaxElement,
placeholders: &[String],
) -> Option<Match> {
that they, there's no implicit invariant of "only use Match if the function returned true"
crates/ra_ide/src/ssr.rs
Outdated
#[derive(Debug, PartialEq)] | ||
pub struct SsrPattern { | ||
pattern: SyntaxNode, | ||
names: Vec<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it's worth it, but we might want add some newtype wrappers here
/// represents an `$var` in an SSR query
struct Var(String);
crates/ra_ide/src/ssr.rs
Outdated
|
||
fn render(binding: &Binding, template: &SsrTemplate) -> String { | ||
fn replace(p: &SyntaxNode, binding: &Binding, builder: &mut TextEditBuilder) { | ||
if let Some(name) = binding.keys().find(|&n| n.as_str() == p.text()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be clearer and perhaps more efficient to pre-process SsrTemplate
like this:
struct SsrTemplates {
node: SyntaxNode,
placeholders: FxHashMap<SyntaxElement, Var>,
}
we can then do roughly the following:
for element in template.node.descendats_with_tokens() {
if let Some(var) = template.placeholders.get(element) {
builder.replace(element.text_range(), binding[var])
}
}
editors/code/src/commands/ssr.ts
Outdated
if (!client) return; | ||
|
||
const options: InputBoxOptions = { | ||
placeHolder: "foo($a:expr, $b:expr) ==>> foo($a: expr, bar($b:expr))", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
placeHolder: "foo($a:expr, $b:expr) ==>> foo($a: expr, bar($b:expr))", | |
placeHolder: "foo($a:expr, $b:expr) ==>> foo($a, bar($b))", |
editors/code/src/commands/ssr.ts
Outdated
@@ -0,0 +1,36 @@ | |||
import { Ctx, Cmd } from '../ctx'; | |||
import { applySourceChange, SourceChange } from '../source_change'; | |||
import { window, InputBoxOptions } from 'vscode'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import * as vscode from 'vscode';
I'd suggest to keep everything under vscode
namespace. I'd like to achieve consistency across the vscode extension on that...
editors/code/src/commands/ssr.ts
Outdated
placeHolder: "foo($a:expr, $b:expr) ==>> foo($a: expr, bar($b:expr))", | ||
prompt: "Enter request", | ||
validateInput: (x: string) => { | ||
if (x.indexOf('==>>') >= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (x.indexOf('==>>') >= 0) { | |
if (x.includes('==>>')) { |
crates/ra_ide/src/lib.rs
Outdated
) -> Cancelable<Result<SourceChange, SsrError>> { | ||
self.with_db(|db| { | ||
let mut edits = vec![]; | ||
let (pattern, template) = ssr::parse(template)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, pattern and template are total synonyms for me (i.e. I don't take them apart). Can we use some other names? E.g. ssr::SearchPattern
and ssr::ReplacementPattern
or something like that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, pattern and template are total synonyms
Pattern/template are specific terms in this case, which precisely capture the difference between the two. We use this terminology for macro rules as well.
Pattern is something you can compare with. Ie, you have an existing thing and can say if it matches the pattern or not.
Template is something you use to make new things.
This is similar to how (actual) argument and (formal) parameter are different.
It's curious that both "design patterns" and "C++ templates" are translated as Шаблоны into Russian. This is the correct semantics for templates, but is totally backwards for design patterns, as the book is very specific about the fact that it describes and names existing similar shapes of code and does not give you a template which you can/should apply to new code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matklad thank you for the clarification! The actual and formal parameter is the core difference berween these words, though it somehow wasn't obvious to me... Though I still can't think of them in the same terms. Though if this is an already settled naming concept in rust-analyzer
I wouldn't like to bring new rules in foreign church ;).
crates/ra_ide/src/ssr.rs
Outdated
#[derive(Debug, PartialEq)] | ||
pub enum SsrError { | ||
ParseError(String), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree on that, but only under the condition that we are the only users of that error (i.e. we are not shipping a library for anyone else where we cannot be sure that the users will want to match on particular error types).
As a reference, we move to opaque error in ra_syntax
crate right now
crates/ra_ide/src/ssr.rs
Outdated
} | ||
|
||
fn is_name(s: &str) -> Result<(), SsrError> { | ||
if s.chars().all(|c| char::is_ascii_alphanumeric(&c) || c == '_') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if s.chars().all(|c| char::is_ascii_alphanumeric(&c) || c == '_') { | |
if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And don't we need to check that the name is not begun with a number? I guess we already have something for this validation somewhere around the project. Maybe @matklad knows?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will leave names from numbers for now, I don't see any harm in $0, to be honest I want to remove this code and reuse parser for macros.
let name = &s[0..end_of_name]; | ||
is_name(name)?; | ||
let type_begin = end_of_name + 1; | ||
let type_length = s[type_begin..].find(|c| !char::is_ascii_alphanumeric(&c)).unwrap_or(s.len()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let type_length = s[type_begin..].find(|c| !char::is_ascii_alphanumeric(&c)).unwrap_or(s.len()); | |
let type_length = s[type_begin..].find(|c| !c.is_ascii_alphanumeric()).unwrap_or(s.len()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't work, comptroller cannot deduce type for c
m: &mut Match, | ||
) -> bool { | ||
match (pattern, code) { | ||
(SyntaxElement::Token(ref pattern), SyntaxElement::Token(ref code)) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess if we change to match (&pattern, &code)
we can avoid writing ref
here and there, can't we?
crates/ra_ide/src/ssr.rs
Outdated
pattern: &SyntaxElement, | ||
code: &SyntaxElement, | ||
placeholders: &[String], | ||
m: &mut Match, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a nit: I'd propose to avoid single-char names as much as we can. While writing the code it is much simpler, but when reading ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
match is a keyword
crates/ra_ide/src/ssr.rs
Outdated
use ra_syntax::SourceFile; | ||
#[test] | ||
fn parser_happy_case() { | ||
let result = parse("foo($a:expr, $b:expr) ==>> bar($b, $a)").unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd propose to destructure tuples into named variables. Access to tuple members by numbers is why we don't like them, but simple in-place destructuring is why we do.
Left small nits, and nothing more. Looks great, big thanks for the implementation! |
I think there's still a lot to do here, but this seems reasonable to merge now! I'll open a follow-up issue bors r+ |
3099: Init implementation of structural search replace r=matklad a=mikhail-m1 next steps: * ignore space and other minor difference * add support to ra_cli * call rust parser to check pattern * documentation original issue #2267 Co-authored-by: Mikhail Modin <[email protected]>
Build succeeded
|
next steps:
original issue #2267