diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index f83ed65d5a6..d1a250d4874 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -41,6 +41,7 @@ macro_rules! eprintln { mod references; mod fn_references; mod runnables; +mod ssr; mod status; mod syntax_highlighting; mod syntax_tree; @@ -51,6 +52,7 @@ macro_rules! eprintln { use std::sync::Arc; use cfg::CfgOptions; + use ide_db::base_db::{ salsa::{self, ParallelDatabase}, CheckCanceled, Env, FileLoader, FileSet, SourceDatabase, VfsPath, @@ -502,7 +504,11 @@ pub fn assists( resolve: bool, frange: FileRange, ) -> Cancelable> { - self.with_db(|db| Assist::get(db, config, resolve, frange)) + self.with_db(|db| { + let mut acc = Assist::get(db, config, resolve, frange); + ssr::add_ssr_assist(db, &mut acc, resolve, frange); + acc + }) } /// Computes the set of diagnostics for the given file. diff --git a/crates/ide/src/ssr.rs b/crates/ide/src/ssr.rs new file mode 100644 index 00000000000..f3638d928c0 --- /dev/null +++ b/crates/ide/src/ssr.rs @@ -0,0 +1,259 @@ +//! This module provides an SSR assist. It is not desirable to include this +//! assist in ide_assists because that would require the ide_assists crate +//! depend on the ide_ssr crate. + +use ide_assists::{Assist, AssistId, AssistKind, GroupLabel}; +use ide_db::{base_db::FileRange, label::Label, source_change::SourceChange, RootDatabase}; + +pub(crate) fn add_ssr_assist( + db: &RootDatabase, + base: &mut Vec, + resolve: bool, + frange: FileRange, +) -> Option<()> { + let (match_finder, comment_range) = ide_ssr::ssr_from_comment(db, frange)?; + + let (source_change_for_file, source_change_for_workspace) = if resolve { + let edits = match_finder.edits(); + + let source_change_for_file = { + let text_edit_for_file = edits.get(&frange.file_id).cloned().unwrap_or_default(); + SourceChange::from_text_edit(frange.file_id, text_edit_for_file) + }; + + let source_change_for_workspace = SourceChange::from(match_finder.edits()); + + (Some(source_change_for_file), Some(source_change_for_workspace)) + } else { + (None, None) + }; + + let assists = vec![ + ("Apply SSR in file", source_change_for_file), + ("Apply SSR in workspace", source_change_for_workspace), + ]; + + for (label, source_change) in assists.into_iter() { + let assist = Assist { + id: AssistId("ssr", AssistKind::RefactorRewrite), + label: Label::new(label), + group: Some(GroupLabel("Apply SSR".into())), + target: comment_range, + source_change, + }; + + base.push(assist); + } + Some(()) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use expect_test::expect; + use ide_assists::Assist; + use ide_db::{ + base_db::{fixture::WithFixture, salsa::Durability, FileRange}, + symbol_index::SymbolsDatabase, + RootDatabase, + }; + use rustc_hash::FxHashSet; + + use super::add_ssr_assist; + + fn get_assists(ra_fixture: &str, resolve: bool) -> Vec { + let (mut db, file_id, range_or_offset) = RootDatabase::with_range_or_offset(ra_fixture); + let mut local_roots = FxHashSet::default(); + local_roots.insert(ide_db::base_db::fixture::WORKSPACE); + db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH); + + let mut assists = vec![]; + + add_ssr_assist( + &db, + &mut assists, + resolve, + FileRange { file_id, range: range_or_offset.into() }, + ); + + assists + } + + #[test] + fn not_applicable_comment_not_ssr() { + let ra_fixture = r#" + //- /lib.rs + + // This is foo $0 + fn foo() {} + "#; + let resolve = true; + + let assists = get_assists(ra_fixture, resolve); + + assert_eq!(0, assists.len()); + } + + #[test] + fn resolve_edits_true() { + let resolve = true; + let assists = get_assists( + r#" + //- /lib.rs + mod bar; + + // 2 ==>> 3$0 + fn foo() { 2 } + + //- /bar.rs + fn bar() { 2 } + "#, + resolve, + ); + + assert_eq!(2, assists.len()); + let mut assists = assists.into_iter(); + + let apply_in_file_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in file", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: Some( + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "3", + delete: 33..34, + }, + ], + }, + }, + file_system_edits: [], + is_snippet: false, + }, + ), + } + "#]] + .assert_debug_eq(&apply_in_file_assist); + + let apply_in_workspace_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in workspace", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: Some( + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "3", + delete: 33..34, + }, + ], + }, + FileId( + 1, + ): TextEdit { + indels: [ + Indel { + insert: "3", + delete: 11..12, + }, + ], + }, + }, + file_system_edits: [], + is_snippet: false, + }, + ), + } + "#]] + .assert_debug_eq(&apply_in_workspace_assist); + } + + #[test] + fn resolve_edits_false() { + let resolve = false; + let assists = get_assists( + r#" + //- /lib.rs + mod bar; + + // 2 ==>> 3$0 + fn foo() { 2 } + + //- /bar.rs + fn bar() { 2 } + "#, + resolve, + ); + + assert_eq!(2, assists.len()); + let mut assists = assists.into_iter(); + + let apply_in_file_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in file", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: None, + } + "#]] + .assert_debug_eq(&apply_in_file_assist); + + let apply_in_workspace_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in workspace", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: None, + } + "#]] + .assert_debug_eq(&apply_in_workspace_assist); + } +} diff --git a/crates/ide_ssr/src/from_comment.rs b/crates/ide_ssr/src/from_comment.rs new file mode 100644 index 00000000000..f1b312284d0 --- /dev/null +++ b/crates/ide_ssr/src/from_comment.rs @@ -0,0 +1,32 @@ +//! This module allows building an SSR MatchFinder by parsing the SSR rule +//! from a comment. + +use ide_db::{ + base_db::{FilePosition, FileRange, SourceDatabase}, + RootDatabase, +}; +use syntax::{ + ast::{self, AstNode, AstToken}, + TextRange, +}; + +use crate::MatchFinder; + +/// Attempts to build an SSR MatchFinder from a comment at the given file +/// range. If successful, returns the MatchFinder and a TextRange covering +/// comment. +pub fn ssr_from_comment(db: &RootDatabase, frange: FileRange) -> Option<(MatchFinder, TextRange)> { + let comment = { + let file = db.parse(frange.file_id); + file.tree().syntax().token_at_offset(frange.range.start()).find_map(ast::Comment::cast) + }?; + let comment_text_without_prefix = comment.text().strip_prefix(comment.prefix()).unwrap(); + let ssr_rule = comment_text_without_prefix.parse().ok()?; + + let lookup_context = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; + + let mut match_finder = MatchFinder::in_context(db, lookup_context, vec![]); + match_finder.add_rule(ssr_rule).ok()?; + + Some((match_finder, comment.syntax().text_range())) +} diff --git a/crates/ide_ssr/src/lib.rs b/crates/ide_ssr/src/lib.rs index a97fc8bcaca..e72c611a3e2 100644 --- a/crates/ide_ssr/src/lib.rs +++ b/crates/ide_ssr/src/lib.rs @@ -58,6 +58,7 @@ // | VS Code | **Rust Analyzer: Structural Search Replace** // |=== +mod from_comment; mod matching; mod nester; mod parsing; @@ -71,6 +72,7 @@ use crate::errors::bail; pub use crate::errors::SsrError; +pub use crate::from_comment::ssr_from_comment; pub use crate::matching::Match; use crate::matching::MatchFailureReason; use hir::Semantics;