File: InlineRename\UI\Adornment\RenameFlyoutViewModel.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_per0z0nh_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures.Wpf)
// 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.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Interop;
using Microsoft.CodeAnalysis.Editor.InlineRename;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.EditorFeatures.Lightup;
using Microsoft.CodeAnalysis.InlineRename;
using Microsoft.CodeAnalysis.InlineRename.UI.SmartRename;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.PlatformUI.OleComponentSupport;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
 
internal class RenameFlyoutViewModel : INotifyPropertyChanged, IDisposable
{
    private readonly bool _registerOleComponent;
    private readonly IGlobalOptionService _globalOptionService;
    private OleComponent? _oleComponent;
    private bool _disposedValue;
    private bool _isReplacementTextValid = true;
    public event PropertyChangedEventHandler? PropertyChanged;
 
    public RenameFlyoutViewModel(
        InlineRenameSession session,
        TextSpan selectionSpan,
        bool registerOleComponent,
        IGlobalOptionService globalOptionService,
        IThreadingContext threadingContext,
        IAsynchronousOperationListenerProvider listenerProvider,
#pragma warning disable CS0618 // Editor team use Obsolete attribute to mark potential changing API
        Lazy<ISmartRenameSessionFactoryWrapper>? smartRenameSessionFactory)
#pragma warning restore CS0618 
    {
        Session = session;
        _registerOleComponent = registerOleComponent;
        _globalOptionService = globalOptionService;
        Session.ReplacementTextChanged += OnReplacementTextChanged;
        Session.ReplacementsComputed += OnReplacementsComputed;
        Session.ReferenceLocationsChanged += OnReferenceLocationsChanged;
        Session.CommitStateChange += CommitStateChange;
        StartingSelection = selectionSpan;
        InitialTrackingSpan = session.TriggerSpan.CreateTrackingSpan(SpanTrackingMode.EdgeInclusive);
        var smartRenameSession = smartRenameSessionFactory?.Value.CreateSmartRenameSession(Session.TriggerSpan);
        if (smartRenameSession is not null)
        {
            SmartRenameViewModel = new SmartRenameViewModel(globalOptionService, threadingContext, listenerProvider, smartRenameSession.Value, this);
        }
 
        RegisterOleComponent();
    }
 
    private void CommitStateChange(object sender, EventArgs args)
        => Visibility = this.Session.IsCommitInProgress ? Visibility.Collapsed : Visibility.Visible;
 
    public SmartRenameViewModel? SmartRenameViewModel { get; }
 
    public string IdentifierText
    {
        get => Session.ReplacementText;
        set
        {
            if (value != Session.ReplacementText)
            {
                Session.ApplyReplacementText(value, propagateEditImmediately: true, updateSelection: false);
                NotifyPropertyChanged(nameof(IdentifierText));
            }
        }
    }
 
    public InlineRenameSession Session { get; }
 
    public ITrackingSpan InitialTrackingSpan { get; }
 
    public bool AllowFileRename => Session.FileRenameInfo == InlineRenameFileRenameInfo.Allowed && _isReplacementTextValid;
    public bool ShowFileRename => Session.FileRenameInfo != InlineRenameFileRenameInfo.NotAllowed;
 
    public string FileRenameString => Session.FileRenameInfo switch
    {
        InlineRenameFileRenameInfo.TypeDoesNotMatchFileName => EditorFeaturesResources.Rename_file_name_doesnt_match,
        InlineRenameFileRenameInfo.TypeWithMultipleLocations => EditorFeaturesResources.Rename_file_partial_type,
        _ => EditorFeaturesResources.Rename_symbols_file
    };
 
    private string? _searchText;
    public string? SearchText
    {
        get => _searchText;
        set => Set(ref _searchText, value);
    }
 
    private string? _statusText;
    public string? StatusText
    {
        get => _statusText;
        set => Set(ref _statusText, value);
    }
 
    private Severity _statusSeverity;
    public Severity StatusSeverity
    {
        get => _statusSeverity;
        set
        {
            if (Set(ref _statusSeverity, value))
            {
                NotifyPropertyChanged(nameof(ShowStatusText));
                NotifyPropertyChanged(nameof(StatusImageMoniker));
            }
        }
    }
 
    public bool ShowStatusText => _statusSeverity != Severity.None;
    public bool ShowSearchText => _statusSeverity != Severity.Error;
 
