Merge #7874
7874: add apply ssr assist r=JoshMcguigan a=JoshMcguigan This PR adds an `Apply SSR` assist which was briefly mentioned in #3186. It allows writing an ssr rule as a comment, and then applying that SSR via an assist. This workflow is much nicer than the default available via `coc-rust-analyzer` when iterating to find the proper replacement. As currently implemented, this requires the ssr rule is written as a single line in the comment, and it doesn't require any kind of prefix. Anything which properly parses as a ssr rule will enable the assist. The benefit of requiring the ssr rule be on a single line is it allows for a workflow where the user has several rules written one after the other, possibly to be triggered in order, without having to try to parse multiple lines of text and determine where one rule ends and the next begins. The benefit of not requiring a prefix is less typing 😆 - plus, I think the chance of something accidentally parsing as an ssr rule is minimal. I think a reasonable extension of this would be to allow either any ssr rule that fits on a single line, or any comment block which in its entirety makes up a single ssr rule (parsing a comment block containing multiple ssr rules and running them all would break the use case that currently works where a user writes multiple ssr rules then runs them each one by one in arbitrary order). I've marked this as a draft because for some reason I am strugging to make the unit tests pass. It does work when I compile rust-analyzer and test it in my editor though, so I'm not sure what is going on. Co-authored-by: Josh Mcguigan <joshmcg88@gmail.com>
This commit is contained in:
commit
9dc13408f9
@ -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<Vec<Assist>> {
|
||||
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.
|
||||
|
259
crates/ide/src/ssr.rs
Normal file
259
crates/ide/src/ssr.rs
Normal file
@ -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<Assist>,
|
||||
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<Assist> {
|
||||
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);
|
||||
}
|
||||
}
|
32
crates/ide_ssr/src/from_comment.rs
Normal file
32
crates/ide_ssr/src/from_comment.rs
Normal file
@ -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()))
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user