|
// 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.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.ExtractMethod;
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.CSharpContentType)]
[ContentType(ContentTypeNames.VisualBasicContentType)]
[Name(PredefinedCommandHandlerNames.ExtractMethod)]
[Order(After = PredefinedCommandHandlerNames.DocumentationComments)]
internal sealed class ExtractMethodCommandHandler : ICommandHandler<ExtractMethodCommandArgs>
{
private readonly IThreadingContext _threadingContext;
private readonly ITextBufferUndoManagerProvider _undoManager;
private readonly IInlineRenameService _renameService;
private readonly IAsynchronousOperationListener _asyncListener;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public ExtractMethodCommandHandler(
IThreadingContext threadingContext,
ITextBufferUndoManagerProvider undoManager,
IInlineRenameService renameService,
IAsynchronousOperationListenerProvider asyncListenerProvider)
{
Contract.ThrowIfNull(threadingContext);
Contract.ThrowIfNull(undoManager);
Contract.ThrowIfNull(renameService);
_threadingContext = threadingContext;
_undoManager = undoManager;
_renameService = renameService;
_asyncListener = asyncListenerProvider.GetListener(FeatureAttribute.ExtractMethod);
}
public string DisplayName => EditorFeaturesResources.Extract_Method;
public CommandState GetCommandState(ExtractMethodCommandArgs args)
{
var spans = args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer);
if (spans.Count(s => s.Length > 0) != 1)
{
return CommandState.Unspecified;
}
if (!args.SubjectBuffer.TryGetWorkspace(out var workspace) ||
!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) ||
!args.SubjectBuffer.SupportsRefactorings())
{
return CommandState.Unspecified;
}
return CommandState.Available;
}
public bool ExecuteCommand(ExtractMethodCommandArgs args, CommandExecutionContext context)
{
// Finish any rename that had been started. We'll do this here before we enter the
// wait indicator for Extract Method
if (_renameService.ActiveSession != null)
{
_threadingContext.JoinableTaskFactory.Run(() => _renameService.ActiveSession.CommitAsync(previewChanges: false, context.OperationContext));
}
if (!args.SubjectBuffer.SupportsRefactorings())
return false;
var view = args.TextView;
var textBuffer = args.SubjectBuffer;
var spans = view.Selection.GetSnapshotSpansOnBuffer(textBuffer).Where(s => s.Length > 0).ToList();
if (spans.Count != 1)
return false;
var span = spans[0];
var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document is null)
return false;
_ = ExecuteAsync(view, textBuffer, document, span);
return true;
}
private async Task ExecuteAsync(
ITextView view,
ITextBuffer textBuffer,
Document document,
SnapshotSpan span)
{
_threadingContext.ThrowIfNotOnUIThread();
var indicatorFactory = document.Project.Solution.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
using var indicatorContext = indicatorFactory.Create(
view, span, EditorFeaturesResources.Applying_Extract_Method_refactoring, cancelOnEdit: true, cancelOnFocusLost: true);
using var asyncToken = _asyncListener.BeginAsyncOperation(nameof(ExecuteCommand));
await ExecuteWorkerAsync(view, textBuffer, span.Span.ToTextSpan(), indicatorContext).ConfigureAwait(false);
}
private async Task ExecuteWorkerAsync(
ITextView view,
ITextBuffer textBuffer,
TextSpan span,
IBackgroundWorkIndicatorContext waitContext)
{
_threadingContext.ThrowIfNotOnUIThread();
var cancellationToken = waitContext.UserCancellationToken;
var document = await textBuffer.CurrentSnapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(waitContext).ConfigureAwait(false);
if (document is null)
return;
var options = await document.GetExtractMethodGenerationOptionsAsync(cancellationToken).ConfigureAwait(false);
var result = await ExtractMethodService.ExtractMethodAsync(document, span, localFunction: false, options, cancellationToken).ConfigureAwait(false);
if (!Succeeded(result))
{
// Extract method didn't succeed. Or succeeded, but had some reasons to notify the user about. See if
// extracting a local function would be better..
var localFunctionResult = await ExtractMethodService.ExtractMethodAsync(document, span, localFunction: true, options, cancellationToken).ConfigureAwait(false);
if (Succeeded(localFunctionResult))
{
// Extract local function completely succeeded. Use that instead.
result = localFunctionResult;
}
else if (!result.Succeeded && localFunctionResult.Succeeded)
{
// Extract method entirely failed. But extract local function was able to proceed, albeit with reasons
// to notify the user about. Continue one with extract local function instead.
result = localFunctionResult;
}
else
{
// Extract local function was just as bad as extract method. Just report the extract method issues below.
}
}
Contract.ThrowIfNull(result);
result = await NotifyUserIfNecessaryAsync(document, result, cancellationToken).ConfigureAwait(false);
if (result is null)
return;
var (formattedDocument, methodNameAtInvocation) = await result.GetDocumentAsync(cancellationToken).ConfigureAwait(false);
var changes = await formattedDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false);
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
ApplyChange_OnUIThread(textBuffer, changes, waitContext);
if (methodNameAtInvocation != null)
{
// start inline rename to allow the user to change the name if they want.
var textSnapshot = textBuffer.CurrentSnapshot;
document = textSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document != null)
_renameService.StartInlineSession(document, methodNameAtInvocation.Value.Span, cancellationToken);
// select invocation span
view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(textSnapshot, methodNameAtInvocation.Value.Span.End));
view.SetSelection(methodNameAtInvocation.Value.Span.ToSnapshotSpan(textSnapshot));
}
}
private void ApplyChange_OnUIThread(
ITextBuffer textBuffer, IEnumerable<TextChange> changes, IBackgroundWorkIndicatorContext waitContext)
{
_threadingContext.ThrowIfNotOnUIThread();
using var undoTransaction = _undoManager.GetTextBufferUndoManager(textBuffer).TextBufferUndoHistory.CreateTransaction("Extract Method");
// We're about to make an edit ourselves. so disable the cancellation that happens on editing.
waitContext.CancelOnEdit = false;
textBuffer.ApplyChanges(changes);
// apply changes
undoTransaction.Complete();
}
private static bool Succeeded(ExtractMethodResult result)
=> result is { Succeeded: true, Reasons.Length: 0 };
private async Task<ExtractMethodResult?> NotifyUserIfNecessaryAsync(
Document document, ExtractMethodResult result, CancellationToken cancellationToken)
{
// If we succeeded without any problems, just proceed without notifying the user.
if (Succeeded(result))
return result;
// We have some sort of issue. See what the user wants to do. If we have no way to inform the user bail
// out rather than doing something wrong.
var notificationService = document.Project.Solution.Services.GetService<INotificationService>();
if (notificationService is null)
return null;
// We're about to show an notification to the user. Switch to the ui thread to do so.
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
// The alternative approach wasn't better. If we failed, just let the user know and bail out. Otherwise,
// if we succeeded with messages, tell the user and let them decide if they want to proceed or not.
if (!result.Succeeded)
{
notificationService.SendNotification(
EditorFeaturesResources.Extract_method_encountered_the_following_issues + Environment.NewLine +
string.Join("", result.Reasons.Select(r => Environment.NewLine + " " + r)),
title: EditorFeaturesResources.Extract_Method,
severity: NotificationSeverity.Error);
return null;
}
// We succeed and have a not null document. We must them have some issues to report to the user (otherwise
// we would have fast returned at the top of this method). Tell the user about them and let them decide
// what they want to do.
Contract.ThrowIfTrue(result.Reasons.Length == 0);
if (!notificationService.ConfirmMessageBox(
EditorFeaturesResources.Extract_method_encountered_the_following_issues + Environment.NewLine +
string.Join("", result.Reasons.Select(r => Environment.NewLine + " " + r)) + Environment.NewLine + Environment.NewLine +
EditorFeaturesResources.Do_you_still_want_to_proceed_This_may_produce_broken_code,
title: EditorFeaturesResources.Extract_Method,
severity: NotificationSeverity.Warning))
{
return null;
}
return result;
}
}
|