diff --git a/crates/assists/src/assist_context.rs b/crates/assists/src/assist_context.rs
index 69499ea326c..80cf9aba11f 100644
--- a/crates/assists/src/assist_context.rs
+++ b/crates/assists/src/assist_context.rs
@@ -4,10 +4,10 @@ use std::mem;
 
 use algo::find_covering_element;
 use hir::Semantics;
-use ide_db::base_db::{FileId, FileRange};
+use ide_db::base_db::{AnchoredPathBuf, FileId, FileRange};
 use ide_db::{
     label::Label,
-    source_change::{SourceChange, SourceFileEdit},
+    source_change::{FileSystemEdit, SourceChange, SourceFileEdit},
     RootDatabase,
 };
 use syntax::{
@@ -209,6 +209,7 @@ pub(crate) struct AssistBuilder {
     file_id: FileId,
     is_snippet: bool,
     source_file_edits: Vec<SourceFileEdit>,
+    file_system_edits: Vec<FileSystemEdit>,
 }
 
 impl AssistBuilder {
@@ -218,6 +219,7 @@ impl AssistBuilder {
             file_id,
             is_snippet: false,
             source_file_edits: Vec::default(),
+            file_system_edits: Vec::default(),
         }
     }
 
@@ -282,12 +284,17 @@ impl AssistBuilder {
             algo::diff(&node, &new).into_text_edit(&mut self.edit);
         }
     }
+    pub(crate) fn create_file(&mut self, dst: AnchoredPathBuf, content: impl Into<String>) {
+        let file_system_edit =
+            FileSystemEdit::CreateFile { dst: dst.clone(), initial_contents: content.into() };
+        self.file_system_edits.push(file_system_edit);
+    }
 
     fn finish(mut self) -> SourceChange {
         self.commit();
         SourceChange {
             source_file_edits: mem::take(&mut self.source_file_edits),
-            file_system_edits: Default::default(),
+            file_system_edits: mem::take(&mut self.file_system_edits),
             is_snippet: self.is_snippet,
         }
     }
