File: AddImports\AbstractAddImportsPasteCommandHandler.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.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImportOnPaste;
using Microsoft.CodeAnalysis.AddMissingImports;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Progress;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.AddImport;
 
internal abstract class AbstractAddImportsPasteCommandHandler(
    IThreadingContext threadingContext,
    IGlobalOptionService globalOptions,
    IAsynchronousOperationListenerProvider listenerProvider) : IChainedCommandHandler<PasteCommandArgs>
{
    /// <summary>
    /// The command handler display name
    /// </summary>
    public abstract string DisplayName { get; }
 
    /// <summary>
    /// The thread await dialog text shown to the user if the operation takes a long time
    /// </summary>
    protected abstract string DialogText { get; }
 
    private readonly IThreadingContext _threadingContext = threadingContext;
    private readonly IGlobalOptionService _globalOptions = globalOptions;
    private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.AddImportsOnPaste);
 
    public CommandState GetCommandState(PasteCommandArgs args, Func<CommandState> nextCommandHandler)
        => nextCommandHandler();
 
    public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
    {
        var language = args.SubjectBuffer.GetLanguageName();
 
        // If the feature is not explicitly enabled we can exit early
        if (language is null || !_globalOptions.GetOption(AddImportOnPasteOptionsStorage.AddImportsOnPaste, language))
        {
            nextCommandHandler();
            return;
        }
 
        // Capture the pre-paste caret position
        var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer);
        if (!caretPosition.HasValue)
        {
            nextCommandHandler();
            return;
        }
 
        // Create a tracking span from the pre-paste caret position that will grow as text is inserted.
        var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive);
 
        // Perform the paste command before adding imports
        nextCommandHandler();
 
        if (executionContext.OperationContext.UserCancellationToken.IsCancellationRequested)
        {
            return;
        }
 
        try
        {
            ExecuteCommandWorker(args, executionContext, trackingSpan);
        }
        catch (OperationCanceledException)
        {
            // According to Editor command handler API guidelines, it's best if we return early if cancellation
            // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already
            // calling nextCommandHandler().
        }
    }
 
    private void ExecuteCommandWorker(
        PasteCommandArgs args,
        CommandExecutionContext executionContext,
        ITrackingSpan trackingSpan)
    {
        if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace())
        {
            return;
        }
 
        // Don't perform work if we're inside the interactive window
        if (args.TextView.IsNotSurfaceBufferOfTextView(args.SubjectBuffer))
        {
            return;
        }
 
        // Applying the post-paste snapshot to the tracking span gives us the span of pasted text.
        var snapshotSpan = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot);
 
        var sourceTextContainer = args.SubjectBuffer.AsTextContainer();
        if (!Workspace.TryGetWorkspace(sourceTextContainer, out var workspace))
        {
            return;
        }
 
        var document = sourceTextContainer.GetOpenDocumentInCurrentContext();
        if (document is null)
        {
            return;
        }
 
        // We're showing our own UI, ensure the editor doesn't show anything itself.
        executionContext.OperationContext.TakeOwnership();
 
        var token = _listener.BeginAsyncOperation(nameof(ExecuteAsync));
 
        ExecuteAsync(document, snapshotSpan, args.TextView)
            .ReportNonFatalErrorAsync()
            .CompletesAsyncOperation(token);
    }
 
    private async Task ExecuteAsync(Document document, SnapshotSpan snapshotSpan, ITextView textView)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        var indicatorFactory = document.Project.Solution.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
        using var backgroundWorkContext = indicatorFactory.Create(
            textView,
            snapshotSpan,
            DialogText,
            cancelOnEdit: true,
            cancelOnFocusLost: true);
 
        var cancellationToken = backgroundWorkContext.UserCancellationToken;
 
        // We're going to log the same thing on success or failure since this blocks the UI thread. This measurement is 
        // intended to tell us how long we're blocking the user from typing with this action. 
        using var blockLogger = Logger.LogBlock(FunctionId.CommandHandler_Paste_ImportsOnPaste, KeyValueLogMessage.Create(LogType.UserAction), cancellationToken);
 
        await TaskScheduler.Default;
 
        var addMissingImportsService = document.GetRequiredLanguageService<IAddMissingImportsFeatureService>();
        var textSpan = snapshotSpan.Span.ToTextSpan();
 
        var updatedDocument = await addMissingImportsService.AddMissingImportsAsync(
            document, textSpan, backgroundWorkContext.GetCodeAnalysisProgress(), cancellationToken).ConfigureAwait(false);
 
        if (updatedDocument is null)
        {
            return;
        }
 
        // Required to switch back to the UI thread to call TryApplyChanges
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        document.Project.Solution.Workspace.TryApplyChanges(updatedDocument.Project.Solution);
    }
}