Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;

Expand All @@ -19,43 +16,38 @@ internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarati
/// </summary>
private abstract class Editor(
TService service,
State state,
SemanticDocument semanticDocument,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken)
{
protected State State { get; } = state;
protected TService Service { get; } = service;
protected SemanticDocument SemanticDocument { get; } = semanticDocument;
protected TTypeDeclarationSyntax TypeDeclaration { get; } = typeDeclaration;
protected string FileName { get; } = fileName;
protected CancellationToken CancellationToken { get; } = cancellationToken;
protected SemanticDocument SemanticDocument => State.SemanticDocument;

/// <summary>
/// Operations performed by CodeAction.
/// </summary>
public virtual async Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync()
{
var solution = await GetModifiedSolutionAsync().ConfigureAwait(false);

if (solution == null)
{
return [];
}

return [new ApplyChangesOperation(solution)];
return solution == null ? [] : [new ApplyChangesOperation(solution)];
}

/// <summary>
/// Incremental solution edits that correlate to code operations
/// </summary>
public abstract Task<Solution> GetModifiedSolutionAsync();
public abstract Task<Solution?> GetModifiedSolutionAsync();

public static Editor GetEditor(MoveTypeOperationKind operationKind, TService service, State state, string fileName, CancellationToken cancellationToken)
public static Editor GetEditor(MoveTypeOperationKind operationKind, TService service, SemanticDocument document, TTypeDeclarationSyntax typeDeclaration, string fileName, CancellationToken cancellationToken)
=> operationKind switch
{
MoveTypeOperationKind.MoveType => new MoveTypeEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.RenameType => new RenameTypeEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.RenameFile => new RenameFileEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.MoveTypeNamespaceScope => new MoveTypeNamespaceScopeEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.MoveType => new MoveTypeEditor(service, document, typeDeclaration, fileName, cancellationToken),
MoveTypeOperationKind.RenameType => new RenameTypeEditor(service, document, typeDeclaration, fileName, cancellationToken),
MoveTypeOperationKind.RenameFile => new RenameFileEditor(service, document, typeDeclaration, fileName, cancellationToken),
MoveTypeOperationKind.MoveTypeNamespaceScope => new MoveTypeNamespaceScopeEditor(service, document, typeDeclaration, fileName, cancellationToken),
_ => throw ExceptionUtilities.UnexpectedValue(operationKind),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,34 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Immutable;
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, TCompilationUnitSyntax>
{
private sealed class MoveTypeCodeAction : CodeAction
{
private readonly State _state;
private readonly TService _service;
private readonly SemanticDocument _document;
private readonly TTypeDeclarationSyntax _typeDeclaration;
private readonly MoveTypeOperationKind _operationKind;
private readonly string _fileName;

public MoveTypeCodeAction(
TService service,
State state,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
MoveTypeOperationKind operationKind,
string fileName)
{
_state = state;
_service = service;
_document = document;
_typeDeclaration = typeDeclaration;
_operationKind = operationKind;
_fileName = fileName;
this.Title = CreateDisplayText();
Expand All @@ -39,7 +39,7 @@ private string CreateDisplayText()
=> _operationKind switch
{
MoveTypeOperationKind.MoveType => string.Format(FeaturesResources.Move_type_to_0, _fileName),
MoveTypeOperationKind.RenameType => string.Format(FeaturesResources.Rename_type_to_0, _state.DocumentNameWithoutExtension),
MoveTypeOperationKind.RenameType => string.Format(FeaturesResources.Rename_type_to_0, GetDocumentNameWithoutExtension(_document)),
MoveTypeOperationKind.RenameFile => string.Format(FeaturesResources.Rename_file_to_0, _fileName),
MoveTypeOperationKind.MoveTypeNamespaceScope => string.Empty,
_ => throw ExceptionUtilities.UnexpectedValue(_operationKind),
Expand All @@ -50,7 +50,7 @@ private string CreateDisplayText()
protected override async Task<ImmutableArray<CodeActionOperation>> ComputeOperationsAsync(
IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
{
var editor = Editor.GetEditor(_operationKind, _service, _state, _fileName, cancellationToken);
var editor = Editor.GetEditor(_operationKind, _service, _document, _typeDeclaration, _fileName, cancellationToken);
return await editor.GetOperationsAsync().ConfigureAwait(false);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarati
{
private sealed class MoveTypeEditor(
TService service,
State state,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken)
CancellationToken cancellationToken) : Editor(service, document, typeDeclaration, fileName, cancellationToken)
{
/// <summary>
/// Given a document and a type contained in it, moves the type
Expand All @@ -42,10 +43,10 @@ private sealed class MoveTypeEditor(
/// 3. Add this forked document to the solution.
/// 4. Finally, update the original document and remove the type from it.
/// </remarks>
public override async Task<Solution> GetModifiedSolutionAsync()
public override async Task<Solution?> GetModifiedSolutionAsync()
{
// Fork, update and add as new document.
var projectToBeUpdated = SemanticDocument.Document.Project;
var projectToBeUpdated = SemanticDocument.Project;
var newDocumentId = DocumentId.CreateNewId(projectToBeUpdated.Id, FileName);

// We do this process in the following steps:
Expand Down Expand Up @@ -142,7 +143,7 @@ private async Task<Document> AddNewDocumentWithSingleTypeDeclarationAsync(Docume
documentEditor.RemoveAllAttributes(root);

// Now remove any leading directives on the type-node that actually correspond to prior nodes we removed.
var leadingTrivia = State.TypeNode.GetLeadingTrivia().ToSet();
var leadingTrivia = this.TypeDeclaration.GetLeadingTrivia().ToSet();
foreach (var directive in correspondingDirectives)
{
if (leadingTrivia.Contains(directive.ParentTrivia))
Expand Down Expand Up @@ -185,7 +186,7 @@ void AddCorrespondingDirectives(SyntaxNode member, HashSet<SyntaxNode> directive

private void RemoveLeadingBlankLinesFromMovedType(DocumentEditor documentEditor)
{
documentEditor.ReplaceNode(State.TypeNode,
documentEditor.ReplaceNode(this.TypeDeclaration,
(currentNode, generator) =>
{
var currentTypeNode = (TTypeDeclarationSyntax)currentNode;
Expand Down Expand Up @@ -240,7 +241,7 @@ private async Task<Solution> RemoveTypeFromSourceDocumentAsync(Document sourceDo

// Now cleanup and remove the type we're moving to the new file.
RemoveLeadingBlankLinesFromMovedType(documentEditor);
documentEditor.RemoveNode(State.TypeNode, SyntaxRemoveOptions.KeepUnbalancedDirectives);
documentEditor.RemoveNode(this.TypeDeclaration, SyntaxRemoveOptions.KeepUnbalancedDirectives);

var updatedDocument = documentEditor.GetChangedDocument();
updatedDocument = await AddFileBannerHelpers.CopyBannerAsync(updatedDocument, sourceDocument.FilePath, sourceDocument, this.CancellationToken).ConfigureAwait(false);
Expand All @@ -260,12 +261,12 @@ private ISet<SyntaxNode> GetMembersToRemove(SyntaxNode root)
var spine = new HashSet<SyntaxNode>();

// collect the parent chain of declarations to keep.
spine.AddRange(State.TypeNode.GetAncestors());
spine.AddRange(this.TypeDeclaration.GetAncestors());

// get potential namespace, types and members to remove.
var removableCandidates = root
.DescendantNodes(spine.Contains)
.Where(n => FilterToTopLevelMembers(n, State.TypeNode)).ToSet();
.Where(n => FilterToTopLevelMembers(n, this.TypeDeclaration)).ToSet();

// diff candidates with items we want to keep.
removableCandidates.ExceptWith(spine);
Expand Down Expand Up @@ -304,12 +305,12 @@ private void AddPartialModifiersToTypeChain(
bool removeTypeInheritance,
bool removePrimaryConstructor)
{
var semanticFacts = State.SemanticDocument.Document.GetRequiredLanguageService<ISemanticFactsService>();
var typeChain = State.TypeNode.Ancestors().OfType<TTypeDeclarationSyntax>();
var semanticFacts = SemanticDocument.GetRequiredLanguageService<ISemanticFactsService>();
var typeChain = this.TypeDeclaration.Ancestors().OfType<TTypeDeclarationSyntax>();

foreach (var node in typeChain)
{
var symbol = (INamedTypeSymbol?)State.SemanticDocument.SemanticModel.GetDeclaredSymbol(node, CancellationToken);
var symbol = (INamedTypeSymbol)SemanticDocument.SemanticModel.GetRequiredDeclaredSymbol(node, CancellationToken);
Contract.ThrowIfNull(symbol);
if (!semanticFacts.IsPartial(symbol, CancellationToken))
{
Expand Down Expand Up @@ -338,8 +339,8 @@ private void AddPartialModifiersToTypeChain(
private TTypeDeclarationSyntax RemoveLeadingBlankLines(
TTypeDeclarationSyntax currentTypeNode)
{
var syntaxFacts = State.SemanticDocument.Document.GetRequiredLanguageService<ISyntaxFactsService>();
var bannerService = State.SemanticDocument.Document.GetRequiredLanguageService<IFileBannerFactsService>();
var syntaxFacts = SemanticDocument.GetRequiredLanguageService<ISyntaxFactsService>();
var bannerService = SemanticDocument.GetRequiredLanguageService<IFileBannerFactsService>();

var withoutBlankLines = bannerService.GetNodeWithoutLeadingBlankLines(currentTypeNode);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,40 @@ internal abstract partial class AbstractMoveTypeService<
/// it will evaluate if the namespace scope needs to be closed and reopened to create a new scope.
/// </summary>
private sealed class MoveTypeNamespaceScopeEditor(
TService service, State state, string fileName, CancellationToken cancellationToken)
: Editor(service, state, fileName, cancellationToken)
TService service,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken)
: Editor(service, document, typeDeclaration, fileName, cancellationToken)
{
public override async Task<Solution?> GetModifiedSolutionAsync()
{
var node = State.TypeNode;
var documentToEdit = State.SemanticDocument.Document;

if (node.Parent is not TNamespaceDeclarationSyntax namespaceDeclaration)
return null;

return await GetNamespaceScopeChangedSolutionAsync(namespaceDeclaration, node, documentToEdit, CancellationToken).ConfigureAwait(false);
return TypeDeclaration.Parent is TNamespaceDeclarationSyntax namespaceDeclaration
? await GetNamespaceScopeChangedSolutionAsync(namespaceDeclaration).ConfigureAwait(false)
: null;
}

private static async Task<Solution?> GetNamespaceScopeChangedSolutionAsync(
TNamespaceDeclarationSyntax namespaceDeclaration,
TTypeDeclarationSyntax typeToMove,
Document documentToEdit,
CancellationToken cancellationToken)
private async Task<Solution?> GetNamespaceScopeChangedSolutionAsync(
TNamespaceDeclarationSyntax namespaceDeclaration)
{
var syntaxFactsService = documentToEdit.GetRequiredLanguageService<ISyntaxFactsService>();
var syntaxFactsService = SemanticDocument.GetRequiredLanguageService<ISyntaxFactsService>();
var childNodes = syntaxFactsService.GetMembersOfBaseNamespaceDeclaration(namespaceDeclaration);

if (childNodes.Count <= 1)
return null;

var editor = await DocumentEditor.CreateAsync(documentToEdit, cancellationToken).ConfigureAwait(false);
editor.RemoveNode(typeToMove, SyntaxRemoveOptions.KeepNoTrivia);
var editor = await DocumentEditor.CreateAsync(SemanticDocument.Document, this.CancellationToken).ConfigureAwait(false);
editor.RemoveNode(this.TypeDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
var generator = editor.Generator;

var index = childNodes.IndexOf(typeToMove);
var index = childNodes.IndexOf(this.TypeDeclaration);

var itemsBefore = childNodes.Take(index).ToImmutableArray();
var itemsAfter = childNodes.Skip(index + 1).ToImmutableArray();

var name = syntaxFactsService.GetDisplayName(namespaceDeclaration, DisplayNameOptions.IncludeNamespaces);
var newNamespaceDeclaration = generator.NamespaceDeclaration(name, WithElasticTrivia(typeToMove)).WithAdditionalAnnotations(NamespaceScopeMovedAnnotation);
var newNamespaceDeclaration = generator.NamespaceDeclaration(name, WithElasticTrivia(this.TypeDeclaration)).WithAdditionalAnnotations(NamespaceScopeMovedAnnotation);

if (itemsBefore.Any() && itemsAfter.Any())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,25 @@ namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;

internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TCompilationUnitSyntax>
{
private sealed class RenameFileEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken)
/// <summary>
/// Renames the file to match the type contained in it.
/// </summary>
private sealed class RenameFileEditor(
TService service,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken) : Editor(service, document, typeDeclaration, fileName, cancellationToken)
{
/// <summary>
/// Renames the file to match the type contained in it.
/// </summary>
public override async Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync()
{
var newSolution = await GetModifiedSolutionAsync().ConfigureAwait(false);
Contract.ThrowIfNull(newSolution);
return [new ApplyChangesOperation(newSolution)];
}

public override Task<Solution> GetModifiedSolutionAsync()
{
var modifiedSolution = SemanticDocument.Project.Solution
.WithDocumentName(SemanticDocument.Document.Id, FileName);

return Task.FromResult(modifiedSolution);
}
public override Task<Solution?> GetModifiedSolutionAsync()
=> Task.FromResult<Solution?>(
SemanticDocument.Project.Solution.WithDocumentName(SemanticDocument.Document.Id, FileName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,32 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;

internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TCompilationUnitSyntax>
{
private sealed class RenameTypeEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken)
private sealed class RenameTypeEditor(
TService service,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken) : Editor(service, document, typeDeclaration, fileName, cancellationToken)
{

/// <summary>
/// Renames a type to match its containing file name.
/// </summary>
public override async Task<Solution> GetModifiedSolutionAsync()
public override async Task<Solution?> GetModifiedSolutionAsync()
{
// 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 solution = SemanticDocument.Project.Solution;
var symbol = SemanticDocument.SemanticModel.GetRequiredDeclaredSymbol(this.TypeDeclaration, CancellationToken);
return await Renamer.RenameSymbolAsync(solution, symbol, new SymbolRenameOptions(), FileName, CancellationToken).ConfigureAwait(false);
}
}
Expand Down
Loading
Loading