|
// 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);
}
}
|