diff --git a/src/EditorFeatures/CSharpTest/CodeActions/MoveType/MoveTypeTests.MoveToNewFile.cs b/src/EditorFeatures/CSharpTest/CodeActions/MoveType/MoveTypeTests.MoveToNewFile.cs index 2795b666bef30..c3a43618a02c9 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/MoveType/MoveTypeTests.MoveToNewFile.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/MoveType/MoveTypeTests.MoveToNewFile.cs @@ -1408,5 +1408,51 @@ record CacheContext(String Message); await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText); } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)] + public async Task MoveClassInTopLevelStatements() + { + var code = @" +using ConsoleApp1; +using System; + +var c = new C(); +Console.WriteLine(c.Hello); + +class [||]C +{ + public string Hello => ""Hello""; +}"; + + var codeAfterMove = @" +using ConsoleApp1; +using System; + +var c = new C(); +Console.WriteLine(c.Hello); +"; + + var expectedDocumentName = "C.cs"; + var destinationDocumentText = @"class C +{ + public string Hello => ""Hello""; +}"; + + await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)] + public async Task MissingInTopLevelStatementsOnly() + { + var code = @" +using ConsoleApp1; +using System; + +var c = new object(); +[||]Console.WriteLine(c.ToString()); +"; + + await TestMissingAsync(code); + } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs index 139c4efddf2ec..56077d699a069 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs @@ -13,7 +13,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -107,6 +109,11 @@ private ImmutableArray CreateActions(State state, CancellationToken var manyTypes = MultipleTopLevelTypeDeclarationInSourceDocument(state.SemanticDocument.Root); var isNestedType = IsNestedType(state.TypeNode); + var syntaxFacts = state.SemanticDocument.Document.GetRequiredLanguageService(); + var isClassNextToGlobalStatements = manyTypes + ? false + : ClassNextToGlobalStatements(state.SemanticDocument.Root, syntaxFacts); + var suggestedFileNames = GetSuggestedFileNames( state.TypeNode, isNestedType, @@ -120,7 +127,9 @@ private ImmutableArray CreateActions(State state, CancellationToken // case 2: This is a nested type, offer to move to new file. // case 3: If there is a single type decl in current file, *do not* offer move to new file, // rename actions are sufficient in this case. - if (manyTypes || isNestedType) + // case 4: If there are top level statements(Global statements) offer to move even + // in cases where there are only one class in the file. + if (manyTypes || isNestedType || isClassNextToGlobalStatements) { foreach (var fileName in suggestedFileNames) { @@ -152,6 +161,9 @@ private ImmutableArray CreateActions(State state, CancellationToken return actions.ToImmutable(); } + private static bool ClassNextToGlobalStatements(SyntaxNode root, ISyntaxFactsService syntaxFacts) + => syntaxFacts.ContainsGlobalStatement(root); + private CodeAction GetCodeAction(State state, string fileName, MoveTypeOperationKind operationKind) => new MoveTypeCodeAction((TService)this, state, operationKind, fileName);