|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.MoveStaticMembers;
using Microsoft.CodeAnalysis.PullMemberUp;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.LanguageServices.Implementation.PullMemberUp;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.MoveStaticMembers;
[ExportWorkspaceService(typeof(IMoveStaticMembersOptionsService), ServiceLayer.Host), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class VisualStudioMoveStaticMembersOptionsService(
IGlyphService glyphService,
IUIThreadOperationExecutor uiThreadOperationExecutor) : IMoveStaticMembersOptionsService
{
private readonly IGlyphService _glyphService = glyphService;
private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor = uiThreadOperationExecutor;
private const int HistorySize = 3;
public readonly LinkedList<INamedTypeSymbol> History = new();
public MoveStaticMembersOptions GetMoveMembersToTypeOptions(Document document, INamedTypeSymbol selectedType, ImmutableArray<ISymbol> selectedNodeSymbols)
{
var viewModel = GetViewModel(document, selectedType, selectedNodeSymbols, History, _glyphService, _uiThreadOperationExecutor);
var dialog = new MoveStaticMembersDialog(viewModel);
var result = dialog.ShowModal();
if (result.GetValueOrDefault())
{
UpdateHistory(viewModel);
return GenerateOptions(document.Project.Language, viewModel, result.GetValueOrDefault());
}
return MoveStaticMembersOptions.Cancelled;
}
// internal for testing purposes
internal static MoveStaticMembersOptions GenerateOptions(string language, MoveStaticMembersDialogViewModel viewModel, bool dialogResult)
{
if (dialogResult)
{
// if the destination name contains extra namespaces, we want the last one as that is the real type name
var typeName = viewModel.DestinationName.FullyQualifiedTypeName.Split('.').Last();
var newFileName = Path.ChangeExtension(typeName, language == LanguageNames.CSharp ? ".cs" : ".vb");
var selectedMembers = viewModel.MemberSelectionViewModel.CheckedMembers.SelectAsArray(vm => vm.Symbol);
if (viewModel.DestinationName.IsNew)
{
return new MoveStaticMembersOptions(
newFileName,
viewModel.DestinationName.FullyQualifiedTypeName,
selectedMembers);
}
RoslynDebug.AssertNotNull(viewModel.DestinationName.NamedType);
return new MoveStaticMembersOptions(
viewModel.DestinationName.NamedType,
selectedMembers);
}
return MoveStaticMembersOptions.Cancelled;
}
// internal for testing purposes, get the view model
internal static MoveStaticMembersDialogViewModel GetViewModel(
Document document,
INamedTypeSymbol selectedType,
ImmutableArray<ISymbol> selectedNodeSymbols,
LinkedList<INamedTypeSymbol> history,
IGlyphService? glyphService,
IUIThreadOperationExecutor uiThreadOperationExecutor)
{
var membersInType = selectedType.GetMembers().
WhereAsArray(member => MemberAndDestinationValidator.IsMemberValid(member) && member.IsStatic);
var memberViewModels = membersInType
.SelectAsArray(member =>
new SymbolViewModel<ISymbol>(member, glyphService)
{
// The member(s) user selected will be checked at the beginning.
IsChecked = selectedNodeSymbols.Any(SymbolEquivalenceComparer.Instance.Equals, member),
});
using var cancellationTokenSource = new CancellationTokenSource();
var memberToDependentsMap = SymbolDependentsBuilder.FindMemberToDependentsMap(membersInType, document.Project, cancellationTokenSource.Token);
var existingTypes = selectedType.ContainingNamespace.GetAllTypes(cancellationTokenSource.Token).ToImmutableArray();
var existingTypeNames = existingTypes.SelectAsArray(t => t.ToDisplayString());
var candidateName = selectedType.Name + "Helpers";
var defaultTypeName = NameGenerator.GenerateUniqueName(candidateName, name => !existingTypeNames.Contains(name));
var containingNamespaceDisplay = selectedType.ContainingNamespace.IsGlobalNamespace
? string.Empty
: selectedType.ContainingNamespace.ToDisplayString();
var selectMembersViewModel = new StaticMemberSelectionViewModel(
uiThreadOperationExecutor,
memberViewModels,
memberToDependentsMap);
var availableTypeNames = MakeTypeNameItems(
selectedType.ContainingNamespace,
selectedType,
document,
history,
cancellationTokenSource.Token);
return new MoveStaticMembersDialogViewModel(
selectMembersViewModel,
defaultTypeName,
availableTypeNames,
containingNamespaceDisplay,
document.GetRequiredLanguageService<ISyntaxFactsService>());
}
private void UpdateHistory(MoveStaticMembersDialogViewModel viewModel)
{
if (viewModel.DestinationName.IsNew || viewModel.DestinationName.IsFromHistory)
{
// if we create a new destination or already have it in the history,
// we don't need to update our history list.
return;
}
History.AddFirst(viewModel.DestinationName.NamedType!);
if (History.Count > HistorySize)
{
History.RemoveLast();
}
}
private static string GetFile(Location loc) => loc.SourceTree!.FilePath;
/// <summary>
/// Construct all the type names declared in the project,
/// </summary>
private static ImmutableArray<TypeNameItem> MakeTypeNameItems(
INamespaceSymbol currentNamespace,
INamedTypeSymbol currentType,
Document currentDocument,
LinkedList<INamedTypeSymbol> history,
CancellationToken cancellationToken)
{
return currentNamespace.GetAllTypes(cancellationToken)
// Remove non-static types only when the current type is static
.Where(destinationType => IsValidTypeToMoveBetween(destinationType, currentType) && (destinationType.IsStaticType() || !currentType.IsStaticType()))
.SelectMany(t =>
{
// for partially declared classes, we may want multiple entries for a single type.
// filter to those actually in a real file, and that is not our current location.
return t.Locations
.Where(l => l.IsInSource && (currentType.Name != t.Name || GetFile(l) != currentDocument.FilePath))
.Select(l => new TypeNameItem(
history.Contains(t),
GetFile(l),
t));
})
.ToImmutableArrayOrEmpty()
.Sort(comparison: TypeNameItem.CompareTo);
}
private static bool IsValidTypeToMoveBetween(INamedTypeSymbol destinationType, INamedTypeSymbol sourceType)
{
// Can only moved to named typed that can actually contain members.
if (destinationType.TypeKind is not (TypeKind.Class or TypeKind.Interface or TypeKind.Module or TypeKind.Struct))
return false;
// Very unlikely to be moving from a non-interface to an interface. Filter out for now.
if (sourceType.TypeKind != TypeKind.Interface && destinationType.TypeKind == TypeKind.Interface)
return false;
return true;
}
}
|