From 3975952601888d9f77e466c12e8e389748984b33 Mon Sep 17 00:00:00 2001 From: David Lattimore Date: Wed, 22 Jul 2020 15:00:28 +1000 Subject: SSR: Pass current file position through to SSR code. In a subsequent commit, it will be used for resolving paths. --- crates/ra_ide/src/lib.rs | 3 ++- crates/ra_ide/src/ssr.rs | 6 ++++-- crates/ra_ssr/src/lib.rs | 30 ++++++++++++++++++++++++-- crates/ra_ssr/src/matching.rs | 4 ++-- crates/ra_ssr/src/tests.rs | 42 ++++++++++++++++++++---------------- crates/rust-analyzer/src/cli/ssr.rs | 4 ++-- crates/rust-analyzer/src/handlers.rs | 3 ++- crates/rust-analyzer/src/lsp_ext.rs | 5 +++++ docs/dev/lsp-extensions.md | 7 +++++- editors/code/src/commands.ts | 14 +++++++++--- editors/code/src/lsp_ext.ts | 2 ++ 11 files changed, 88 insertions(+), 32 deletions(-) diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index dc9192d42..7356e947b 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs @@ -505,9 +505,10 @@ impl Analysis { &self, query: &str, parse_only: bool, + position: FilePosition, ) -> Cancelable> { self.with_db(|db| { - let edits = ssr::parse_search_replace(query, parse_only, db)?; + let edits = ssr::parse_search_replace(query, parse_only, db, position)?; Ok(SourceChange::from(edits)) }) } diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs index ca7e0ad86..3e2705d62 100644 --- a/crates/ra_ide/src/ssr.rs +++ b/crates/ra_ide/src/ssr.rs @@ -1,3 +1,4 @@ +use ra_db::FilePosition; use ra_ide_db::RootDatabase; use crate::SourceFileEdit; @@ -42,12 +43,13 @@ pub fn parse_search_replace( rule: &str, parse_only: bool, db: &RootDatabase, + position: FilePosition, ) -> Result, SsrError> { let rule: SsrRule = rule.parse()?; + let mut match_finder = MatchFinder::in_context(db, position); + match_finder.add_rule(rule); if parse_only { return Ok(Vec::new()); } - let mut match_finder = MatchFinder::new(db); - match_finder.add_rule(rule); Ok(match_finder.edits()) } diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs index 6d578610b..a0a5c9762 100644 --- a/crates/ra_ssr/src/lib.rs +++ b/crates/ra_ssr/src/lib.rs @@ -13,11 +13,12 @@ mod errors; #[cfg(test)] mod tests; +use crate::errors::bail; pub use crate::errors::SsrError; pub use crate::matching::Match; use crate::matching::MatchFailureReason; use hir::Semantics; -use ra_db::{FileId, FileRange}; +use ra_db::{FileId, FilePosition, FileRange}; use ra_ide_db::source_change::SourceFileEdit; use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; use rustc_hash::FxHashMap; @@ -51,10 +52,35 @@ pub struct MatchFinder<'db> { } impl<'db> MatchFinder<'db> { - pub fn new(db: &'db ra_ide_db::RootDatabase) -> MatchFinder<'db> { + /// Constructs a new instance where names will be looked up as if they appeared at + /// `lookup_context`. + pub fn in_context( + db: &'db ra_ide_db::RootDatabase, + _lookup_context: FilePosition, + ) -> MatchFinder<'db> { + // FIXME: Use lookup_context MatchFinder { sema: Semantics::new(db), rules: Vec::new() } } + /// Constructs an instance using the start of the first file in `db` as the lookup context. + pub fn at_first_file(db: &'db ra_ide_db::RootDatabase) -> Result, SsrError> { + use ra_db::SourceDatabaseExt; + use ra_ide_db::symbol_index::SymbolsDatabase; + if let Some(first_file_id) = db + .local_roots() + .iter() + .next() + .and_then(|root| db.source_root(root.clone()).iter().next()) + { + Ok(MatchFinder::in_context( + db, + FilePosition { file_id: first_file_id, offset: 0.into() }, + )) + } else { + bail!("No files to search"); + } + } + /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to /// match to it. diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs index 005569f6f..a43d57c34 100644 --- a/crates/ra_ssr/src/matching.rs +++ b/crates/ra_ssr/src/matching.rs @@ -576,8 +576,8 @@ mod tests { let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap(); let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }"; - let (db, _) = crate::tests::single_file(input); - let mut match_finder = MatchFinder::new(&db); + let (db, position) = crate::tests::single_file(input); + let mut match_finder = MatchFinder::in_context(&db, position); match_finder.add_rule(rule); let matches = match_finder.matches(); assert_eq!(matches.matches.len(), 1); diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs index 523035b31..63d527894 100644 --- a/crates/ra_ssr/src/tests.rs +++ b/crates/ra_ssr/src/tests.rs @@ -1,6 +1,6 @@ use crate::{MatchFinder, SsrRule}; use expect::{expect, Expect}; -use ra_db::{salsa::Durability, FileId, SourceDatabaseExt}; +use ra_db::{salsa::Durability, FileId, FilePosition, SourceDatabaseExt}; use rustc_hash::FxHashSet; use std::sync::Arc; use test_utils::mark; @@ -59,15 +59,21 @@ fn parser_undefined_placeholder_in_replacement() { ); } -pub(crate) fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FileId) { +/// `code` may optionally contain a cursor marker `<|>`. If it doesn't, then the position will be +/// the start of the file. +pub(crate) fn single_file(code: &str) -> (ra_ide_db::RootDatabase, FilePosition) { use ra_db::fixture::WithFixture; use ra_ide_db::symbol_index::SymbolsDatabase; - let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(code); - let mut db = db; + let (mut db, position) = if code.contains(test_utils::CURSOR_MARKER) { + ra_ide_db::RootDatabase::with_position(code) + } else { + let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(code); + (db, FilePosition { file_id, offset: 0.into() }) + }; let mut local_roots = FxHashSet::default(); local_roots.insert(ra_db::fixture::WORKSPACE); db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH); - (db, file_id) + (db, position) } fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) { @@ -75,8 +81,8 @@ fn assert_ssr_transform(rule: &str, input: &str, expected: Expect) { } fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) { - let (db, file_id) = single_file(input); - let mut match_finder = MatchFinder::new(&db); + let (db, position) = single_file(input); + let mut match_finder = MatchFinder::in_context(&db, position); for rule in rules { let rule: SsrRule = rule.parse().unwrap(); match_finder.add_rule(rule); @@ -85,10 +91,10 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) { if edits.is_empty() { panic!("No edits were made"); } - assert_eq!(edits[0].file_id, file_id); + assert_eq!(edits[0].file_id, position.file_id); // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters // stuff. - let mut actual = db.file_text(file_id).to_string(); + let mut actual = db.file_text(position.file_id).to_string(); edits[0].edit.apply(&mut actual); expected.assert_eq(&actual); } @@ -106,34 +112,34 @@ fn print_match_debug_info(match_finder: &MatchFinder, file_id: FileId, snippet: } fn assert_matches(pattern: &str, code: &str, expected: &[&str]) { - let (db, file_id) = single_file(code); - let mut match_finder = MatchFinder::new(&db); + let (db, position) = single_file(code); + let mut match_finder = MatchFinder::in_context(&db, position); match_finder.add_search_pattern(pattern.parse().unwrap()); let matched_strings: Vec = match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect(); if matched_strings != expected && !expected.is_empty() { - print_match_debug_info(&match_finder, file_id, &expected[0]); + print_match_debug_info(&match_finder, position.file_id, &expected[0]); } assert_eq!(matched_strings, expected); } fn assert_no_match(pattern: &str, code: &str) { - let (db, file_id) = single_file(code); - let mut match_finder = MatchFinder::new(&db); + let (db, position) = single_file(code); + let mut match_finder = MatchFinder::in_context(&db, position); match_finder.add_search_pattern(pattern.parse().unwrap()); let matches = match_finder.matches().flattened().matches; if !matches.is_empty() { - print_match_debug_info(&match_finder, file_id, &matches[0].matched_text()); + print_match_debug_info(&match_finder, position.file_id, &matches[0].matched_text()); panic!("Got {} matches when we expected none: {:#?}", matches.len(), matches); } } fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { - let (db, file_id) = single_file(code); - let mut match_finder = MatchFinder::new(&db); + let (db, position) = single_file(code); + let mut match_finder = MatchFinder::in_context(&db, position); match_finder.add_search_pattern(pattern.parse().unwrap()); let mut reasons = Vec::new(); - for d in match_finder.debug_where_text_equal(file_id, snippet) { + for d in match_finder.debug_where_text_equal(position.file_id, snippet) { if let Some(reason) = d.match_failure_reason() { reasons.push(reason.to_owned()); } diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs index 014bc70a4..22f5b4be0 100644 --- a/crates/rust-analyzer/src/cli/ssr.rs +++ b/crates/rust-analyzer/src/cli/ssr.rs @@ -7,7 +7,7 @@ pub fn apply_ssr_rules(rules: Vec) -> Result<()> { use ra_db::SourceDatabaseExt; let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?; let db = host.raw_database(); - let mut match_finder = MatchFinder::new(db); + let mut match_finder = MatchFinder::at_first_file(db)?; for rule in rules { match_finder.add_rule(rule); } @@ -30,7 +30,7 @@ pub fn search_for_patterns(patterns: Vec, debug_snippet: Option Result { let _p = profile("handle_ssr"); + let position = from_proto::file_position(&snap, params.position)?; let source_change = - snap.analysis.structural_search_replace(¶ms.query, params.parse_only)??; + snap.analysis.structural_search_replace(¶ms.query, params.parse_only, position)??; to_proto::workspace_edit(&snap, source_change) } diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index 13ebb18fb..113e0e070 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs @@ -216,6 +216,11 @@ impl Request for Ssr { pub struct SsrParams { pub query: String, pub parse_only: bool, + + /// File position where SSR was invoked. Paths in `query` will be resolved relative to this + /// position. + #[serde(flatten)] + pub position: lsp_types::TextDocumentPositionParams, } pub enum StatusNotification {} diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 98d14450b..1be01fd88 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -274,6 +274,11 @@ interface SsrParams { query: string, /// If true, only check the syntax of the query and don't compute the actual edit. parseOnly: bool, + /// The current text document. This and `position` will be used to determine in what scope + /// paths in `query` should be resolved. + textDocument: lc.TextDocumentIdentifier; + /// Position where SSR was invoked. + position: lc.Position; } ``` @@ -285,7 +290,7 @@ WorkspaceEdit ### Example -SSR with query `foo($a:expr, $b:expr) ==>> ($a).foo($b)` will transform, eg `foo(y + 5, z)` into `(y + 5).foo(z)`. +SSR with query `foo($a, $b) ==>> ($a).foo($b)` will transform, eg `foo(y + 5, z)` into `(y + 5).foo(z)`. ### Unresolved Question diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 1f3a7cf7e..3ae995705 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -185,15 +185,21 @@ export function parentModule(ctx: Ctx): Cmd { export function ssr(ctx: Ctx): Cmd { return async () => { + const editor = vscode.window.activeTextEditor; const client = ctx.client; - if (!client) return; + if (!editor || !client) return; + + const position = editor.selection.active; + let textDocument = { uri: editor.document.uri.toString() }; const options: vscode.InputBoxOptions = { value: "() ==>> ()", prompt: "Enter request, for example 'Foo($a) ==> Foo::new($a)' ", validateInput: async (x: string) => { try { - await client.sendRequest(ra.ssr, { query: x, parseOnly: true }); + await client.sendRequest(ra.ssr, { + query: x, parseOnly: true, textDocument, position, + }); } catch (e) { return e.toString(); } @@ -208,7 +214,9 @@ export function ssr(ctx: Ctx): Cmd { title: "Structured search replace in progress...", cancellable: false, }, async (_progress, _token) => { - const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false }); + const edit = await client.sendRequest(ra.ssr, { + query: request, parseOnly: false, textDocument, position + }); await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit)); }); diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index 5f32cb40e..149f9a0d6 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts @@ -93,6 +93,8 @@ export const inlayHints = new lc.RequestType('experimental/ssr'); -- cgit v1.2.3