File: InlineRename\AbstractEditorInlineRenameService.SymbolRenameInfo.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
 
internal abstract partial class AbstractEditorInlineRenameService
{
    /// <summary>
    /// Represents information about the ability to rename a particular location.
    /// </summary>
    private partial class SymbolInlineRenameInfo : IInlineRenameInfo
    {
        private const string AttributeSuffix = "Attribute";
 
        private readonly SymbolicRenameInfo _info;
 
        private Document Document => _info.Document!;
        private readonly IEnumerable<IRefactorNotifyService> _refactorNotifyServices;
 
        /// <summary>
        /// Whether or not we shortened the trigger span (say because we were renaming an attribute,
        /// and we didn't select the 'Attribute' portion of the name).
        /// </summary>
        private bool IsRenamingAttributePrefix => _info.IsRenamingAttributePrefix;
 
        public bool CanRename { get; }
        public string? LocalizedErrorMessage => null;
        public TextSpan TriggerSpan { get; }
        public bool HasOverloads { get; }
        public bool MustRenameOverloads => _info.ForceRenameOverloads;
 
        /// <summary>
        /// The locations of the potential rename candidates for the symbol.
        /// </summary>
        public ImmutableArray<DocumentSpan> DefinitionLocations => _info.DocumentSpans;
 
        public ISymbol RenameSymbol => _info.Symbol!;
 
        public SymbolInlineRenameInfo(
            IEnumerable<IRefactorNotifyService> refactorNotifyServices,
            SymbolicRenameInfo info,
            CancellationToken cancellationToken)
        {
            Contract.ThrowIfTrue(info.IsError);
            this.CanRename = true;
 
            _info = info;
            _refactorNotifyServices = refactorNotifyServices;
 
            this.HasOverloads = RenameUtilities.GetOverloadedSymbols(this.RenameSymbol).Any();
 
            this.TriggerSpan = GetReferenceEditSpan(new InlineRenameLocation(this.Document, info.TriggerToken.Span), info.TriggerText, cancellationToken);
        }
 
        /// <summary>
        /// Given a span of text, we need to return the subspan that is editable and
        /// contains the current replacementText.
        ///
        /// These cases are currently handled:
        ///     - Escaped identifiers                          [goo] => goo
        ///     - Type suffixes in VB                          goo$ => goo
        ///     - Qualified names from complexification        A.goo => goo
        ///     - Optional Attribute suffixes                  XAttribute => X
        ///         Careful here:                              XAttribute => XAttribute if renamesymbol is XAttributeAttribute
        ///     - Compiler-generated EventHandler suffix       XEventHandler => X
        ///     - Compiler-generated get_ and set_ prefixes    get_X => X
        /// </summary>
        public TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken)
        {
            var searchName = this.RenameSymbol.Name;
            if (this.IsRenamingAttributePrefix)
            {
                // We're only renaming the attribute prefix part.  We want to adjust the span of
                // the reference we've found to only update the prefix portion.
                searchName = _info.GetWithoutAttributeSuffix(this.RenameSymbol.Name);
            }
 
            var index = triggerText.LastIndexOf(searchName, StringComparison.Ordinal);
            if (index < 0)
            {
                // Couldn't even find the search text at this reference location.  This might happen
                // if the user used things like unicode escapes.  IN that case, we'll have to rename
                // the entire identifier.
                return location.TextSpan;
            }
 
            return new TextSpan(location.TextSpan.Start + index, searchName.Length);
        }
 
        public TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken)
        {
            var position = triggerText.LastIndexOf(replacementText, StringComparison.Ordinal);
 
            if (this.IsRenamingAttributePrefix)
            {
                // We're only renaming the attribute prefix part.  We want to adjust the span of
                // the reference we've found to only update the prefix portion.
                var index = triggerText.LastIndexOf(replacementText + AttributeSuffix, StringComparison.Ordinal);
                position = index >= 0 ? index : position;
            }
 
            if (position < 0)
            {
                return null;
            }
 
            return new TextSpan(location.TextSpan.Start + position, replacementText.Length);
        }
 
        public string DisplayName => RenameSymbol.Name;
        public string FullDisplayName => RenameSymbol.ToDisplayString();
        public Glyph Glyph => RenameSymbol.GetGlyph();
 
        public string GetFinalSymbolName(string replacementText)
            => _info.GetFinalSymbolName(replacementText);
 
        public async Task<IInlineRenameLocationSet> FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken)
        {
            var solution = this.Document.Project.Solution;
            var locations = await Renamer.FindRenameLocationsAsync(
                solution, this.RenameSymbol, options, cancellationToken).ConfigureAwait(false);
 
            return new InlineRenameLocationSet(this, locations);
        }
 
        public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable<DocumentId> changedDocumentIDs, string replacementText)
        {
            return _refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol,
                this.GetFinalSymbolName(replacementText), throwOnFailure: false);
        }
 
        public bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable<DocumentId> changedDocumentIDs, string replacementText)
        {
            return _refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol,
                this.GetFinalSymbolName(replacementText), throwOnFailure: false);
        }
 
        public InlineRenameFileRenameInfo GetFileRenameInfo()
        {
            if (RenameSymbol.Kind == SymbolKind.NamedType &&
                this.Document.Project.Solution.CanApplyChange(ApplyChangesKind.ChangeDocumentInfo))
            {
                if (RenameSymbol.Locations.Length > 1)
                {
                    return InlineRenameFileRenameInfo.TypeWithMultipleLocations;
                }
 
                // Get the document that the symbol is defined in to compare
                // the name with the symbol name. If they match allow
                // rename file rename as part of the symbol rename
                var symbolSourceDocument = this.Document.Project.Solution.GetDocument(RenameSymbol.Locations.Single().SourceTree);
                if (symbolSourceDocument != null && WorkspacePathUtilities.TypeNameMatchesDocumentName(symbolSourceDocument, RenameSymbol.Name))
                {
                    return InlineRenameFileRenameInfo.Allowed;
                }
 
                return InlineRenameFileRenameInfo.TypeDoesNotMatchFileName;
            }
 
            return InlineRenameFileRenameInfo.NotAllowed;
        }
    }
}