diff --git a/crates/assists/src/assist_context.rs b/crates/assists/src/assist_context.rs index 69499ea326..80cf9aba11 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, + file_system_edits: Vec, } 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) { + 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 0000000000..5fc190fa67 --- /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::()?; + 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 6e736ccb38..6b89b2d044 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 709a34d03a..b41f4874a5 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 cc7c4a3433..e9093ec536 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 049f808dc6..3ad30f0c9e 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 e8b8966238..d79f5c1700 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 e87d98dadc..10c0abdaca 100644 --- a/crates/ide_db/src/source_change.rs +++ b/crates/ide_db/src/source_change.rs @@ -44,7 +44,7 @@ impl From> 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 e0561b5a7a..5a1ae96aa0 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 { + 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 { let mut document_changes: Vec = 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 92bc4d7f72..9d4823a34d 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); }; }