diff --git a/crates/assists/src/handlers/extract_module_to_file.rs b/crates/assists/src/handlers/extract_module_to_file.rs
new file mode 100644
index 00000000000..5fc190fa67d
--- /dev/null
+++ b/crates/assists/src/handlers/extract_module_to_file.rs
@@ -0,0 +1,170 @@
+use ast::edit::IndentLevel;
+use ide_db::base_db::{AnchoredPathBuf, SourceDatabaseExt};
+use syntax::{
+    ast::{self, edit::AstNodeEdit, NameOwner},
+    AstNode,
+};
+
+use crate::{AssistContext, AssistId, AssistKind, Assists};
+
+// Assist: extract_module_to_file
+//
+// This assist extract module to file.
+//
+// ```
+// mod foo {<|>
+//     fn t() {}
+// }
+// ```
+// ->
+// ```
+// mod foo;
+// ```
+pub(crate) fn extract_module_to_file(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+    let assist_id = AssistId("extract_module_to_file", AssistKind::RefactorExtract);
+    let assist_label = "Extract module to file";
+    let db = ctx.db();
+    let module_ast = ctx.find_node_at_offset::<ast::Module>()?;
+    let module_items = module_ast.item_list()?;
+    let dedent_module_items_text = module_items.dedent(IndentLevel(1)).to_string();
+    let module_name = module_ast.name()?;
+    let target = module_ast.syntax().text_range();
+    let anchor_file_id = ctx.frange.file_id;
+    let sr = db.file_source_root(anchor_file_id);
+    let sr = db.source_root(sr);
+    let file_path = sr.path_for_file(&anchor_file_id)?;
+    let (file_name, file_ext) = file_path.name_and_extension()?;
+    acc.add(assist_id, assist_label, target, |builder| {
+        builder.replace(target, format!("mod {};", module_name));
+        let path = if is_main_or_lib(file_name) {
+            format!("./{}.{}", module_name, file_ext.unwrap())
+        } else {
+            format!("./{}/{}.{}", file_name, module_name, file_ext.unwrap())
+        };
+        let dst = AnchoredPathBuf { anchor: anchor_file_id, path };
+        let contents = update_module_items_string(dedent_module_items_text);
+        builder.create_file(dst, contents);
+    })
+}
+fn is_main_or_lib(file_name: &str) -> bool {
+    file_name == "main".to_string() || file_name == "lib".to_string()
+}
+fn update_module_items_string(items_str: String) -> String {
+    let mut items_string_lines: Vec<&str> = items_str.lines().collect();
+    items_string_lines.pop(); // Delete last line
+    items_string_lines.reverse();
+    items_string_lines.pop(); // Delete first line
+    items_string_lines.reverse();
+
+    let string = items_string_lines.join("\n");
+    format!("{}", string)
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::check_assist;
+
+    use super::*;
+
+    #[test]
+    fn extract_module_to_file_with_basic_module() {
+        check_assist(
+            extract_module_to_file,
+            r#"
+//- /foo.rs crate:foo
+mod tests {<|>
+    #[test] fn t() {}
+}
+"#,
+            r#"
+//- /foo.rs
+mod tests;
+//- /foo/tests.rs
+#[test] fn t() {}"#,
+        )
+    }
+
+    #[test]
+    fn extract_module_to_file_with_file_path() {
+        check_assist(
+            extract_module_to_file,
+            r#"
+//- /src/foo.rs crate:foo
+mod bar {<|>
+    fn f() {
+
+    }
+}
+fn main() {
+    println!("Hello, world!");
+}
+"#,
+            r#"
+//- /src/foo.rs
+mod bar;
+fn main() {
+    println!("Hello, world!");
+}
+//- /src/foo/bar.rs
+fn f() {
+
+}"#,
+        )
+    }
+
+    #[test]
+    fn extract_module_to_file_with_main_filw() {
+        check_assist(
+            extract_module_to_file,
+            r#"
+//- /main.rs
+mod foo {<|>
+    fn f() {
+
+    }
+}
+fn main() {
+    println!("Hello, world!");
+}
+"#,
+            r#"
+//- /main.rs
+mod foo;
+fn main() {
+    println!("Hello, world!");
+}
+//- /foo.rs
+fn f() {
+
+}"#,
+        )
+    }
+
+    #[test]
+    fn extract_module_to_file_with_lib_file() {
+        check_assist(
+            extract_module_to_file,
+            r#"
+//- /lib.rs
+mod foo {<|>
+    fn f() {
+
+    }
+}
+fn main() {
+    println!("Hello, world!");
+}
+"#,
+            r#"
+//- /lib.rs
+mod foo;
+fn main() {
+    println!("Hello, world!");
+}
+//- /foo.rs
+fn f() {
+
+}"#,
+        )
+    }
+}
diff --git a/crates/assists/src/lib.rs b/crates/assists/src/lib.rs
index 6e736ccb389..6b89b2d0443 100644
--- a/crates/assists/src/lib.rs
+++ b/crates/assists/src/lib.rs
@@ -129,6 +129,7 @@ mod handlers {
     mod convert_integer_literal;
     mod early_return;
     mod expand_glob_import;
+    mod extract_module_to_file;
     mod extract_struct_from_enum_variant;
     mod extract_variable;
     mod fill_match_arms;
@@ -179,6 +180,7 @@ mod handlers {
             convert_integer_literal::convert_integer_literal,
             early_return::convert_to_guarded_return,
             expand_glob_import::expand_glob_import,
+            extract_module_to_file::extract_module_to_file,
             extract_struct_from_enum_variant::extract_struct_from_enum_variant,
             extract_variable::extract_variable,
             fill_match_arms::fill_match_arms,
diff --git a/crates/assists/src/tests.rs b/crates/assists/src/tests.rs
index 709a34d03aa..b41f4874a5d 100644
--- a/crates/assists/src/tests.rs
+++ b/crates/assists/src/tests.rs
@@ -2,6 +2,7 @@ mod generated;
 
 use hir::Semantics;
 use ide_db::base_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt};
+use ide_db::source_change::FileSystemEdit;
 use ide_db::RootDatabase;
 use syntax::TextRange;
 use test_utils::{assert_eq_text, extract_offset, extract_range};
@@ -47,7 +48,7 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
     let before = db.file_text(file_id).to_string();
     let frange = FileRange { file_id, range: selection.into() };
 
-    let mut assist = Assist::resolved(&db, &AssistConfig::default(), frange)
+    let assist = Assist::resolved(&db, &AssistConfig::default(), frange)
         .into_iter()
         .find(|assist| assist.assist.id.0 == assist_id)
         .unwrap_or_else(|| {
@@ -63,9 +64,12 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
         });
 
     let actual = {
-        let change = assist.source_change.source_file_edits.pop().unwrap();
         let mut actual = before;
-        change.edit.apply(&mut actual);
+        for source_file_edit in assist.source_change.source_file_edits {
+            if source_file_edit.file_id == file_id {
+                source_file_edit.edit.apply(&mut actual)
+            }
+        }
         actual
     };
     assert_eq_text!(&after, &actual);
@@ -99,7 +103,8 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label:
         (Some(assist), ExpectedResult::After(after)) => {
             let mut source_change = assist.source_change;
             assert!(!source_change.source_file_edits.is_empty());
-            let skip_header = source_change.source_file_edits.len() == 1;
+            let skip_header = source_change.source_file_edits.len() == 1
+                && source_change.file_system_edits.len() == 0;
             source_change.source_file_edits.sort_by_key(|it| it.file_id);
 
             let mut buf = String::new();
@@ -115,6 +120,21 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label:
                 buf.push_str(&text);
             }
 
+            for file_system_edit in source_change.file_system_edits.clone() {
+                match file_system_edit {
+                    FileSystemEdit::CreateFile { dst, initial_contents } => {
+                        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);
+                    }
+                    _ => (),
+                }
+            }
+
             assert_eq_text!(after, &buf);
         }
         (Some(assist), ExpectedResult::Target(target)) => {
diff --git a/crates/assists/src/tests/generated.rs b/crates/assists/src/tests/generated.rs
index cc7c4a3433a..e9093ec5361 100644
--- a/crates/assists/src/tests/generated.rs
+++ b/crates/assists/src/tests/generated.rs
@@ -235,6 +235,21 @@ fn qux(bar: Bar, baz: Baz) {}
     )
 }
 
