提交 6316f3d6 编写于 作者: B Balaji Krishnan 提交者: GitHub

Merge pull request #12624 from balajikris/MoveType2

Implement Move type to file refactoring
......@@ -156,6 +156,10 @@
<Compile Include="CodeActions\IntroduceVariable\IntroduceVariableTests.cs" />
<Compile Include="CodeActions\InvertIf\InvertIfTests.cs" />
<Compile Include="CodeActions\LambdaSimplifier\LambdaSimplifierTests.cs" />
<Compile Include="CodeActions\MoveType\CSharpMoveTypeTestsBase.cs" />
<Compile Include="CodeActions\MoveType\MoveTypeTests.MoveToNewFile.cs" />
<Compile Include="CodeActions\MoveType\MoveTypeTests.RenameType.cs" />
<Compile Include="CodeActions\MoveType\MoveTypeTests.RenameFile.cs" />
<Compile Include="CodeActions\ReplacePropertyWithMethods\ReplacePropertyWithMethodsTests.cs" />
<Compile Include="Completion\CompletionServiceTests.cs" />
<Compile Include="Diagnostics\AddUsing\AddUsingTests_NuGet.cs" />
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CodeRefactorings.MoveType;
using Microsoft.CodeAnalysis.Editor.UnitTests.MoveType;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.MoveType
{
public abstract class CSharpMoveTypeTestsBase : AbstractMoveTypeTest
{
protected override ParseOptions GetScriptOptions()
{
return Options.Script;
}
protected override Task<TestWorkspace> CreateWorkspaceFromFileAsync(string definition, ParseOptions parseOptions, CompilationOptions compilationOptions)
{
return TestWorkspace.CreateCSharpAsync(definition, parseOptions, compilationOptions);
}
protected override string GetLanguage()
{
return LanguageNames.CSharp;
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.MoveType
{
public partial class MoveTypeTests : CSharpMoveTypeTestsBase
{
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task TestForSpans1()
{
var code =
@"[|clas|]s Class1 { }
class Class2 { }";
var codeAfterMove = @"class Class2 { }";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText = @"class Class1 { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task TestForSpans2()
{
var code =
@"[|class Class1|] { }
class Class2 { }";
var codeAfterMove = @"class Class2 { }";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText = @"class Class1 { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task TestForSpans3()
{
var code =
@"class Class1[||] { }
class Class2 { }";
var codeAfterMove = @"class Class2 { }";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText = @"class Class1 { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveTypeWithNoContainerNamespace()
{
var code =
@"[||]class Class1 { }
class Class2 { }";
var codeAfterMove = @"class Class2 { }";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText = @"class Class1 { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveTypeWithWithUsingsAndNoContainerNamespace()
{
var code =
@"// Banner Text
using System;
[||]class Class1 { }
class Class2 { }";
var codeAfterMove =
@"// Banner Text
class Class2 { }";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText =
@"class Class1 { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveTypeWithWithMembers()
{
var code =
@"// Banner Text
using System;
[||]class Class1
{
void Print(int x)
{
Console.WriteLine(x);
}
}
class Class2 { }";
var codeAfterMove =
@"// Banner Text
class Class2 { }";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText =
@"
using System;
class Class1
{
void Print(int x)
{
Console.WriteLine(x);
}
}";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveTypeWithWithMembers2()
{
var code =
@"// Banner Text
using System;
[||]class Class1
{
void Print(int x)
{
Console.WriteLine(x);
}
}
class Class2
{
void Print(int x)
{
Console.WriteLine(x);
}
}";
var codeAfterMove =
@"// Banner Text
using System;
class Class2
{
void Print(int x)
{
Console.WriteLine(x);
}
}";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText =
@"
using System;
class Class1
{
void Print(int x)
{
Console.WriteLine(x);
}
}";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveAnInterface()
{
var code =
@"[||]interface IMoveType { }
class Class2 { }";
var codeAfterMove = @"class Class2 { }";
var expectedDocumentName = "IMoveType.cs";
var destinationDocumentText = @"interface IMoveType { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveAStruct()
{
var code =
@"[||]struct MyStruct { }
class Class2 { }";
var codeAfterMove = @"class Class2 { }";
var expectedDocumentName = "MyStruct.cs";
var destinationDocumentText = @"struct MyStruct { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveAnEnum()
{
var code =
@"[||]enum MyEnum { }
class Class2 { }";
var codeAfterMove = @"class Class2 { }";
var expectedDocumentName = "MyEnum.cs";
var destinationDocumentText = @"enum MyEnum { }";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveTypeWithWithContainerNamespace()
{
var code =
@"namespace N1
{
[||]class Class1 { }
class Class2 { }
}";
var codeAfterMove =
@"namespace N1
{
class Class2 { }
}";
var expectedDocumentName = "Class1.cs";
var destinationDocumentText =
@"namespace N1
{
class Class1 { }
}";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveNestedTypeToNewFile_Simple()
{
var code =
@"namespace N1
{
class Class1
{
[||]class Class2 { }
}
}";
var codeAfterMove =
@"namespace N1
{
partial class Class1
{
}
}";
var expectedDocumentName = "Class2.cs";
var destinationDocumentText =
@"namespace N1
{
partial class Class1
{
class Class2
{
}
}
}";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveNestedTypeToNewFile_ParentHasOtherMembers()
{
var code =
@"namespace N1
{
class Class1
{
private int _field1;
[||]class Class2 { }
public void Method1() { }
}
}";
var codeAfterMove =
@"namespace N1
{
partial class Class1
{
private int _field1;
public void Method1() { }
}
}";
var expectedDocumentName = "Class2.cs";
var destinationDocumentText =
@"namespace N1
{
partial class Class1
{
class Class2
{
}
}
}";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveNestedTypeToNewFile_HasOtherTopLevelMembers()
{
var code =
@"namespace N1
{
class Class1
{
private int _field1;
[||]class Class2 { }
public void Method1() { }
}
internal class Class3
{
private void Method1() { }
}
}";
var codeAfterMove =
@"namespace N1
{
partial class Class1
{
private int _field1;
public void Method1() { }
}
internal class Class3
{
private void Method1() { }
}
}";
var expectedDocumentName = "Class2.cs";
var destinationDocumentText =
@"namespace N1
{
partial class Class1
{
class Class2
{
}
}
}";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveNestedTypeToNewFile_HasMembers()
{
var code =
@"namespace N1
{
class Class1
{
private int _field1;
[||]class Class2
{
private string _field1;
public void InnerMethod() { }
}
public void Method1() { }
}
}";
var codeAfterMove =
@"namespace N1
{
partial class Class1
{
private int _field1;
public void Method1() { }
}
}";
var expectedDocumentName = "Class2.cs";
var destinationDocumentText =
@"namespace N1
{
partial class Class1
{
class Class2
{
private string _field1;
public void InnerMethod() { }
}
}
}";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.MoveType
{
public partial class MoveTypeTests : CSharpMoveTypeTestsBase
{
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task SingleClassInFile_RenameFile()
{
var code =
@"[||]class Class1 { }";
var expectedDocumentName = "Class1.cs";
await TestRenameFileToMatchTypeAsync(code, expectedDocumentName);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoreThanOneTypeInFile_RenameFile()
{
var code =
@"[||]class Class1
{
class Inner { }
}";
var expectedDocumentName = "Class1.cs";
await TestRenameFileToMatchTypeAsync(code, expectedDocumentName);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task TypeNameMatchesFileName_RenameFile()
{
// testworkspace creates files like test1.cs, test2.cs and so on..
// so type name matches filename here and rename file action should not be offered.
var code =
@"[||]class test1 { }";
await TestRenameFileToMatchTypeAsync(code, expectedCodeAction: false);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoreThanOneTopLevelTypeInFile_RenameFile()
{
var code =
@"[||]class Class1 { }
class Class2 { }";
await TestRenameFileToMatchTypeAsync(code, expectedCodeAction: false);
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.MoveType
{
public partial class MoveTypeTests : CSharpMoveTypeTestsBase
{
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task SingleClassInFile_RenameType()
{
var code =
@"[||]class Class1 { }";
var codeWithTypeRenamedToMatchFileName =
@"class [|test1|] { }";
await TestRenameTypeToMatchFileAsync(code, codeWithTypeRenamedToMatchFileName);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoreThanOneTypeInFile_RenameType()
{
var code =
@"[||]class Class1
{
class Inner { }
}";
var codeWithTypeRenamedToMatchFileName =
@"class [|test1|]
{
class Inner { }
}";
await TestRenameTypeToMatchFileAsync(code, codeWithTypeRenamedToMatchFileName);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task TypeNameMatchesFileName_RenameType()
{
// testworkspace creates files like test1.cs, test2.cs and so on..
// so type name matches filename here and rename file action should not be offered.
var code =
@"[||]class test1 { }";
await TestRenameTypeToMatchFileAsync(code, expectedCodeAction: false);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoreThanOneTopLevelTypeInFile_RenameType()
{
var code =
@"[||]class Class1 { }
class Class2 { }";
await TestRenameTypeToMatchFileAsync(code, expectedCodeAction: false);
}
}
}
......@@ -8,6 +8,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.Editor.Implementation.Preview;
using Microsoft.CodeAnalysis.Editor.UnitTests.Extensions;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Options;
......@@ -152,6 +153,147 @@ public abstract class AbstractCodeActionOrUserDiagnosticTest
}
}
protected async Task TestAddDocument(
string initialMarkup, string expectedMarkup,
IList<string> expectedContainers,
string expectedDocumentName,
int index = 0,
bool compareTokens = true, bool isLine = true)
{
await TestAddDocument(initialMarkup, expectedMarkup, index, expectedContainers, expectedDocumentName, parseOptions: null, compilationOptions: null, compareTokens: compareTokens, isLine: isLine);
await TestAddDocument(initialMarkup, expectedMarkup, index, expectedContainers, expectedDocumentName, GetScriptOptions(), compilationOptions: null, compareTokens: compareTokens, isLine: isLine);
}
protected async Task<Tuple<Solution, Solution>> TestAddDocumentAsync(
TestWorkspace workspace,
string expectedMarkup,
int index,
string expectedDocumentName,
IList<string> expectedContainers,
bool compareTokens = true)
{
var codeActions = await GetCodeActionsAsync(workspace, fixAllActionEquivalenceKey: null);
return await TestAddDocument(workspace, expectedMarkup, index, expectedContainers, expectedDocumentName,
codeActions, compareTokens);
}
private async Task TestAddDocument(
string initialMarkup, string expectedMarkup,
int index,
IList<string> expectedContainers,
string expectedDocumentName,
ParseOptions parseOptions, CompilationOptions compilationOptions,
bool compareTokens, bool isLine)
{
using (var workspace = isLine
? await CreateWorkspaceFromFileAsync(initialMarkup, parseOptions, compilationOptions)
: await TestWorkspace.CreateAsync(initialMarkup))
{
var codeActions = await GetCodeActionsAsync(workspace, fixAllActionEquivalenceKey: null);
await TestAddDocument(workspace, expectedMarkup, index, expectedContainers, expectedDocumentName,
codeActions, compareTokens);
}
}
private async Task<Tuple<Solution, Solution>> TestAddDocument(
TestWorkspace workspace,
string expectedMarkup,
int index,
IList<string> expectedFolders,
string expectedDocumentName,
IList<CodeAction> actions,
bool compareTokens)
{
var operations = await VerifyInputsAndGetOperationsAsync(index, actions);
return await TestAddDocument(
workspace,
expectedMarkup,
operations,
hasProjectChange: false,
modifiedProjectId: null,
expectedFolders: expectedFolders,
expectedDocumentName: expectedDocumentName,
compareTokens: compareTokens);
}
protected async Task<Tuple<Solution, Solution>> TestAddDocument(
TestWorkspace workspace,
string expected,
IEnumerable<CodeActionOperation> operations,
bool hasProjectChange,
ProjectId modifiedProjectId,
IList<string> expectedFolders,
string expectedDocumentName,
bool compareTokens)
{
var appliedChanges = ApplyOperationsAndGetSolution(workspace, operations);
var oldSolution = appliedChanges.Item1;
var newSolution = appliedChanges.Item2;
Document addedDocument = null;
if (!hasProjectChange)
{
addedDocument = SolutionUtilities.GetSingleAddedDocument(oldSolution, newSolution);
}
else
{
Assert.NotNull(modifiedProjectId);
addedDocument = newSolution.GetProject(modifiedProjectId).Documents.SingleOrDefault(doc => doc.Name == expectedDocumentName);
}
Assert.NotNull(addedDocument);
AssertEx.Equal(expectedFolders, addedDocument.Folders);
Assert.Equal(expectedDocumentName, addedDocument.Name);
if (compareTokens)
{
TokenUtilities.AssertTokensEqual(
expected, (await addedDocument.GetTextAsync()).ToString(), GetLanguage());
}
else
{
Assert.Equal(expected, (await addedDocument.GetTextAsync()).ToString());
}
var editHandler = workspace.ExportProvider.GetExportedValue<ICodeActionEditHandlerService>();
if (!hasProjectChange)
{
// If there is just one document change then we expect the preview to be a WpfTextView
var content = (await editHandler.GetPreviews(workspace, operations, CancellationToken.None).GetPreviewsAsync())[0];
using (var diffView = content as DifferenceViewerPreview)
{
Assert.NotNull(diffView.Viewer);
}
}
else
{
// If there are more changes than just the document we need to browse all the changes and get the document change
var contents = editHandler.GetPreviews(workspace, operations, CancellationToken.None);
bool hasPreview = false;
var previews = await contents.GetPreviewsAsync();
if (previews != null)
{
foreach (var preview in previews)
{
if (preview != null)
{
var diffView = preview as DifferenceViewerPreview;
if (diffView?.Viewer != null)
{
hasPreview = true;
diffView.Dispose();
break;
}
}
}
}
Assert.True(hasPreview);
}
return Tuple.Create(oldSolution, newSolution);
}
internal async Task TestAsync(
string initialMarkup, string expectedMarkup,
int index = 0, bool compareTokens = true,
......
......@@ -23,7 +23,6 @@
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Microsoft.CodeAnalysis.Editor.Implementation.Preview;
namespace Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics
{
......@@ -281,133 +280,6 @@ protected async Task TestEquivalenceKeyAsync(string initialMarkup, string equiva
}
}
protected async Task TestAddDocument(
string initialMarkup, string expectedMarkup,
IList<string> expectedContainers,
string expectedDocumentName,
int index = 0,
bool compareTokens = true, bool isLine = true)
{
await TestAddDocument(initialMarkup, expectedMarkup, index, expectedContainers, expectedDocumentName, null, null, compareTokens, isLine);
await TestAddDocument(initialMarkup, expectedMarkup, index, expectedContainers, expectedDocumentName, GetScriptOptions(), null, compareTokens, isLine);
}
private async Task TestAddDocument(
string initialMarkup, string expectedMarkup,
int index,
IList<string> expectedContainers,
string expectedDocumentName,
ParseOptions parseOptions, CompilationOptions compilationOptions,
bool compareTokens, bool isLine)
{
using (var workspace = isLine
? await CreateWorkspaceFromFileAsync(initialMarkup, parseOptions, compilationOptions)
: await TestWorkspace.CreateAsync(initialMarkup))
{
var codeActions = await GetCodeActionsAsync(workspace, fixAllActionEquivalenceKey: null);
await TestAddDocument(workspace, expectedMarkup, index, expectedContainers, expectedDocumentName,
codeActions, compareTokens);
}
}
private async Task TestAddDocument(
TestWorkspace workspace,
string expectedMarkup,
int index,
IList<string> expectedFolders,
string expectedDocumentName,
IList<CodeAction> actions,
bool compareTokens)
{
var operations = await VerifyInputsAndGetOperationsAsync(index, actions);
await TestAddDocument(
workspace,
expectedMarkup,
operations,
hasProjectChange: false,
modifiedProjectId: null,
expectedFolders: expectedFolders,
expectedDocumentName: expectedDocumentName,
compareTokens: compareTokens);
}
private async Task<Tuple<Solution, Solution>> TestAddDocument(
TestWorkspace workspace,
string expected,
IEnumerable<CodeActionOperation> operations,
bool hasProjectChange,
ProjectId modifiedProjectId,
IList<string> expectedFolders,
string expectedDocumentName,
bool compareTokens)
{
var appliedChanges = ApplyOperationsAndGetSolution(workspace, operations);
var oldSolution = appliedChanges.Item1;
var newSolution = appliedChanges.Item2;
Document addedDocument = null;
if (!hasProjectChange)
{
addedDocument = SolutionUtilities.GetSingleAddedDocument(oldSolution, newSolution);
}
else
{
Assert.NotNull(modifiedProjectId);
addedDocument = newSolution.GetProject(modifiedProjectId).Documents.SingleOrDefault(doc => doc.Name == expectedDocumentName);
}
Assert.NotNull(addedDocument);
AssertEx.Equal(expectedFolders, addedDocument.Folders);
Assert.Equal(expectedDocumentName, addedDocument.Name);
if (compareTokens)
{
TokenUtilities.AssertTokensEqual(
expected, (await addedDocument.GetTextAsync()).ToString(), GetLanguage());
}
else
{
Assert.Equal(expected, (await addedDocument.GetTextAsync()).ToString());
}
var editHandler = workspace.ExportProvider.GetExportedValue<ICodeActionEditHandlerService>();
if (!hasProjectChange)
{
// If there is just one document change then we expect the preview to be a WpfTextView
var content = (await editHandler.GetPreviews(workspace, operations, CancellationToken.None).GetPreviewsAsync())[0];
var diffView = content as DifferenceViewerPreview;
Assert.NotNull(diffView.Viewer);
diffView.Dispose();
}
else
{
// If there are more changes than just the document we need to browse all the changes and get the document change
var contents = editHandler.GetPreviews(workspace, operations, CancellationToken.None);
bool hasPreview = false;
var previews = await contents.GetPreviewsAsync();
if (previews != null)
{
foreach (var preview in previews)
{
if (preview != null)
{
var diffView = preview as DifferenceViewerPreview;
if (diffView?.Viewer != null)
{
hasPreview = true;
diffView.Dispose();
break;
}
}
}
}
Assert.True(hasPreview);
}
return Tuple.Create(oldSolution, newSolution);
}
internal async Task TestWithMockedGenerateTypeDialog(
string initial,
string languageName,
......
......@@ -218,6 +218,7 @@
<Compile Include="Extensions\SourceTextContainerExtensionsTests.cs" />
<Compile Include="Extensions\WorkspaceExtensions.cs" />
<Compile Include="GoToAdjacentMember\AbstractGoToAdjacentMemberTests.cs" />
<Compile Include="MoveType\AbstractMoveTypeTest.cs" />
<Compile Include="Outlining\AbstractSyntaxNodeOutlinerTests.cs" />
<Compile Include="Outlining\AbstractSyntaxOutlinerTests.cs" />
<Compile Include="Outlining\AbstractSyntaxTriviaOutlinerTests.cs" />
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CodeRefactorings.MoveType;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.UnitTests.MoveType
{
public abstract class AbstractMoveTypeTest : AbstractCodeActionTest
{
private string RenameFileCodeActionTitle = FeaturesResources.Rename_file_to_0;
private string RenameTypeCodeActionTitle = FeaturesResources.Rename_type_to_0;
protected override CodeRefactoringProvider CreateCodeRefactoringProvider(Workspace workspace)
{
return new MoveTypeCodeRefactoringProvider();
}
protected async Task TestRenameTypeToMatchFileAsync(
string originalCode,
string expectedCode = null,
bool expectedCodeAction = true,
bool compareTokens = true)
{
using (var workspace = await CreateWorkspaceFromFileAsync(originalCode, parseOptions: null, compilationOptions: null))
{
if (expectedCodeAction)
{
Assert.True(expectedCode != null, $"{nameof(expectedCode)} should be present if {nameof(expectedCodeAction)} is true.");
var documentId = workspace.Documents[0].Id;
var documentName = workspace.Documents[0].Name;
string expectedText;
TextSpan span;
MarkupTestFile.GetSpan(expectedCode, out expectedText, out span);
var codeActionTitle = string.Format(RenameTypeCodeActionTitle, expectedText.Substring(span.Start, span.Length));
var oldSolutionAndNewSolution = await TestOperationAsync(
workspace, expectedText, codeActionTitle, compareTokens);
// the original source document does not exist in the new solution.
var newSolution = oldSolutionAndNewSolution.Item2;
var document = newSolution.GetDocument(documentId);
Assert.NotNull(document);
Assert.Equal(documentName, document.Name);
}
else
{
var actions = await GetCodeActionsAsync(workspace, fixAllActionEquivalenceKey: null);
if (actions != null)
{
var renameFileAction = actions.Any(action => action.Title.StartsWith(RenameTypeCodeActionTitle));
Assert.False(renameFileAction, "Rename Type to match file name code action was not expected, but shows up.");
}
}
}
}
protected async Task TestRenameFileToMatchTypeAsync(
string originalCode,
string expectedDocumentName = null,
bool expectedCodeAction = true,
bool compareTokens = true)
{
using (var workspace = await CreateWorkspaceFromFileAsync(originalCode, parseOptions: null, compilationOptions: null))
{
if (expectedCodeAction)
{
Assert.True(expectedDocumentName != null, $"{nameof(expectedDocumentName)} should be present if {nameof(expectedCodeAction)} is true.");
var oldDocumentId = workspace.Documents[0].Id;
string expectedText;
IList<TextSpan> spans;
MarkupTestFile.GetSpans(originalCode, out expectedText, out spans);
var codeActionTitle = string.Format(RenameFileCodeActionTitle, expectedDocumentName);
// a new document with the same text as old document is added.
var oldSolutionAndNewSolution = await TestOperationAsync(
workspace, expectedText, codeActionTitle, compareTokens);
// the original source document does not exist in the new solution.
var newSolution = oldSolutionAndNewSolution.Item2;
Assert.Null(newSolution.GetDocument(oldDocumentId));
}
else
{
var actions = await GetCodeActionsAsync(workspace, fixAllActionEquivalenceKey: null);
if (actions != null)
{
var renameFileAction = actions.Any(action => action.Title.StartsWith(RenameFileCodeActionTitle));
Assert.False(renameFileAction, "Rename File to match type code action was not expected, but shows up.");
}
}
}
}
private async Task<Tuple<Solution, Solution>> TestOperationAsync(
Workspaces.TestWorkspace workspace,
string expectedCode,
string operation,
bool compareTokens)
{
var actions = await GetCodeActionsAsync(workspace, fixAllActionEquivalenceKey: null);
var action = actions.Single(a => a.Title.StartsWith(operation));
var operations = await action.GetOperationsAsync(CancellationToken.None);
return await TestOperationsAsync(workspace,
expectedText: expectedCode,
operations: operations,
conflictSpans: null,
renameSpans: null,
warningSpans: null,
compareTokens: compareTokens,
expectedChangedDocumentId: null);
}
protected async Task TestMoveTypeToNewFileAsync(
string originalCode,
string expectedSourceTextAfterRefactoring,
string expectedDocumentName,
string destinationDocumentText,
IList<string> destinationDocumentContainers = null,
bool expectedCodeAction = true,
int index = 0,
bool compareTokens = true)
{
if (expectedCodeAction)
{
using (var workspace = await CreateWorkspaceFromFileAsync(originalCode, parseOptions: null, compilationOptions: null))
{
// replace with default values on null.
if (destinationDocumentContainers == null)
{
destinationDocumentContainers = Array.Empty<string>();
}
var sourceDocumentId = workspace.Documents[0].Id;
// Verify the newly added document and its text
var oldSolutionAndNewSolution = await TestAddDocumentAsync(workspace,
destinationDocumentText, index, expectedDocumentName, destinationDocumentContainers, compareTokens: compareTokens);
// Verify source document's text after moving type.
var oldSolution = oldSolutionAndNewSolution.Item1;
var newSolution = oldSolutionAndNewSolution.Item2;
var changedDocumentIds = SolutionUtilities.GetChangedDocuments(oldSolution, newSolution);
Assert.True(changedDocumentIds.Contains(sourceDocumentId), "source document was not changed.");
var modifiedSourceDocument = newSolution.GetDocument(sourceDocumentId);
if (compareTokens)
{
TokenUtilities.AssertTokensEqual(
expectedSourceTextAfterRefactoring, (await modifiedSourceDocument.GetTextAsync()).ToString(), GetLanguage());
}
else
{
Assert.Equal(expectedSourceTextAfterRefactoring, (await modifiedSourceDocument.GetTextAsync()).ToString());
}
}
}
else
{
await TestMissingAsync(originalCode, parseOptions: null);
}
}
}
}
......@@ -68,6 +68,7 @@ public static class Features
public const string CodeActionsMakeMethodSynchronous = "CodeActions.MakeMethodSynchronous";
public const string CodeActionsMoveDeclarationNearReference = "CodeActions.MoveDeclarationNearReference";
public const string CodeActionsMoveToTopOfFile = "CodeActions.MoveToTopOfFile";
public const string CodeActionsMoveType = "CodeActions.MoveType";
public const string CodeActionsPopulateSwitch = "CodeActions.PopulateSwitch";
public const string CodeActionsQualifyMemberAccess = "CodeActions.QualifyMemberAccess";
public const string CodeActionsReplaceMethodWithProperty = "CodeActions.ReplaceMethodWithProperty";
......
......@@ -136,6 +136,10 @@
<Compile Include="CodeActions\InlineTemporary\InlineTemporaryTests.vb" />
<Compile Include="CodeActions\IntroduceVariable\IntroduceVariableTests.vb" />
<Compile Include="CodeActions\InvertIf\InvertIfTests.vb" />
<Compile Include="CodeActions\MoveType\BasicMoveTypeTestsBase.vb" />
<Compile Include="CodeActions\MoveType\MoveTypeTests.MoveToNewFile.vb" />
<Compile Include="CodeActions\MoveType\MoveTypeTests.RenameType.vb" />
<Compile Include="CodeActions\MoveType\MoveTypeTests.RenameFile.vb" />
<Compile Include="CodeActions\Preview\PreviewTests.vb" />
<Compile Include="CodeActions\ReplacePropertyWithMethods\ReplacePropertyWithMethodsTests.vb" />
<Compile Include="Completion\CompletionServiceTests.vb" />
......
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Imports System.Xml.Linq
Imports Microsoft.CodeAnalysis.CodeRefactorings
Imports Microsoft.CodeAnalysis.CodeRefactorings.MoveType
Imports Microsoft.CodeAnalysis.Editor.UnitTests.MoveType
Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings.MoveType
Public Class BasicMoveTypeTestsBase
Inherits AbstractMoveTypeTest
Protected Overrides Function CreateWorkspaceFromFileAsync(
definition As String,
ParseOptions As ParseOptions,
CompilationOptions As CompilationOptions
) As Task(Of TestWorkspace)
Return TestWorkspace.CreateVisualBasicAsync(
definition,
ParseOptions,
If(CompilationOptions, New VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary)))
End Function
Protected Overrides Function GetLanguage() As String
Return LanguageNames.VisualBasic
End Function
Protected Overrides Function GetScriptOptions() As ParseOptions
Return TestOptions.Script
End Function
Protected Overloads Function TestRenameTypeToMatchFileAsync(
originalCode As XElement,
Optional expectedCode As XElement = Nothing,
Optional expectedCodeAction As Boolean = True,
Optional compareTokens As Boolean = True
) As Task
Dim expectedText As String = Nothing
If Not expectedCode Is Nothing Then
expectedText = expectedCode.ConvertTestSourceTag()
End If
Return MyBase.TestRenameTypeToMatchFileAsync(
originalCode.ConvertTestSourceTag(), expectedText, expectedCodeAction, compareTokens)
End Function
Protected Overloads Function TestRenameFileToMatchTypeAsync(
originalCode As XElement,
Optional expectedDocumentName As String = Nothing,
Optional expectedCodeAction As Boolean = True,
Optional compareTokens As Boolean = True
) As Task
Return MyBase.TestRenameFileToMatchTypeAsync(
originalCode.ConvertTestSourceTag(), expectedDocumentName, expectedCodeAction, compareTokens)
End Function
Protected Overloads Function TestMoveTypeToNewFileAsync(
originalCode As XElement,
expectedSourceTextAfterRefactoring As XElement,
expectedDocumentName As String,
destinationDocumentText As XElement,
Optional destinationDocumentContainers As IList(Of String) = Nothing,
Optional expectedCodeAction As Boolean = True,
Optional index As Integer = 0,
Optional compareTokens As Boolean = True
) As Task
Dim originalCodeText = originalCode.ConvertTestSourceTag()
Dim expectedSourceText = expectedSourceTextAfterRefactoring.ConvertTestSourceTag()
Dim expectedDestinationText = destinationDocumentText.ConvertTestSourceTag()
Return MyBase.TestMoveTypeToNewFileAsync(
originalCodeText,
expectedSourceText,
expectedDocumentName,
expectedDestinationText,
destinationDocumentContainers,
expectedCodeAction,
index,
compareTokens)
End Function
End Class
End Namespace
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings.MoveType
Partial Public Class MoveTypeTests
Inherits BasicMoveTypeTestsBase
<WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)>
Public Async Function MultipleTypesInFileWithNoContainerNamespace() As Task
Dim code =
<File>
[||]Class Class1
End Class
Class Class2
End Class
</File>
Dim codeAfterMove =
<File>
Class Class2
End Class
</File>
Dim expectedDocumentName = "Class1.vb"
Dim destinationDocumentText =
<File>
Class Class1
End Class
</File>
Await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText)
End Function
End Class
End Namespace
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings.MoveType
Partial Public Class MoveTypeTests
<WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)>
Public Async Function SingleClassInFileWithNoContainerNamespace_RenameFile() As Task
Dim code =
<File>
[||]Class Class1
End Class
</File>
Dim expectedDocumentName = "Class1.vb"
Await TestRenameFileToMatchTypeAsync(code, expectedDocumentName)
End Function
<WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)>
Public Async Function TypeNameMatchesFileName_RenameFile() As Task
' testworkspace creates files Like test1.cs, test2.cs And so on..
' so type name matches filename here And rename file action should Not be offered.
Dim code =
<File>
[||]Class test1
End Class
</File>
Await TestRenameFileToMatchTypeAsync(code, expectedCodeAction:=False)
End Function
<WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)>
Public Async Function MoreThanOneTypeInFile_RenameFile() As Task
Dim code =
<File>
[||]Class Class1
End Class
Class Class2
End Class
</File>
Await TestRenameFileToMatchTypeAsync(code, expectedCodeAction:=False)
End Function
End Class
End Namespace
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings.MoveType
Partial Public Class MoveTypeTests
<WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)>
Public Async Function SingleClassInFileWithNoContainerNamespace_RenameType() As Task
Dim code =
<File>
[||]Class Class1
End Class
</File>
Dim codeAfterRenamingType =
<File>
Class [|test1|]
End Class
</File>
Await TestRenameTypeToMatchFileAsync(code, codeAfterRenamingType)
End Function
<WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)>
Public Async Function TypeNameMatchesFileName_RenameType() As Task
' testworkspace creates files Like test1.cs, test2.cs And so on..
' so type name matches filename here And rename file action should Not be offered.
Dim code =
<File>
[||]Class test1
End Class
</File>
Await TestRenameTypeToMatchFileAsync(code, expectedCodeAction:=False)
End Function
<WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)>
Public Async Function MoreThanOneTypeInFile_RenameType() As Task
Dim code =
<File>
[||]Class Class1
End Class
Class Class2
End Class
</File>
Await TestRenameTypeToMatchFileAsync(code, expectedCodeAction:=False)
End Function
End Class
End Namespace
......@@ -104,6 +104,7 @@
<Compile Include="CodeRefactorings\MoveDeclarationNearReference\MoveDeclarationNearReferenceCodeRefactoringProvider.cs" />
<Compile Include="CodeRefactorings\MoveDeclarationNearReference\MoveDeclarationNearReferenceCodeRefactoringProvider.Rewriter.cs" />
<Compile Include="CodeRefactorings\MoveDeclarationNearReference\MoveDeclarationNearReferenceCodeRefactoringProvider.State.cs" />
<Compile Include="CodeRefactorings\MoveType\CSharpMoveTypeService.cs" />
<Compile Include="Completion\CompletionProviders\AttributeNamedParameterCompletionProvider.cs" />
<Compile Include="Completion\CompletionProviders\CompletionUtilities.cs" />
<Compile Include="Completion\CompletionProviders\CrefCompletionProvider.cs" />
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Composition;
using Microsoft.CodeAnalysis.CodeRefactorings.MoveType;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.MoveType
{
[ExportLanguageService(typeof(IMoveTypeService), LanguageNames.CSharp), Shared]
internal class CSharpMoveTypeService :
AbstractMoveTypeService<CSharpMoveTypeService, BaseTypeDeclarationSyntax, NamespaceDeclarationSyntax, MemberDeclarationSyntax, CompilationUnitSyntax>
{
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax>
{
/// <summary>
/// An abstract class for different edits performed by the Move Type Code Action.
/// </summary>
private abstract class Editor
{
public Editor(
TService service,
State state,
CancellationToken cancellationToken)
{
State = state;
Service = service;
CancellationToken = cancellationToken;
}
protected State State { get; }
protected TService Service { get; }
protected CancellationToken CancellationToken { get; }
protected SemanticDocument SemanticDocument => State.SemanticDocument;
/// <summary>
/// operations performed by CodeAction.
/// </summary>
internal abstract Task<IEnumerable<CodeActionOperation>> GetOperationsAsync();
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax>
{
private class MoveTypeCodeAction : CodeAction
{
private readonly State _state;
private readonly TService _service;
private readonly OperationKind _operationKind;
private readonly string _title;
public MoveTypeCodeAction(
TService service,
State state,
OperationKind operationKind)
{
_state = state;
_service = service;
_operationKind = operationKind;
_title = CreateDisplayText();
}
private string CreateDisplayText()
{
switch (_operationKind)
{
case OperationKind.MoveType:
return string.Format(FeaturesResources.Move_type_to_0, _state.TargetFileNameCandidate);
case OperationKind.RenameType:
return string.Format(FeaturesResources.Rename_type_to_0, _state.DocumentName);
case OperationKind.RenameFile:
return string.Format(FeaturesResources.Rename_file_to_0, _state.TargetFileNameCandidate);
}
throw ExceptionUtilities.Unreachable;
}
public override string Title => _title;
protected override Task<IEnumerable<CodeActionOperation>> ComputeOperationsAsync(CancellationToken cancellationToken)
{
var editor = GetEditor(cancellationToken);
return editor.GetOperationsAsync();
}
private Editor GetEditor(CancellationToken cancellationToken)
{
switch (_operationKind)
{
case OperationKind.MoveType:
return new MoveTypeEditor(_service, _state, cancellationToken);
case OperationKind.RenameType:
return new RenameTypeEditor(_service, _state, cancellationToken);
case OperationKind.RenameFile:
return new RenameFileEditor(_service, _state, cancellationToken);
}
throw ExceptionUtilities.Unreachable;
}
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.RemoveUnnecessaryImports;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax>
{
private class MoveTypeEditor : Editor
{
public MoveTypeEditor(
TService service,
State state,
CancellationToken cancellationToken) : base(service, state, cancellationToken)
{
}
/// <summary>
/// Given a document and a type contained in it, moves the type
/// out to its own document. The new document's name typically
/// is the type name, or is atleast based on the type name.
/// </summary>
/// <remarks>
/// The algorithm for this, is as follows:
/// 1. Fork the original document that contains the type to be moved.
/// 2. Keep the type, required namespace containers and using statements.
/// remove everything else from the forked document.
/// 3. Add this forked document to the solution.
/// 4. Finally, update the original document and remove the type from it.
/// </remarks>
internal override async Task<IEnumerable<CodeActionOperation>> GetOperationsAsync()
{
var solution = SemanticDocument.Document.Project.Solution;
// Fork, update and add as new document.
var projectToBeUpdated = SemanticDocument.Document.Project;
var newDocumentId = DocumentId.CreateNewId(projectToBeUpdated.Id, State.TargetFileNameCandidate);
var solutionWithNewDocument = await AddNewDocumentWithSingleTypeDeclarationAndImportsAsync(newDocumentId).ConfigureAwait(false);
// Get the original source document again, from the latest forked solution.
var sourceDocument = solutionWithNewDocument.GetDocument(SemanticDocument.Document.Id);
// update source document to add partial modifiers to type chain
// and/or remove type declaration from original source document.
var solutionWithBothDocumentsUpdated = await RemoveTypeFromSourceDocumentAsync(
sourceDocument).ConfigureAwait(false);
return SpecializedCollections.SingletonEnumerable(new ApplyChangesOperation(solutionWithBothDocumentsUpdated));
}
/// <summary>
/// Forks the source document, keeps required type, namespace containers
/// and adds it the solution.
/// </summary>
/// <param name="newDocumentId">id for the new document to be added</param>
/// <returns>the new solution which contains a new document with the type being moved</returns>
private async Task<Solution> AddNewDocumentWithSingleTypeDeclarationAndImportsAsync(
DocumentId newDocumentId)
{
Debug.Assert(SemanticDocument.Document.Name != State.TargetFileNameCandidate,
$"New document name is same as old document name:{State.TargetFileNameCandidate}");
var root = SemanticDocument.Root;
var projectToBeUpdated = SemanticDocument.Document.Project;
var documentEditor = await DocumentEditor.CreateAsync(SemanticDocument.Document, CancellationToken).ConfigureAwait(false);
AddPartialModifiersToTypeChain(documentEditor);
// remove things that are not being moved, from the forked document.
var membersToRemove = GetMembersToRemove(root);
foreach (var member in membersToRemove)
{
documentEditor.RemoveNode(member, SyntaxRemoveOptions.KeepNoTrivia);
}
var modifiedRoot = documentEditor.GetChangedRoot();
// add an empty document to solution, so that we'll have options from the right context.
var solutionWithNewDocument = projectToBeUpdated.Solution.AddDocument(newDocumentId, State.TargetFileNameCandidate, text: string.Empty);
// update the text for the new document
solutionWithNewDocument = solutionWithNewDocument.WithDocumentSyntaxRoot(newDocumentId, modifiedRoot, PreservationMode.PreserveIdentity);
// get the updated document, perform clean up like remove unused usings.
var newDocument = solutionWithNewDocument.GetDocument(newDocumentId);
newDocument = await CleanUpDocumentAsync(newDocument).ConfigureAwait(false);
return newDocument.Project.Solution;
}
/// <summary>
/// update the original document and remove the type that was moved.
/// perform other fix ups as necessary.
/// </summary>
/// <param name="sourceDocument">original document</param>
/// <returns>an updated solution with the original document fixed up as appropriate.</returns>
private async Task<Solution> RemoveTypeFromSourceDocumentAsync(Document sourceDocument)
{
var documentEditor = await DocumentEditor.CreateAsync(sourceDocument, CancellationToken).ConfigureAwait(false);
AddPartialModifiersToTypeChain(documentEditor);
documentEditor.RemoveNode(State.TypeNode, SyntaxRemoveOptions.KeepNoTrivia);
var updatedDocument = documentEditor.GetChangedDocument();
updatedDocument = await CleanUpDocumentAsync(updatedDocument).ConfigureAwait(false);
return updatedDocument.Project.Solution;
}
/// <summary>
/// Traverses the syntax tree of the forked document and
/// collects a list of nodes that are not being moved.
/// This list of nodes are then removed from the forked copy.
/// </summary>
/// <param name="root">root, of the syntax tree of forked document</param>
/// <returns>list of syntax nodes, to be removed from the forked copy.</returns>
private IEnumerable<SyntaxNode> GetMembersToRemove(SyntaxNode root)
{
var spine = new HashSet<SyntaxNode>();
// collect the parent chain of declarations to keep.
spine.AddRange(State.TypeNode.GetAncestors());
// get potential namespace, types and members to remove.
var removableCandidates = root
.DescendantNodes(n => DescendIntoChildren(n, spine.Contains(n)))
.Where(n => FilterToTopLevelMembers(n, State.TypeNode));
// diff candidates with items we want to keep.
return removableCandidates.Except(spine);
}
private static bool DescendIntoChildren(SyntaxNode node, bool shouldDescendIntoType)
{
// 1. get top level types and namespaces to remove.
// 2. descend into types and get members to remove, only if type is part of spine, which means
// we'll be keeping the type declaration but not other members, in the new file.
return node is TCompilationUnitSyntax
|| node is TNamespaceDeclarationSyntax
|| (node is TTypeDeclarationSyntax && shouldDescendIntoType);
}
private static bool FilterToTopLevelMembers(SyntaxNode node, SyntaxNode typeNode)
{
// It is a type declaration that is not the node we've moving
// or its a container namespace, or a member declaration that is not a type,
// thereby ignoring other stuff like statements and identifiers.
return node is TTypeDeclarationSyntax
? !node.Equals(typeNode)
: (node is TNamespaceDeclarationSyntax || node is TMemberDeclarationSyntax);
}
/// <summary>
/// if a nested type is being moved, this ensures its containing type is partial.
/// </summary>
/// <param name="documentEditor">document editor for the new document being created</param>
private void AddPartialModifiersToTypeChain(DocumentEditor documentEditor)
{
var semanticFacts = State.SemanticDocument.Document.GetLanguageService<ISemanticFactsService>();
var typeChain = State.TypeNode.Ancestors().OfType<TTypeDeclarationSyntax>();
foreach (var node in typeChain)
{
var symbol = (ITypeSymbol)State.SemanticDocument.SemanticModel.GetDeclaredSymbol(node, CancellationToken);
if (!semanticFacts.IsPartial(symbol, CancellationToken))
{
documentEditor.SetModifiers(node, DeclarationModifiers.Partial);
}
}
}
/// <summary>
/// Perform clean ups on a given document.
/// </summary>
private Task<Document> CleanUpDocumentAsync(Document document)
{
return document
.GetLanguageService<IRemoveUnnecessaryImportsService>()
.RemoveUnnecessaryImportsAsync(document, CancellationToken);
}
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax>
{
private class RenameFileEditor : Editor
{
public RenameFileEditor(TService service, State state, CancellationToken cancellationToken)
: base(service, state, cancellationToken)
{
}
internal override Task<IEnumerable<CodeActionOperation>> GetOperationsAsync()
{
return Task.FromResult(RenameFileToMatchTypeName());
}
/// <summary>
/// Renames the file to match the type contained in it.
/// </summary>
private IEnumerable<CodeActionOperation> RenameFileToMatchTypeName()
{
var solution = SemanticDocument.Document.Project.Solution;
var text = SemanticDocument.Text;
var oldDocumentId = SemanticDocument.Document.Id;
var newDocumentId = DocumentId.CreateNewId(SemanticDocument.Document.Project.Id, State.TargetFileNameCandidate);
// currently, document rename is accomplished by a remove followed by an add.
// the workspace takes care of resolving conflicts if the document name is not unique in the project
// by adding numeric suffixes to the new document being added.
var newSolution = solution.RemoveDocument(oldDocumentId);
newSolution = newSolution.AddDocument(newDocumentId, State.TargetFileNameCandidate, text);
return new CodeActionOperation[]
{
new ApplyChangesOperation(newSolution),
new OpenDocumentOperation(newDocumentId)
};
}
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Rename;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax>
{
private class RenameTypeEditor : Editor
{
public RenameTypeEditor(TService service, State state, CancellationToken cancellationToken)
: base(service, state, cancellationToken)
{
}
internal override Task<IEnumerable<CodeActionOperation>> GetOperationsAsync()
{
return RenameTypeToMatchFileAsync();
}
/// <summary>
/// Renames a type to match its containing file name.
/// </summary>
private async Task<IEnumerable<CodeActionOperation>> RenameTypeToMatchFileAsync()
{
// TODO: detect conflicts ahead of time and open an inline rename session if any exists.
// this will bring up dashboard with conflicts and will allow the user to resolve them.
// if no such conflicts exist, proceed with RenameSymbolAsync.
var solution = SemanticDocument.Document.Project.Solution;
var symbol = State.SemanticDocument.SemanticModel.GetDeclaredSymbol(State.TypeNode, CancellationToken);
var newSolution = await Renamer.RenameSymbolAsync(solution, symbol, State.DocumentName, SemanticDocument.Document.Options, CancellationToken).ConfigureAwait(false);
return SpecializedCollections.SingletonEnumerable(new ApplyChangesOperation(newSolution));
}
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax>
{
private class State
{
private readonly TService _service;
public SemanticDocument SemanticDocument { get; }
public TTypeDeclarationSyntax TypeNode { get; set; }
public string TypeName { get; set; }
public string DocumentName { get; set; }
public string TargetFileNameCandidate { get; set; }
public bool IsDocumentNameAValidIdentifier { get; set; }
private State(TService service, SemanticDocument document)
{
this._service = service;
this.SemanticDocument = document;
}
internal static State Generate(TService service, SemanticDocument document, TextSpan textSpan, CancellationToken cancellationToken)
{
var state = new State(service, document);
if (!state.TryInitialize(textSpan, cancellationToken))
{
return null;
}
return state;
}
private bool TryInitialize(
TextSpan textSpan,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return false;
}
var tree = this.SemanticDocument.SyntaxTree;
var root = this.SemanticDocument.Root;
var syntaxFacts = this.SemanticDocument.Project.LanguageServices.GetService<ISyntaxFactsService>();
var typeDeclaration = _service.GetNodeToAnalyze(root, textSpan) as TTypeDeclarationSyntax;
if (typeDeclaration == null)
{
return false;
}
var typeSymbol = this.SemanticDocument.SemanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) as INamedTypeSymbol;
// compiler declared types, anonymous types, types defined in metadata should be filtered out.
if (typeSymbol == null ||
typeSymbol.Locations.Any(loc => loc.IsInMetadata) ||
typeSymbol.IsAnonymousType ||
typeSymbol.IsImplicitlyDeclared)
{
return false;
}
TypeNode = typeDeclaration;
TypeName = typeSymbol.Name;
DocumentName = Path.GetFileNameWithoutExtension(this.SemanticDocument.Document.Name);
IsDocumentNameAValidIdentifier = syntaxFacts.IsValidIdentifier(DocumentName);
// TODO: Make this check better, it won't detect Outer.Inner.cs cases.
if (string.Equals(DocumentName, TypeName, StringComparison.CurrentCulture))
{
// if type name matches document name in a case sensitive manner, we have nothing more to do.
return false;
}
TargetFileNameCandidate = Path.Combine(typeSymbol.Name + Path.GetExtension(this.SemanticDocument.Document.Name));
return true;
}
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax> :
IMoveTypeService
where TService : AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TMemberDeclarationSyntax, TCompilationUnitSyntax>
where TTypeDeclarationSyntax : SyntaxNode
where TNamespaceDeclarationSyntax : SyntaxNode
where TMemberDeclarationSyntax : SyntaxNode
where TCompilationUnitSyntax : SyntaxNode
{
private enum OperationKind
{
MoveType,
RenameType,
RenameFile
}
protected bool ShouldAnalyze(SyntaxNode root, TextSpan span)
{
return GetNodeToAnalyze(root, span) is TTypeDeclarationSyntax;
}
protected virtual SyntaxNode GetNodeToAnalyze(SyntaxNode root, TextSpan span)
{
return root.FindNode(span);
}
private bool IsNestedType(TTypeDeclarationSyntax typeNode) =>
typeNode.Parent is TTypeDeclarationSyntax;
/// <summary>
/// checks if there is a single top level type declaration in a document
/// </summary>
private bool MultipleTopLevelTypeDeclarationInSourceDocument(SyntaxNode root) =>
root.DescendantNodes(n => (n is TCompilationUnitSyntax || n is TNamespaceDeclarationSyntax))
.OfType<TTypeDeclarationSyntax>()
.Count() > 1;
public async Task<ImmutableArray<CodeAction>> GetRefactoringAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (!ShouldAnalyze(root, textSpan))
{
return default(ImmutableArray<CodeAction>);
}
var semanticDocument = await SemanticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var state = State.Generate((TService)this, semanticDocument, textSpan, cancellationToken);
if (state == null)
{
return default(ImmutableArray<CodeAction>);
}
var actions = CreateActions(state, cancellationToken);
Debug.Assert(actions.Count() != 0, "No code actions found for MoveType Refactoring");
return actions;
}
private ImmutableArray<CodeAction> CreateActions(State state, CancellationToken cancellationToken)
{
var actions = new List<CodeAction>();
var manyTypes = MultipleTopLevelTypeDeclarationInSourceDocument(state.SemanticDocument.Root);
if (manyTypes || IsNestedType(state.TypeNode))
{
// If there are multiple type declarations in current document. offer, move to new file.
// Or if this is a nested type, offer to move to new file.
actions.Add(GetCodeAction(state, operationKind: OperationKind.MoveType));
}
else
{
// one type declaration in current document. No moving around required, just sync
// document name and type name by offering rename in both directions between type and document.
actions.Add(GetCodeAction(state, operationKind: OperationKind.RenameFile));
// only if the document name can be legal identifier in the language,
// offer to rename type with document name
if (state.IsDocumentNameAValidIdentifier)
{
actions.Add(GetCodeAction(state, operationKind: OperationKind.RenameType));
}
}
return actions.ToImmutableArray();
}
private CodeAction GetCodeAction(
State state,
OperationKind operationKind)
{
return new MoveTypeCodeAction((TService)this, state, operationKind);
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
internal interface IMoveTypeService : ILanguageService
{
Task<ImmutableArray<CodeAction>> GetRefactoringAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken);
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Composition;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.GeneratedCodeRecognition;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic,
Name = PredefinedCodeRefactoringProviderNames.MoveTypeToFile), Shared]
internal class MoveTypeCodeRefactoringProvider : CodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var document = context.Document;
var textSpan = context.Span;
var cancellationToken = context.CancellationToken;
var workspace = document.Project.Solution.Workspace;
if (workspace.Kind == WorkspaceKind.MiscellaneousFiles)
{
return;
}
var generatedCodeRecognitionService = workspace.Services.GetService<IGeneratedCodeRecognitionService>();
if (generatedCodeRecognitionService.IsGeneratedCode(document))
{
return;
}
var service = document.GetLanguageService<IMoveTypeService>();
var actions = await service.GetRefactoringAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
if (!actions.IsDefault)
{
context.RegisterRefactorings(actions);
}
}
}
}
......@@ -18,5 +18,6 @@ internal static class PredefinedCodeRefactoringProviderNames
public const string MoveDeclarationNearReference = "Move Declaration Near Reference Code Action Provider";
public const string SimplifyLambda = "Simplify Lambda Code Action Provider";
public const string ConvertToInterpolatedString = "Convert To Interpolated String Code Action Provider";
public const string MoveTypeToFile = "Move Type To File Code Action Provider";
}
}
......@@ -156,6 +156,15 @@
<Compile Include="CodeRefactorings\ICodeRefactoringHelpersService.cs" />
<Compile Include="CodeRefactorings\ICodeRefactoringService.cs" />
<Compile Include="CodeRefactorings\IntroduceVariable\IntroduceVariableCodeRefactoringProvider.cs" />
<Compile Include="CodeRefactorings\MoveType\AbstractMoveTypeService.MoveTypeEditor.cs" />
<Compile Include="CodeRefactorings\MoveType\AbstractMoveTypeService.RenameTypeEditor.cs" />
<Compile Include="CodeRefactorings\MoveType\AbstractMoveTypeService.RenameFileEditor.cs" />
<Compile Include="CodeRefactorings\MoveType\MoveTypeCodeRefactoringProvider.cs" />
<Compile Include="CodeRefactorings\MoveType\AbstractMoveTypeService.Editor.cs" />
<Compile Include="CodeRefactorings\MoveType\AbstractMoveTypeService.MoveTypeCodeAction.cs" />
<Compile Include="CodeRefactorings\MoveType\AbstractMoveTypeService.State.cs" />
<Compile Include="CodeRefactorings\MoveType\AbstractMoveTypeService.cs" />
<Compile Include="CodeRefactorings\MoveType\IMoveTypeService.cs" />
<Compile Include="CodeRefactorings\PredefinedCodeRefactoringProviderNames.cs" />
<Compile Include="CodeRefactorings\ServicesLayerCodeActionHelpersService.cs" />
<Compile Include="CodeRefactorings\WorkspaceServices\IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService.cs" />
......
......@@ -1822,6 +1822,15 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Move type to {0}.
/// </summary>
internal static string Move_type_to_0 {
get {
return ResourceManager.GetString("Move_type_to_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Moving &apos;{0}&apos; will prevent the debug session from continuing..
/// </summary>
......@@ -2105,6 +2114,24 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Rename file to {0}.
/// </summary>
internal static string Rename_file_to_0 {
get {
return ResourceManager.GetString("Rename_file_to_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Rename type to {0}.
/// </summary>
internal static string Rename_type_to_0 {
get {
return ResourceManager.GetString("Rename_type_to_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Renaming &apos;{0}&apos; will prevent the debug session from continuing..
/// </summary>
......
......@@ -1035,4 +1035,13 @@ This version used in: {2}</value>
<data name="Convert_to_interpolated_string" xml:space="preserve">
<value>Convert to interpolated string</value>
</data>
<data name="Move_type_to_0" xml:space="preserve">
<value>Move type to {0}</value>
</data>
<data name="Rename_file_to_0" xml:space="preserve">
<value>Rename file to {0}</value>
</data>
<data name="Rename_type_to_0" xml:space="preserve">
<value>Rename type to {0}</value>
</data>
</root>
\ No newline at end of file
......@@ -132,6 +132,7 @@
<Compile Include="CodeRefactorings\InlineTemporary\InlineTemporaryCodeRefactoringProvider.ReferenceRewriter.vb" />
<Compile Include="CodeRefactorings\InlineTemporary\InlineTemporaryCodeRefactoringProvider.vb" />
<Compile Include="CodeRefactorings\InvertIf\InvertIfCodeRefactoringProvider.vb" />
<Compile Include="CodeRefactorings\MoveType\VisualBasicMoveTypeService.vb" />
<Compile Include="CodeRefactorings\RemoveStatementCodeAction.vb" />
<Compile Include="Completion\CompletionProviders\CompletionListTagCompletionProvider.vb" />
<Compile Include="Completion\CompletionProviders\CompletionUtilities.vb" />
......
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Imports System.Composition
Imports Microsoft.CodeAnalysis.CodeRefactorings.MoveType
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Namespace Microsoft.CodeAnalysis.VisualBasic.CodeRefactorings.MoveType
<ExportLanguageService(GetType(IMoveTypeService), LanguageNames.VisualBasic), [Shared]>
Friend Class VisualBasicMoveTypeService
Inherits AbstractMoveTypeService(Of VisualBasicMoveTypeService, TypeBlockSyntax, NamespaceBlockSyntax, MethodBaseSyntax, CompilationUnitSyntax)
''' <summary>
''' Gets the TypeBlock node to analyze
''' </summary>
Protected Overrides Function GetNodeToAnalyze(root As SyntaxNode, span As TextSpan) As SyntaxNode
Dim node = MyBase.GetNodeToAnalyze(root, span)
If node.IsKind(SyntaxKind.ModuleStatement,
SyntaxKind.ClassStatement,
SyntaxKind.StructureStatement,
SyntaxKind.InterfaceStatement,
SyntaxKind.EnumStatement) Then
Return node.Parent
End If
Return Nothing
End Function
End Class
End Namespace
......@@ -45,7 +45,7 @@
<Compile Include="..\..\..\Compilers\Core\Portable\InternalUtilities\ConcurrentLruCache.cs">
<Link>Shared\ConcurrentLruCache.cs</Link>
</Compile>
<Compile Include="..\..\..\Compilers\Core\Portable\Interop\IVsSQM.cs" />
<Compile Include="..\..\..\Compilers\Core\Portable\Interop\IVsSQM.cs" />
<Compile Include="..\..\..\Compilers\Shared\ShadowCopyAnalyzerAssemblyLoader.cs">
<Link>InternalUtilities\ShadowCopyAnalyzerAssemblyLoader.cs</Link>
</Compile>
......
......@@ -262,5 +262,11 @@ public bool IsNameOfContext(SemanticModel semanticModel, int position, Cancellat
{
return semanticModel.SyntaxTree.IsNameOfContext(position, semanticModel, cancellationToken);
}
public bool IsPartial(ITypeSymbol typeSymbol, CancellationToken cancellationToken)
{
var syntaxRefs = typeSymbol.DeclaringSyntaxReferences;
return syntaxRefs.Any(n => ((BaseTypeDeclarationSyntax)n.GetSyntax(cancellationToken)).Modifiers.Any(SyntaxKind.PartialKeyword));
}
}
}
......@@ -89,5 +89,7 @@ internal interface ISemanticFactsService : ILanguageService
ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode forEachStatement);
bool IsAssignableTo(ITypeSymbol fromSymbol, ITypeSymbol toSymbol, Compilation compilation);
bool IsPartial(ITypeSymbol typeSymbol, CancellationToken cancellationToken);
}
}
\ No newline at end of file
......@@ -247,5 +247,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic
Public Function IsNamespaceDeclarationNameContext(semanticModel As SemanticModel, position As Integer, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsNamespaceDeclarationNameContext
Return semanticModel.SyntaxTree.IsNamespaceDeclarationNameContext(position, cancellationToken)
End Function
Public Function IsPartial(typeSymbol As ITypeSymbol, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsPartial
Dim syntaxRefs = typeSymbol.DeclaringSyntaxReferences
Return syntaxRefs.Any(
Function(n As SyntaxReference)
Return DirectCast(n.GetSyntax(cancellationToken), TypeStatementSyntax).Modifiers.Any(SyntaxKind.PartialKeyword)
End Function)
End Function
End Class
End Namespace
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册