File: SpellCheck\RoslynSpellCheckFixerProvider.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.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))]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class RoslynSpellCheckFixerProvider(
    IThreadingContext threadingContext) : ISpellCheckFixerProvider
{
    private readonly IThreadingContext _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(RoslynSpellCheckFixerProvider provider)
    {
        private readonly RoslynSpellCheckFixerProvider _provider = provider;
 
        public Task<(FunctionId functionId, string? message)?> TryRenameAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken)
            => _provider.RenameWordAsync(span, replacement, cancellationToken);
    }
}