Add move_const_to_impl assist

This commit is contained in:
Ryo Yoshida 2022-11-22 18:36:58 +09:00
parent 398a71affb
commit 6d4538734e
No known key found for this signature in database
GPG key ID: E25698A930586171
4 changed files with 529 additions and 0 deletions

View file

@ -0,0 +1,481 @@
use hir::{AsAssocItem, AssocItemContainer, HasCrate, HasSource};
use ide_db::{assists::AssistId, base_db::FileRange, defs::Definition, search::SearchScope};
use syntax::{
ast::{self, edit::IndentLevel, edit_in_place::Indent, AstNode},
SyntaxKind,
};
use crate::{
assist_context::{AssistContext, Assists},
utils,
};
// NOTE: Code may break if the self type implements a trait that has associated const with the same
// name, but it's pretty expensive to check that (`hir::Impl::all_for_type()`) and we assume that's
// pretty rare case.
// Assist: move_const_to_impl
//
// Move a local constant item in a method to impl's associated constant. All the references will be
// qualified with `Self::`.
//
// ```
// struct S;
// impl S {
// fn foo() -> usize {
// /// The answer.
// const C$0: usize = 42;
//
// C * C
// }
// }
// ```
// ->
// ```
// struct S;
// impl S {
// /// The answer.
// const C: usize = 42;
//
// fn foo() -> usize {
// Self::C * Self::C
// }
// }
// ```
pub(crate) fn move_const_to_impl(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let db = ctx.db();
let const_: ast::Const = ctx.find_node_at_offset()?;
// Don't show the assist when the cursor is at the const's body.
if let Some(body) = const_.body() {
if body.syntax().text_range().contains(ctx.offset()) {
return None;
}
}
let parent_fn = const_.syntax().ancestors().find_map(ast::Fn::cast)?;
// NOTE: We can technically provide this assist for default methods in trait definitions, but
// it's somewhat complex to handle it correctly when the const's name conflicts with
// supertrait's item. We may want to consider implementing it in the future.
let AssocItemContainer::Impl(impl_) = ctx.sema.to_def(&parent_fn)?.as_assoc_item(db)?.container(db) else { return None; };
if impl_.trait_(db).is_some() {
return None;
}
let def = ctx.sema.to_def(&const_)?;
let name = def.name(db)?;
let items = impl_.source(db)?.value.assoc_item_list()?;
let ty = impl_.self_ty(db);
// If there exists another associated item with the same name, skip the assist.
if ty
.iterate_assoc_items(db, ty.krate(db), |assoc| {
// Type aliases wouldn't conflict due to different namespaces, but we're only checking
// the items in inherent impls, so we assume `assoc` is never type alias for the sake
// of brevity (inherent associated types exist in nightly Rust, but it's *very*
// unstable and we don't support them either).
assoc.name(db).filter(|it| it == &name)
})
.is_some()
{
return None;
}
let usages =
Definition::Const(def).usages(&ctx.sema).in_scope(SearchScope::file_range(FileRange {
file_id: ctx.file_id(),
range: parent_fn.syntax().text_range(),
}));
acc.add(
AssistId("move_const_to_impl", crate::AssistKind::RefactorRewrite),
"Move const to impl block",
const_.syntax().text_range(),
|builder| {
let range_to_delete = match const_.syntax().next_sibling_or_token() {
Some(s) if matches!(s.kind(), SyntaxKind::WHITESPACE) => {
// Remove following whitespaces too.
const_.syntax().text_range().cover(s.text_range())
}
_ => const_.syntax().text_range(),
};
builder.delete(range_to_delete);
let const_ref = format!("Self::{name}");
for range in usages.all().file_ranges().map(|it| it.range) {
builder.replace(range, const_ref.clone());
}
// Heuristically inserting the extracted const after the consecutive existing consts
// from the beginning of assoc items. We assume there are no inherent assoc type as
// above.
let last_const =
items.assoc_items().take_while(|it| matches!(it, ast::AssocItem::Const(_))).last();
let insert_offset = match &last_const {
Some(it) => it.syntax().text_range().end(),
None => match items.l_curly_token() {
Some(l_curly) => l_curly.text_range().end(),
// Not sure if this branch is ever reachable, but it wouldn't hurt to have a
// fallback.
None => items.syntax().text_range().start(),
},
};
// If the moved const will be the first item of the impl, add a new line after that.
//
// We're assuming the code is formatted according to Rust's standard style guidelines
// (i.e. no empty lines between impl's `{` token and its first assoc item).
let fixup = if last_const.is_none() { "\n" } else { "" };
let indent = IndentLevel::from_node(parent_fn.syntax());
let const_ = const_.clone_for_update();
const_.reindent_to(indent);
let mut const_text = format!("\n{indent}{const_}{fixup}");
utils::escape_non_snippet(&mut const_text);
builder.insert(insert_offset, const_text);
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn not_applicable_to_top_level_const() {
check_assist_not_applicable(
move_const_to_impl,
r#"
const C$0: () = ();
"#,
);
}
#[test]
fn not_applicable_to_free_fn() {
check_assist_not_applicable(
move_const_to_impl,
r#"
fn f() {
const C$0: () = ();
}
"#,
);
}
#[test]
fn not_applicable_when_at_const_body() {
check_assist_not_applicable(
move_const_to_impl,
r#"
struct S;
impl S {
fn f() {
const C: () = ($0);
}
}
"#,
);
}
#[test]
fn not_applicable_when_inside_const_body_block() {
check_assist_not_applicable(
move_const_to_impl,
r#"
struct S;
impl S {
fn f() {
const C: () = {
($0)
};
}
}
"#,
);
}
#[test]
fn not_applicable_to_trait_impl_fn() {
check_assist_not_applicable(
move_const_to_impl,
r#"
trait Trait {
fn f();
}
impl Trait for () {
fn f() {
const C$0: () = ();
}
}
"#,
);
}
#[test]
fn not_applicable_to_non_assoc_fn_inside_impl() {
check_assist_not_applicable(
move_const_to_impl,
r#"
struct S;
impl S {
fn f() {
fn g() {
const C$0: () = ();
}
}
}
"#,
);
}
#[test]
fn not_applicable_when_const_with_same_name_exists() {
check_assist_not_applicable(
move_const_to_impl,
r#"
struct S;
impl S {
const C: usize = 42;
fn f() {
const C$0: () = ();
}
"#,
);
check_assist_not_applicable(
move_const_to_impl,
r#"
struct S;
impl S {
const C: usize = 42;
}
impl S {
fn f() {
const C$0: () = ();
}
"#,
);
}
#[test]
fn move_const_simple_body() {
check_assist(
move_const_to_impl,
r#"
struct S;
impl S {
fn f() -> usize {
/// doc comment
const C$0: usize = 42;
C * C
}
}
"#,
r#"
struct S;
impl S {
/// doc comment
const C: usize = 42;
fn f() -> usize {
Self::C * Self::C
}
}
"#,
);
}
#[test]
fn move_const_simple_body_existing_const() {
check_assist(
move_const_to_impl,
r#"
struct S;
impl S {
const X: () = ();
const Y: () = ();
fn f() -> usize {
/// doc comment
const C$0: usize = 42;
C * C
}
}
"#,
r#"
struct S;
impl S {
const X: () = ();
const Y: () = ();
/// doc comment
const C: usize = 42;
fn f() -> usize {
Self::C * Self::C
}
}
"#,
);
}
#[test]
fn move_const_block_body() {
check_assist(
move_const_to_impl,
r#"
struct S;
impl S {
fn f() -> usize {
/// doc comment
const C$0: usize = {
let a = 3;
let b = 4;
a * b
};
C * C
}
}
"#,
r#"
struct S;
impl S {
/// doc comment
const C: usize = {
let a = 3;
let b = 4;
a * b
};
fn f() -> usize {
Self::C * Self::C
}
}
"#,
);
}
#[test]
fn correct_indent_when_nested() {
check_assist(
move_const_to_impl,
r#"
fn main() {
struct S;
impl S {
fn f() -> usize {
/// doc comment
const C$0: usize = 42;
C * C
}
}
}
"#,
r#"
fn main() {
struct S;
impl S {
/// doc comment
const C: usize = 42;
fn f() -> usize {
Self::C * Self::C
}
}
}
"#,
)
}
#[test]
fn move_const_in_nested_scope_with_same_name_in_other_scope() {
check_assist(
move_const_to_impl,
r#"
struct S;
impl S {
fn f() -> usize {
const C: &str = "outer";
let n = {
/// doc comment
const C$0: usize = 42;
let m = {
const C: &str = "inner";
C.len()
};
C * m
};
n + C.len()
}
}
"#,
r#"
struct S;
impl S {
/// doc comment
const C: usize = 42;
fn f() -> usize {
const C: &str = "outer";
let n = {
let m = {
const C: &str = "inner";
C.len()
};
Self::C * m
};
n + C.len()
}
}
"#,
);
}
#[test]
fn moved_const_body_is_escaped() {
// Note that the last argument is what *lsp clients would see* rather than
// what users would see. Unescaping happens thereafter.
check_assist(
move_const_to_impl,
r#"
struct S;
impl S {
fn f() -> usize {
/// doc comment
/// \\
/// ${snippet}
const C$0: &str = "\ and $1";
C.len()
}
}
"#,
r#"
struct S;
impl S {
/// doc comment
/// \\\\
/// \${snippet}
const C: &str = "\\ and \$1";
fn f() -> usize {
Self::C.len()
}
}
"#,
)
}
}

View file

@ -165,6 +165,7 @@ mod handlers {
mod merge_imports;
mod merge_match_arms;
mod move_bounds;
mod move_const_to_impl;
mod move_guard;
mod move_module_to_file;
mod move_to_mod_rs;
@ -261,6 +262,7 @@ mod handlers {
merge_imports::merge_imports,
merge_match_arms::merge_match_arms,
move_bounds::move_bounds_to_where_clause,
move_const_to_impl::move_const_to_impl,
move_format_string_arg::move_format_string_arg,
move_guard::move_arm_cond_to_match_guard,
move_guard::move_guard_to_arm_body,

View file

@ -1674,6 +1674,35 @@ fn apply<T, U, F>(f: F, x: T) -> U where F: FnOnce(T) -> U {
)
}
#[test]
fn doctest_move_const_to_impl() {
check_doc_test(
"move_const_to_impl",
r#####"
struct S;
impl S {
fn foo() -> usize {
/// The answer.
const C$0: usize = 42;
C * C
}
}
"#####,
r#####"
struct S;
impl S {
/// The answer.
const C: usize = 42;
fn foo() -> usize {
Self::C * Self::C
}
}
"#####,
)
}
#[test]
fn doctest_move_format_string_arg() {
check_doc_test(

View file

@ -208,6 +208,23 @@ pub(crate) fn render_snippet(_cap: SnippetCap, node: &SyntaxNode, cursor: Cursor
}
}
/// Escapes text that should be rendered as-is, typically those that we're copy-pasting what the
/// users wrote.
///
/// This function should only be used when the text doesn't contain snippet **AND** the text
/// wouldn't be included in a snippet.
pub(crate) fn escape_non_snippet(text: &mut String) {
// While we *can* escape `}`, we don't really have to in this specific case. We only need to
// escape it inside `${}` to disambiguate it from the ending token of the syntax, but after we
// escape every occurrence of `$`, we wouldn't have `${}` in the first place.
//
// This will break if the text contains snippet or it will be included in a snippet (hence doc
// comment). Compare `fn escape(buf)` in `render_snippet()` above, where the escaped text is
// included in a snippet.
stdx::replace(text, '\\', r"\\");
stdx::replace(text, '$', r"\$");
}
pub(crate) fn vis_offset(node: &SyntaxNode) -> TextSize {
node.children_with_tokens()
.find(|it| !matches!(it.kind(), WHITESPACE | COMMENT | ATTR))