    public ImageMoniker StatusImageMoniker => _statusSeverity switch
    {
        Severity.Error => KnownMonikers.StatusError,
        Severity.Warning => KnownMonikers.StatusWarning,
        _ => ImageLibrary.InvalidImageMoniker
    };
 
    public bool RenameInCommentsFlag
    {
        get => Session.Options.RenameInComments;
        set
        {
            _globalOptionService.SetGlobalOption(InlineRenameSessionOptionsStorage.RenameInComments, value);
            Session.RefreshRenameSessionWithOptionsChanged(Session.Options with { RenameInComments = value });
        }
    }
 
    public bool RenameInStringsFlag
    {
        get => Session.Options.RenameInStrings;
        set
        {
            _globalOptionService.SetGlobalOption(InlineRenameSessionOptionsStorage.RenameInStrings, value);
            Session.RefreshRenameSessionWithOptionsChanged(Session.Options with { RenameInStrings = value });
        }
    }
 
    public bool RenameFileFlag
    {
        get => Session.Options.RenameFile;
        set
        {
            _globalOptionService.SetGlobalOption(InlineRenameSessionOptionsStorage.RenameFile, value);
            Session.RefreshRenameSessionWithOptionsChanged(Session.Options with { RenameFile = value });
        }
    }
 
    public bool PreviewChangesFlag
    {
        get => Session.PreviewChanges;
        set
        {
            _globalOptionService.SetGlobalOption(InlineRenameSessionOptionsStorage.PreviewChanges, value);
            Session.SetPreviewChanges(value);
        }
    }
 
    public bool RenameOverloadsFlag
    {
        get => Session.Options.RenameOverloads;
        set
        {
            _globalOptionService.SetGlobalOption(InlineRenameSessionOptionsStorage.RenameOverloads, value);
            Session.RefreshRenameSessionWithOptionsChanged(Session.Options with { RenameOverloads = value });
        }
    }
 
    public bool IsCollapsed
    {
        get => _globalOptionService.GetOption(InlineRenameUIOptionsStorage.CollapseUI);
        set
        {
            if (value != IsCollapsed)
            {
                _globalOptionService.SetGlobalOption(InlineRenameUIOptionsStorage.CollapseUI, value);
                NotifyPropertyChanged(nameof(IsCollapsed));
                NotifyPropertyChanged(nameof(IsExpanded));
            }
        }
    }
 
    public bool IsExpanded
    {
        get => !IsCollapsed;
        set => IsCollapsed = !value;
    }
 
    public bool IsRenameOverloadsEditable
        => !Session.MustRenameOverloads;
 
    public bool IsRenameOverloadsVisible
        => Session.HasRenameOverloads;
 
    public TextSpan StartingSelection { get; }
 
    private Visibility _visibility;
    public Visibility Visibility
    {
        get => _visibility;
        set => Set(ref _visibility, value);
    }
 
    public bool Submit()
    {
        if (StatusSeverity == Severity.Error)
        {
            return false;
        }
 
        SmartRenameViewModel?.Commit(IdentifierText);
        Session.InitiateCommit();
        return true;
    }
 
    public void Cancel()
    {
        SmartRenameViewModel?.Cancel();
        Session.Cancel();
    }
 
    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
 
    /// <summary>
    /// Shell routes commands based on focused tool window. Since we're outside of a tool window,
    /// Editor can end up intercepting commands and TYPECHARs sent to us, even when we're focused,
    /// so hook in and intercept each message for WPF.
    /// </summary>
    public void RegisterOleComponent()
    {
        // In unit testing we won't have an OleComponentManager available, so 
        // calls to OleComponent.CreateHostedComponent will throw
        if (!_registerOleComponent)
        {
            return;
        }
 
        Debug.Assert(_oleComponent is null);
 
        _oleComponent = OleComponent.CreateHostedComponent("Microsoft CodeAnalysis Inline Rename");
        _oleComponent.PreTranslateMessage += OnPreTranslateMessage;
        _oleComponent.BeginTracking();
    }
 
    private void UnregisterOleComponent()
    {
        if (_oleComponent is not null)
        {
            _oleComponent.EndTracking();
            _oleComponent.PreTranslateMessage -= OnPreTranslateMessage;
            _oleComponent.Dispose();
            _oleComponent = null;
        }
    }
 
