4576: Add implementation of extract struct from enum variant r=matklad a=mcrakhman

Hi guys! I implemented the extraction functionality including modifying multiple files. The only thing I didn't change the cursor position. I've done it with a previous API, but now snippets have been introduced and I need to figure out how to do it.

Please bear in mind that I am a newcomer in the rust-analyzer (and also Rust) world, so I tried to implement the feature to the best of my knowledge, but the API is very new to me, so I am very welcome to introducing changes etc. 

Co-authored-by: Mikhail Rakhmanov <rakhmanov.m@gmail.com>
This commit is contained in:
bors[bot] 2020-06-08 16:28:48 +00:00 committed by GitHub
commit 3a7c218fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 403 additions and 0 deletions

View File

@ -19,6 +19,7 @@ use crate::{
assist_config::{AssistConfig, SnippetCap},
Assist, AssistId, GroupLabel, ResolvedAssist,
};
use rustc_hash::FxHashMap;
/// `AssistContext` allows to apply an assist or check if it could be applied.
///
@ -138,6 +139,16 @@ impl Assists {
let label = Assist::new(id, label.into(), None, target);
self.add_impl(label, f)
}
pub(crate) fn add_in_multiple_files(
&mut self,
id: AssistId,
label: impl Into<String>,
target: TextRange,
f: impl FnOnce(&mut AssistDirector),
) -> Option<()> {
let label = Assist::new(id, label.into(), None, target);
self.add_impl_multiple_files(label, f)
}
pub(crate) fn add_group(
&mut self,
group: &GroupLabel,
@ -162,6 +173,31 @@ impl Assists {
Some(())
}
fn add_impl_multiple_files(
&mut self,
label: Assist,
f: impl FnOnce(&mut AssistDirector),
) -> Option<()> {
if !self.resolve {
self.buf.push((label, None));
return None;
}
let mut director = AssistDirector::default();
f(&mut director);
let changes = director.finish();
let file_edits: Vec<SourceFileEdit> =
changes.into_iter().map(|mut change| change.source_file_edits.pop().unwrap()).collect();
let source_change = SourceChange {
source_file_edits: file_edits,
file_system_edits: vec![],
is_snippet: false,
};
self.buf.push((label, Some(source_change)));
Some(())
}
fn finish(mut self) -> Vec<(Assist, Option<SourceChange>)> {
self.buf.sort_by_key(|(label, _edit)| label.target.len());
self.buf
@ -255,3 +291,27 @@ impl AssistBuilder {
res
}
}
pub(crate) struct AssistDirector {
builders: FxHashMap<FileId, AssistBuilder>,
}
impl AssistDirector {
pub(crate) fn perform(&mut self, file_id: FileId, f: impl FnOnce(&mut AssistBuilder)) {
let mut builder = self.builders.entry(file_id).or_insert(AssistBuilder::new(file_id));
f(&mut builder);
}
fn finish(self) -> Vec<SourceChange> {
self.builders
.into_iter()
.map(|(_, builder)| builder.finish())
.collect::<Vec<SourceChange>>()
}
}
impl Default for AssistDirector {
fn default() -> Self {
AssistDirector { builders: FxHashMap::default() }
}
}

View File

@ -0,0 +1,326 @@
use ra_ide_db::{defs::Definition, search::Reference, RootDatabase};
use ra_syntax::{
algo::find_node_at_offset,
ast::{self, AstNode, NameOwner},
SourceFile, SyntaxNode, TextRange, TextSize,
};
use crate::{
assist_context::{AssistBuilder, AssistDirector},
utils::insert_use_statement,
AssistContext, AssistId, Assists,
};
use ast::{ArgListOwner, VisibilityOwner};
use hir::{EnumVariant, Module, ModuleDef, Name};
use ra_db::FileId;
use ra_fmt::leading_indent;
use rustc_hash::FxHashSet;
// Assist: extract_struct_from_enum_variant
//
// Extracts a struct from enum variant.
//
// ```
// enum A { <|>One(u32, u32) }
// ```
// ->
// ```
// struct One(pub u32, pub u32);
//
// enum A { One(One) }
// ```
pub(crate) fn extract_struct_from_enum_variant(
acc: &mut Assists,
ctx: &AssistContext,
) -> Option<()> {
let variant = ctx.find_node_at_offset::<ast::EnumVariant>()?;
let field_list = match variant.kind() {
ast::StructKind::Tuple(field_list) => field_list,
_ => return None,
};
let variant_name = variant.name()?.to_string();
let variant_hir = ctx.sema.to_def(&variant)?;
if existing_struct_def(ctx.db, &variant_name, &variant_hir) {
return None;
}
let enum_ast = variant.parent_enum();
let visibility = enum_ast.visibility();
let enum_hir = ctx.sema.to_def(&enum_ast)?;
let variant_hir_name = variant_hir.name(ctx.db);
let enum_module_def = ModuleDef::from(enum_hir);
let current_module = enum_hir.module(ctx.db);
let target = variant.syntax().text_range();
acc.add_in_multiple_files(
AssistId("extract_struct_from_enum_variant"),
"Extract struct from enum variant",
target,
|edit| {
let definition = Definition::ModuleDef(ModuleDef::EnumVariant(variant_hir));
let res = definition.find_usages(&ctx.db, None);
let start_offset = variant.parent_enum().syntax().text_range().start();
let mut visited_modules_set = FxHashSet::default();
visited_modules_set.insert(current_module);
for reference in res {
let source_file = ctx.sema.parse(reference.file_range.file_id);
update_reference(
ctx,
edit,
reference,
&source_file,
&enum_module_def,
&variant_hir_name,
&mut visited_modules_set,
);
}
extract_struct_def(
edit,
enum_ast.syntax(),
&variant_name,
&field_list.to_string(),
start_offset,
ctx.frange.file_id,
&visibility,
);
let list_range = field_list.syntax().text_range();
update_variant(edit, &variant_name, ctx.frange.file_id, list_range);
},
)
}
fn existing_struct_def(db: &RootDatabase, variant_name: &str, variant: &EnumVariant) -> bool {
variant
.parent_enum(db)
.module(db)
.scope(db, None)
.into_iter()
.any(|(name, _)| name.to_string() == variant_name.to_string())
}
fn insert_import(
ctx: &AssistContext,
builder: &mut AssistBuilder,
path: &ast::PathExpr,
module: &Module,
enum_module_def: &ModuleDef,
variant_hir_name: &Name,
) -> Option<()> {
let db = ctx.db;
let mod_path = module.find_use_path(db, enum_module_def.clone());
if let Some(mut mod_path) = mod_path {
mod_path.segments.pop();
mod_path.segments.push(variant_hir_name.clone());
insert_use_statement(path.syntax(), &mod_path, ctx, builder.text_edit_builder());
}
Some(())
}
fn extract_struct_def(
edit: &mut AssistDirector,
enum_ast: &SyntaxNode,
variant_name: &str,
variant_list: &str,
start_offset: TextSize,
file_id: FileId,
visibility: &Option<ast::Visibility>,
) -> Option<()> {
let visibility_string = if let Some(visibility) = visibility {
format!("{} ", visibility.to_string())
} else {
"".to_string()
};
let indent = if let Some(indent) = leading_indent(enum_ast) {
indent.to_string()
} else {
"".to_string()
};
let struct_def = format!(
r#"{}struct {}{};
{}"#,
visibility_string,
variant_name,
list_with_visibility(variant_list),
indent
);
edit.perform(file_id, |builder| {
builder.insert(start_offset, struct_def);
});
Some(())
}
fn update_variant(
edit: &mut AssistDirector,
variant_name: &str,
file_id: FileId,
list_range: TextRange,
) -> Option<()> {
let inside_variant_range = TextRange::new(
list_range.start().checked_add(TextSize::from(1))?,
list_range.end().checked_sub(TextSize::from(1))?,
);
edit.perform(file_id, |builder| {
builder.replace(inside_variant_range, variant_name);
});
Some(())
}
fn update_reference(
ctx: &AssistContext,
edit: &mut AssistDirector,
reference: Reference,
source_file: &SourceFile,
enum_module_def: &ModuleDef,
variant_hir_name: &Name,
visited_modules_set: &mut FxHashSet<Module>,
) -> Option<()> {
let path_expr: ast::PathExpr = find_node_at_offset::<ast::PathExpr>(
source_file.syntax(),
reference.file_range.range.start(),
)?;
let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
let list = call.arg_list()?;
let segment = path_expr.path()?.segment()?;
let module = ctx.sema.scope(&path_expr.syntax()).module()?;
let list_range = list.syntax().text_range();
let inside_list_range = TextRange::new(
list_range.start().checked_add(TextSize::from(1))?,
list_range.end().checked_sub(TextSize::from(1))?,
);
edit.perform(reference.file_range.file_id, |builder| {
if !visited_modules_set.contains(&module) {
if insert_import(ctx, builder, &path_expr, &module, enum_module_def, variant_hir_name)
.is_some()
{
visited_modules_set.insert(module);
}
}
builder.replace(inside_list_range, format!("{}{}", segment, list));
});
Some(())
}
fn list_with_visibility(list: &str) -> String {
list.split(',')
.map(|part| {
let index = if part.chars().next().unwrap() == '(' { 1usize } else { 0 };
let mut mod_part = part.trim().to_string();
mod_part.insert_str(index, "pub ");
mod_part
})
.collect::<Vec<String>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use crate::{
tests::{check_assist, check_assist_not_applicable},
utils::FamousDefs,
};
use super::*;
#[test]
fn test_extract_struct_several_fields() {
check_assist(
extract_struct_from_enum_variant,
"enum A { <|>One(u32, u32) }",
r#"struct One(pub u32, pub u32);
enum A { One(One) }"#,
);
}
#[test]
fn test_extract_struct_one_field() {
check_assist(
extract_struct_from_enum_variant,
"enum A { <|>One(u32) }",
r#"struct One(pub u32);
enum A { One(One) }"#,
);
}
#[test]
fn test_extract_struct_pub_visibility() {
check_assist(
extract_struct_from_enum_variant,
"pub enum A { <|>One(u32, u32) }",
r#"pub struct One(pub u32, pub u32);
pub enum A { One(One) }"#,
);
}
#[test]
fn test_extract_struct_with_complex_imports() {
check_assist(
extract_struct_from_enum_variant,
r#"mod my_mod {
fn another_fn() {
let m = my_other_mod::MyEnum::MyField(1, 1);
}
pub mod my_other_mod {
fn another_fn() {
let m = MyEnum::MyField(1, 1);
}
pub enum MyEnum {
<|>MyField(u8, u8),
}
}
}
fn another_fn() {
let m = my_mod::my_other_mod::MyEnum::MyField(1, 1);
}"#,
r#"use my_mod::my_other_mod::MyField;
mod my_mod {
use my_other_mod::MyField;
fn another_fn() {
let m = my_other_mod::MyEnum::MyField(MyField(1, 1));
}
pub mod my_other_mod {
fn another_fn() {
let m = MyEnum::MyField(MyField(1, 1));
}
pub struct MyField(pub u8, pub u8);
pub enum MyEnum {
MyField(MyField),
}
}
}
fn another_fn() {
let m = my_mod::my_other_mod::MyEnum::MyField(MyField(1, 1));
}"#,
);
}
fn check_not_applicable(ra_fixture: &str) {
let fixture =
format!("//- main.rs crate:main deps:core\n{}\n{}", ra_fixture, FamousDefs::FIXTURE);
check_assist_not_applicable(extract_struct_from_enum_variant, &fixture)
}
#[test]
fn test_extract_enum_not_applicable_for_element_with_no_fields() {
check_not_applicable("enum A { <|>One }");
}
#[test]
fn test_extract_enum_not_applicable_if_struct_exists() {
check_not_applicable(
r#"struct One;
enum A { <|>One(u8) }"#,
);
}
}

View File

@ -115,6 +115,7 @@ mod handlers {
mod change_return_type_to_result;
mod change_visibility;
mod early_return;
mod extract_struct_from_enum_variant;
mod fill_match_arms;
mod fix_visibility;
mod flip_binexpr;
@ -155,6 +156,7 @@ mod handlers {
change_return_type_to_result::change_return_type_to_result,
change_visibility::change_visibility,
early_return::convert_to_guarded_return,
extract_struct_from_enum_variant::extract_struct_from_enum_variant,
fill_match_arms::fill_match_arms,
fix_visibility::fix_visibility,
flip_binexpr::flip_binexpr,

View File

@ -337,6 +337,21 @@ fn main() {
)
}
#[test]
fn doctest_extract_struct_from_enum_variant() {
check_doc_test(
"extract_struct_from_enum_variant",
r#####"
enum A { <|>One(u32, u32) }
"#####,
r#####"
struct One(pub u32, pub u32);
enum A { One(One) }
"#####,
)
}
#[test]
fn doctest_fill_match_arms() {
check_doc_test(