From 363a61afbc0b971bed233b05d9575a8f3be0751c Mon Sep 17 00:00:00 2001 From: CyrusNajmabadi Date: Thu, 9 Mar 2017 16:43:03 -0800 Subject: [PATCH] Move nested type into its own file. --- src/EditorFeatures/Core/EditorFeatures.csproj | 1 + .../Suggestions/SuggestedActionsSource.cs | 857 ++++++++++++++++++ .../SuggestedActionsSourceProvider.cs | 841 +---------------- 3 files changed, 859 insertions(+), 840 deletions(-) create mode 100644 src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSource.cs diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj index 87fb824c95f..18c7547bc58 100644 --- a/src/EditorFeatures/Core/EditorFeatures.csproj +++ b/src/EditorFeatures/Core/EditorFeatures.csproj @@ -113,6 +113,7 @@ + diff --git a/src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSource.cs b/src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSource.cs new file mode 100644 index 00000000000..8592b9ffe30 --- /dev/null +++ b/src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSource.cs @@ -0,0 +1,857 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeFixes.Suppression; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.Shared; +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; +using Microsoft.CodeAnalysis.Editor.Shared.Options; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Text.Shared.Extensions; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions +{ + using CodeFixGroupKey = Tuple; + + internal partial class SuggestedActionsSourceProvider + { + private class SuggestedActionsSource : ForegroundThreadAffinitizedObject, ISuggestedActionsSource + { + // state that will be only reset when source is disposed. + private SuggestedActionsSourceProvider _owner; + private ITextView _textView; + private ITextBuffer _subjectBuffer; + private WorkspaceRegistration _registration; + + // mutable state + private Workspace _workspace; + private int _lastSolutionVersionReported; + + public SuggestedActionsSource(SuggestedActionsSourceProvider owner, ITextView textView, ITextBuffer textBuffer) + { + _owner = owner; + _textView = textView; + _textView.Closed += OnTextViewClosed; + + _subjectBuffer = textBuffer; + _registration = Workspace.GetWorkspaceRegistration(textBuffer.AsTextContainer()); + + _lastSolutionVersionReported = InvalidSolutionVersion; + var updateSource = (IDiagnosticUpdateSource)_owner._diagnosticService; + updateSource.DiagnosticsUpdated += OnDiagnosticsUpdated; + + if (_registration.Workspace != null) + { + _workspace = _registration.Workspace; + _workspace.DocumentActiveContextChanged += OnActiveContextChanged; + } + + _registration.WorkspaceChanged += OnWorkspaceChanged; + } + + public event EventHandler SuggestedActionsChanged; + + public bool TryGetTelemetryId(out Guid telemetryId) + { + telemetryId = default(Guid); + + var workspace = _workspace; + if (workspace == null || _subjectBuffer == null) + { + return false; + } + + var documentId = workspace.GetDocumentIdInCurrentContext(_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 GetSuggestedActions( + ISuggestedActionCategorySet requestedActionCategories, + SnapshotSpan range, + CancellationToken cancellationToken) + { + AssertIsForeground(); + + using (Logger.LogBlock(FunctionId.SuggestedActions_GetSuggestedActions, cancellationToken)) + { + var document = GetMatchingDocumentAsync(range.Snapshot, cancellationToken).WaitAndGetResult(cancellationToken); + if (document == null) + { + // this is here to fail test and see why it is failed. + Trace.WriteLine("given range is not current"); + return null; + } + + var workspace = document.Project.Solution.Workspace; + var supportsFeatureService = workspace.Services.GetService(); + + var fixes = GetCodeFixes(supportsFeatureService, requestedActionCategories, workspace, document, range, cancellationToken); + var refactorings = GetRefactorings(supportsFeatureService, requestedActionCategories, workspace, document, range, cancellationToken); + + var result = fixes.Concat(refactorings); + + if (result.IsEmpty) + { + return null; + } + + var allActionSets = InlineActionSetsIfDesirable(result); + var orderedActionSets = OrderActionSets(allActionSets); + var filteredSets = FilterActionSetsByTitle(orderedActionSets); + + return filteredSets; + } + } + + private ImmutableArray OrderActionSets( + ImmutableArray actionSets) + { + var caretPoint = _textView.GetCaretPoint(_subjectBuffer); + return actionSets.OrderByDescending(s => s.Priority) + .ThenBy(s => s, new SuggestedActionSetComparer(caretPoint)) + .ToImmutableArray(); + } + + private ImmutableArray FilterActionSetsByTitle(ImmutableArray allActionSets) + { + var result = ArrayBuilder.GetInstance(); + + var seenTitles = new HashSet(); + + foreach (var set in allActionSets) + { + var filteredSet = FilterActionSetByTitle(set, seenTitles); + if (filteredSet != null) + { + result.Add(filteredSet); + } + } + + return result.ToImmutableAndFree(); + } + + private SuggestedActionSet FilterActionSetByTitle(SuggestedActionSet set, HashSet seenTitles) + { + var actions = ArrayBuilder.GetInstance(); + + foreach (var action in set.Actions) + { + if (seenTitles.Add(action.DisplayText)) + { + actions.Add(action); + } + } + + try + { + return actions.Count == 0 + ? null + : new SuggestedActionSet(actions.ToImmutable(), set.Title, set.Priority, set.ApplicableToSpan); + } + finally + { + actions.Free(); + } + } + + private ImmutableArray InlineActionSetsIfDesirable(ImmutableArray allActionSets) + { + // If we only have a single set of items, and that set only has three max suggestion + // offered. Then we can consider inlining any nested actions into the top level list. + // (but we only do this if the parent of the nested actions isn't invokable itself). + if (allActionSets.Sum(a => a.Actions.Count()) > 3) + { + return allActionSets; + } + + return allActionSets.SelectAsArray(InlineActions); + } + + private SuggestedActionSet InlineActions(SuggestedActionSet actionSet) + { + var newActions = ArrayBuilder.GetInstance(); + foreach (var action in actionSet.Actions) + { + var actionWithNestedActions = action as SuggestedActionWithNestedActions; + + // Only inline if the underlying code action allows it. + if (actionWithNestedActions?.CodeAction.IsInlinable == true) + { + newActions.AddRange(actionWithNestedActions.NestedActionSet.Actions); + } + else + { + newActions.Add(action); + } + } + + return new SuggestedActionSet( + newActions.ToImmutableAndFree(), actionSet.Title, actionSet.Priority, actionSet.ApplicableToSpan); + } + + private ImmutableArray GetCodeFixes( + IDocumentSupportsFeatureService supportsFeatureService, + ISuggestedActionCategorySet requestedActionCategories, + Workspace workspace, + Document document, + SnapshotSpan range, + CancellationToken cancellationToken) + { + this.AssertIsForeground(); + + if (_owner._codeFixService != null && + supportsFeatureService.SupportsCodeFixes(document) && + requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.CodeFix)) + { + // We only include suppressions if light bulb is asking for everything. + // If the light bulb is only asking for code fixes, then we don't include suppressions. + var includeSuppressionFixes = requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Any); + + var fixes = Task.Run( + () => _owner._codeFixService.GetFixesAsync( + document, range.Span.ToTextSpan(), includeSuppressionFixes, cancellationToken), + cancellationToken).WaitAndGetResult(cancellationToken); + + var filteredFixes = FilterOnUIThread(fixes, workspace); + + return OrganizeFixes(workspace, filteredFixes, includeSuppressionFixes); + } + + return ImmutableArray.Empty; + } + + private ImmutableArray FilterOnUIThread( + ImmutableArray collections, Workspace workspace) + { + this.AssertIsForeground(); + + return collections.Select(c => FilterOnUIThread(c, workspace)).WhereNotNull().ToImmutableArray(); + } + + private CodeFixCollection FilterOnUIThread( + CodeFixCollection collection, + Workspace workspace) + { + this.AssertIsForeground(); + + var applicableFixes = collection.Fixes.WhereAsArray(f => IsApplicable(f.Action, workspace)); + return applicableFixes.Length == 0 + ? null + : applicableFixes.Length == collection.Fixes.Length + ? collection + : new CodeFixCollection( + collection.Provider, collection.TextSpan, applicableFixes, + collection.FixAllState, collection.SupportedScopes, collection.FirstDiagnostic); + } + + private bool IsApplicable(CodeAction action, Workspace workspace) + { + if (!action.PerformFinalApplicabilityCheck) + { + // If we don't even need to perform the final applicability check, + // then the code actoin is applicable. + return true; + } + + // Otherwise, defer to the action to make the decision. + this.AssertIsForeground(); + return action.IsApplicable(workspace); + } + + private ImmutableArray FilterOnUIThread(ImmutableArray refactorings, Workspace workspace) + { + return refactorings.Select(r => FilterOnUIThread(r, workspace)).WhereNotNull().ToImmutableArray(); + } + + private CodeRefactoring FilterOnUIThread(CodeRefactoring refactoring, Workspace workspace) + { + var actions = refactoring.Actions.WhereAsArray(a => IsApplicable(a, workspace)); + return actions.Length == 0 + ? null + : actions.Length == refactoring.Actions.Length + ? refactoring + : new CodeRefactoring(refactoring.Provider, actions); + } + + /// + /// Arrange fixes into groups based on the issue (diagnostic being fixed) and prioritize these groups. + /// + private ImmutableArray OrganizeFixes( + Workspace workspace, ImmutableArray fixCollections, + bool includeSuppressionFixes) + { + var map = ImmutableDictionary.CreateBuilder>(); + var order = ArrayBuilder.GetInstance(); + + // First group fixes by diagnostic and priority. + GroupFixes(workspace, fixCollections, map, order, includeSuppressionFixes); + + // Then prioritize between the groups. + return PrioritizeFixGroups(map.ToImmutable(), order.ToImmutableAndFree()); + } + + /// + /// Groups fixes by the diagnostic being addressed by each fix. + /// + private void GroupFixes( + Workspace workspace, + ImmutableArray fixCollections, + IDictionary> map, + ArrayBuilder order, + bool includeSuppressionFixes) + { + foreach (var fixCollection in fixCollections) + { + ProcessFixCollection( + workspace, map, order, includeSuppressionFixes, fixCollection); + } + } + + private void ProcessFixCollection( + Workspace workspace, + IDictionary> map, + ArrayBuilder order, + bool includeSuppressionFixes, + CodeFixCollection fixCollection) + { + var fixes = fixCollection.Fixes; + var fixCount = fixes.Length; + + Func getFixAllSuggestedActionSet = + codeAction => GetFixAllSuggestedActionSet( + codeAction, fixCount, fixCollection.FixAllState, + fixCollection.SupportedScopes, fixCollection.FirstDiagnostic, + workspace); + + var nonSupressionCodeFixes = fixes.WhereAsArray(f => !(f.Action is TopLevelSuppressionCodeAction)); + var supressionCodeFixes = fixes.WhereAsArray(f => f.Action is TopLevelSuppressionCodeAction); + + AddCodeActions(workspace, map, order, fixCollection, + getFixAllSuggestedActionSet, nonSupressionCodeFixes); + + // Add suppression fixes to the end of a given SuggestedActionSet so that they + // always show up last in a group. + if (includeSuppressionFixes) + { + AddCodeActions(workspace, map, order, fixCollection, + getFixAllSuggestedActionSet, supressionCodeFixes); + } + } + + private void AddCodeActions( + Workspace workspace, IDictionary> map, + ArrayBuilder order, CodeFixCollection fixCollection, + Func getFixAllSuggestedActionSet, + ImmutableArray codeFixes) + { + foreach (var fix in codeFixes) + { + SuggestedAction suggestedAction; + if (fix.Action.NestedCodeActions.Length > 0) + { + var nestedActions = fix.Action.NestedCodeActions.SelectAsArray( + nestedAction => new CodeFixSuggestedAction( + _owner, workspace, _subjectBuffer, fix, fixCollection.Provider, + nestedAction, getFixAllSuggestedActionSet(nestedAction))); + + var set = new SuggestedActionSet( + nestedActions, SuggestedActionSetPriority.Medium, + fix.PrimaryDiagnostic.Location.SourceSpan.ToSpan()); + + suggestedAction = new SuggestedActionWithNestedActions( + _owner, workspace, _subjectBuffer, + fixCollection.Provider, fix.Action, set); + } + else + { + suggestedAction = new CodeFixSuggestedAction( + _owner, workspace, _subjectBuffer, fix, fixCollection.Provider, + fix.Action, getFixAllSuggestedActionSet(fix.Action)); + } + + AddFix(fix, suggestedAction, map, order); + } + } + + private static void AddFix( + CodeFix fix, SuggestedAction suggestedAction, + IDictionary> map, + ArrayBuilder order) + { + var diag = fix.GetPrimaryDiagnosticData(); + + var groupKey = new CodeFixGroupKey(diag, fix.Action.Priority); + if (!map.ContainsKey(groupKey)) + { + order.Add(groupKey); + map[groupKey] = ImmutableArray.CreateBuilder(); + } + + map[groupKey].Add(suggestedAction); + } + + /// + /// If the provided fix all context is non-null and the context's code action Id matches the given code action's Id then, + /// returns the set of fix all occurrences actions associated with the code action. + /// + internal SuggestedActionSet GetFixAllSuggestedActionSet( + CodeAction action, + int actionCount, + FixAllState fixAllState, + ImmutableArray supportedScopes, + Diagnostic firstDiagnostic, + Workspace workspace) + { + + if (fixAllState == null) + { + return null; + } + + if (actionCount > 1 && action.EquivalenceKey == null) + { + return null; + } + + var fixAllSuggestedActions = ArrayBuilder.GetInstance(); + foreach (var scope in supportedScopes) + { + var fixAllStateForScope = fixAllState.WithScopeAndEquivalenceKey(scope, action.EquivalenceKey); + var fixAllSuggestedAction = new FixAllSuggestedAction( + _owner, workspace, _subjectBuffer, fixAllStateForScope, + firstDiagnostic, action); + + fixAllSuggestedActions.Add(fixAllSuggestedAction); + } + + return new SuggestedActionSet( + fixAllSuggestedActions.ToImmutableAndFree(), + title: EditorFeaturesResources.Fix_all_occurrences_in); + } + + /// + /// Return prioritized set of fix groups such that fix group for suppression always show up at the bottom of the list. + /// + /// + /// Fix groups are returned in priority order determined based on . + /// Priority for all s containing fixes is set to by default. + /// The only exception is the case where a only contains suppression fixes - + /// the priority of such s is set to so that suppression fixes + /// always show up last after all other fixes (and refactorings) for the selected line of code. + /// + private static ImmutableArray PrioritizeFixGroups( + IDictionary> map, IList order) + { + var sets = ArrayBuilder.GetInstance(); + + foreach (var diag in order) + { + var actions = map[diag]; + + foreach (var group in actions.GroupBy(a => a.Priority)) + { + var priority = GetSuggestedActionSetPriority(group.Key); + + // diagnostic from things like build shouldn't reach here since we don't support LB for those diagnostics + Contract.Requires(diag.Item1.HasTextSpan); + sets.Add(new SuggestedActionSet(group, priority, diag.Item1.TextSpan.ToSpan())); + } + } + + return sets.ToImmutableAndFree(); + } + + private static SuggestedActionSetPriority GetSuggestedActionSetPriority(CodeActionPriority key) + { + switch (key) + { + case CodeActionPriority.None: return SuggestedActionSetPriority.None; + case CodeActionPriority.Low: return SuggestedActionSetPriority.Low; + case CodeActionPriority.Medium: return SuggestedActionSetPriority.Medium; + case CodeActionPriority.High: return SuggestedActionSetPriority.High; + default: + throw new InvalidOperationException(); + } + } + + private ImmutableArray GetRefactorings( + IDocumentSupportsFeatureService supportsFeatureService, + ISuggestedActionCategorySet requestedActionCategories, + Workspace workspace, + Document document, + SnapshotSpan range, + CancellationToken cancellationToken) + { + this.AssertIsForeground(); + + if (workspace.Options.GetOption(EditorComponentOnOffOptions.CodeRefactorings) && + _owner._codeRefactoringService != null && + supportsFeatureService.SupportsRefactorings(document) && + requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Refactoring)) + { + // Get the selection while on the UI thread. + var selection = TryGetCodeRefactoringSelection(_subjectBuffer, _textView, range); + if (!selection.HasValue) + { + // this is here to fail test and see why it is failed. + Trace.WriteLine("given range is not current"); + return ImmutableArray.Empty; + } + + // It may seem strange that we kick off a task, but then immediately 'Wait' on + // it. However, it's deliberate. We want to make sure that the code runs on + // the background so that no one takes an accidentally dependency on running on + // the UI thread. + var refactorings = Task.Run( + () => _owner._codeRefactoringService.GetRefactoringsAsync( + document, selection.Value, cancellationToken), + cancellationToken).WaitAndGetResult(cancellationToken); + + var filteredRefactorings = FilterOnUIThread(refactorings, workspace); + + return filteredRefactorings.SelectAsArray(r => OrganizeRefactorings(workspace, r)); + } + + return ImmutableArray.Empty; + } + + /// + /// Arrange refactorings into groups. + /// + /// + /// Refactorings are returned in priority order determined based on . + /// Priority for all s containing refactorings is set to + /// and should show up after fixes but before suppression fixes in the light bulb menu. + /// + private SuggestedActionSet OrganizeRefactorings(Workspace workspace, CodeRefactoring refactoring) + { + var refactoringSuggestedActions = ArrayBuilder.GetInstance(); + + foreach (var action in refactoring.Actions) + { + refactoringSuggestedActions.Add(new CodeRefactoringSuggestedAction( + _owner, workspace, _subjectBuffer, refactoring.Provider, action)); + } + + return new SuggestedActionSet( + refactoringSuggestedActions.ToImmutableAndFree(), SuggestedActionSetPriority.Low); + } + + public async Task HasSuggestedActionsAsync(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken) + { + // Explicitly hold onto below fields in locals and use these locals throughout this code path to avoid crashes + // if these fields happen to be cleared by Dispose() below. This is required since this code path involves + // code that can run asynchronously from background thread. + var view = _textView; + var buffer = _subjectBuffer; + var provider = _owner; + + if (view == null || buffer == null || provider == null) + { + return false; + } + + using (var asyncToken = provider.OperationListener.BeginAsyncOperation("HasSuggestedActionsAsync")) + { + var document = await GetMatchingDocumentAsync(range.Snapshot, cancellationToken).ConfigureAwait(false); + if (document == null) + { + // this is here to fail test and see why it is failed. + Trace.WriteLine("given range is not current"); + return false; + } + + return + await HasFixesAsync(provider, document, range, cancellationToken).ConfigureAwait(false) || + await HasRefactoringsAsync(provider, document, buffer, view, range, cancellationToken).ConfigureAwait(false); + } + } + + private async Task HasFixesAsync( + SuggestedActionsSourceProvider provider, + Document document, + SnapshotSpan range, + CancellationToken cancellationToken) + { + var workspace = document.Project.Solution.Workspace; + var supportsFeatureService = workspace.Services.GetService(); + + if (provider._codeFixService != null && + supportsFeatureService.SupportsCodeFixes(document)) + { + var result = await Task.Run( + () => provider._codeFixService.GetFirstDiagnosticWithFixAsync( + document, range.Span.ToTextSpan(), cancellationToken), + cancellationToken).ConfigureAwait(false); + + if (result.HasFix) + { + Logger.Log(FunctionId.SuggestedActions_HasSuggestedActionsAsync); + return true; + } + + if (result.PartialResult) + { + // reset solution version number so that we can raise suggested action changed event + Volatile.Write(ref _lastSolutionVersionReported, InvalidSolutionVersion); + return false; + } + } + + return false; + } + + private async Task HasRefactoringsAsync( + SuggestedActionsSourceProvider provider, + Document document, + ITextBuffer buffer, + ITextView view, + SnapshotSpan range, + CancellationToken cancellationToken) + { + var workspace = document.Project.Solution.Workspace; + var supportsFeatureService = workspace.Services.GetService(); + + if (document.Project.Solution.Options.GetOption(EditorComponentOnOffOptions.CodeRefactorings) && + provider._codeRefactoringService != null && + supportsFeatureService.SupportsRefactorings(document)) + { + TextSpan? selection = null; + if (IsForeground()) + { + // This operation needs to happen on UI thread because it needs to access textView.Selection. + selection = TryGetCodeRefactoringSelection(buffer, view, range); + } + else + { + await InvokeBelowInputPriority(() => + { + // This operation needs to happen on UI thread because it needs to access textView.Selection. + selection = TryGetCodeRefactoringSelection(buffer, view, range); + }).ConfigureAwait(false); + } + + if (!selection.HasValue) + { + // this is here to fail test and see why it is failed. + Trace.WriteLine("given range is not current"); + return false; + } + + return await Task.Run( + () => provider._codeRefactoringService.HasRefactoringsAsync( + document, selection.Value, cancellationToken), + cancellationToken).ConfigureAwait(false); + } + + return false; + } + + private static TextSpan? TryGetCodeRefactoringSelection(ITextBuffer buffer, ITextView view, SnapshotSpan range) + { + var selectedSpans = view.Selection.SelectedSpans + .SelectMany(ss => view.BufferGraph.MapDownToBuffer(ss, SpanTrackingMode.EdgeExclusive, buffer)) + .Where(ss => !view.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 static async Task GetMatchingDocumentAsync(ITextSnapshot givenSnapshot, CancellationToken cancellationToken) + { + var buffer = givenSnapshot.TextBuffer; + if (buffer == null) + { + return null; + } + + var workspace = buffer.GetWorkspace(); + if (workspace == null) + { + return null; + } + + var documentId = workspace.GetDocumentIdInCurrentContext(buffer.AsTextContainer()); + if (documentId == null) + { + return null; + } + + var document = workspace.CurrentSolution.GetDocument(documentId); + if (document == null) + { + return null; + } + + var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + var snapshot = sourceText.FindCorrespondingEditorTextSnapshot(); + if (snapshot == null || snapshot.Version.ReiteratedVersionNumber != givenSnapshot.Version.ReiteratedVersionNumber) + { + return null; + } + + return document; + } + + private void OnTextViewClosed(object sender, EventArgs e) + { + Dispose(); + } + + private void OnWorkspaceChanged(object sender, EventArgs e) + { + // REVIEW: this event should give both old and new workspace as argument so that + // one doesn't need to hold onto workspace in field. + + // remove existing event registration + if (_workspace != null) + { + _workspace.DocumentActiveContextChanged -= OnActiveContextChanged; + } + + // REVIEW: why one need to get new workspace from registration? why not just pass in the new workspace? + // add new event registration + _workspace = _registration.Workspace; + + if (_workspace != null) + { + _workspace.DocumentActiveContextChanged += OnActiveContextChanged; + } + } + + private void OnActiveContextChanged(object sender, DocumentActiveContextChangedEventArgs e) + { + // REVIEW: it would be nice for changed event to pass in both old and new document. + OnSuggestedActionsChanged(e.Solution.Workspace, e.NewActiveContextDocumentId, e.Solution.WorkspaceVersion); + } + + private void OnDiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs e) + { + // document removed case. no reason to raise event + if (e.Solution == null) + { + return; + } + + OnSuggestedActionsChanged(e.Workspace, e.DocumentId, e.Solution.WorkspaceVersion); + } + + private void OnSuggestedActionsChanged(Workspace currentWorkspace, DocumentId currentDocumentId, int solutionVersion, DiagnosticsUpdatedArgs args = null) + { + // Explicitly hold onto the _subjectBuffer field in a local and use this local in this function to avoid crashes + // if this field happens to be cleared by Dispose() below. This is required since this code path involves code + // that can run on background thread. + var buffer = _subjectBuffer; + if (buffer == null) + { + return; + } + + var workspace = buffer.GetWorkspace(); + + // workspace is not ready, nothing to do. + if (workspace == null || workspace != currentWorkspace) + { + return; + } + + if (currentDocumentId != workspace.GetDocumentIdInCurrentContext(buffer.AsTextContainer()) || + solutionVersion == Volatile.Read(ref _lastSolutionVersionReported)) + { + return; + } + this.SuggestedActionsChanged?.Invoke(this, EventArgs.Empty); + + Volatile.Write(ref _lastSolutionVersionReported, solutionVersion); + } + + public void Dispose() + { + if (_owner != null) + { + var updateSource = (IDiagnosticUpdateSource)_owner._diagnosticService; + updateSource.DiagnosticsUpdated -= OnDiagnosticsUpdated; + _owner = null; + } + + if (_workspace != null) + { + _workspace.DocumentActiveContextChanged -= OnActiveContextChanged; + _workspace = null; + } + + if (_registration != null) + { + _registration.WorkspaceChanged -= OnWorkspaceChanged; + _registration = null; + } + + if (_textView != null) + { + _textView.Closed -= OnTextViewClosed; + _textView = null; + } + + if (_subjectBuffer != null) + { + _subjectBuffer = null; + } + } + } + } +} \ No newline at end of file diff --git a/src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSourceProvider.cs b/src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSourceProvider.cs index b87eee592b9..357f649bb15 100644 --- a/src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSourceProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Suggestions/SuggestedActionsSourceProvider.cs @@ -4,26 +4,13 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CodeFixes.Suppression; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.Host; -using Microsoft.CodeAnalysis.Editor.Shared; -using Microsoft.CodeAnalysis.Editor.Shared.Extensions; -using Microsoft.CodeAnalysis.Editor.Shared.Options; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Editor.Tags; -using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -31,15 +18,13 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions { - using CodeFixGroupKey = Tuple; - [Export(typeof(ISuggestedActionsSourceProvider))] [Export(typeof(SuggestedActionsSourceProvider))] [VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] [VisualStudio.Utilities.ContentType(ContentTypeNames.XamlContentType)] [VisualStudio.Utilities.Name("Roslyn Code Fix")] [VisualStudio.Utilities.Order] - internal class SuggestedActionsSourceProvider : ISuggestedActionsSourceProvider + internal partial class SuggestedActionsSourceProvider : ISuggestedActionsSourceProvider { private static readonly Guid s_CSharpSourceGuid = new Guid("b967fea8-e2c3-4984-87d4-71a38f49e16a"); private static readonly Guid s_visualBasicSourceGuid = new Guid("4de30e93-3e0c-40c2-a4ba-1124da4539f6"); @@ -84,829 +69,5 @@ public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, return new SuggestedActionsSource(this, textView, textBuffer); } - - private class SuggestedActionsSource : ForegroundThreadAffinitizedObject, ISuggestedActionsSource - { - // state that will be only reset when source is disposed. - private SuggestedActionsSourceProvider _owner; - private ITextView _textView; - private ITextBuffer _subjectBuffer; - private WorkspaceRegistration _registration; - - // mutable state - private Workspace _workspace; - private int _lastSolutionVersionReported; - - public SuggestedActionsSource(SuggestedActionsSourceProvider owner, ITextView textView, ITextBuffer textBuffer) - { - _owner = owner; - _textView = textView; - _textView.Closed += OnTextViewClosed; - - _subjectBuffer = textBuffer; - _registration = Workspace.GetWorkspaceRegistration(textBuffer.AsTextContainer()); - - _lastSolutionVersionReported = InvalidSolutionVersion; - var updateSource = (IDiagnosticUpdateSource)_owner._diagnosticService; - updateSource.DiagnosticsUpdated += OnDiagnosticsUpdated; - - if (_registration.Workspace != null) - { - _workspace = _registration.Workspace; - _workspace.DocumentActiveContextChanged += OnActiveContextChanged; - } - - _registration.WorkspaceChanged += OnWorkspaceChanged; - } - - public event EventHandler SuggestedActionsChanged; - - public bool TryGetTelemetryId(out Guid telemetryId) - { - telemetryId = default(Guid); - - var workspace = _workspace; - if (workspace == null || _subjectBuffer == null) - { - return false; - } - - var documentId = workspace.GetDocumentIdInCurrentContext(_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 GetSuggestedActions( - ISuggestedActionCategorySet requestedActionCategories, - SnapshotSpan range, - CancellationToken cancellationToken) - { - AssertIsForeground(); - - using (Logger.LogBlock(FunctionId.SuggestedActions_GetSuggestedActions, cancellationToken)) - { - var document = GetMatchingDocumentAsync(range.Snapshot, cancellationToken).WaitAndGetResult(cancellationToken); - if (document == null) - { - // this is here to fail test and see why it is failed. - Trace.WriteLine("given range is not current"); - return null; - } - - var workspace = document.Project.Solution.Workspace; - var supportsFeatureService = workspace.Services.GetService(); - - var fixes = GetCodeFixes(supportsFeatureService, requestedActionCategories, workspace, document, range, cancellationToken); - var refactorings = GetRefactorings(supportsFeatureService, requestedActionCategories, workspace, document, range, cancellationToken); - - var result = fixes.Concat(refactorings); - - if (result.IsEmpty) - { - return null; - } - - var allActionSets = InlineActionSetsIfDesirable(result); - var orderedActionSets = OrderActionSets(allActionSets); - var filteredSets = FilterActionSetsByTitle(orderedActionSets); - - return filteredSets; - } - } - - private ImmutableArray OrderActionSets( - ImmutableArray actionSets) - { - var caretPoint = _textView.GetCaretPoint(_subjectBuffer); - return actionSets.OrderByDescending(s => s.Priority) - .ThenBy(s => s, new SuggestedActionSetComparer(caretPoint)) - .ToImmutableArray(); - } - - private ImmutableArray FilterActionSetsByTitle(ImmutableArray allActionSets) - { - var result = ArrayBuilder.GetInstance(); - - var seenTitles = new HashSet(); - - foreach (var set in allActionSets) - { - var filteredSet = FilterActionSetByTitle(set, seenTitles); - if (filteredSet != null) - { - result.Add(filteredSet); - } - } - - return result.ToImmutableAndFree(); - } - - private SuggestedActionSet FilterActionSetByTitle(SuggestedActionSet set, HashSet seenTitles) - { - var actions = ArrayBuilder.GetInstance(); - - foreach (var action in set.Actions) - { - if (seenTitles.Add(action.DisplayText)) - { - actions.Add(action); - } - } - - try - { - return actions.Count == 0 - ? null - : new SuggestedActionSet(actions.ToImmutable(), set.Title, set.Priority, set.ApplicableToSpan); - } - finally - { - actions.Free(); - } - } - - private ImmutableArray InlineActionSetsIfDesirable(ImmutableArray allActionSets) - { - // If we only have a single set of items, and that set only has three max suggestion - // offered. Then we can consider inlining any nested actions into the top level list. - // (but we only do this if the parent of the nested actions isn't invokable itself). - if (allActionSets.Sum(a => a.Actions.Count()) > 3) - { - return allActionSets; - } - - return allActionSets.SelectAsArray(InlineActions); - } - - private SuggestedActionSet InlineActions(SuggestedActionSet actionSet) - { - var newActions = ArrayBuilder.GetInstance(); - foreach (var action in actionSet.Actions) - { - var actionWithNestedActions = action as SuggestedActionWithNestedActions; - - // Only inline if the underlying code action allows it. - if (actionWithNestedActions?.CodeAction.IsInlinable == true) - { - newActions.AddRange(actionWithNestedActions.NestedActionSet.Actions); - } - else - { - newActions.Add(action); - } - } - - return new SuggestedActionSet( - newActions.ToImmutableAndFree(), actionSet.Title, actionSet.Priority, actionSet.ApplicableToSpan); - } - - private ImmutableArray GetCodeFixes( - IDocumentSupportsFeatureService supportsFeatureService, - ISuggestedActionCategorySet requestedActionCategories, - Workspace workspace, - Document document, - SnapshotSpan range, - CancellationToken cancellationToken) - { - this.AssertIsForeground(); - - if (_owner._codeFixService != null && - supportsFeatureService.SupportsCodeFixes(document) && - requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.CodeFix)) - { - // We only include suppressions if light bulb is asking for everything. - // If the light bulb is only asking for code fixes, then we don't include suppressions. - var includeSuppressionFixes = requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Any); - - var fixes = Task.Run( - () => _owner._codeFixService.GetFixesAsync( - document, range.Span.ToTextSpan(), includeSuppressionFixes, cancellationToken), - cancellationToken).WaitAndGetResult(cancellationToken); - - var filteredFixes = FilterOnUIThread(fixes, workspace); - - return OrganizeFixes(workspace, filteredFixes, includeSuppressionFixes); - } - - return ImmutableArray.Empty; - } - - private ImmutableArray FilterOnUIThread( - ImmutableArray collections, Workspace workspace) - { - this.AssertIsForeground(); - - return collections.Select(c => FilterOnUIThread(c, workspace)).WhereNotNull().ToImmutableArray(); - } - - private CodeFixCollection FilterOnUIThread( - CodeFixCollection collection, - Workspace workspace) - { - this.AssertIsForeground(); - - var applicableFixes = collection.Fixes.WhereAsArray(f => IsApplicable(f.Action, workspace)); - return applicableFixes.Length == 0 - ? null - : applicableFixes.Length == collection.Fixes.Length - ? collection - : new CodeFixCollection( - collection.Provider, collection.TextSpan, applicableFixes, - collection.FixAllState, collection.SupportedScopes, collection.FirstDiagnostic); - } - - private bool IsApplicable(CodeAction action, Workspace workspace) - { - if (!action.PerformFinalApplicabilityCheck) - { - // If we don't even need to perform the final applicability check, - // then the code actoin is applicable. - return true; - } - - // Otherwise, defer to the action to make the decision. - this.AssertIsForeground(); - return action.IsApplicable(workspace); - } - - private ImmutableArray FilterOnUIThread(ImmutableArray refactorings, Workspace workspace) - { - return refactorings.Select(r => FilterOnUIThread(r, workspace)).WhereNotNull().ToImmutableArray(); - } - - private CodeRefactoring FilterOnUIThread(CodeRefactoring refactoring, Workspace workspace) - { - var actions = refactoring.Actions.WhereAsArray(a => IsApplicable(a, workspace)); - return actions.Length == 0 - ? null - : actions.Length == refactoring.Actions.Length - ? refactoring - : new CodeRefactoring(refactoring.Provider, actions); - } - - /// - /// Arrange fixes into groups based on the issue (diagnostic being fixed) and prioritize these groups. - /// - private ImmutableArray OrganizeFixes( - Workspace workspace, ImmutableArray fixCollections, - bool includeSuppressionFixes) - { - var map = ImmutableDictionary.CreateBuilder>(); - var order = ArrayBuilder.GetInstance(); - - // First group fixes by diagnostic and priority. - GroupFixes(workspace, fixCollections, map, order, includeSuppressionFixes); - - // Then prioritize between the groups. - return PrioritizeFixGroups(map.ToImmutable(), order.ToImmutableAndFree()); - } - - /// - /// Groups fixes by the diagnostic being addressed by each fix. - /// - private void GroupFixes( - Workspace workspace, - ImmutableArray fixCollections, - IDictionary> map, - ArrayBuilder order, - bool includeSuppressionFixes) - { - foreach (var fixCollection in fixCollections) - { - ProcessFixCollection( - workspace, map, order, includeSuppressionFixes, fixCollection); - } - } - - private void ProcessFixCollection( - Workspace workspace, - IDictionary> map, - ArrayBuilder order, - bool includeSuppressionFixes, - CodeFixCollection fixCollection) - { - var fixes = fixCollection.Fixes; - var fixCount = fixes.Length; - - Func getFixAllSuggestedActionSet = - codeAction => GetFixAllSuggestedActionSet( - codeAction, fixCount, fixCollection.FixAllState, - fixCollection.SupportedScopes, fixCollection.FirstDiagnostic, - workspace); - - var nonSupressionCodeFixes = fixes.WhereAsArray(f => !(f.Action is TopLevelSuppressionCodeAction)); - var supressionCodeFixes = fixes.WhereAsArray(f => f.Action is TopLevelSuppressionCodeAction); - - AddCodeActions(workspace, map, order, fixCollection, - getFixAllSuggestedActionSet, nonSupressionCodeFixes); - - // Add suppression fixes to the end of a given SuggestedActionSet so that they - // always show up last in a group. - if (includeSuppressionFixes) - { - AddCodeActions(workspace, map, order, fixCollection, - getFixAllSuggestedActionSet, supressionCodeFixes); - } - } - - private void AddCodeActions( - Workspace workspace, IDictionary> map, - ArrayBuilder order, CodeFixCollection fixCollection, - Func getFixAllSuggestedActionSet, - ImmutableArray codeFixes) - { - foreach (var fix in codeFixes) - { - SuggestedAction suggestedAction; - if (fix.Action.NestedCodeActions.Length > 0) - { - var nestedActions = fix.Action.NestedCodeActions.SelectAsArray( - nestedAction => new CodeFixSuggestedAction( - _owner, workspace, _subjectBuffer, fix, fixCollection.Provider, - nestedAction, getFixAllSuggestedActionSet(nestedAction))); - - var set = new SuggestedActionSet( - nestedActions, SuggestedActionSetPriority.Medium, - fix.PrimaryDiagnostic.Location.SourceSpan.ToSpan()); - - suggestedAction = new SuggestedActionWithNestedActions( - _owner, workspace, _subjectBuffer, - fixCollection.Provider, fix.Action, set); - } - else - { - suggestedAction = new CodeFixSuggestedAction( - _owner, workspace, _subjectBuffer, fix, fixCollection.Provider, - fix.Action, getFixAllSuggestedActionSet(fix.Action)); - } - - AddFix(fix, suggestedAction, map, order); - } - } - - private static void AddFix( - CodeFix fix, SuggestedAction suggestedAction, - IDictionary> map, - ArrayBuilder order) - { - var diag = fix.GetPrimaryDiagnosticData(); - - var groupKey = new CodeFixGroupKey(diag, fix.Action.Priority); - if (!map.ContainsKey(groupKey)) - { - order.Add(groupKey); - map[groupKey] = ImmutableArray.CreateBuilder(); - } - - map[groupKey].Add(suggestedAction); - } - - /// - /// If the provided fix all context is non-null and the context's code action Id matches the given code action's Id then, - /// returns the set of fix all occurrences actions associated with the code action. - /// - internal SuggestedActionSet GetFixAllSuggestedActionSet( - CodeAction action, - int actionCount, - FixAllState fixAllState, - ImmutableArray supportedScopes, - Diagnostic firstDiagnostic, - Workspace workspace) - { - - if (fixAllState == null) - { - return null; - } - - if (actionCount > 1 && action.EquivalenceKey == null) - { - return null; - } - - var fixAllSuggestedActions = ArrayBuilder.GetInstance(); - foreach (var scope in supportedScopes) - { - var fixAllStateForScope = fixAllState.WithScopeAndEquivalenceKey(scope, action.EquivalenceKey); - var fixAllSuggestedAction = new FixAllSuggestedAction( - _owner, workspace, _subjectBuffer, fixAllStateForScope, - firstDiagnostic, action); - - fixAllSuggestedActions.Add(fixAllSuggestedAction); - } - - return new SuggestedActionSet( - fixAllSuggestedActions.ToImmutableAndFree(), - title: EditorFeaturesResources.Fix_all_occurrences_in); - } - - /// - /// Return prioritized set of fix groups such that fix group for suppression always show up at the bottom of the list. - /// - /// - /// Fix groups are returned in priority order determined based on . - /// Priority for all s containing fixes is set to by default. - /// The only exception is the case where a only contains suppression fixes - - /// the priority of such s is set to so that suppression fixes - /// always show up last after all other fixes (and refactorings) for the selected line of code. - /// - private static ImmutableArray PrioritizeFixGroups( - IDictionary> map, IList order) - { - var sets = ArrayBuilder.GetInstance(); - - foreach (var diag in order) - { - var actions = map[diag]; - - foreach (var group in actions.GroupBy(a => a.Priority)) - { - var priority = GetSuggestedActionSetPriority(group.Key); - - // diagnostic from things like build shouldn't reach here since we don't support LB for those diagnostics - Contract.Requires(diag.Item1.HasTextSpan); - sets.Add(new SuggestedActionSet(group, priority, diag.Item1.TextSpan.ToSpan())); - } - } - - return sets.ToImmutableAndFree(); - } - - private static SuggestedActionSetPriority GetSuggestedActionSetPriority(CodeActionPriority key) - { - switch (key) - { - case CodeActionPriority.None: return SuggestedActionSetPriority.None; - case CodeActionPriority.Low: return SuggestedActionSetPriority.Low; - case CodeActionPriority.Medium: return SuggestedActionSetPriority.Medium; - case CodeActionPriority.High: return SuggestedActionSetPriority.High; - default: - throw new InvalidOperationException(); - } - } - - private ImmutableArray GetRefactorings( - IDocumentSupportsFeatureService supportsFeatureService, - ISuggestedActionCategorySet requestedActionCategories, - Workspace workspace, - Document document, - SnapshotSpan range, - CancellationToken cancellationToken) - { - this.AssertIsForeground(); - - if (workspace.Options.GetOption(EditorComponentOnOffOptions.CodeRefactorings) && - _owner._codeRefactoringService != null && - supportsFeatureService.SupportsRefactorings(document) && - requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Refactoring)) - { - // Get the selection while on the UI thread. - var selection = TryGetCodeRefactoringSelection(_subjectBuffer, _textView, range); - if (!selection.HasValue) - { - // this is here to fail test and see why it is failed. - Trace.WriteLine("given range is not current"); - return ImmutableArray.Empty; - } - - // It may seem strange that we kick off a task, but then immediately 'Wait' on - // it. However, it's deliberate. We want to make sure that the code runs on - // the background so that no one takes an accidentally dependency on running on - // the UI thread. - var refactorings = Task.Run( - () => _owner._codeRefactoringService.GetRefactoringsAsync( - document, selection.Value, cancellationToken), - cancellationToken).WaitAndGetResult(cancellationToken); - - var filteredRefactorings = FilterOnUIThread(refactorings, workspace); - - return filteredRefactorings.SelectAsArray(r => OrganizeRefactorings(workspace, r)); - } - - return ImmutableArray.Empty; - } - - /// - /// Arrange refactorings into groups. - /// - /// - /// Refactorings are returned in priority order determined based on . - /// Priority for all s containing refactorings is set to - /// and should show up after fixes but before suppression fixes in the light bulb menu. - /// - private SuggestedActionSet OrganizeRefactorings(Workspace workspace, CodeRefactoring refactoring) - { - var refactoringSuggestedActions = ArrayBuilder.GetInstance(); - - foreach (var action in refactoring.Actions) - { - refactoringSuggestedActions.Add(new CodeRefactoringSuggestedAction( - _owner, workspace, _subjectBuffer, refactoring.Provider, action)); - } - - return new SuggestedActionSet( - refactoringSuggestedActions.ToImmutableAndFree(), SuggestedActionSetPriority.Low); - } - - public async Task HasSuggestedActionsAsync(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken) - { - // Explicitly hold onto below fields in locals and use these locals throughout this code path to avoid crashes - // if these fields happen to be cleared by Dispose() below. This is required since this code path involves - // code that can run asynchronously from background thread. - var view = _textView; - var buffer = _subjectBuffer; - var provider = _owner; - - if (view == null || buffer == null || provider == null) - { - return false; - } - - using (var asyncToken = provider.OperationListener.BeginAsyncOperation("HasSuggestedActionsAsync")) - { - var document = await GetMatchingDocumentAsync(range.Snapshot, cancellationToken).ConfigureAwait(false); - if (document == null) - { - // this is here to fail test and see why it is failed. - Trace.WriteLine("given range is not current"); - return false; - } - - return - await HasFixesAsync(provider, document, range, cancellationToken).ConfigureAwait(false) || - await HasRefactoringsAsync(provider, document, buffer, view, range, cancellationToken).ConfigureAwait(false); - } - } - - private async Task HasFixesAsync( - SuggestedActionsSourceProvider provider, - Document document, - SnapshotSpan range, - CancellationToken cancellationToken) - { - var workspace = document.Project.Solution.Workspace; - var supportsFeatureService = workspace.Services.GetService(); - - if (provider._codeFixService != null && - supportsFeatureService.SupportsCodeFixes(document)) - { - var result = await Task.Run( - () => provider._codeFixService.GetFirstDiagnosticWithFixAsync( - document, range.Span.ToTextSpan(), cancellationToken), - cancellationToken).ConfigureAwait(false); - - if (result.HasFix) - { - Logger.Log(FunctionId.SuggestedActions_HasSuggestedActionsAsync); - return true; - } - - if (result.PartialResult) - { - // reset solution version number so that we can raise suggested action changed event - Volatile.Write(ref _lastSolutionVersionReported, InvalidSolutionVersion); - return false; - } - } - - return false; - } - - private async Task HasRefactoringsAsync( - SuggestedActionsSourceProvider provider, - Document document, - ITextBuffer buffer, - ITextView view, - SnapshotSpan range, - CancellationToken cancellationToken) - { - var workspace = document.Project.Solution.Workspace; - var supportsFeatureService = workspace.Services.GetService(); - - if (document.Project.Solution.Options.GetOption(EditorComponentOnOffOptions.CodeRefactorings) && - provider._codeRefactoringService != null && - supportsFeatureService.SupportsRefactorings(document)) - { - TextSpan? selection = null; - if (IsForeground()) - { - // This operation needs to happen on UI thread because it needs to access textView.Selection. - selection = TryGetCodeRefactoringSelection(buffer, view, range); - } - else - { - await InvokeBelowInputPriority(() => - { - // This operation needs to happen on UI thread because it needs to access textView.Selection. - selection = TryGetCodeRefactoringSelection(buffer, view, range); - }).ConfigureAwait(false); - } - - if (!selection.HasValue) - { - // this is here to fail test and see why it is failed. - Trace.WriteLine("given range is not current"); - return false; - } - - return await Task.Run( - () => provider._codeRefactoringService.HasRefactoringsAsync( - document, selection.Value, cancellationToken), - cancellationToken).ConfigureAwait(false); - } - - return false; - } - - private static TextSpan? TryGetCodeRefactoringSelection(ITextBuffer buffer, ITextView view, SnapshotSpan range) - { - var selectedSpans = view.Selection.SelectedSpans - .SelectMany(ss => view.BufferGraph.MapDownToBuffer(ss, SpanTrackingMode.EdgeExclusive, buffer)) - .Where(ss => !view.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 static async Task GetMatchingDocumentAsync(ITextSnapshot givenSnapshot, CancellationToken cancellationToken) - { - var buffer = givenSnapshot.TextBuffer; - if (buffer == null) - { - return null; - } - - var workspace = buffer.GetWorkspace(); - if (workspace == null) - { - return null; - } - - var documentId = workspace.GetDocumentIdInCurrentContext(buffer.AsTextContainer()); - if (documentId == null) - { - return null; - } - - var document = workspace.CurrentSolution.GetDocument(documentId); - if (document == null) - { - return null; - } - - var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - - var snapshot = sourceText.FindCorrespondingEditorTextSnapshot(); - if (snapshot == null || snapshot.Version.ReiteratedVersionNumber != givenSnapshot.Version.ReiteratedVersionNumber) - { - return null; - } - - return document; - } - - private void OnTextViewClosed(object sender, EventArgs e) - { - Dispose(); - } - - private void OnWorkspaceChanged(object sender, EventArgs e) - { - // REVIEW: this event should give both old and new workspace as argument so that - // one doesn't need to hold onto workspace in field. - - // remove existing event registration - if (_workspace != null) - { - _workspace.DocumentActiveContextChanged -= OnActiveContextChanged; - } - - // REVIEW: why one need to get new workspace from registration? why not just pass in the new workspace? - // add new event registration - _workspace = _registration.Workspace; - - if (_workspace != null) - { - _workspace.DocumentActiveContextChanged += OnActiveContextChanged; - } - } - - private void OnActiveContextChanged(object sender, DocumentActiveContextChangedEventArgs e) - { - // REVIEW: it would be nice for changed event to pass in both old and new document. - OnSuggestedActionsChanged(e.Solution.Workspace, e.NewActiveContextDocumentId, e.Solution.WorkspaceVersion); - } - - private void OnDiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs e) - { - // document removed case. no reason to raise event - if (e.Solution == null) - { - return; - } - - OnSuggestedActionsChanged(e.Workspace, e.DocumentId, e.Solution.WorkspaceVersion); - } - - private void OnSuggestedActionsChanged(Workspace currentWorkspace, DocumentId currentDocumentId, int solutionVersion, DiagnosticsUpdatedArgs args = null) - { - // Explicitly hold onto the _subjectBuffer field in a local and use this local in this function to avoid crashes - // if this field happens to be cleared by Dispose() below. This is required since this code path involves code - // that can run on background thread. - var buffer = _subjectBuffer; - if (buffer == null) - { - return; - } - - var workspace = buffer.GetWorkspace(); - - // workspace is not ready, nothing to do. - if (workspace == null || workspace != currentWorkspace) - { - return; - } - - if (currentDocumentId != workspace.GetDocumentIdInCurrentContext(buffer.AsTextContainer()) || - solutionVersion == Volatile.Read(ref _lastSolutionVersionReported)) - { - return; - } - this.SuggestedActionsChanged?.Invoke(this, EventArgs.Empty); - - Volatile.Write(ref _lastSolutionVersionReported, solutionVersion); - } - - public void Dispose() - { - if (_owner != null) - { - var updateSource = (IDiagnosticUpdateSource)_owner._diagnosticService; - updateSource.DiagnosticsUpdated -= OnDiagnosticsUpdated; - _owner = null; - } - - if (_workspace != null) - { - _workspace.DocumentActiveContextChanged -= OnActiveContextChanged; - _workspace = null; - } - - if (_registration != null) - { - _registration.WorkspaceChanged -= OnWorkspaceChanged; - _registration = null; - } - - if (_textView != null) - { - _textView.Closed -= OnTextViewClosed; - _textView = null; - } - - if (_subjectBuffer != null) - { - _subjectBuffer = null; - } - } - } } } \ No newline at end of file -- GitLab