]> git.lizzy.rs Git - rust.git/commitdiff
Add preliminary implementation of extract struct from enum variant
authorMikhail Rakhmanov <rakhmanov.m@gmail.com>
Fri, 22 May 2020 20:28:30 +0000 (22:28 +0200)
committerMikhail Rakhmanov <rakhmanov.m@gmail.com>
Fri, 22 May 2020 20:28:30 +0000 (22:28 +0200)
Cargo.lock
crates/ra_assists/Cargo.toml
crates/ra_assists/src/assist_context.rs
crates/ra_assists/src/handlers/extract_struct_from_enum_variant.rs [new file with mode: 0644]
crates/ra_assists/src/lib.rs

index 007f05b4d03e8d62f036c88363e5b5434a8b1fa5..c604298ec1e36bdd46184f61984a9ed280918750 100644 (file)
@@ -921,6 +921,7 @@ dependencies = [
  "ra_db",
  "ra_fmt",
  "ra_hir",
+ "ra_hir_expand",
  "ra_ide_db",
  "ra_prof",
  "ra_syntax",
index 3bcf58ba4b0962a2219d22b3d5d04168a9666bf2..f3481bdeb1a296c76229daf76d6ded91acef2f6c 100644 (file)
@@ -20,5 +20,6 @@ ra_fmt = { path = "../ra_fmt" }
 ra_prof = { path = "../ra_prof" }
 ra_db = { path = "../ra_db" }
 ra_ide_db = { path = "../ra_ide_db" }
+hir_expand = { path = "../ra_hir_expand", package = "ra_hir_expand" }
 hir = { path = "../ra_hir", package = "ra_hir" }
 test_utils = { path = "../test_utils" }
index 5b1a4680b43907488d4415ccb5094656edcdb455..6291c68def184ee56523410d6d19ff991047c099 100644 (file)
@@ -2,7 +2,7 @@
 
 use algo::find_covering_element;
 use hir::Semantics;
-use ra_db::{FileId, FileRange};
+use ra_db::{FileId, FileRange, FilePosition};
 use ra_fmt::{leading_indent, reindent};
 use ra_ide_db::{
     source_change::{SourceChange, SourceFileEdit},
@@ -19,6 +19,7 @@
     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 @@ pub(crate) fn add(
         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,27 @@ fn add_impl(&mut self, label: Assist, f: impl FnOnce(&mut AssistBuilder)) -> Opt
         Some(())
     }
 
+    fn add_impl_multiple_files(&mut self, label: Assist, f: impl FnOnce(&mut AssistDirector)) -> Option<()> {
+        let change_label = label.label.clone();
+        if !self.resolve {
+            return None
+        }
+        let mut director = AssistDirector::new(change_label.clone());
+        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 +287,31 @@ fn finish(self) -> SourceChange {
         res
     }
 }
+
+pub(crate) struct AssistDirector {
+    source_changes: Vec<SourceChange>,
+    builders: FxHashMap<FileId, AssistBuilder>,
+    change_label: String 
+} 
+
+impl AssistDirector {
+    fn new(change_label: String) -> AssistDirector {
+        AssistDirector {
+            source_changes: vec![],
+            builders: FxHashMap::default(),
+            change_label
+        }
+    }
+
+    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(mut self) -> Vec<SourceChange> {
+        for (file_id, builder) in self.builders.into_iter().collect::<Vec<(FileId, AssistBuilder)>>() {
+            self.source_changes.push(builder.finish());    
+        }
+        self.source_changes
+    }
+}
diff --git a/crates/ra_assists/src/handlers/extract_struct_from_enum_variant.rs b/crates/ra_assists/src/handlers/extract_struct_from_enum_variant.rs
new file mode 100644 (file)
index 0000000..6e19a6f
--- /dev/null
@@ -0,0 +1,338 @@
+use hir_expand::name::AsName;
+use ra_ide_db::{
+    defs::Definition, imports_locator::ImportsLocator, search::Reference, RootDatabase,
+};
+use ra_syntax::{
+    algo::find_node_at_offset,
+    ast::{self, AstNode, NameOwner},
+    SourceFile, SyntaxNode, TextRange, TextSize,
+};
+use stdx::format_to;
+
+use crate::{
+    assist_context::{AssistBuilder, AssistDirector},
+    utils::insert_use_statement,
+    AssistContext, AssistId, Assists,
+};
+use ast::{ArgListOwner, VisibilityOwner};
+use hir::{EnumVariant, Module, ModuleDef};
+use ra_fmt::leading_indent;
+use rustc_hash::FxHashSet;
+use ra_db::FileId;
+
+// Assist extract_struct_from_enum
+//
+// Extracts a from 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(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 enum_ast = variant.parent_enum();
+    let enum_name = enum_ast.name().unwrap().to_string();
+    let visibility = enum_ast.visibility();
+    let variant_hir = ctx.sema.to_def(&variant)?;
+
+    if existing_struct_def(ctx.db, &variant_name, &variant_hir) {
+        return None;
+    }
+
+    let target = variant.syntax().text_range();
+    return 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 module_def = mod_def_for_target_module(ctx, &enum_name);
+            let start_offset = variant.parent_enum().syntax().text_range().start();
+            let mut seen_files_map: FxHashSet<Module> = FxHashSet::default();
+            seen_files_map.insert(module_def.module(ctx.db).unwrap());
+            for reference in res {
+                let source_file = ctx.sema.parse(reference.file_range.file_id);
+                update_reference(
+                    ctx,
+                    edit,
+                    reference,
+                    &source_file,
+                    &module_def,
+                    &mut seen_files_map,
+                );
+            }
+            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 {
+    let module_defs = variant.parent_enum(db).module(db).scope(db, None);
+    for (name, _) in module_defs {
+        if name.to_string() == variant_name.to_string() {
+            return true;
+        }
+    }
+    false
+}
+
+fn mod_def_for_target_module(ctx: &AssistContext, enum_name: &str) -> ModuleDef {
+    ImportsLocator::new(ctx.db).find_imports(enum_name).first().unwrap().left().unwrap()
+}
+
+fn insert_use_import(
+    ctx: &AssistContext,
+    builder: &mut AssistBuilder,
+    path: &ast::PathExpr,
+    module: &Module,
+    module_def: &ModuleDef,
+    path_segment: ast::NameRef,
+) -> Option<()> {
+    let db = ctx.db;
+    let mod_path = module.find_use_path(db, module_def.clone());
+    if let Some(mut mod_path) = mod_path {
+        mod_path.segments.pop();
+        mod_path.segments.push(path_segment.as_name());
+        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 mut buf = String::new();
+    let indent = if let Some(indent) = leading_indent(enum_ast) {
+        indent.to_string()
+    } else {
+        "".to_string()
+    };
+
+    format_to!(
+        buf,
+        r#"{}struct {}{};
+
+{}"#,
+        visibility_string,
+        variant_name,
+        list_with_visibility(variant_list),
+        indent
+    );
+    edit.perform(file_id, |builder| {
+        builder.insert(start_offset, buf);
+    });
+    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.set_file(file_id);
+        builder.replace(inside_variant_range, variant_name);
+    });
+    Some(())
+}
+
+fn update_reference(
+    ctx: &AssistContext,
+    edit: &mut AssistDirector,
+    reference: Reference,
+    source_file: &SourceFile,
+    module_def: &ModuleDef,
+    seen_files_map: &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 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| {
+        let module = ctx.sema.scope(&path_expr.syntax()).module().unwrap();
+        if !seen_files_map.contains(&module) {
+            if insert_use_import(
+                ctx,
+                builder,
+                &path_expr,
+                &module,
+                module_def,
+                segment.name_ref().unwrap(),
+            )
+            .is_some()
+            {
+                seen_files_map.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::{utils::FamousDefs, tests::{check_assist, check_assist_not_applicable}};
+
+    use super::*;
+
+    #[test]
+    fn test_extract_struct_several_fields() {
+        check_assist(
+            extract_struct_from_enum,
+            "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,
+            "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,
+            "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,
+            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, &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) }"#,
+        );
+    }
+}
index 464bc03dde9e77e0ca2359786424da8014f39675..9933f7a50546b60600401effba77c2fbc9f2a1ec 100644 (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;
@@ -154,6 +155,7 @@ pub(crate) fn all() -> &'static [Handler] {
             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,
             fill_match_arms::fill_match_arms,
             fix_visibility::fix_visibility,
             flip_binexpr::flip_binexpr,