    private void OnPreTranslateMessage(object sender, PreTranslateMessageEventArgs e)
    {
        var msg = e.Message;
        if (ComponentDispatcher.RaiseThreadMessage(ref msg) || IsSuppressedMessage(msg))
        {
            e.MessageConsumed = true;
        }
 
        // When the adornment is focused, we register an OleComponent to divert window messages
        // away from the editor and back to WPF to enable proper handling of arrows, backspace,
        // delete, etc. Unfortunately, anything not handled by WPF is then propagated back to the
        // shell command system where it is handled by the open editor window.
        // To avoid unhandled arrow commands from being handled by editor,
        // we mark them as handled so long as the adornment is focused.
        static bool IsSuppressedMessage(MSG msg)
            => msg.message switch
            {
                0x0100 or // WM_KEYDOWN
                0x0101    // WM_KEYUP
                    => msg.wParam.ToInt32() switch
                    {
                        >= 0x0025 and <= 0x0028 => true, // VK_LEFT, VK_UP, VK_RIGHT, and VK_DOWN
 
                        0x0021 or       // VK_PRIOR (Page Up)
                        0x0022 or       // VK_NEXT (Page Down)
                        0x0023 or       // VK_END
                        0x0024 or       // VK_HOME
                        0x0D00 or       // VK_RETURN
                        0x0009 => true, // VK_TAB
 
                        _ => false
                    },
 
                _ => false
            };
    }
 
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                Session.ReplacementTextChanged -= OnReplacementTextChanged;
                Session.ReplacementsComputed -= OnReplacementsComputed;
                Session.CommitStateChange -= CommitStateChange;
 
                if (SmartRenameViewModel is not null)
                {
                    SmartRenameViewModel.Dispose();
                }
 
                UnregisterOleComponent();
            }
 
            _disposedValue = true;
        }
    }
 
    private void OnReplacementTextChanged(object sender, EventArgs e)
    {
        NotifyPropertyChanged(nameof(IdentifierText));
    }
 
    private void OnReplacementsComputed(object sender, IInlineRenameReplacementInfo result)
    {
        if (Set(ref _isReplacementTextValid, result.ReplacementTextValid, "IsReplacementTextValid"))
        {
            NotifyPropertyChanged(nameof(AllowFileRename));
 
            if (!_isReplacementTextValid && !string.IsNullOrEmpty(IdentifierText))
            {
                StatusText = EditorFeaturesResources.The_new_name_is_not_a_valid_identifier;
                StatusSeverity = Severity.Error;
                return;
            }
 
            var resolvableConflicts = 0;
            var unresolvedConflicts = 0;
            foreach (var replacementKind in result.GetAllReplacementKinds())
            {
                switch (replacementKind)
                {
                    case InlineRenameReplacementKind.UnresolvedConflict:
                        unresolvedConflicts++;
                        break;
 
                    case InlineRenameReplacementKind.ResolvedReferenceConflict:
                    case InlineRenameReplacementKind.ResolvedNonReferenceConflict:
                        resolvableConflicts++;
                        break;
                }
            }
 
            if (unresolvedConflicts > 0)
            {
                StatusText = string.Format(EditorFeaturesResources._0_unresolvable_conflict_s, unresolvedConflicts);
                StatusSeverity = Severity.Error;
                return;
            }
 
            if (resolvableConflicts > 0)
            {
                StatusText = string.Format(EditorFeaturesResources._0_conflict_s_will_be_resolved, resolvableConflicts);
                StatusSeverity = Severity.Warning;
                return;
            }
 
            StatusText = null;
            StatusSeverity = Severity.None;
        }
    }
 
    private void OnReferenceLocationsChanged(object sender, ImmutableArray<InlineRenameLocation> renameLocations)
    {
        // Collapse the same edits across multiple instances of the same linked-file.
        var fileCount = renameLocations.GroupBy(s => s.Document.FilePath).Count();
        var referenceCount = renameLocations.Select(loc => (loc.Document.FilePath, loc.TextSpan)).Distinct().Count();
 
        if (referenceCount == 1 && fileCount == 1)
        {
            SearchText = EditorFeaturesResources.Rename_will_update_1_reference_in_1_file;
        }
        else if (fileCount == 1)
        {
            SearchText = string.Format(EditorFeaturesResources.Rename_will_update_0_references_in_1_file, referenceCount);
        }
        else
        {
            SearchText = string.Format(EditorFeaturesResources.Rename_will_update_0_references_in_1_files, referenceCount, fileCount);
        }
    }
 
    private void NotifyPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
 
    private bool Set<T>(ref T field, T newValue, [CallerMemberName] string? name = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return false;
        }
 
        field = newValue;
        NotifyPropertyChanged(name);
        return true;
    }
 
    public enum Severity
    {
        None,
        Warning,
        Error
    }
}