+#[test]
+fn doctest_extract_module_to_file() {
+    check_doc_test(
+        "extract_module_to_file",
+        r#####"
+mod foo {<|>
+    fn t() {}
+}
+"#####,
+        r#####"
+mod foo;
+"#####,
+    )
+}
+
 #[test]
 fn doctest_extract_struct_from_enum_variant() {
     check_doc_test(
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 049f808dc60..3ad30f0c9e2 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -619,6 +619,7 @@ fn test_fn() {
                                                 ),
                                                 path: "foo.rs",
                                             },
+                                            initial_contents: "",
                                         },
                                     ],
                                     is_snippet: false,
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index e8b89662385..d79f5c17006 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -40,6 +40,7 @@ impl DiagnosticWithFix for UnresolvedModule {
                     anchor: self.file.original_file(sema.db),
                     path: self.candidate.clone(),
                 },
+                initial_contents: "".to_string(),
             }
             .into(),
             unresolved_module.syntax().text_range(),
diff --git a/crates/ide_db/src/source_change.rs b/crates/ide_db/src/source_change.rs
index e87d98dadcf..10c0abdacae 100644
--- a/crates/ide_db/src/source_change.rs
+++ b/crates/ide_db/src/source_change.rs
@@ -44,7 +44,7 @@ impl From<Vec<SourceFileEdit>> for SourceChange {
 
 #[derive(Debug, Clone)]
 pub enum FileSystemEdit {
-    CreateFile { dst: AnchoredPathBuf },
+    CreateFile { dst: AnchoredPathBuf, initial_contents: String },
     MoveFile { src: FileId, dst: AnchoredPathBuf },
 }
 
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index e0561b5a7a8..5a1ae96aa09 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -634,30 +634,47 @@ pub(crate) fn snippet_text_document_edit(
     Ok(lsp_ext::SnippetTextDocumentEdit { text_document, edits })
 }
 
-pub(crate) fn resource_op(
+pub(crate) fn snippet_text_document_ops(
     snap: &GlobalStateSnapshot,
     file_system_edit: FileSystemEdit,
-) -> lsp_types::ResourceOp {
+) -> Vec<lsp_ext::SnippetDocumentChangeOperation> {
+    let mut ops = Vec::new();
     match file_system_edit {
-        FileSystemEdit::CreateFile { dst } => {
+        FileSystemEdit::CreateFile { dst, initial_contents } => {
             let uri = snap.anchored_path(&dst);
-            lsp_types::ResourceOp::Create(lsp_types::CreateFile {
-                uri,
+            let create_file = lsp_types::ResourceOp::Create(lsp_types::CreateFile {
+                uri: uri.clone(),
                 options: None,
                 annotation_id: None,
-            })
+            });
+            ops.push(lsp_ext::SnippetDocumentChangeOperation::Op(create_file));
+            if !initial_contents.is_empty() {
+                let text_document =
+                    lsp_types::OptionalVersionedTextDocumentIdentifier { uri, version: None };
+                let range = range(&LineIndex::new(""), TextRange::empty(TextSize::from(0)));
+                let text_edit = lsp_ext::SnippetTextEdit {
+                    range,
+                    new_text: initial_contents,
+                    insert_text_format: Some(lsp_types::InsertTextFormat::PlainText),
+                };
+                let edit_file =
+                    lsp_ext::SnippetTextDocumentEdit { text_document, edits: vec![text_edit] };
+                ops.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit_file));
+            }
         }
         FileSystemEdit::MoveFile { src, dst } => {
             let old_uri = snap.file_id_to_url(src);
             let new_uri = snap.anchored_path(&dst);
-            lsp_types::ResourceOp::Rename(lsp_types::RenameFile {
+            let rename_file = lsp_types::ResourceOp::Rename(lsp_types::RenameFile {
                 old_uri,
                 new_uri,
                 options: None,
                 annotation_id: None,
-            })
+            });
+            ops.push(lsp_ext::SnippetDocumentChangeOperation::Op(rename_file))
         }
     }
+    ops
 }
 
 pub(crate) fn snippet_workspace_edit(
@@ -666,8 +683,8 @@ pub(crate) fn snippet_workspace_edit(
 ) -> Result<lsp_ext::SnippetWorkspaceEdit> {
     let mut document_changes: Vec<lsp_ext::SnippetDocumentChangeOperation> = Vec::new();
     for op in source_change.file_system_edits {
-        let op = resource_op(&snap, op);
-        document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Op(op));
+        let ops = snippet_text_document_ops(snap, op);
+        document_changes.extend_from_slice(&ops);
     }
     for edit in source_change.source_file_edits {
         let edit = snippet_text_document_edit(&snap, source_change.is_snippet, edit)?;
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 92bc4d7f723..9d4823a34de 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -470,7 +470,7 @@ export function resolveCodeAction(ctx: Ctx): Cmd {
             return;
         }
         const edit = client.protocol2CodeConverter.asWorkspaceEdit(item.edit);
-        await applySnippetWorkspaceEdit(edit);
+        await vscode.workspace.applyEdit(edit);
     };
 }