Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
lwm1986
roslyn
提交
f17a6315
R
roslyn
项目概览
lwm1986
/
roslyn
与 Fork 源项目一致
从无法访问的项目Fork
通知
1
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
R
roslyn
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
未验证
提交
f17a6315
编写于
12月 30, 2019
作者:
G
Gen Lu
提交者:
GitHub
12月 30, 2019
浏览文件
操作
浏览文件
下载
差异文件
Merge pull request #40018 from genlu/FixChangeNamespace
Fix move to namespace refactoring
上级
136edb12
aa6dc49d
变更
7
隐藏空白更改
内联
并排
Showing
7 changed file
with
103 addition
and
15 deletion
+103
-15
src/EditorFeatures/CSharpTest/MoveToNamespace/MoveToNamespaceTests.cs
...atures/CSharpTest/MoveToNamespace/MoveToNamespaceTests.cs
+63
-0
src/EditorFeatures/TestUtilities/MoveToNamespace/AbstractMoveToNamespaceTests.cs
...Utilities/MoveToNamespace/AbstractMoveToNamespaceTests.cs
+0
-1
src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs
...efactorings/SyncNamespace/CSharpChangeNamespaceService.cs
+8
-3
src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs
...actorings/SyncNamespace/AbstractChangeNamespaceService.cs
+4
-4
src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.cs
...Namespace/AbstractSyncNamespaceCodeRefactoringProvider.cs
+2
-2
src/Features/Core/Portable/MoveToNamespace/AbstractMoveToNamespaceService.cs
...ortable/MoveToNamespace/AbstractMoveToNamespaceService.cs
+25
-4
src/Workspaces/CoreTestUtilities/MEF/ExportProviderCache.cs
src/Workspaces/CoreTestUtilities/MEF/ExportProviderCache.cs
+1
-1
未找到文件。
src/EditorFeatures/CSharpTest/MoveToNamespace/MoveToNamespaceTests.cs
浏览文件 @
f17a6315
...
@@ -7,6 +7,7 @@
...
@@ -7,6 +7,7 @@
using
Microsoft.CodeAnalysis.Editor.UnitTests
;
using
Microsoft.CodeAnalysis.Editor.UnitTests
;
using
Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
;
using
Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
;
using
Microsoft.CodeAnalysis.MoveToNamespace
;
using
Microsoft.CodeAnalysis.MoveToNamespace
;
using
Microsoft.CodeAnalysis.Shared.Extensions
;
using
Microsoft.CodeAnalysis.Test.Utilities
;
using
Microsoft.CodeAnalysis.Test.Utilities
;
using
Microsoft.CodeAnalysis.Test.Utilities.MoveToNamespace
;
using
Microsoft.CodeAnalysis.Test.Utilities.MoveToNamespace
;
using
Microsoft.VisualStudio.Composition
;
using
Microsoft.VisualStudio.Composition
;
...
@@ -1226,5 +1227,67 @@ partial class MyClass
...
@@ -1226,5 +1227,67 @@ partial class MyClass
{
{
}"
,
}"
,
expectedSuccess
:
false
);
expectedSuccess
:
false
);
[
Fact
,
Trait
(
Traits
.
Feature
,
Traits
.
Features
.
MoveToNamespace
)]
[
WorkItem
(
39234
,
"https://github.com/dotnet/roslyn/issues/39234"
)]
public
async
Task
TestMultiTargetingProject
()
{
// Create two projects with same project file path and single linked document to simulate a multi-targeting project.
var
input
=
@"<Workspace>
<Project Language=""C#"" CommonReferences=""true"" AssemblyName=""Proj1"" FilePath=""SharedProj.csproj"">
<Document FilePath=""CurrentDocument.cs"">
namespace A
{
public class Class1
{
}
public class Class2[||]
{
}
}
</Document>
</Project>
<Project Language=""C#"" CommonReferences=""true"" AssemblyName=""Proj2"" FilePath=""SharedProj.csproj"">
<Document IsLinkFile=""true"" LinkAssemblyName=""Proj1"" LinkFilePath=""CurrentDocument.cs""/>
</Project>
</Workspace>"
;
var
expected
=
@"namespace A
{
public class Class1
{
}
}
namespace B
{
public class Class2
{
}
}"
;
using
var
workspace
=
TestWorkspace
.
Create
(
System
.
Xml
.
Linq
.
XElement
.
Parse
(
input
),
exportProvider
:
ExportProviderFactory
.
CreateExportProvider
());
// Set the target namespace to "B"
var
testDocument
=
workspace
.
Projects
.
Single
(
p
=>
p
.
Name
==
"Proj1"
).
Documents
.
Single
();
var
document
=
workspace
.
CurrentSolution
.
GetDocument
(
testDocument
.
Id
);
var
movenamespaceService
=
document
.
GetLanguageService
<
IMoveToNamespaceService
>();
var
moveToNamespaceOptions
=
new
MoveToNamespaceOptionsResult
(
"B"
);
((
TestMoveToNamespaceOptionsService
)
movenamespaceService
.
OptionsService
).
SetOptions
(
moveToNamespaceOptions
);
var
(
_
,
action
)
=
await
GetCodeActionsAsync
(
workspace
,
default
);
var
operations
=
await
VerifyActionAndGetOperationsAsync
(
workspace
,
action
,
default
);
var
result
=
ApplyOperationsAndGetSolution
(
workspace
,
operations
);
// Make sure both linked documents are changed.
foreach
(
var
id
in
workspace
.
Documents
.
Select
(
d
=>
d
.
Id
))
{
var
changedDocument
=
result
.
Item2
.
GetDocument
(
id
);
var
changedRoot
=
await
changedDocument
.
GetSyntaxRootAsync
();
var
actualText
=
changedRoot
.
ToFullString
();
Assert
.
Equal
(
expected
,
actualText
);
}
}
}
}
}
}
src/EditorFeatures/TestUtilities/MoveToNamespace/AbstractMoveToNamespaceTests.cs
浏览文件 @
f17a6315
...
@@ -83,7 +83,6 @@ protected override CodeRefactoringProvider CreateCodeRefactoringProvider(Workspa
...
@@ -83,7 +83,6 @@ protected override CodeRefactoringProvider CreateCodeRefactoringProvider(Workspa
checkedCodeActions
.
Add
(
codeAction
);
checkedCodeActions
.
Add
(
codeAction
);
}
}
}
}
}
}
if
(!
optionCancelled
&&
!
string
.
IsNullOrEmpty
(
targetNamespace
))
if
(!
optionCancelled
&&
!
string
.
IsNullOrEmpty
(
targetNamespace
))
...
...
src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs
浏览文件 @
f17a6315
...
@@ -99,11 +99,11 @@ protected override SyntaxList<MemberDeclarationSyntax> GetMemberDeclarationsInCo
...
@@ -99,11 +99,11 @@ protected override SyntaxList<MemberDeclarationSyntax> GetMemberDeclarationsInCo
}
}
/// <summary>
/// <summary>
/// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the namespce to be changed.
/// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the namesp
a
ce to be changed.
/// If this reference is the right side of a qualified name, the new node returned would be the entire qualified name. Depends on
/// If this reference is the right side of a qualified name, the new node returned would be the entire qualified name. Depends on
/// whether <paramref name="newNamespaceParts"/> is provided, the name in the new node might be qualified with this new namespace instead.
/// whether <paramref name="newNamespaceParts"/> is provided, the name in the new node might be qualified with this new namespace instead.
/// </summary>
/// </summary>
/// <param name="reference">A reference to a type declared inside the namespce to be changed, which is calculated based on results from
/// <param name="reference">A reference to a type declared inside the namesp
a
ce to be changed, which is calculated based on results from
/// `SymbolFinder.FindReferencesAsync`.</param>
/// `SymbolFinder.FindReferencesAsync`.</param>
/// <param name="newNamespaceParts">If specified, and the reference is qualified with namespace, the namespace part of original reference
/// <param name="newNamespaceParts">If specified, and the reference is qualified with namespace, the namespace part of original reference
/// will be replaced with given namespace in the new node.</param>
/// will be replaced with given namespace in the new node.</param>
...
@@ -275,7 +275,7 @@ private static CompilationUnitSyntax MoveMembersFromNamespaceToGlobal(Compilatio
...
@@ -275,7 +275,7 @@ private static CompilationUnitSyntax MoveMembersFromNamespaceToGlobal(Compilatio
var
eofToken
=
root
.
EndOfFileToken
var
eofToken
=
root
.
EndOfFileToken
.
WithAdditionalAnnotations
(
WarningAnnotation
);
.
WithAdditionalAnnotations
(
WarningAnnotation
);
// Try to preserve trivia from original names
ap
ce declaration.
// Try to preserve trivia from original names
pa
ce declaration.
// If there's any member inside the declaration, we attach them to the
// If there's any member inside the declaration, we attach them to the
// first and last member, otherwise, simply attach all to the EOF token.
// first and last member, otherwise, simply attach all to the EOF token.
if
(
members
.
Count
>
0
)
if
(
members
.
Count
>
0
)
...
@@ -362,6 +362,11 @@ private static CompilationUnitSyntax MoveMembersFromGlobalToNamespace(Compilatio
...
@@ -362,6 +362,11 @@ private static CompilationUnitSyntax MoveMembersFromGlobalToNamespace(Compilatio
{
{
// Otherwise, the span should contain a namespace declaration node, which must be the only one
// Otherwise, the span should contain a namespace declaration node, which must be the only one
// in the entire syntax spine to enable the change namespace operation.
// in the entire syntax spine to enable the change namespace operation.
if
(!
compilationUnit
.
Span
.
Contains
(
span
))
{
return
null
;
}
var
node
=
compilationUnit
.
FindNode
(
span
,
getInnermostNodeForTie
:
true
);
var
node
=
compilationUnit
.
FindNode
(
span
,
getInnermostNodeForTie
:
true
);
var
namespaceDecl
=
node
.
AncestorsAndSelf
().
OfType
<
NamespaceDeclarationSyntax
>().
SingleOrDefault
();
var
namespaceDecl
=
node
.
AncestorsAndSelf
().
OfType
<
NamespaceDeclarationSyntax
>().
SingleOrDefault
();
...
...
src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs
浏览文件 @
f17a6315
...
@@ -33,11 +33,11 @@ internal abstract class AbstractChangeNamespaceService : IChangeNamespaceService
...
@@ -33,11 +33,11 @@ internal abstract class AbstractChangeNamespaceService : IChangeNamespaceService
/// <summary>
/// <summary>
/// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the
/// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the
/// namespce to be changed. If this reference is the right side of a qualified name, the new node returned would
/// namesp
a
ce to be changed. If this reference is the right side of a qualified name, the new node returned would
/// be the entire qualified name. Depends on whether <paramref name="newNamespaceParts"/> is provided, the name
/// be the entire qualified name. Depends on whether <paramref name="newNamespaceParts"/> is provided, the name
/// in the new node might be qualified with this new namespace instead.
/// in the new node might be qualified with this new namespace instead.
/// </summary>
/// </summary>
/// <param name="reference">A reference to a type declared inside the namespce to be changed, which is calculated
/// <param name="reference">A reference to a type declared inside the namesp
a
ce to be changed, which is calculated
/// based on results from `SymbolFinder.FindReferencesAsync`.</param>
/// based on results from `SymbolFinder.FindReferencesAsync`.</param>
/// <param name="newNamespaceParts">If specified, the namespace of original reference will be replaced with given
/// <param name="newNamespaceParts">If specified, the namespace of original reference will be replaced with given
/// namespace in the replacement node.</param>
/// namespace in the replacement node.</param>
...
@@ -164,7 +164,7 @@ await ChangeNamespaceInSingleDocumentAsync(solutionAfterNamespaceChange, documen
...
@@ -164,7 +164,7 @@ await ChangeNamespaceInSingleDocumentAsync(solutionAfterNamespaceChange, documen
// After changing documents, we still need to remove unnecessary imports related to our change.
// After changing documents, we still need to remove unnecessary imports related to our change.
// We don't try to remove all imports that might become unnecessary/invalid after the namespace change,
// We don't try to remove all imports that might become unnecessary/invalid after the namespace change,
// just ones that fully match
e
the old/new namespace. Because it's hard to get it right and will almost
// just ones that fully match the old/new namespace. Because it's hard to get it right and will almost
// certainly cause perf issue.
// certainly cause perf issue.
// For example, if we are changing namespace `Foo.Bar` (which is the only namespace declaration with such name)
// For example, if we are changing namespace `Foo.Bar` (which is the only namespace declaration with such name)
// to `A.B`, the using of name `Bar` in a different file below would remain untouched, even it's no longer valid:
// to `A.B`, the using of name `Bar` in a different file below would remain untouched, even it's no longer valid:
...
@@ -287,7 +287,7 @@ protected static bool IsSupportedLinkedDocument(Document document, out Immutable
...
@@ -287,7 +287,7 @@ protected static bool IsSupportedLinkedDocument(Document document, out Immutable
// TODO: figure out how to properly determine if and how a document is linked using project system.
// TODO: figure out how to properly determine if and how a document is linked using project system.
// If we found a linked document which is part of a project with differen
c
t project file,
// If we found a linked document which is part of a project with different project file,
// then it's an actual linked file (i.e. not a multi-targeting project). We don't support that for now.
// then it's an actual linked file (i.e. not a multi-targeting project). We don't support that for now.
if
(
linkedDocumentIds
.
Any
(
id
=>
if
(
linkedDocumentIds
.
Any
(
id
=>
!
PathUtilities
.
PathsEqual
(
solution
.
GetDocument
(
id
).
Project
.
FilePath
,
document
.
Project
.
FilePath
)))
!
PathUtilities
.
PathsEqual
(
solution
.
GetDocument
(
id
).
Project
.
FilePath
,
document
.
Project
.
FilePath
)))
...
...
src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.cs
浏览文件 @
f17a6315
...
@@ -36,7 +36,7 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
...
@@ -36,7 +36,7 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
{
{
// These code actions try to move file to a new location based on declared namespace
// These code actions try to move file to a new location based on declared namespace
// and the default namespace of the project. The new location is a list of folders
// and the default namespace of the project. The new location is a list of folders
// determined by the relat
e
ive part of the declared namespace compare to the default namespace.
// determined by the relative part of the declared namespace compare to the default namespace.
//
//
// For example, if he default namespace is `A.B.C`, file path is
// For example, if he default namespace is `A.B.C`, file path is
// "[project root dir]\Class1.cs" and declared namespace in the file is
// "[project root dir]\Class1.cs" and declared namespace in the file is
...
@@ -81,7 +81,7 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
...
@@ -81,7 +81,7 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
/// Try to get the node that can be used to trigger the refactoring based on current cursor position.
/// Try to get the node that can be used to trigger the refactoring based on current cursor position.
/// </summary>
/// </summary>
/// <returns>
/// <returns>
/// (1) a node of type <typeparamref name="TNamespaceDeclarationSyntax"/> node, if cur
os
r in the name and it's the
/// (1) a node of type <typeparamref name="TNamespaceDeclarationSyntax"/> node, if cur
so
r in the name and it's the
/// only namespace declaration in the document.
/// only namespace declaration in the document.
/// (2) a node of type <typeparamref name="TCompilationUnitSyntax"/> node, if the cursor is in the name of first
/// (2) a node of type <typeparamref name="TCompilationUnitSyntax"/> node, if the cursor is in the name of first
/// declaration in global namespace and there's no namespace declaration in this document.
/// declaration in global namespace and there's no namespace declaration in this document.
...
...
src/Features/Core/Portable/MoveToNamespace/AbstractMoveToNamespaceService.cs
浏览文件 @
f17a6315
...
@@ -9,6 +9,7 @@
...
@@ -9,6 +9,7 @@
using
System.Threading.Tasks
;
using
System.Threading.Tasks
;
using
Microsoft.CodeAnalysis.ChangeNamespace
;
using
Microsoft.CodeAnalysis.ChangeNamespace
;
using
Microsoft.CodeAnalysis.CodeRefactorings.MoveType
;
using
Microsoft.CodeAnalysis.CodeRefactorings.MoveType
;
using
Microsoft.CodeAnalysis.Formatting
;
using
Microsoft.CodeAnalysis.Host
;
using
Microsoft.CodeAnalysis.Host
;
using
Microsoft.CodeAnalysis.LanguageServices
;
using
Microsoft.CodeAnalysis.LanguageServices
;
using
Microsoft.CodeAnalysis.Shared.Extensions
;
using
Microsoft.CodeAnalysis.Shared.Extensions
;
...
@@ -93,7 +94,7 @@ protected AbstractMoveToNamespaceService(IMoveToNamespaceOptionsService moveToNa
...
@@ -93,7 +94,7 @@ protected AbstractMoveToNamespaceService(IMoveToNamespaceOptionsService moveToNa
return
null
;
return
null
;
}
}
// The underlying ChangeNamespace service doesn't support nested namespace dec
al
ration.
// The underlying ChangeNamespace service doesn't support nested namespace dec
la
ration.
if
(
GetNamespaceInSpineCount
(
declarationSyntax
)
==
1
)
if
(
GetNamespaceInSpineCount
(
declarationSyntax
)
==
1
)
{
{
var
changeNamespaceService
=
document
.
GetLanguageService
<
IChangeNamespaceService
>();
var
changeNamespaceService
=
document
.
GetLanguageService
<
IChangeNamespaceService
>();
...
@@ -252,11 +253,16 @@ private static async Task<ImmutableArray<ISymbol>> GetMemberSymbolsAsync(Documen
...
@@ -252,11 +253,16 @@ private static async Task<ImmutableArray<ISymbol>> GetMemberSymbolsAsync(Documen
moveSpan
,
moveSpan
,
MoveTypeOperationKind
.
MoveTypeNamespaceScope
,
MoveTypeOperationKind
.
MoveTypeNamespaceScope
,
cancellationToken
).
ConfigureAwait
(
false
);
cancellationToken
).
ConfigureAwait
(
false
);
var
modifiedDocument
=
modifiedSolution
.
GetDocument
(
document
.
Id
);
var
modifiedDocument
=
modifiedSolution
.
GetDocument
(
document
.
Id
);
var
syntaxRoot
=
await
modifiedDocument
.
GetSyntaxRootAsync
(
cancellationToken
).
ConfigureAwait
(
false
);
// Since MoveTypeService doesn't handle linked files, we need to merge the diff ourselves,
// otherwise, we will end up with multiple linked documents with different content.
var
mergedSolution
=
await
PropagateChangeToLinkedDocuments
(
modifiedDocument
,
cancellationToken
).
ConfigureAwait
(
false
);
var
mergedDocument
=
mergedSolution
.
GetDocument
(
document
.
Id
);
var
syntaxRoot
=
await
mergedDocument
.
GetSyntaxRootAsync
(
cancellationToken
).
ConfigureAwait
(
false
);
var
syntaxNode
=
syntaxRoot
.
GetAnnotatedNodes
(
AbstractMoveTypeService
.
NamespaceScopeMovedAnnotation
).
SingleOrDefault
();
var
syntaxNode
=
syntaxRoot
.
GetAnnotatedNodes
(
AbstractMoveTypeService
.
NamespaceScopeMovedAnnotation
).
SingleOrDefault
();
if
(
syntaxNode
==
null
)
if
(
syntaxNode
==
null
)
{
{
// The type might be declared in global namespace
// The type might be declared in global namespace
...
@@ -264,12 +270,27 @@ private static async Task<ImmutableArray<ISymbol>> GetMemberSymbolsAsync(Documen
...
@@ -264,12 +270,27 @@ private static async Task<ImmutableArray<ISymbol>> GetMemberSymbolsAsync(Documen
}
}
return
await
MoveItemsInNamespaceAsync
(
return
await
MoveItemsInNamespaceAsync
(
m
odifi
edDocument
,
m
erg
edDocument
,
syntaxNode
,
syntaxNode
,
targetNamespace
,
targetNamespace
,
cancellationToken
).
ConfigureAwait
(
false
);
cancellationToken
).
ConfigureAwait
(
false
);
}
}
private
static
async
Task
<
Solution
>
PropagateChangeToLinkedDocuments
(
Document
document
,
CancellationToken
cancellationToken
)
{
// Need to make sure elastic trivia is formatted properly before pushing the text to other documents.
var
formattedDocument
=
await
Formatter
.
FormatAsync
(
document
,
SyntaxAnnotation
.
ElasticAnnotation
,
cancellationToken
:
cancellationToken
).
ConfigureAwait
(
false
);
var
formattedText
=
await
formattedDocument
.
GetTextAsync
(
cancellationToken
).
ConfigureAwait
(
false
);
var
solution
=
formattedDocument
.
Project
.
Solution
;
foreach
(
var
documentId
in
formattedDocument
.
GetLinkedDocumentIds
())
{
solution
=
solution
.
WithDocumentText
(
documentId
,
formattedText
);
}
return
solution
;
}
private
static
string
GetNewSymbolName
(
ISymbol
symbol
,
string
targetNamespace
)
private
static
string
GetNewSymbolName
(
ISymbol
symbol
,
string
targetNamespace
)
{
{
Debug
.
Assert
(
symbol
!=
null
&&
!
string
.
IsNullOrEmpty
(
targetNamespace
));
Debug
.
Assert
(
symbol
!=
null
&&
!
string
.
IsNullOrEmpty
(
targetNamespace
));
...
...
src/Workspaces/CoreTestUtilities/MEF/ExportProviderCache.cs
浏览文件 @
f17a6315
...
@@ -275,7 +275,7 @@ private static void RequireForSingleExportProvider(bool condition)
...
@@ -275,7 +275,7 @@ private static void RequireForSingleExportProvider(bool condition)
// method attempting to create a default ExportProvider which did not match the one assigned to
// method attempting to create a default ExportProvider which did not match the one assigned to
// the test.
// the test.
// * A test attempted to perform multiple test sequences in the context of a single test method,
// * A test attempted to perform multiple test sequences in the context of a single test method,
// rather than break up the test into distict tests for each case.
// rather than break up the test into disti
n
ct tests for each case.
// * A test referenced different predefined ExportProvider instances within the context of a test.
// * A test referenced different predefined ExportProvider instances within the context of a test.
// Each test is expected to use the same ExportProvider throughout the test.
// Each test is expected to use the same ExportProvider throughout the test.
throw
new
InvalidOperationException
(
$"Only one
{
nameof
(
ExportProvider
)}
can be created in the context of a single test."
);
throw
new
InvalidOperationException
(
$"Only one
{
nameof
(
ExportProvider
)}
can be created in the context of a single test."
);
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录