File: Organizing\OrganizeDocumentCommandHandler.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.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Commanding.Commands;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.OrganizeImports;
using Microsoft.CodeAnalysis.Organizing;
using Microsoft.CodeAnalysis.RemoveUnnecessaryImports;
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.Commanding;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.Organizing;
 
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.CSharpContentType)]
[ContentType(ContentTypeNames.VisualBasicContentType)]
[ContentType(ContentTypeNames.XamlContentType)]
[Name(PredefinedCommandHandlerNames.OrganizeDocument)]
[method: ImportingConstructor]
[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal class OrganizeDocumentCommandHandler(
    IThreadingContext threadingContext,
    IAsynchronousOperationListenerProvider listenerProvider) :
    ICommandHandler<OrganizeDocumentCommandArgs>,
    ICommandHandler<SortImportsCommandArgs>,
    ICommandHandler<SortAndRemoveUnnecessaryImportsCommandArgs>
{
    private readonly IThreadingContext _threadingContext = threadingContext;
    private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.OrganizeDocument);
 
    public string DisplayName => EditorFeaturesResources.Organize_Document;
 
    public CommandState GetCommandState(OrganizeDocumentCommandArgs args)
        => GetCommandState(args, _ => EditorFeaturesResources.Organize_Document, needsSemantics: true);
 
    public CommandState GetCommandState(SortImportsCommandArgs args)
        => GetCommandState(args, o => o.SortImportsDisplayStringWithAccelerator, needsSemantics: false);
 
    public CommandState GetCommandState(SortAndRemoveUnnecessaryImportsCommandArgs args)
        => GetCommandState(args, o => o.SortAndRemoveUnusedImportsDisplayStringWithAccelerator, needsSemantics: true);
 
    private static CommandState GetCommandState(EditorCommandArgs args, Func<IOrganizeImportsService, string> descriptionString, bool needsSemantics)
    {
        if (IsCommandSupported(args, needsSemantics, out var workspace))
        {
            var organizeImportsService = workspace.Services.SolutionServices.GetProjectServices(args.SubjectBuffer)!.GetRequiredService<IOrganizeImportsService>();
            return new CommandState(isAvailable: true, displayText: descriptionString(organizeImportsService));
        }
        else
        {
            return CommandState.Unspecified;
        }
    }
 
    private static bool IsCommandSupported(EditorCommandArgs args, bool needsSemantics, [NotNullWhen(true)] out Workspace? workspace)
    {
        workspace = null;
        if (args.SubjectBuffer.TryGetWorkspace(out var retrievedWorkspace))
        {
            workspace = retrievedWorkspace;
            if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument))
            {
                return false;
            }
 
            if (workspace.Kind == WorkspaceKind.MiscellaneousFiles)
            {
                return !needsSemantics;
            }
 
            return args.SubjectBuffer.SupportsRefactorings();
        }
 
        return false;
    }
 
    private bool ExecuteCommand(
        EditorCommandArgs commandArgs,
        CommandExecutionContext context,
        Func<ITextSnapshot, IUIThreadOperationContext, Task<Document?>> getCurrentDocumentAsync,
        Func<Document, CancellationToken, Task<Document>> getChangedDocumentAsync)
    {
        // We're showing our own UI, ensure the editor doesn't show anything itself.
        context.OperationContext.TakeOwnership();
 
        var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand));
        _ = ExecuteAsync(commandArgs, getCurrentDocumentAsync, getChangedDocumentAsync)
            .ReportNonFatalErrorAsync()
            .CompletesAsyncOperation(token);
 
        return true;
    }
 
    private async Task ExecuteAsync(
        EditorCommandArgs commandArgs,
        Func<ITextSnapshot, IUIThreadOperationContext, Task<Document?>> getCurrentDocumentAsync,
        Func<Document, CancellationToken, Task<Document>> getChangedDocumentAsync)
    {
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
 
        var subjectBuffer = commandArgs.SubjectBuffer;
        var textView = commandArgs.TextView;
 
        var caretPoint = textView.GetCaretPoint(subjectBuffer);
        if (caretPoint is null)
            return;
 
        if (!subjectBuffer.TryGetWorkspace(out var workspace))
            return;
 
        var snapshotSpan = textView.GetTextElementSpan(caretPoint.Value);
 
        var indicatorFactory = workspace.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
        using var backgroundWorkContext = indicatorFactory.Create(
            commandArgs.TextView,
            snapshotSpan,
            EditorFeaturesResources.Organizing_document,
            cancelOnEdit: true,
            cancelOnFocusLost: false);
 
        var cancellationToken = backgroundWorkContext.UserCancellationToken;
 
        await TaskScheduler.Default;
 
        var currentDocument = await getCurrentDocumentAsync(snapshotSpan.Snapshot, backgroundWorkContext).ConfigureAwait(false);
        if (currentDocument is null)
            return;
 
        var newDocument = await getChangedDocumentAsync(currentDocument, cancellationToken).ConfigureAwait(false);
        if (currentDocument == newDocument)
            return;
 
        var changes = await newDocument.GetTextChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false);
 
        // Required to switch back to the UI thread to call TryApplyChanges
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        // We're about to make an edit ourselves.  so disable the cancellation that happens on editing.
        backgroundWorkContext.CancelOnEdit = false;
 
        commandArgs.SubjectBuffer.ApplyChanges(changes);
    }
 
    public bool ExecuteCommand(OrganizeDocumentCommandArgs args, CommandExecutionContext context)
        => ExecuteCommand(
            args, context,
            // Need full semantics for this operation.
            (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context),
            (document, cancellationToken) => OrganizingService.OrganizeAsync(document, cancellationToken: cancellationToken));
 
    public bool ExecuteCommand(SortImportsCommandArgs args, CommandExecutionContext context)
        => ExecuteCommand(
            args, context,
            // Only need syntax for this operation.
            (snapshot, context) => Task.FromResult(snapshot.GetOpenDocumentInCurrentContextWithChanges()),
            async (document, cancellationToken) =>
            {
                var organizeImportsService = document.GetRequiredLanguageService<IOrganizeImportsService>();
                var options = await document.GetOrganizeImportsOptionsAsync(cancellationToken).ConfigureAwait(false);
                return await organizeImportsService.OrganizeImportsAsync(document, options, cancellationToken).ConfigureAwait(false);
            });
 
    public bool ExecuteCommand(SortAndRemoveUnnecessaryImportsCommandArgs args, CommandExecutionContext context)
        => ExecuteCommand(
            args, context,
            // Need full semantics for this operation.
            (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context),
            async (document, cancellationToken) =>
            {
                var formattingOptions = document.SupportsSyntaxTree
                    ? await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false)
                    : null;
 
                var removeImportsService = document.GetRequiredLanguageService<IRemoveUnnecessaryImportsService>();
                var organizeImportsService = document.GetRequiredLanguageService<IOrganizeImportsService>();
 
                var newDocument = await removeImportsService.RemoveUnnecessaryImportsAsync(document, cancellationToken).ConfigureAwait(false);
                var options = await document.GetOrganizeImportsOptionsAsync(cancellationToken).ConfigureAwait(false);
                return await organizeImportsService.OrganizeImportsAsync(newDocument, options, cancellationToken).ConfigureAwait(false);
            });
}