File: Suggestions\SuggestedActionsSource.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_s3pc0hsu_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures.Wpf)
// 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using IUIThreadOperationContext = Microsoft.VisualStudio.Utilities.IUIThreadOperationContext;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions
{
    internal partial class SuggestedActionsSourceProvider
    {
        private sealed partial class SuggestedActionsSource : ISuggestedActionsSource3
        {
            private readonly ISuggestedActionCategoryRegistryService _suggestedActionCategoryRegistry;
 
            private readonly ReferenceCountedDisposable<State> _state;
            private readonly IAsynchronousOperationListener _listener;
 
            public event EventHandler<EventArgs>? SuggestedActionsChanged { add { } remove { } }
 
            private readonly IThreadingContext _threadingContext;
            public readonly IGlobalOptionService GlobalOptions;
 
            public SuggestedActionsSource(
                IThreadingContext threadingContext,
                IGlobalOptionService globalOptions,
                SuggestedActionsSourceProvider owner,
                ITextView textView,
                ITextBuffer textBuffer,
                ISuggestedActionCategoryRegistryService suggestedActionCategoryRegistry,
                IAsynchronousOperationListener listener)
            {
                _threadingContext = threadingContext;
                GlobalOptions = globalOptions;
 
                _suggestedActionCategoryRegistry = suggestedActionCategoryRegistry;
                _state = new ReferenceCountedDisposable<State>(new State(this, owner, textView, textBuffer));
 
                _state.Target.TextView.Closed += OnTextViewClosed;
                _listener = listener;
            }
 
            public void Dispose()
                => _state.Dispose();
 
            public bool TryGetTelemetryId(out Guid telemetryId)
            {
                telemetryId = default;
 
                using var state = _state.TryAddReference();
                if (state is null)
                {
                    return false;
                }
 
                var workspace = state.Target.Workspace;
                if (workspace == null)
                {
                    return false;
                }
 
                var documentId = workspace.GetDocumentIdInCurrentContext(state.Target.SubjectBuffer.AsTextContainer());
                if (documentId == null)
                {
                    return false;
                }
 
                var project = workspace.CurrentSolution.GetProject(documentId.ProjectId);
                if (project == null)
                {
                    return false;
                }
 
                switch (project.Language)
                {
                    case LanguageNames.CSharp:
                        telemetryId = s_CSharpSourceGuid;
                        return true;
                    case LanguageNames.VisualBasic:
                        telemetryId = s_visualBasicSourceGuid;
                        return true;
                    case "Xaml":
                        telemetryId = s_xamlSourceGuid;
                        return true;
                    default:
                        return false;
                }
            }
 
            public IEnumerable<SuggestedActionSet>? GetSuggestedActions(
                ISuggestedActionCategorySet requestedActionCategories,
                SnapshotSpan range,
                CancellationToken cancellationToken)
                => null;
 
            public IEnumerable<SuggestedActionSet>? GetSuggestedActions(
                ISuggestedActionCategorySet requestedActionCategories,
                SnapshotSpan range,
                IUIThreadOperationContext operationContext)
                => null;
 
            public Task<bool> HasSuggestedActionsAsync(
                ISuggestedActionCategorySet requestedActionCategories,
                SnapshotSpan range,
                CancellationToken cancellationToken)
            {
                // We implement GetSuggestedActionCategoriesAsync so this should not be called
                throw new NotImplementedException($"We implement {nameof(GetSuggestedActionCategoriesAsync)}. This should not be called.");
            }
 
            private TextSpan? TryGetCodeRefactoringSelection(ReferenceCountedDisposable<State> state, SnapshotSpan range)
            {
                _threadingContext.ThrowIfNotOnUIThread();
 
                var selectedSpans = state.Target.TextView.Selection.SelectedSpans
                    .SelectMany(ss => state.Target.TextView.BufferGraph.MapDownToBuffer(ss, SpanTrackingMode.EdgeExclusive, state.Target.SubjectBuffer))
                    .Where(ss => !state.Target.TextView.IsReadOnlyOnSurfaceBuffer(ss))
                    .ToList();
 
                // We only support refactorings when there is a single selection in the document.
                if (selectedSpans.Count != 1)
                    return null;
 
                var translatedSpan = selectedSpans[0].TranslateTo(range.Snapshot, SpanTrackingMode.EdgeInclusive);
 
                // We only support refactorings when selected span intersects with the span that the light bulb is asking for.
                if (!translatedSpan.IntersectsWith(range))
                    return null;
 
                return translatedSpan.Span.ToTextSpan();
            }
 
            private void OnTextViewClosed(object sender, EventArgs e)
                => Dispose();
 
            public async Task<ISuggestedActionCategorySet?> GetSuggestedActionCategoriesAsync(
                ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken)
            {
                // Make sure we're explicitly on the background, to do as much as possible in a non-blocking fashion.
                await TaskScheduler.Default;
                cancellationToken.ThrowIfCancellationRequested();
 
                // This function gets called immediately after operations like scrolling.  We want to wait just a small
                // amount to ensure that we don't immediately start consuming CPU/memory which then impedes the very
                // action the user is trying to perform.  To accomplish this, we wait 100ms.  That's longer than normal
                // keyboard repeat rates (usually around 30ms), but short enough that it's not noticeable to the user.
                await Task.Delay(100, cancellationToken).NoThrowAwaitable();
                if (cancellationToken.IsCancellationRequested)
                    return null;
 
                using var state = _state.TryAddReference();
                if (state is null)
                    return null;
 
                // Make sure the range is from the same buffer that this source was created for.
                Contract.ThrowIfFalse(
                    range.Snapshot.TextBuffer.Equals(state.Target.SubjectBuffer),
                    $"Invalid text buffer passed to {nameof(HasSuggestedActionsAsync)}");
 
                var workspace = state.Target.Workspace;
                if (workspace == null)
                    return null;
 
                using var asyncToken = state.Target.Owner.OperationListener.BeginAsyncOperation(nameof(GetSuggestedActionCategoriesAsync));
                var document = range.Snapshot.GetOpenTextDocumentInCurrentContextWithChanges();
                if (document == null)
                    return null;
 
                using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                // Assign over cancellation token so no one accidentally uses the wrong token.
                cancellationToken = linkedTokenSource.Token;
 
                // Kick off the work to get errors.
                var errorTask = GetFixLevelAsync(document, range, cancellationToken);
 
                // Make a quick jump back to the UI thread to get the user's selection, then go back to the thread pool..
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
 
                var selection = TryGetCodeRefactoringSelection(state, range);
                await TaskScheduler.Default;
 
                // If we have a selection, kick off the work to get refactorings concurrently with the above work to get errors.
                var refactoringTask = selection != null
                    ? TryGetRefactoringSuggestedActionCategoryAsync(document, selection, cancellationToken)
                    : SpecializedTasks.Null<string>();
 
                // If we happen to get the result of the error task before the refactoring task,
                // and that result is non-null, we can just cancel the refactoring task.
                var result = await errorTask.ConfigureAwait(false) ?? await refactoringTask.ConfigureAwait(false);
                linkedTokenSource.Cancel();
 
                return result == null
                    ? null
                    : _suggestedActionCategoryRegistry.CreateSuggestedActionCategorySet(result);
            }
 
            private async Task<string?> GetFixLevelAsync(
                TextDocument document,
                SnapshotSpan range,
                CancellationToken cancellationToken)
            {
                // Ensure we yield the thread that called into us, allowing it to continue onwards.
                await TaskScheduler.Default.SwitchTo(alwaysYield: true);
 
                // Ensure we can get the snapshot of our state.  If not, we were disposed between this task being
                // created, and eventually run.
                using var state = _state.TryAddReference();
                if (state is null)
                    return null;
 
                var lowPriorityAnalyzerData = new SuggestedActionPriorityProvider.LowPriorityAnalyzersAndDiagnosticIds();
 
                foreach (var order in Orderings)
                {
                    var priority = TryGetPriority(order);
                    Contract.ThrowIfNull(priority);
                    var priorityProvider = new SuggestedActionPriorityProvider(priority.Value, lowPriorityAnalyzerData);
 
                    var result = await GetFixCategoryAsync(priorityProvider).ConfigureAwait(false);
                    if (result != null)
                        return result;
                }
 
                return null;
 
                async Task<string?> GetFixCategoryAsync(ICodeActionRequestPriorityProvider priorityProvider)
                {
                    if (state.Target.Owner._codeFixService != null &&
                        state.Target.SubjectBuffer.SupportsCodeFixes())
                    {
                        var result = await state.Target.Owner._codeFixService.GetMostSevereFixAsync(
                            document, range.Span.ToTextSpan(), priorityProvider, cancellationToken).ConfigureAwait(false);
 
                        if (result != null)
                        {
                            Logger.Log(FunctionId.SuggestedActions_HasSuggestedActionsAsync);
                            return result.FirstDiagnostic.Severity switch
                            {
 
                                DiagnosticSeverity.Hidden or DiagnosticSeverity.Info or DiagnosticSeverity.Warning => PredefinedSuggestedActionCategoryNames.CodeFix,
                                DiagnosticSeverity.Error => PredefinedSuggestedActionCategoryNames.ErrorFix,
                                _ => throw ExceptionUtilities.Unreachable(),
                            };
                        }
                    }
 
                    return null;
                }
            }
 
            private async Task<string?> TryGetRefactoringSuggestedActionCategoryAsync(
                TextDocument document,
                TextSpan? selection,
                CancellationToken cancellationToken)
            {
                // Ensure we yield the thread that called into us, allowing it to continue onwards.
                await TaskScheduler.Default.SwitchTo(alwaysYield: true);
 
                // Ensure we can get the snapshot of our state.  If not, we were disposed between this task being
                // created, and eventually run.
                using var state = _state.TryAddReference();
                if (state is null)
                    return null;
 
                if (!selection.HasValue)
                {
                    // this is here to fail test and see why it is failed.
                    Trace.WriteLine("given range is not current");
                    return null;
                }
 
                if (GlobalOptions.GetOption(EditorComponentOnOffOptions.CodeRefactorings) &&
                    state.Target.Owner._codeRefactoringService != null &&
                    state.Target.SubjectBuffer.SupportsRefactorings())
                {
                    if (await state.Target.Owner._codeRefactoringService.HasRefactoringsAsync(
                            document, selection.Value, cancellationToken).ConfigureAwait(false))
                    {
                        return PredefinedSuggestedActionCategoryNames.Refactoring;
                    }
                }
 
                return null;
            }
        }
    }
}