SSR: Pass current file position through to SSR code.

In a subsequent commit, it will be used for resolving paths.
This commit is contained in:
David Lattimore 2020-07-22 15:00:28 +10:00
parent 02fc3d50ee
commit 3975952601
11 changed files with 88 additions and 32 deletions

View file

@ -505,9 +505,10 @@ impl Analysis {
&self, &self,
query: &str, query: &str,
parse_only: bool, parse_only: bool,
position: FilePosition,
) -> Cancelable<Result<SourceChange, SsrError>> { ) -> Cancelable<Result<SourceChange, SsrError>> {
self.with_db(|db| { 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)) Ok(SourceChange::from(edits))
}) })
} }

View file

@ -1,3 +1,4 @@
use ra_db::FilePosition;
use ra_ide_db::RootDatabase; use ra_ide_db::RootDatabase;
use crate::SourceFileEdit; use crate::SourceFileEdit;
@ -42,12 +43,13 @@ pub fn parse_search_replace(
rule: &str, rule: &str,
parse_only: bool, parse_only: bool,
db: &RootDatabase, db: &RootDatabase,
position: FilePosition,
) -> Result<Vec<SourceFileEdit>, SsrError> { ) -> Result<Vec<SourceFileEdit>, SsrError> {
let rule: SsrRule = rule.parse()?; let rule: SsrRule = rule.parse()?;
let mut match_finder = MatchFinder::in_context(db, position);
match_finder.add_rule(rule);
if parse_only { if parse_only {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let mut match_finder = MatchFinder::new(db);
match_finder.add_rule(rule);
Ok(match_finder.edits()) Ok(match_finder.edits())
} }

View file

@ -13,11 +13,12 @@ mod errors;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
use crate::errors::bail;
pub use crate::errors::SsrError; pub use crate::errors::SsrError;
pub use crate::matching::Match; pub use crate::matching::Match;
use crate::matching::MatchFailureReason; use crate::matching::MatchFailureReason;
use hir::Semantics; use hir::Semantics;
use ra_db::{FileId, FileRange}; use ra_db::{FileId, FilePosition, FileRange};
use ra_ide_db::source_change::SourceFileEdit; use ra_ide_db::source_change::SourceFileEdit;
use ra_syntax::{ast, AstNode, SyntaxNode, TextRange}; use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -51,10 +52,35 @@ pub struct MatchFinder<'db> {
} }
impl<'db> 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() } 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<MatchFinder<'db>, 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 /// 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 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
/// match to it. /// match to it.

View file

@ -576,8 +576,8 @@ mod tests {
let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap(); let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap();
let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }"; let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }";
let (db, _) = crate::tests::single_file(input); let (db, position) = crate::tests::single_file(input);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::in_context(&db, position);
match_finder.add_rule(rule); match_finder.add_rule(rule);
let matches = match_finder.matches(); let matches = match_finder.matches();
assert_eq!(matches.matches.len(), 1); assert_eq!(matches.matches.len(), 1);

View file

@ -1,6 +1,6 @@
use crate::{MatchFinder, SsrRule}; use crate::{MatchFinder, SsrRule};
use expect::{expect, Expect}; use expect::{expect, Expect};
use ra_db::{salsa::Durability, FileId, SourceDatabaseExt}; use ra_db::{salsa::Durability, FileId, FilePosition, SourceDatabaseExt};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use std::sync::Arc; use std::sync::Arc;
use test_utils::mark; 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_db::fixture::WithFixture;
use ra_ide_db::symbol_index::SymbolsDatabase; use ra_ide_db::symbol_index::SymbolsDatabase;
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); let (db, file_id) = ra_ide_db::RootDatabase::with_single_file(code);
let mut db = db; (db, FilePosition { file_id, offset: 0.into() })
};
let mut local_roots = FxHashSet::default(); let mut local_roots = FxHashSet::default();
local_roots.insert(ra_db::fixture::WORKSPACE); local_roots.insert(ra_db::fixture::WORKSPACE);
db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH); 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) { 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) { fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
let (db, file_id) = single_file(input); let (db, position) = single_file(input);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::in_context(&db, position);
for rule in rules { for rule in rules {
let rule: SsrRule = rule.parse().unwrap(); let rule: SsrRule = rule.parse().unwrap();
match_finder.add_rule(rule); match_finder.add_rule(rule);
@ -85,10 +91,10 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
if edits.is_empty() { if edits.is_empty() {
panic!("No edits were made"); 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 // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters
// stuff. // 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); edits[0].edit.apply(&mut actual);
expected.assert_eq(&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]) { fn assert_matches(pattern: &str, code: &str, expected: &[&str]) {
let (db, file_id) = single_file(code); let (db, position) = single_file(code);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::in_context(&db, position);
match_finder.add_search_pattern(pattern.parse().unwrap()); match_finder.add_search_pattern(pattern.parse().unwrap());
let matched_strings: Vec<String> = let matched_strings: Vec<String> =
match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect(); match_finder.matches().flattened().matches.iter().map(|m| m.matched_text()).collect();
if matched_strings != expected && !expected.is_empty() { 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); assert_eq!(matched_strings, expected);
} }
fn assert_no_match(pattern: &str, code: &str) { fn assert_no_match(pattern: &str, code: &str) {
let (db, file_id) = single_file(code); let (db, position) = single_file(code);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::in_context(&db, position);
match_finder.add_search_pattern(pattern.parse().unwrap()); match_finder.add_search_pattern(pattern.parse().unwrap());
let matches = match_finder.matches().flattened().matches; let matches = match_finder.matches().flattened().matches;
if !matches.is_empty() { 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); panic!("Got {} matches when we expected none: {:#?}", matches.len(), matches);
} }
} }
fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) { fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) {
let (db, file_id) = single_file(code); let (db, position) = single_file(code);
let mut match_finder = MatchFinder::new(&db); let mut match_finder = MatchFinder::in_context(&db, position);
match_finder.add_search_pattern(pattern.parse().unwrap()); match_finder.add_search_pattern(pattern.parse().unwrap());
let mut reasons = Vec::new(); 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() { if let Some(reason) = d.match_failure_reason() {
reasons.push(reason.to_owned()); reasons.push(reason.to_owned());
} }

View file

@ -7,7 +7,7 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> {
use ra_db::SourceDatabaseExt; use ra_db::SourceDatabaseExt;
let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?; let (host, vfs) = load_cargo(&std::env::current_dir()?, true, true)?;
let db = host.raw_database(); 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 { for rule in rules {
match_finder.add_rule(rule); match_finder.add_rule(rule);
} }
@ -30,7 +30,7 @@ pub fn search_for_patterns(patterns: Vec<SsrPattern>, debug_snippet: Option<Stri
use ra_ide_db::symbol_index::SymbolsDatabase; use ra_ide_db::symbol_index::SymbolsDatabase;
let (host, _vfs) = load_cargo(&std::env::current_dir()?, true, true)?; let (host, _vfs) = load_cargo(&std::env::current_dir()?, true, true)?;
let db = host.raw_database(); let db = host.raw_database();
let mut match_finder = MatchFinder::new(db); let mut match_finder = MatchFinder::at_first_file(db)?;
for pattern in patterns { for pattern in patterns {
match_finder.add_search_pattern(pattern); match_finder.add_search_pattern(pattern);
} }

View file

@ -1026,8 +1026,9 @@ pub(crate) fn handle_ssr(
params: lsp_ext::SsrParams, params: lsp_ext::SsrParams,
) -> Result<lsp_types::WorkspaceEdit> { ) -> Result<lsp_types::WorkspaceEdit> {
let _p = profile("handle_ssr"); let _p = profile("handle_ssr");
let position = from_proto::file_position(&snap, params.position)?;
let source_change = let source_change =
snap.analysis.structural_search_replace(&params.query, params.parse_only)??; snap.analysis.structural_search_replace(&params.query, params.parse_only, position)??;
to_proto::workspace_edit(&snap, source_change) to_proto::workspace_edit(&snap, source_change)
} }

View file

@ -216,6 +216,11 @@ impl Request for Ssr {
pub struct SsrParams { pub struct SsrParams {
pub query: String, pub query: String,
pub parse_only: bool, 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 {} pub enum StatusNotification {}

View file

@ -274,6 +274,11 @@ interface SsrParams {
query: string, query: string,
/// If true, only check the syntax of the query and don't compute the actual edit. /// If true, only check the syntax of the query and don't compute the actual edit.
parseOnly: bool, 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 ### 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 ### Unresolved Question

View file

@ -185,15 +185,21 @@ export function parentModule(ctx: Ctx): Cmd {
export function ssr(ctx: Ctx): Cmd { export function ssr(ctx: Ctx): Cmd {
return async () => { return async () => {
const editor = vscode.window.activeTextEditor;
const client = ctx.client; 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 = { const options: vscode.InputBoxOptions = {
value: "() ==>> ()", value: "() ==>> ()",
prompt: "Enter request, for example 'Foo($a) ==> Foo::new($a)' ", prompt: "Enter request, for example 'Foo($a) ==> Foo::new($a)' ",
validateInput: async (x: string) => { validateInput: async (x: string) => {
try { try {
await client.sendRequest(ra.ssr, { query: x, parseOnly: true }); await client.sendRequest(ra.ssr, {
query: x, parseOnly: true, textDocument, position,
});
} catch (e) { } catch (e) {
return e.toString(); return e.toString();
} }
@ -208,7 +214,9 @@ export function ssr(ctx: Ctx): Cmd {
title: "Structured search replace in progress...", title: "Structured search replace in progress...",
cancellable: false, cancellable: false,
}, async (_progress, _token) => { }, 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)); await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
}); });

View file

@ -93,6 +93,8 @@ export const inlayHints = new lc.RequestType<InlayHintsParams, InlayHint[], void
export interface SsrParams { export interface SsrParams {
query: string; query: string;
parseOnly: boolean; parseOnly: boolean;
textDocument: lc.TextDocumentIdentifier;
position: lc.Position;
} }
export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr'); export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr');