From fede1a3beb17ae65194bf000586a062724bf47cf Mon Sep 17 00:00:00 2001
From: longfangsong <longfangsong@icloud.com>
Date: Sun, 12 Sep 2021 02:30:56 +0800
Subject: [PATCH 1/5] Add promote_mod_file assist

---
 crates/ide_assists/src/assist_context.rs      |   4 +
 .../src/handlers/promote_mod_file.rs          | 148 ++++++++++++++++++
 crates/ide_assists/src/lib.rs                 |   2 +
 crates/ide_assists/src/tests.rs               |  23 +--
 4 files changed, 167 insertions(+), 10 deletions(-)
 create mode 100644 crates/ide_assists/src/handlers/promote_mod_file.rs

diff --git a/crates/ide_assists/src/assist_context.rs b/crates/ide_assists/src/assist_context.rs
index 0244f5fb291..11e19304f1e 100644
--- a/crates/ide_assists/src/assist_context.rs
+++ b/crates/ide_assists/src/assist_context.rs
@@ -294,6 +294,10 @@ impl AssistBuilder {
         let file_system_edit = FileSystemEdit::CreateFile { dst, initial_contents: content.into() };
         self.source_change.push_file_system_edit(file_system_edit);
     }
+    pub(crate) fn move_file(&mut self, src: FileId, dst: AnchoredPathBuf) {
+        let file_system_edit = FileSystemEdit::MoveFile { src, dst };
+        self.source_change.push_file_system_edit(file_system_edit);
+    }
 
     fn finish(mut self) -> SourceChange {
         self.commit();
diff --git a/crates/ide_assists/src/handlers/promote_mod_file.rs b/crates/ide_assists/src/handlers/promote_mod_file.rs
new file mode 100644
index 00000000000..c5c37def079
--- /dev/null
+++ b/crates/ide_assists/src/handlers/promote_mod_file.rs
@@ -0,0 +1,148 @@
+use ide_db::{
+    assists::{AssistId, AssistKind},
+    base_db::AnchoredPathBuf,
+};
+use syntax::{
+    ast::{self},
+    AstNode, TextRange,
+};
+
+use crate::assist_context::{AssistContext, Assists};
+
+pub(crate) fn promote_mod_file(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+    let source_file = ctx.find_node_at_offset::<ast::SourceFile>()?;
+    let module = ctx.sema.to_module_def(ctx.frange.file_id)?;
+    if module.is_mod_rs(ctx.db()) {
+        return None;
+    }
+    let target = TextRange::new(
+        source_file.syntax().text_range().start(),
+        source_file.syntax().text_range().end(),
+    );
+    let path = format!("./{}/mod.rs", module.name(ctx.db())?.to_string());
+    let dst = AnchoredPathBuf { anchor: ctx.frange.file_id, path };
+    acc.add(
+        AssistId("promote_mod_file", AssistKind::Refactor),
+        "Promote Module to directory",
+        target,
+        |builder| {
+            builder.move_file(ctx.frange.file_id, dst);
+        },
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::{check_assist, check_assist_not_applicable};
+
+    use super::*;
+
+    #[test]
+    fn trivial() {
+        check_assist(
+            promote_mod_file,
+            r#"
+//- /main.rs
+mod a;
+//- /a.rs
+$0fn t() {}
+"#,
+            r#"
+//- /a/mod.rs
+fn t() {}
+"#,
+        );
+    }
+
+    #[test]
+    fn cursor_can_be_putted_anywhere() {
+        check_assist(
+            promote_mod_file,
+            r#"
+//- /main.rs
+mod a;
+//- /a.rs
+fn t() {}$0
+"#,
+            r#"
+//- /a/mod.rs
+fn t() {}
+"#,
+        );
+        check_assist(
+            promote_mod_file,
+            r#"
+//- /main.rs
+mod a;
+//- /a.rs
+fn t()$0 {}
+"#,
+            r#"
+//- /a/mod.rs
+fn t() {}
+"#,
+        );
+        check_assist(
+            promote_mod_file,
+            r#"
+//- /main.rs
+mod a;
+//- /a.rs
+fn t($0) {}
+"#,
+            r#"
+//- /a/mod.rs
+fn t() {}
+"#,
+        );
+    }
+
+    #[test]
+    fn cannot_promote_mod_rs() {
+        check_assist_not_applicable(
+            promote_mod_file,
+            r#"//- /main.rs
+mod a;
+//- /a/mod.rs
+$0fn t() {}
+"#,
+        );
+    }
+
+    #[test]
+    fn cannot_promote_main_and_lib_rs() {
+        check_assist_not_applicable(
+            promote_mod_file,
+            r#"//- /main.rs
+$0fn t() {}
+"#,
+        );
+        check_assist_not_applicable(
+            promote_mod_file,
+            r#"//- /lib.rs
+$0fn t() {}
+"#,
+        );
+    }
+
+    #[test]
+    fn works_in_mod() {
+        // note: /a/b.rs remains untouched
+        check_assist(
+            promote_mod_file,
+            r#"//- /main.rs
+mod a;
+//- /a.rs
+mod b;
+$0fn t() {}
+//- /a/b.rs
+fn t1() {}
+"#,
+            r#"
+//- /a/mod.rs
+mod b;
+fn t() {}
+"#,
+        );
+    }
+}
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 7c074a4f632..57007691b5e 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -96,6 +96,7 @@ mod handlers {
     mod move_bounds;
     mod move_guard;
     mod move_module_to_file;
+    mod promote_mod_file;
     mod pull_assignment_up;
     mod qualify_path;
     mod raw_string;
@@ -167,6 +168,7 @@ mod handlers {
             move_guard::move_arm_cond_to_match_guard,
             move_guard::move_guard_to_arm_body,
             move_module_to_file::move_module_to_file,
+            promote_mod_file::promote_mod_file,
             pull_assignment_up::pull_assignment_up,
             qualify_path::qualify_path,
             raw_string::add_hash,
diff --git a/crates/ide_assists/src/tests.rs b/crates/ide_assists/src/tests.rs
index 5cd3642ce0d..e211b09987b 100644
--- a/crates/ide_assists/src/tests.rs
+++ b/crates/ide_assists/src/tests.rs
@@ -142,7 +142,6 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label:
         (Some(assist), ExpectedResult::After(after)) => {
             let source_change =
                 assist.source_change.expect("Assist did not contain any source changes");
-            assert!(!source_change.source_file_edits.is_empty());
             let skip_header = source_change.source_file_edits.len() == 1
                 && source_change.file_system_edits.len() == 0;
 
@@ -160,15 +159,19 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label:
             }
 
             for file_system_edit in source_change.file_system_edits {
-                if let FileSystemEdit::CreateFile { dst, initial_contents } = file_system_edit {
-                    let sr = db.file_source_root(dst.anchor);
-                    let sr = db.source_root(sr);
-                    let mut base = sr.path_for_file(&dst.anchor).unwrap().clone();
-                    base.pop();
-                    let created_file_path = format!("{}{}", base.to_string(), &dst.path[1..]);
-                    format_to!(buf, "//- {}\n", created_file_path);
-                    buf.push_str(&initial_contents);
-                }
+                let (dst, contents) = match file_system_edit {
+                    FileSystemEdit::CreateFile { dst, initial_contents } => (dst, initial_contents),
+                    FileSystemEdit::MoveFile { src, dst } => {
+                        (dst, db.file_text(src).as_ref().to_owned())
+                    }
+                };
+                let sr = db.file_source_root(dst.anchor);
+                let sr = db.source_root(sr);
+                let mut base = sr.path_for_file(&dst.anchor).unwrap().clone();
+                base.pop();
+                let created_file_path = format!("{}{}", base.to_string(), &dst.path[1..]);
+                format_to!(buf, "//- {}\n", created_file_path);
+                buf.push_str(&contents);
             }
 
             assert_eq_text!(after, &buf);

From 3edc25dc2699c90621d20d85e7883d24edbf1794 Mon Sep 17 00:00:00 2001
From: longfangsong <longfangsong@icloud.com>
Date: Sun, 12 Sep 2021 10:53:56 +0800
Subject: [PATCH 2/5] Add docs strings

---
 crates/ide_assists/src/handlers/promote_mod_file.rs | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/crates/ide_assists/src/handlers/promote_mod_file.rs b/crates/ide_assists/src/handlers/promote_mod_file.rs
index c5c37def079..2ba51d16992 100644
--- a/crates/ide_assists/src/handlers/promote_mod_file.rs
+++ b/crates/ide_assists/src/handlers/promote_mod_file.rs
@@ -9,6 +9,19 @@ use syntax::{
 
 use crate::assist_context::{AssistContext, Assists};
 
+// Assist: promote_mod_file
+//
+// Moves inline module's contents to a separate file.
+//
+// ```
+// // a.rs
+// $0fn t() {}
+// ```
+// ->
+// ```
+// // /a/mod.rs
+// fn t() {}
+// ```
 pub(crate) fn promote_mod_file(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
     let source_file = ctx.find_node_at_offset::<ast::SourceFile>()?;
     let module = ctx.sema.to_module_def(ctx.frange.file_id)?;

From cafc7e350132f8121c45f863f601eba44392c63a Mon Sep 17 00:00:00 2001
From: longfangsong <longfangsong@icloud.com>
Date: Sun, 12 Sep 2021 11:20:28 +0800
Subject: [PATCH 3/5] Fix doc test

---
 .../ide_assists/src/handlers/promote_mod_file.rs |  5 +++--
 crates/ide_assists/src/tests/generated.rs        | 16 ++++++++++++++++
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/crates/ide_assists/src/handlers/promote_mod_file.rs b/crates/ide_assists/src/handlers/promote_mod_file.rs
index 2ba51d16992..5991c0989fe 100644
--- a/crates/ide_assists/src/handlers/promote_mod_file.rs
+++ b/crates/ide_assists/src/handlers/promote_mod_file.rs
@@ -14,12 +14,13 @@ use crate::assist_context::{AssistContext, Assists};
 // Moves inline module's contents to a separate file.
 //
 // ```
-// // a.rs
+// //- /main.rs
+// mod a;
+// //- /a.rs
 // $0fn t() {}
 // ```
 // ->
 // ```
-// // /a/mod.rs
 // fn t() {}
 // ```
 pub(crate) fn promote_mod_file(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index 39ab8c7b74e..98e0bd67489 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -1226,6 +1226,22 @@ mod foo;
     )
 }
 
+#[test]
+fn doctest_promote_mod_file() {
+    check_doc_test(
+        "promote_mod_file",
+        r#####"
+//- /main.rs
+mod a;
+//- /a.rs
+$0fn t() {}
+"#####,
+        r#####"
+fn t() {}
+"#####,
+    )
+}
+
 #[test]
 fn doctest_pull_assignment_up() {
     check_doc_test(

From cd599ec202d49d044d6d71fd3471e3c5199476e1 Mon Sep 17 00:00:00 2001
From: longfangsong <longfangsong@icloud.com>
Date: Mon, 20 Sep 2021 20:43:13 +0800
Subject: [PATCH 4/5] Address comments

---
 .../src/handlers/promote_mod_file.rs          | 78 +++++++++++--------
 crates/ide_assists/src/tests/generated.rs     |  2 +-
 2 files changed, 45 insertions(+), 35 deletions(-)

diff --git a/crates/ide_assists/src/handlers/promote_mod_file.rs b/crates/ide_assists/src/handlers/promote_mod_file.rs
index 5991c0989fe..1a177893f0c 100644
--- a/crates/ide_assists/src/handlers/promote_mod_file.rs
+++ b/crates/ide_assists/src/handlers/promote_mod_file.rs
@@ -3,12 +3,38 @@ use ide_db::{
     base_db::AnchoredPathBuf,
 };
 use syntax::{
-    ast::{self},
-    AstNode, TextRange,
+    ast::{self, Whitespace},
+    AstNode, AstToken, SourceFile, TextRange, TextSize,
 };
 
 use crate::assist_context::{AssistContext, Assists};
 
+/// Trim(remove leading and trailing whitespace) `initial_range` in `source_file`, return the trimmed range.
+fn trimmed_text_range(source_file: &SourceFile, initial_range: TextRange) -> TextRange {
+    let mut trimmed_range = initial_range;
+    while source_file
+        .syntax()
+        .token_at_offset(trimmed_range.start())
+        .find_map(Whitespace::cast)
+        .is_some()
+        && trimmed_range.start() < trimmed_range.end()
+    {
+        let start = trimmed_range.start() + TextSize::from(1);
+        trimmed_range = TextRange::new(start, trimmed_range.end());
+    }
+    while source_file
+        .syntax()
+        .token_at_offset(trimmed_range.end())
+        .find_map(Whitespace::cast)
+        .is_some()
+        && trimmed_range.start() < trimmed_range.end()
+    {
+        let end = trimmed_range.end() - TextSize::from(1);
+        trimmed_range = TextRange::new(trimmed_range.start(), end);
+    }
+    trimmed_range
+}
+
 // Assist: promote_mod_file
 //
 // Moves inline module's contents to a separate file.
@@ -17,7 +43,7 @@ use crate::assist_context::{AssistContext, Assists};
 // //- /main.rs
 // mod a;
 // //- /a.rs
-// $0fn t() {}
+// $0fn t() {}$0
 // ```
 // ->
 // ```
@@ -26,18 +52,23 @@ use crate::assist_context::{AssistContext, Assists};
 pub(crate) fn promote_mod_file(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
     let source_file = ctx.find_node_at_offset::<ast::SourceFile>()?;
     let module = ctx.sema.to_module_def(ctx.frange.file_id)?;
-    if module.is_mod_rs(ctx.db()) {
+    // Enable this assist if the user select all "meaningful" content in the source file
+    let trimmed_selected_range = trimmed_text_range(&source_file, ctx.frange.range);
+    let trimmed_file_range = trimmed_text_range(&source_file, source_file.syntax().text_range());
+    if module.is_mod_rs(ctx.db()) || trimmed_selected_range != trimmed_file_range {
         return None;
     }
+
     let target = TextRange::new(
         source_file.syntax().text_range().start(),
         source_file.syntax().text_range().end(),
     );
-    let path = format!("./{}/mod.rs", module.name(ctx.db())?.to_string());
+    let module_name = module.name(ctx.db())?.to_string();
+    let path = format!("./{}/mod.rs", module_name);
     let dst = AnchoredPathBuf { anchor: ctx.frange.file_id, path };
     acc.add(
         AssistId("promote_mod_file", AssistKind::Refactor),
-        "Promote Module to directory",
+        format!("Turn {}.rs to {}/mod.rs", module_name, module_name),
         target,
         |builder| {
             builder.move_file(ctx.frange.file_id, dst);
@@ -60,7 +91,7 @@ mod tests {
 mod a;
 //- /a.rs
 $0fn t() {}
-"#,
+$0"#,
             r#"
 //- /a/mod.rs
 fn t() {}
@@ -69,44 +100,23 @@ fn t() {}
     }
 
     #[test]
-    fn cursor_can_be_putted_anywhere() {
-        check_assist(
+    fn must_select_all_file() {
+        check_assist_not_applicable(
             promote_mod_file,
             r#"
 //- /main.rs
 mod a;
 //- /a.rs
 fn t() {}$0
-"#,
-            r#"
-//- /a/mod.rs
-fn t() {}
 "#,
         );
-        check_assist(
+        check_assist_not_applicable(
             promote_mod_file,
             r#"
 //- /main.rs
 mod a;
 //- /a.rs
-fn t()$0 {}
-"#,
-            r#"
-//- /a/mod.rs
-fn t() {}
-"#,
-        );
-        check_assist(
-            promote_mod_file,
-            r#"
-//- /main.rs
-mod a;
-//- /a.rs
-fn t($0) {}
-"#,
-            r#"
-//- /a/mod.rs
-fn t() {}
+$0fn$0 t() {}
 "#,
         );
     }
@@ -147,8 +157,8 @@ $0fn t() {}
             r#"//- /main.rs
 mod a;
 //- /a.rs
-mod b;
-$0fn t() {}
+$0mod b;
+fn t() {}$0
 //- /a/b.rs
 fn t1() {}
 "#,
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index 98e0bd67489..4ec3e0bb1bd 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -1234,7 +1234,7 @@ fn doctest_promote_mod_file() {
 //- /main.rs
 mod a;
 //- /a.rs
-$0fn t() {}
+$0fn t() {}$0
 "#####,
         r#####"
 fn t() {}

From 22abbe86f343a2c3ce639a15ba88502cf6eebfe6 Mon Sep 17 00:00:00 2001
From: longfangsong <longfangsong@icloud.com>
Date: Sat, 25 Sep 2021 22:47:20 +0800
Subject: [PATCH 5/5] Address comments

---
 ...{promote_mod_file.rs => move_to_mod_rs.rs} | 38 +++++++++++--------
 crates/ide_assists/src/lib.rs                 |  4 +-
 crates/ide_assists/src/tests/generated.rs     |  4 +-
 3 files changed, 27 insertions(+), 19 deletions(-)
 rename crates/ide_assists/src/handlers/{promote_mod_file.rs => move_to_mod_rs.rs} (81%)

diff --git a/crates/ide_assists/src/handlers/promote_mod_file.rs b/crates/ide_assists/src/handlers/move_to_mod_rs.rs
similarity index 81%
rename from crates/ide_assists/src/handlers/promote_mod_file.rs
rename to crates/ide_assists/src/handlers/move_to_mod_rs.rs
index 1a177893f0c..9b060bb710f 100644
--- a/crates/ide_assists/src/handlers/promote_mod_file.rs
+++ b/crates/ide_assists/src/handlers/move_to_mod_rs.rs
@@ -35,9 +35,9 @@ fn trimmed_text_range(source_file: &SourceFile, initial_range: TextRange) -> Tex
     trimmed_range
 }
 
-// Assist: promote_mod_file
+// Assist: move_to_mod_rs
 //
-// Moves inline module's contents to a separate file.
+// Moves xxx.rs to xxx/mod.rs.
 //
 // ```
 // //- /main.rs
@@ -49,13 +49,18 @@ fn trimmed_text_range(source_file: &SourceFile, initial_range: TextRange) -> Tex
 // ```
 // fn t() {}
 // ```
-pub(crate) fn promote_mod_file(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+pub(crate) fn move_to_mod_rs(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
     let source_file = ctx.find_node_at_offset::<ast::SourceFile>()?;
     let module = ctx.sema.to_module_def(ctx.frange.file_id)?;
     // Enable this assist if the user select all "meaningful" content in the source file
     let trimmed_selected_range = trimmed_text_range(&source_file, ctx.frange.range);
     let trimmed_file_range = trimmed_text_range(&source_file, source_file.syntax().text_range());
-    if module.is_mod_rs(ctx.db()) || trimmed_selected_range != trimmed_file_range {
+    if module.is_mod_rs(ctx.db()) {
+        cov_mark::hit!(already_mod_rs);
+        return None;
+    }
+    if trimmed_selected_range != trimmed_file_range {
+        cov_mark::hit!(not_all_selected);
         return None;
     }
 
@@ -67,7 +72,7 @@ pub(crate) fn promote_mod_file(acc: &mut Assists, ctx: &AssistContext) -> Option
     let path = format!("./{}/mod.rs", module_name);
     let dst = AnchoredPathBuf { anchor: ctx.frange.file_id, path };
     acc.add(
-        AssistId("promote_mod_file", AssistKind::Refactor),
+        AssistId("move_to_mod_rs", AssistKind::Refactor),
         format!("Turn {}.rs to {}/mod.rs", module_name, module_name),
         target,
         |builder| {
@@ -85,7 +90,7 @@ mod tests {
     #[test]
     fn trivial() {
         check_assist(
-            promote_mod_file,
+            move_to_mod_rs,
             r#"
 //- /main.rs
 mod a;
@@ -101,8 +106,9 @@ fn t() {}
 
     #[test]
     fn must_select_all_file() {
+        cov_mark::check!(not_all_selected);
         check_assist_not_applicable(
-            promote_mod_file,
+            move_to_mod_rs,
             r#"
 //- /main.rs
 mod a;
@@ -110,8 +116,9 @@ mod a;
 fn t() {}$0
 "#,
         );
+        cov_mark::check!(not_all_selected);
         check_assist_not_applicable(
-            promote_mod_file,
+            move_to_mod_rs,
             r#"
 //- /main.rs
 mod a;
@@ -123,12 +130,13 @@ $0fn$0 t() {}
 
     #[test]
     fn cannot_promote_mod_rs() {
+        cov_mark::check!(already_mod_rs);
         check_assist_not_applicable(
-            promote_mod_file,
+            move_to_mod_rs,
             r#"//- /main.rs
 mod a;
 //- /a/mod.rs
-$0fn t() {}
+$0fn t() {}$0
 "#,
         );
     }
@@ -136,15 +144,15 @@ $0fn t() {}
     #[test]
     fn cannot_promote_main_and_lib_rs() {
         check_assist_not_applicable(
-            promote_mod_file,
+            move_to_mod_rs,
             r#"//- /main.rs
-$0fn t() {}
+$0fn t() {}$0
 "#,
         );
         check_assist_not_applicable(
-            promote_mod_file,
+            move_to_mod_rs,
             r#"//- /lib.rs
-$0fn t() {}
+$0fn t() {}$0
 "#,
         );
     }
@@ -153,7 +161,7 @@ $0fn t() {}
     fn works_in_mod() {
         // note: /a/b.rs remains untouched
         check_assist(
-            promote_mod_file,
+            move_to_mod_rs,
             r#"//- /main.rs
 mod a;
 //- /a.rs
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 57007691b5e..1f5e2f036e0 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -96,7 +96,7 @@ mod handlers {
     mod move_bounds;
     mod move_guard;
     mod move_module_to_file;
-    mod promote_mod_file;
+    mod move_to_mod_rs;
     mod pull_assignment_up;
     mod qualify_path;
     mod raw_string;
@@ -168,7 +168,7 @@ mod handlers {
             move_guard::move_arm_cond_to_match_guard,
             move_guard::move_guard_to_arm_body,
             move_module_to_file::move_module_to_file,
-            promote_mod_file::promote_mod_file,
+            move_to_mod_rs::move_to_mod_rs,
             pull_assignment_up::pull_assignment_up,
             qualify_path::qualify_path,
             raw_string::add_hash,
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index 4ec3e0bb1bd..f371ca80178 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -1227,9 +1227,9 @@ mod foo;
 }
 
 #[test]
-fn doctest_promote_mod_file() {
+fn doctest_move_to_mod_rs() {
     check_doc_test(
-        "promote_mod_file",
+        "move_to_mod_rs",
         r#####"
 //- /main.rs
 mod a;