File: SpellCheck\RoslynSpellCheckFixerProvider.cs
Web Access
Project: ..\..\..\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.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.SpellChecker;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.CodeAnalysis.SpellCheck
{
    [Obsolete]
    [Export(typeof(ISpellCheckFixerProvider))]
    [ContentType(ContentTypeNames.RoslynContentType)]
    [Name(nameof(RoslynSpellCheckFixerProvider))]
    internal sealed class RoslynSpellCheckFixerProvider : ISpellCheckFixerProvider
    {
        private readonly IThreadingContext _threadingContext;
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public RoslynSpellCheckFixerProvider(
            IThreadingContext threadingContext)
        {
            _threadingContext = threadingContext;
        }
 
        public Task RenameWordAsync(
            SnapshotSpan span,
            string replacement,
            IUIThreadOperationContext operationContext)
        {
            var cancellationToken = operationContext.UserCancellationToken;
            return RenameWordAsync(span, replacement, cancellationToken);
        }
 
        private async Task<(FunctionId functionId, string? message)?> RenameWordAsync(
            SnapshotSpan span,
            string replacement,
            CancellationToken cancellationToken)
        {
            var result = await TryRenameAsync(span, replacement, cancellationToken).ConfigureAwait(false);
 
            // If we succeeded at renaming then nothing more to do.
            if (result != null)
            {
                // Record why we failed so we can determine what issues may be arising in the wild.
                var (functionId, message) = result.Value;
                Logger.Log(functionId, message);
 
                // Then just apply the text change directly.
                await ApplySimpleChangeAsync(span, replacement, cancellationToken).ConfigureAwait(false);
            }
 
            return result;
        }
 
        private async Task ApplySimpleChangeAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken)
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            var buffer = span.Snapshot.TextBuffer;
            var edit = buffer.CreateEdit();
 
            edit.Replace(span.TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive), replacement);
            edit.Apply();
        }
 
        private async Task<(FunctionId functionId, string? message)?> TryRenameAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken)
        {
            // See if we can map this to a roslyn document.
            var snapshot = span.Snapshot;
            var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document is null)
                return (FunctionId.SpellCheckFixer_CouldNotFindDocument, null);
 
            // If so, see if the language supports smart rename capabilities.
            var renameService = document.GetLanguageService<IEditorInlineRenameService>();
            if (renameService is null)
                return (FunctionId.SpellCheckFixer_LanguageDoesNotSupportRename, null);
 
            // Attempt to figure out what the language would rename here given the position of the misspelled word in
            // the full token.
            var info = await renameService.GetRenameInfoAsync(document, span.Span.Start, cancellationToken).ConfigureAwait(false);
            if (!info.CanRename)
                return (FunctionId.SpellCheckFixer_LanguageCouldNotGetRenameInfo, null);
 
            // The subspan we're being asked to rename better fall entirely within the span of the token we're renaming.
            var fullTokenSpan = info.TriggerSpan;
            var subSpanBeingRenamed = span.Span.ToTextSpan();
            if (!fullTokenSpan.Contains(subSpanBeingRenamed))
                return (FunctionId.SpellCheckFixer_RenameSpanNotWithinTokenSpan, null);
 
            // Now attempt to call into the language to actually perform the rename.
            var options = new SymbolRenameOptions();
            var renameLocations = await info.FindRenameLocationsAsync(options, cancellationToken).ConfigureAwait(false);
            var replacements = await renameLocations.GetReplacementsAsync(replacement, options, cancellationToken).ConfigureAwait(false);
            if (!replacements.ReplacementTextValid)
                return (FunctionId.SpellCheckFixer_ReplacementTextInvalid, $"Renaming: '{span.GetText()}' to '{replacement}'");
 
            // Finally, apply the rename to the solution.
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            var workspace = document.Project.Solution.Workspace;
            if (!workspace.TryApplyChanges(replacements.NewSolution))
                return (FunctionId.SpellCheckFixer_TryApplyChangesFailure, null);
 
            return null;
        }
 
        public TestAccessor GetTestAccessor()
            => new(this);
 
        public readonly struct TestAccessor
        {
            private readonly RoslynSpellCheckFixerProvider _provider;
 
            public TestAccessor(RoslynSpellCheckFixerProvider provider)
                => _provider = provider;
 
            public Task<(FunctionId functionId, string? message)?> TryRenameAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken)
                => _provider.RenameWordAsync(span, replacement, cancellationToken);
        }
    }
}