Skip to content

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

Merged
merged 1 commit into from
Feb 17, 2020

Conversation

mikhail-m1
Copy link
Contributor

@mikhail-m1 mikhail-m1 commented Feb 10, 2020

next steps:

  • ignore space and other minor difference
  • add support to ra_cli
  • call rust parser to check pattern
  • documentation

original issue #2267

@matklad
Copy link
Member

matklad commented Feb 11, 2020

Gave it a shoot, it seems to work perfectly for small cases. Spend at least couple of minutes mindlessly marveling at flipping $a <-> $b :)

However I've found at least one case where it doesn't quite work:

image

After SSR

image

Haven't looked into the code closely yet, so can't tell what's the issue

@matklad
Copy link
Member

matklad commented Feb 11, 2020

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

pub fn structural_search_replace(
&self,
template: &str,
Copy link
Member

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

});
}
}
}
Copy link
Member

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.

#[derive(Debug, PartialEq)]
pub enum SsrError {
ParseError(String),
}
Copy link
Member

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.

Copy link
Contributor

@Veetaha Veetaha Feb 12, 2020

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 {
Copy link
Member

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"

#[derive(Debug, PartialEq)]
pub struct SsrPattern {
pattern: SyntaxNode,
names: Vec<String>,
Copy link
Member

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);


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()) {
Copy link
Member

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])
    }
}

if (!client) return;

const options: InputBoxOptions = {
placeHolder: "foo($a:expr, $b:expr) ==>> foo($a: expr, bar($b:expr))",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
placeHolder: "foo($a:expr, $b:expr) ==>> foo($a: expr, bar($b:expr))",
placeHolder: "foo($a:expr, $b:expr) ==>> foo($a, bar($b))",

@@ -0,0 +1,36 @@
import { Ctx, Cmd } from '../ctx';
import { applySourceChange, SourceChange } from '../source_change';
import { window, InputBoxOptions } from 'vscode';
Copy link
Contributor

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...

placeHolder: "foo($a:expr, $b:expr) ==>> foo($a: expr, bar($b:expr))",
prompt: "Enter request",
validateInput: (x: string) => {
if (x.indexOf('==>>') >= 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (x.indexOf('==>>') >= 0) {
if (x.includes('==>>')) {

) -> Cancelable<Result<SourceChange, SsrError>> {
self.with_db(|db| {
let mut edits = vec![];
let (pattern, template) = ssr::parse(template)?;
Copy link
Contributor

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?

Copy link
Member

@matklad matklad Feb 13, 2020

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.

Copy link
Contributor

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 ;).

#[derive(Debug, PartialEq)]
pub enum SsrError {
ParseError(String),
}
Copy link
Contributor

@Veetaha Veetaha Feb 12, 2020

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

}

fn is_name(s: &str) -> Result<(), SsrError> {
if s.chars().all(|c| char::is_ascii_alphanumeric(&c) || c == '_') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if s.chars().all(|c| char::is_ascii_alphanumeric(&c) || c == '_') {
if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {

Copy link
Contributor

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?

Copy link
Contributor Author

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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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());

Copy link
Contributor Author

@mikhail-m1 mikhail-m1 Feb 12, 2020

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)) => {
Copy link
Contributor

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?

pattern: &SyntaxElement,
code: &SyntaxElement,
placeholders: &[String],
m: &mut Match,
Copy link
Contributor

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 ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

match is a keyword

use ra_syntax::SourceFile;
#[test]
fn parser_happy_case() {
let result = parse("foo($a:expr, $b:expr) ==>> bar($b, $a)").unwrap();
Copy link
Contributor

@Veetaha Veetaha Feb 12, 2020

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.

@Veetaha
Copy link
Contributor

Veetaha commented Feb 12, 2020

Left small nits, and nothing more. Looks great, big thanks for the implementation!

@matklad
Copy link
Member

matklad commented Feb 17, 2020

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+

bors bot added a commit that referenced this pull request Feb 17, 2020
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]>
@bors
Copy link
Contributor

bors bot commented Feb 17, 2020

Build succeeded

  • Rust (macos-latest)
  • Rust (ubuntu-latest)
  • Rust (windows-latest)
  • TypeScript

@bors bors bot merged commit f8f454a into rust-lang:master Feb 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants