diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index c60e86aead5..40276d4fef1 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs @@ -478,9 +478,10 @@ pub fn rename( pub fn structural_search_replace( &self, query: &str, + parse_only: bool, ) -> Cancelable> { self.with_db(|db| { - let edits = ssr::parse_search_replace(query, db)?; + let edits = ssr::parse_search_replace(query, parse_only, db)?; Ok(SourceChange::source_file_edits("ssr", edits)) }) } diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs index c011a2e74f3..1c9710a5d51 100644 --- a/crates/ra_ide/src/ssr.rs +++ b/crates/ra_ide/src/ssr.rs @@ -1,8 +1,10 @@ //! structural search replace use crate::source_change::SourceFileEdit; +use ra_db::{SourceDatabase, SourceDatabaseExt}; +use ra_ide_db::symbol_index::SymbolsDatabase; use ra_ide_db::RootDatabase; -use ra_syntax::ast::make::expr_from_text; +use ra_syntax::ast::make::try_expr_from_text; use ra_syntax::ast::{AstToken, Comment}; use ra_syntax::{AstNode, SyntaxElement, SyntaxNode}; use ra_text_edit::{TextEdit, TextEditBuilder}; @@ -10,9 +12,6 @@ use std::collections::HashMap; use std::str::FromStr; -pub use ra_db::{SourceDatabase, SourceDatabaseExt}; -use ra_ide_db::symbol_index::SymbolsDatabase; - #[derive(Debug, PartialEq)] pub struct SsrError(String); @@ -26,14 +25,17 @@ impl std::error::Error for SsrError {} pub fn parse_search_replace( query: &str, + parse_only: bool, db: &RootDatabase, ) -> Result, SsrError> { let mut edits = vec![]; let query: SsrQuery = query.parse()?; + if parse_only { + return Ok(edits); + } for &root in db.local_roots().iter() { let sr = db.source_root(root); for file_id in sr.walk() { - dbg!(db.file_relative_path(file_id)); let matches = find(&query.pattern, db.parse(file_id).tree().syntax()); if !matches.matches.is_empty() { edits.push(SourceFileEdit { file_id, edit: replace(&matches, &query.template) }); @@ -106,7 +108,10 @@ fn from_str(query: &str) -> Result { template = replace_in_template(template, var, new_var); } - let template = expr_from_text(&template).syntax().clone(); + let template = try_expr_from_text(&template) + .ok_or(SsrError("Template is not an expression".into()))? + .syntax() + .clone(); let mut placeholders = FxHashMap::default(); traverse(&template, &mut |n| { @@ -118,7 +123,13 @@ fn from_str(query: &str) -> Result { } }); - let pattern = SsrPattern { pattern: expr_from_text(&pattern).syntax().clone(), vars }; + let pattern = SsrPattern { + pattern: try_expr_from_text(&pattern) + .ok_or(SsrError("Pattern is not an expression".into()))? + .syntax() + .clone(), + vars, + }; let template = SsrTemplate { template, placeholders }; Ok(SsrQuery { pattern, template }) } @@ -284,7 +295,6 @@ fn parser_happy_case() { assert_eq!(result.pattern.vars[0].0, "__search_pattern_a"); assert_eq!(result.pattern.vars[1].0, "__search_pattern_b"); assert_eq!(&result.template.template.text(), "bar(__search_pattern_b, __search_pattern_a)"); - dbg!(result.template.placeholders); } #[test] @@ -334,6 +344,16 @@ fn parser_repeated_name() { ); } + #[test] + fn parser_invlid_pattern() { + assert_eq!(parse_error_text(" ==>> ()"), "Parse error: Pattern is not an expression"); + } + + #[test] + fn parser_invlid_template() { + assert_eq!(parse_error_text("() ==>> )"), "Parse error: Template is not an expression"); + } + #[test] fn parse_match_replace() { let query: SsrQuery = "foo($x:expr) ==>> bar($x)".parse().unwrap(); diff --git a/crates/ra_syntax/src/ast/make.rs b/crates/ra_syntax/src/ast/make.rs index ae8829807ce..9f6f1cc5310 100644 --- a/crates/ra_syntax/src/ast/make.rs +++ b/crates/ra_syntax/src/ast/make.rs @@ -112,10 +112,14 @@ pub fn expr_prefix(op: SyntaxKind, expr: ast::Expr) -> ast::Expr { let token = token(op); expr_from_text(&format!("{}{}", token, expr.syntax())) } -pub fn expr_from_text(text: &str) -> ast::Expr { +fn expr_from_text(text: &str) -> ast::Expr { ast_from_text(&format!("const C: () = {};", text)) } +pub fn try_expr_from_text(text: &str) -> Option { + try_ast_from_text(&format!("const C: () = {};", text)) +} + pub fn bind_pat(name: ast::Name) -> ast::BindPat { return from_text(name.text()); @@ -239,6 +243,16 @@ fn ast_from_text(text: &str) -> N { node } +fn try_ast_from_text(text: &str) -> Option { + let parse = SourceFile::parse(text); + let node = parse.tree().syntax().descendants().find_map(N::cast)?; + let node = node.syntax().clone(); + let node = unroot(node); + let node = N::cast(node).unwrap(); + assert_eq!(node.syntax().text_range().start(), 0.into()); + Some(node) +} + fn unroot(n: SyntaxNode) -> SyntaxNode { SyntaxNode::new_root(n.green().clone()) } diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs index 8dc6e8dc05d..3a31a6dd761 100644 --- a/crates/rust-analyzer/src/main_loop/handlers.rs +++ b/crates/rust-analyzer/src/main_loop/handlers.rs @@ -906,7 +906,10 @@ pub fn handle_document_highlight( pub fn handle_ssr(world: WorldSnapshot, params: req::SsrParams) -> Result { let _p = profile("handle_ssr"); - world.analysis().structural_search_replace(¶ms.arg)??.try_conv_with(&world) + world + .analysis() + .structural_search_replace(¶ms.query, params.parse_only)?? + .try_conv_with(&world) } pub fn publish_diagnostics(world: &WorldSnapshot, file_id: FileId) -> Result { diff --git a/crates/rust-analyzer/src/req.rs b/crates/rust-analyzer/src/req.rs index a3efe3b9feb..435f717aeb2 100644 --- a/crates/rust-analyzer/src/req.rs +++ b/crates/rust-analyzer/src/req.rs @@ -217,6 +217,8 @@ impl Request for Ssr { } #[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct SsrParams { - pub arg: String, + pub query: String, + pub parse_only: bool, } diff --git a/editors/code/src/commands/ssr.ts b/editors/code/src/commands/ssr.ts index eee48c69352..6fee051fdf6 100644 --- a/editors/code/src/commands/ssr.ts +++ b/editors/code/src/commands/ssr.ts @@ -10,20 +10,22 @@ export function ssr(ctx: Ctx): Cmd { if (!client) return; const options: vscode.InputBoxOptions = { - placeHolder: "foo($a:expr, $b:expr) ==>> bar($a, foo($b))", - prompt: "Enter request", - validateInput: (x: string) => { - if (x.includes('==>>')) { - return null; + value: "() ==>> ()", + prompt: "EnteR request, for example 'Foo($a:expr) ==> Foo::new($a)' ", + validateInput: async (x: string) => { + try { + await client.sendRequest(ra.ssr, { query: x, parseOnly: true }); + } catch (e) { + return e.toString(); } - return "Enter request: pattern ==>> template"; + return null; } }; const request = await vscode.window.showInputBox(options); if (!request) return; - const change = await client.sendRequest(ra.ssr, { arg: request }); + const change = await client.sendRequest(ra.ssr, { query: request, parseOnly: false }); await applySourceChange(ctx, change); }; diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts index bd6e3ada083..6ad93715f93 100644 --- a/editors/code/src/rust-analyzer-api.ts +++ b/editors/code/src/rust-analyzer-api.ts @@ -108,7 +108,8 @@ export const inlayHints = request>("inlayHints" export interface SsrParams { - arg: string; + query: string; + parseOnly: boolean; } export const ssr = request("ssr");