File: ChangeSignature\ChangeSignatureDialogViewModel.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_e5lazejx_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ChangeSignature;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.Text.Classification;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ChangeSignature;
 
internal partial class ChangeSignatureDialogViewModel : AbstractNotifyPropertyChanged
{
    private readonly IClassificationFormatMap _classificationFormatMap;
    private readonly ClassificationTypeMap _classificationTypeMap;
    private readonly INotificationService _notificationService;
    private readonly ParameterConfiguration _originalParameterConfiguration;
 
    // This can be changed to ParameterViewModel if we will allow adding 'this' parameter.
    private readonly ExistingParameterViewModel? _thisParameter;
    private readonly List<ParameterViewModel> _parametersWithoutDefaultValues;
    private readonly List<ParameterViewModel> _parametersWithDefaultValues;
 
    // This can be changed to ParameterViewModel if we will allow adding 'params' parameter.
    private readonly ExistingParameterViewModel? _paramsParameter;
    private readonly HashSet<ParameterViewModel> _disabledParameters = [];
 
    private readonly ImmutableArray<SymbolDisplayPart> _declarationParts;
 
    /// <summary>
    /// The document where the symbol we are changing signature is defined.
    /// </summary>
    private readonly Document _document;
    private readonly int _positionForTypeBinding;
 
    internal ChangeSignatureDialogViewModel(
        ParameterConfiguration parameters,
        ISymbol symbol,
        Document document,
        int positionForTypeBinding,
        IClassificationFormatMap classificationFormatMap,
        ClassificationTypeMap classificationTypeMap)
    {
        _originalParameterConfiguration = parameters;
        _document = document;
        _positionForTypeBinding = positionForTypeBinding;
        _classificationFormatMap = classificationFormatMap;
        _classificationTypeMap = classificationTypeMap;
 
        _notificationService = document.Project.Solution.Services.GetRequiredService<INotificationService>();
 
        // This index is displayed to users. That is why we start it from 1.
        var initialDisplayIndex = 1;
 
        if (parameters.ThisParameter != null)
        {
            _thisParameter = new ExistingParameterViewModel(this, parameters.ThisParameter, initialDisplayIndex++);
            _disabledParameters.Add(_thisParameter);
        }
 
        _declarationParts = symbol.ToDisplayParts(s_symbolDeclarationDisplayFormat);
 
        _parametersWithoutDefaultValues = CreateParameterViewModels(parameters.ParametersWithoutDefaultValues, ref initialDisplayIndex);
        _parametersWithDefaultValues = CreateParameterViewModels(parameters.RemainingEditableParameters, ref initialDisplayIndex);
 
        if (parameters.ParamsParameter != null)
        {
            _paramsParameter = new ExistingParameterViewModel(this, parameters.ParamsParameter, initialDisplayIndex++);
        }
 
        UpdateNameConflictMarkers();
 
        var selectedIndex = parameters.SelectedIndex;
        // Currently, we do not support editing the ThisParameter. 
        // Therefore, if there is such parameter, we should move the selectedIndex.
        if (parameters.ThisParameter != null && selectedIndex == 0)
        {
            // If we have at least one parameter after the ThisParameter, select the first one after This.
            // Otherwise, do not select anything.
            if (parameters.ParametersWithoutDefaultValues.Length + parameters.RemainingEditableParameters.Length > 0)
            {
                this.SelectedIndex = 1;
            }
            else
            {
                this.SelectedIndex = null;
            }
        }
        else
        {
            this.SelectedIndex = selectedIndex;
        }
    }
 
    private void UpdateNameConflictMarkers()
    {
        var parameterNameOverlapMap = new Dictionary<string, List<ParameterViewModel>>();
        foreach (var parameter in AllParameters)
        {
            if (!parameter.IsRemoved)
            {
                parameterNameOverlapMap
                    .GetOrAdd(parameter.ParameterName, _ => new List<ParameterViewModel>())
                    .Add(parameter);
            }
            else
            {
                parameter.HasParameterNameConflict = Visibility.Collapsed;
            }
        }
 
        foreach (var parameterName in parameterNameOverlapMap.Keys)
        {
            var matchingParameters = parameterNameOverlapMap[parameterName];
            if (matchingParameters.Count > 1)
            {
                foreach (var matchingParameter in matchingParameters)
                {
                    matchingParameter.HasParameterNameConflict = Visibility.Visible;
                }
            }
            else
            {
                matchingParameters.Single().HasParameterNameConflict = Visibility.Collapsed;
            }
        }
 
        NotifyPropertyChanged(nameof(AllParameters));
    }
 
    public AddParameterDialogViewModel CreateAddParameterDialogViewModel()
        => new(_document, _positionForTypeBinding);
 
    private List<ParameterViewModel> CreateParameterViewModels(ImmutableArray<Parameter> parameters, ref int initialIndex)
    {
        var list = new List<ParameterViewModel>();
        foreach (ExistingParameter existingParameter in parameters)
        {
            list.Add(new ExistingParameterViewModel(this, existingParameter, initialIndex));
            initialIndex++;
        }
 
        return list;
    }
 
    public int GetStartingSelectionIndex()
    {
        if (_thisParameter == null)
        {
            return 0;
        }
 
        if (_parametersWithDefaultValues.Count + _parametersWithoutDefaultValues.Count > 0)
        {
            return 1;
        }
 
        return -1;
    }
 
    public bool PreviewChanges { get; set; }
 
    public bool CanRemove
    {
        get
        {
            if (!EditableParameterSelected(out var index))
            {
                return false;
            }
 
            return !AllParameters[index].IsRemoved;
        }
    }
 
    public bool CanRestore
    {
        get
        {
            if (!EditableParameterSelected(out var index))
            {
                return false;
            }
 
            return AllParameters[index].IsRemoved;
        }
    }
 
    private bool EditableParameterSelected(out int index)
    {
        index = -1;
 
        if (!AllParameters.Any())
        {
            return false;
        }
 
        if (!SelectedIndex.HasValue)
        {
            return false;
        }
 
        index = SelectedIndex.Value;
 
        if (index == 0 && _thisParameter != null)
        {
            return false;
        }
 
        return true;
    }
 
    internal void Remove()
    {
        if (AllParameters[_selectedIndex!.Value] is AddedParameterViewModel)
        {
            var parameterToRemove = AllParameters[_selectedIndex!.Value];
 
            if (!_parametersWithoutDefaultValues.Remove(parameterToRemove))
            {
                _parametersWithDefaultValues.Remove(parameterToRemove);
            }
        }
        else
        {
            AllParameters[_selectedIndex!.Value].IsRemoved = true;
        }
 
        UpdateNameConflictMarkers();
        RemoveRestoreNotifyPropertyChanged();
    }
 
    internal void Restore()
    {
        AllParameters[_selectedIndex!.Value].IsRemoved = false;
        UpdateNameConflictMarkers();
        RemoveRestoreNotifyPropertyChanged();
    }
 
    internal void AddParameter(AddedParameter addedParameter)
    {
        if (addedParameter.IsRequired)
        {
            _parametersWithoutDefaultValues.Add(new AddedParameterViewModel(this, addedParameter));
        }
        else
        {
            _parametersWithDefaultValues.Add(new AddedParameterViewModel(this, addedParameter));
        }
 
        UpdateNameConflictMarkers();
        RemoveRestoreNotifyPropertyChanged();
    }
 
    internal void RemoveRestoreNotifyPropertyChanged()
    {
        NotifyPropertyChanged(nameof(AllParameters));
        NotifyPropertyChanged(nameof(SignatureDisplay));
        NotifyPropertyChanged(nameof(SignaturePreviewAutomationText));
        NotifyPropertyChanged(nameof(CanRemove));
        NotifyPropertyChanged(nameof(RemoveAutomationText));
        NotifyPropertyChanged(nameof(CanRestore));
        NotifyPropertyChanged(nameof(RestoreAutomationText));
    }
 
    internal ParameterConfiguration GetParameterConfiguration()
    {
        return new ParameterConfiguration(
            _originalParameterConfiguration.ThisParameter,
            _parametersWithoutDefaultValues.Where(p => !p.IsRemoved).Select(p => p.Parameter).ToImmutableArray(),
            _parametersWithDefaultValues.Where(p => !p.IsRemoved).Select(p => p.Parameter).ToImmutableArray(),
            (_paramsParameter == null || _paramsParameter.IsRemoved) ? null : (ExistingParameter)_paramsParameter.Parameter,
            selectedIndex: -1);
    }
 
    private static readonly SymbolDisplayFormat s_symbolDeclarationDisplayFormat = new(
        genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
        miscellaneousOptions:
            SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
            SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
            SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier,
        extensionMethodStyle: SymbolDisplayExtensionMethodStyle.StaticMethod,
        memberOptions:
            SymbolDisplayMemberOptions.IncludeType |
            SymbolDisplayMemberOptions.IncludeExplicitInterface |
            SymbolDisplayMemberOptions.IncludeAccessibility |
            SymbolDisplayMemberOptions.IncludeModifiers |
            SymbolDisplayMemberOptions.IncludeRef);
 
    private static readonly SymbolDisplayFormat s_parameterDisplayFormat = new(
        genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
        miscellaneousOptions:
            SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
            SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
            SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier,
        parameterOptions:
            SymbolDisplayParameterOptions.IncludeType |
            SymbolDisplayParameterOptions.IncludeParamsRefOut |
            SymbolDisplayParameterOptions.IncludeDefaultValue |
            SymbolDisplayParameterOptions.IncludeExtensionThis |
            SymbolDisplayParameterOptions.IncludeName);
 
    public TextBlock SignatureDisplay
    {
        get
        {
            // TODO: Should probably use original syntax & formatting exactly instead of regenerating here
            var displayParts = GetSignatureDisplayParts();
 
            var textBlock = displayParts.ToTaggedText().ToTextBlock(_classificationFormatMap, _classificationTypeMap);
 
            foreach (var inline in textBlock.Inlines)
            {
                inline.FontSize = 12;
            }
 
            textBlock.IsEnabled = false;
            return textBlock;
        }
    }
 
    public string SignaturePreviewAutomationText
    {
        get
        {
            return GetSignatureDisplayParts().Select(sdp => sdp.ToString()).Join(" ");
        }
    }
 
    internal string TEST_GetSignatureDisplayText()
        => GetSignatureDisplayParts().Select(p => p.ToString()).Join("");
 
    private List<SymbolDisplayPart> GetSignatureDisplayParts()
    {
        var displayParts = new List<SymbolDisplayPart>();
 
        displayParts.AddRange(_declarationParts);
        displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, "("));
 
        var first = true;
        foreach (var parameter in AllParameters.Where(p => !p.IsRemoved))
        {
            if (!first)
            {
                displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, ","));
                displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Space, null, " "));
            }
 
            first = false;
 
            switch (parameter)
            {
                case ExistingParameterViewModel existingParameter:
                    displayParts.AddRange(existingParameter.ParameterSymbol.ToDisplayParts(s_parameterDisplayFormat));
                    break;
 
                case AddedParameterViewModel addedParameterViewModel:
                    var languageService = _document.GetRequiredLanguageService<IChangeSignatureViewModelFactoryService>();
                    displayParts.AddRange(languageService.GeneratePreviewDisplayParts(addedParameterViewModel));
                    break;
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(parameter.GetType().ToString());
            }
        }
 
        displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, ")"));
        return displayParts;
    }
 
    public List<ParameterViewModel> AllParameters
    {
        get
        {
            var list = new List<ParameterViewModel>();
            if (_thisParameter != null)
            {
                list.Add(_thisParameter);
            }
 
            list.AddRange(_parametersWithoutDefaultValues);
            list.AddRange(_parametersWithDefaultValues);
 
            if (_paramsParameter != null)
            {
                list.Add(_paramsParameter);
            }
 
            return list;
        }
    }
 
    public bool CanMoveUp
    {
        get
        {
            if (!SelectedIndex.HasValue)
            {
                return false;
            }
 
            var index = SelectedIndex.Value;
            index = _thisParameter == null ? index : index - 1;
            if (index <= 0 || index == _parametersWithoutDefaultValues.Count || index >= _parametersWithoutDefaultValues.Count + _parametersWithDefaultValues.Count)
            {
                return false;
            }
 
            return true;
        }
    }
 
    public bool CanMoveDown
    {
        get
        {
            if (!SelectedIndex.HasValue)
            {
                return false;
            }
 
            var index = SelectedIndex.Value;
            index = _thisParameter == null ? index : index - 1;
            if (index < 0 || index == _parametersWithoutDefaultValues.Count - 1 || index >= _parametersWithoutDefaultValues.Count + _parametersWithDefaultValues.Count - 1)
            {
                return false;
            }
 
            return true;
        }
    }
 
    internal void MoveUp()
    {
        Debug.Assert(CanMoveUp);
 
        var index = SelectedIndex!.Value;
        index = _thisParameter == null ? index : index - 1;
        Move(index < _parametersWithoutDefaultValues.Count ? _parametersWithoutDefaultValues : _parametersWithDefaultValues, index < _parametersWithoutDefaultValues.Count ? index : index - _parametersWithoutDefaultValues.Count, delta: -1);
    }
 
    internal void MoveDown()
    {
        Debug.Assert(CanMoveDown);
 
        var index = SelectedIndex!.Value;
        index = _thisParameter == null ? index : index - 1;
        Move(index < _parametersWithoutDefaultValues.Count ? _parametersWithoutDefaultValues : _parametersWithDefaultValues, index < _parametersWithoutDefaultValues.Count ? index : index - _parametersWithoutDefaultValues.Count, delta: 1);
    }
 
    private void Move(List<ParameterViewModel> list, int index, int delta)
    {
        var param = list[index];
        list.RemoveAt(index);
        list.Insert(index + delta, param);
 
        SelectedIndex += delta;
 
        NotifyPropertyChanged(nameof(AllParameters));
        NotifyPropertyChanged(nameof(SignatureDisplay));
        NotifyPropertyChanged(nameof(SignaturePreviewAutomationText));
    }
 
    internal bool CanSubmit([NotNullWhen(false)] out string? message)
    {
        var canSubmit = AllParameters.Any(p => p.IsRemoved) ||
            AllParameters.Any(p => p is AddedParameterViewModel) ||
                !_parametersWithoutDefaultValues.OfType<ExistingParameterViewModel>().Select(p => p.ParameterSymbol).SequenceEqual(_originalParameterConfiguration.ParametersWithoutDefaultValues.Cast<ExistingParameter>().Select(p => p.Symbol)) ||
                !_parametersWithDefaultValues.OfType<ExistingParameterViewModel>().Select(p => p.ParameterSymbol).SequenceEqual(_originalParameterConfiguration.RemainingEditableParameters.Cast<ExistingParameter>().Select(p => p.Symbol));
 
        if (!canSubmit)
        {
            message = ServicesVSResources.You_must_change_the_signature;
            return false;
        }
 
        message = null;
        return true;
    }
 
    internal bool TrySubmit()
    {
        if (!CanSubmit(out var message))
        {
            _notificationService.SendNotification(message, severity: NotificationSeverity.Information);
            return false;
        }
 
        return true;
    }
 
    private bool IsDisabled(ParameterViewModel parameterViewModel)
    {
        return _disabledParameters.Contains(parameterViewModel);
    }
 
    private int? _selectedIndex;
    public int? SelectedIndex
    {
        get
        {
            return _selectedIndex;
        }
 
        set
        {
            var newSelectedIndex = value == -1 ? null : value;
            if (newSelectedIndex == _selectedIndex)
            {
                return;
            }
 
            _selectedIndex = newSelectedIndex;
 
            NotifyPropertyChanged(nameof(CanMoveUp));
            NotifyPropertyChanged(nameof(MoveUpAutomationText));
            NotifyPropertyChanged(nameof(CanMoveDown));
            NotifyPropertyChanged(nameof(MoveDownAutomationText));
            NotifyPropertyChanged(nameof(CanRemove));
            NotifyPropertyChanged(nameof(RemoveAutomationText));
            NotifyPropertyChanged(nameof(CanRestore));
            NotifyPropertyChanged(nameof(RestoreAutomationText));
        }
    }
 
    public string MoveUpAutomationText
    {
        get
        {
            if (!CanMoveUp)
            {
                return string.Empty;
            }
 
            return string.Format(ServicesVSResources.Move_0_above_1, AllParameters[SelectedIndex!.Value].ShortAutomationText, AllParameters[SelectedIndex!.Value - 1].ShortAutomationText);
        }
    }
 
    public string MoveDownAutomationText
    {
        get
        {
            if (!CanMoveDown)
            {
                return string.Empty;
            }
 
            return string.Format(ServicesVSResources.Move_0_below_1, AllParameters[SelectedIndex!.Value].ShortAutomationText, AllParameters[SelectedIndex!.Value + 1].ShortAutomationText);
        }
    }
 
    public string RemoveAutomationText
    {
        get
        {
            if (!CanRemove)
            {
                return string.Empty;
            }
 
            return string.Format(ServicesVSResources.Remove_0, AllParameters[SelectedIndex!.Value].ShortAutomationText);
        }
    }
 
    public string RestoreAutomationText
    {
        get
        {
            if (!CanRestore)
            {
                return string.Empty;
            }
 
            return string.Format(ServicesVSResources.Restore_0, AllParameters[SelectedIndex!.Value].ShortAutomationText);
        }
    }
}