|
// 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.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.CodeFixesAndRefactorings;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.CodeActions.CodeAction;
using CodeFixGroupKey = System.Tuple<Microsoft.CodeAnalysis.Diagnostics.DiagnosticData, Microsoft.CodeAnalysis.CodeActions.CodeActionPriority, Microsoft.CodeAnalysis.CodeActions.CodeActionPriority?>;
namespace Microsoft.CodeAnalysis.UnifiedSuggestions
{
/// <summary>
/// Provides mutual code action logic for both local and LSP scenarios
/// via intermediate interface <see cref="IUnifiedSuggestedAction"/>.
/// </summary>
internal class UnifiedSuggestedActionsSource
{
/// <summary>
/// Gets, filters, and orders code fixes.
/// </summary>
public static async ValueTask<ImmutableArray<UnifiedSuggestedActionSet>> GetFilterAndOrderCodeFixesAsync(
Workspace workspace,
ICodeFixService codeFixService,
TextDocument document,
TextSpan selection,
ICodeActionRequestPriorityProvider priorityProvider,
CancellationToken cancellationToken)
{
var originalSolution = document.Project.Solution;
// Intentionally switch to a threadpool thread to compute fixes. We do not want to accidentally run any of
// this on the UI thread and potentially allow any code to take a dependency on that.
await TaskScheduler.Default;
var fixes = await codeFixService.GetFixesAsync(
document,
selection,
priorityProvider,
cancellationToken).ConfigureAwait(false);
var filteredFixes = fixes.WhereAsArray(c => c.Fixes.Length > 0);
var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
var organizedFixes = await OrganizeFixesAsync(workspace, originalSolution, text, filteredFixes, cancellationToken).ConfigureAwait(false);
return organizedFixes;
}
/// <summary>
/// Arrange fixes into groups based on the issue (diagnostic being fixed) and prioritize these groups.
/// </summary>
private static async Task<ImmutableArray<UnifiedSuggestedActionSet>> OrganizeFixesAsync(
Workspace workspace,
Solution originalSolution,
SourceText text,
ImmutableArray<CodeFixCollection> fixCollections,
CancellationToken cancellationToken)
{
var map = ImmutableDictionary.CreateBuilder<CodeFixGroupKey, IList<IUnifiedSuggestedAction>>();
using var _ = ArrayBuilder<CodeFixGroupKey>.GetInstance(out var order);
// First group fixes by diagnostic and priority.
await GroupFixesAsync(workspace, originalSolution, fixCollections, map, order, cancellationToken).ConfigureAwait(false);
// Then prioritize between the groups.
var prioritizedFixes = PrioritizeFixGroups(originalSolution, text, map.ToImmutable(), order.ToImmutable(), workspace);
return prioritizedFixes;
}
/// <summary>
/// Groups fixes by the diagnostic being addressed by each fix.
/// </summary>
private static async Task GroupFixesAsync(
Workspace workspace,
Solution originalSolution,
ImmutableArray<CodeFixCollection> fixCollections,
IDictionary<CodeFixGroupKey, IList<IUnifiedSuggestedAction>> map,
ArrayBuilder<CodeFixGroupKey> order,
CancellationToken cancellationToken)
{
foreach (var fixCollection in fixCollections)
await ProcessFixCollectionAsync(workspace, originalSolution, map, order, fixCollection, cancellationToken).ConfigureAwait(false);
}
private static async Task ProcessFixCollectionAsync(
Workspace workspace,
Solution originalSolution,
IDictionary<CodeFixGroupKey, IList<IUnifiedSuggestedAction>> map,
ArrayBuilder<CodeFixGroupKey> order,
CodeFixCollection fixCollection,
CancellationToken cancellationToken)
{
var fixes = fixCollection.Fixes;
var fixCount = fixes.Length;
var nonSupressionCodeFixes = fixes.WhereAsArray(f => !IsTopLevelSuppressionAction(f.Action));
var supressionCodeFixes = fixes.WhereAsArray(f => IsTopLevelSuppressionAction(f.Action));
await AddCodeActionsAsync(workspace, originalSolution, map, order, fixCollection, GetFixAllSuggestedActionSetAsync, nonSupressionCodeFixes).ConfigureAwait(false);
// Add suppression fixes to the end of a given SuggestedActionSet so that they
// always show up last in a group.
await AddCodeActionsAsync(workspace, originalSolution, map, order, fixCollection, GetFixAllSuggestedActionSetAsync, supressionCodeFixes).ConfigureAwait(false);
return;
// Local functions
Task<UnifiedSuggestedActionSet?> GetFixAllSuggestedActionSetAsync(CodeAction codeAction)
=> GetUnifiedFixAllSuggestedActionSetAsync(
codeAction, fixCount, fixCollection.FixAllState,
fixCollection.SupportedScopes, fixCollection.FirstDiagnostic,
workspace, originalSolution, cancellationToken);
}
private static async Task AddCodeActionsAsync(
Workspace workspace,
Solution originalSolution,
IDictionary<CodeFixGroupKey, IList<IUnifiedSuggestedAction>> map,
ArrayBuilder<CodeFixGroupKey> order,
CodeFixCollection fixCollection,
Func<CodeAction, Task<UnifiedSuggestedActionSet?>> getFixAllSuggestedActionSetAsync,
ImmutableArray<CodeFix> codeFixes)
{
foreach (var fix in codeFixes)
{
var unifiedSuggestedAction = await GetUnifiedSuggestedActionAsync(originalSolution, fix.Action, fix).ConfigureAwait(false);
AddFix(fix, unifiedSuggestedAction, map, order);
}
return;
// Local functions
async Task<IUnifiedSuggestedAction> GetUnifiedSuggestedActionAsync(Solution originalSolution, CodeAction action, CodeFix fix)
{
if (action.NestedActions.Length > 0)
{
var unifiedNestedActions = new FixedSizeArrayBuilder<IUnifiedSuggestedAction>(action.NestedActions.Length);
foreach (var nestedAction in action.NestedActions)
{
var unifiedNestedAction = await GetUnifiedSuggestedActionAsync(originalSolution, nestedAction, fix).ConfigureAwait(false);
unifiedNestedActions.Add(unifiedNestedAction);
}
var set = new UnifiedSuggestedActionSet(
originalSolution,
categoryName: null,
actions: unifiedNestedActions.MoveToImmutable(),
title: null,
priority: action.Priority,
applicableToSpan: fix.PrimaryDiagnostic.Location.SourceSpan);
return new UnifiedSuggestedActionWithNestedActions(
workspace, action, action.Priority, fixCollection.Provider, [set]);
}
else
{
return new UnifiedCodeFixSuggestedAction(
workspace, action, action.Priority, fix, fixCollection.Provider,
await getFixAllSuggestedActionSetAsync(action).ConfigureAwait(false));
}
}
}
private static void AddFix(
CodeFix fix, IUnifiedSuggestedAction suggestedAction,
IDictionary<CodeFixGroupKey, IList<IUnifiedSuggestedAction>> map,
ArrayBuilder<CodeFixGroupKey> order)
{
var groupKey = GetGroupKey(fix);
if (!map.TryGetValue(groupKey, out var suggestedActions))
{
order.Add(groupKey);
suggestedActions = ImmutableArray.CreateBuilder<IUnifiedSuggestedAction>();
map[groupKey] = suggestedActions;
}
suggestedActions.Add(suggestedAction);
return;
static CodeFixGroupKey GetGroupKey(CodeFix fix)
{
var diag = fix.GetPrimaryDiagnosticData();
if (fix.Action is AbstractConfigurationActionWithNestedActions configurationAction)
{
return new CodeFixGroupKey(
diag, configurationAction.Priority, configurationAction.AdditionalPriority);
}
return new CodeFixGroupKey(diag, fix.Action.Priority, null);
}
}
// If the provided fix all context is non-null and the context's code action Id matches
// the given code action's Id, returns the set of fix all occurrences actions associated
// with the code action.
private static async Task<UnifiedSuggestedActionSet?> GetUnifiedFixAllSuggestedActionSetAsync(
CodeAction action,
int actionCount,
IFixAllState fixAllState,
ImmutableArray<FixAllScope> supportedScopes,
Diagnostic firstDiagnostic,
Workspace workspace,
Solution originalSolution,
CancellationToken cancellationToken)
{
if (fixAllState == null)
{
return null;
}
if (actionCount > 1 && action.EquivalenceKey == null)
{
return null;
}
var textDocument = fixAllState.Document!;
using var fixAllSuggestedActionsDisposer = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var fixAllSuggestedActions);
foreach (var scope in supportedScopes)
{
if (scope is FixAllScope.ContainingMember or FixAllScope.ContainingType)
{
if (textDocument is not Document document)
continue;
// Skip showing ContainingMember and ContainingType FixAll scopes if the language
// does not implement 'IFixAllSpanMappingService' langauge service or
// we have no mapped FixAll spans to fix.
var spanMappingService = document.GetLanguageService<IFixAllSpanMappingService>();
if (spanMappingService is null)
continue;
var documentsAndSpans = await spanMappingService.GetFixAllSpansAsync(
document, firstDiagnostic.Location.SourceSpan, scope, cancellationToken).ConfigureAwait(false);
if (documentsAndSpans.IsEmpty)
continue;
}
var fixAllStateForScope = fixAllState.With(scope: scope, codeActionEquivalenceKey: action.EquivalenceKey);
var fixAllSuggestedAction = new UnifiedFixAllCodeFixSuggestedAction(
workspace, action, action.Priority, fixAllStateForScope, firstDiagnostic);
fixAllSuggestedActions.Add(fixAllSuggestedAction);
}
return new UnifiedSuggestedActionSet(
originalSolution,
categoryName: null,
actions: fixAllSuggestedActions.ToImmutable(),
title: CodeFixesResources.Fix_all_occurrences_in,
priority: CodeActionPriority.Lowest,
applicableToSpan: null);
}
/// <summary>
/// Return prioritized set of fix groups such that fix group for suppression always show up at the bottom of the list.
/// </summary>
/// <remarks>
/// Fix groups are returned in priority order determined based on <see cref="ExtensionOrderAttribute"/>.
/// Priority for all <see cref="UnifiedSuggestedActionSet"/>s containing fixes is set to <see
/// cref="CodeActionPriority.Default"/> by default. The only exception is the case where a <see
/// cref="UnifiedSuggestedActionSet"/> only contains suppression fixes - the priority of such <see
/// cref="UnifiedSuggestedActionSet"/>s is set to <see cref="CodeActionPriority.Lowest"/> so that suppression
/// fixes always show up last after all other fixes (and refactorings) for the selected line of code.
/// </remarks>
private static ImmutableArray<UnifiedSuggestedActionSet> PrioritizeFixGroups(
Solution originalSolution,
SourceText text,
ImmutableDictionary<CodeFixGroupKey, IList<IUnifiedSuggestedAction>> map,
ImmutableArray<CodeFixGroupKey> order,
Workspace workspace)
{
using var _1 = ArrayBuilder<UnifiedSuggestedActionSet>.GetInstance(out var nonSuppressionSets);
using var _2 = ArrayBuilder<UnifiedSuggestedActionSet>.GetInstance(out var suppressionSets);
using var _3 = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var bulkConfigurationActions);
foreach (var groupKey in order)
{
var actions = map[groupKey];
var nonSuppressionActions = actions.Where(a => !IsTopLevelSuppressionAction(a.OriginalCodeAction)).ToImmutableArray();
AddUnifiedSuggestedActionsSet(originalSolution, text, nonSuppressionActions, groupKey, nonSuppressionSets);
var suppressionActions = actions.Where(a => IsTopLevelSuppressionAction(a.OriginalCodeAction) &&
!IsBulkConfigurationAction(a.OriginalCodeAction)).ToImmutableArray();
AddUnifiedSuggestedActionsSet(originalSolution, text, suppressionActions, groupKey, suppressionSets);
bulkConfigurationActions.AddRange(actions.Where(a => IsBulkConfigurationAction(a.OriginalCodeAction)));
}
var sets = nonSuppressionSets.ToImmutable();
// Append bulk configuration fixes at the end of suppression/configuration fixes.
if (bulkConfigurationActions.Count > 0)
{
var bulkConfigurationSet = new UnifiedSuggestedActionSet(
originalSolution,
UnifiedPredefinedSuggestedActionCategoryNames.CodeFix,
bulkConfigurationActions.ToImmutable(),
title: null,
priority: CodeActionPriority.Lowest,
applicableToSpan: null);
suppressionSets.Add(bulkConfigurationSet);
}
if (suppressionSets.Count > 0)
{
// Wrap the suppression/configuration actions within another top level suggested action
// to avoid clutter in the light bulb menu.
var suppressOrConfigureCodeAction = NoChangeAction.Create(CodeFixesResources.Suppress_or_configure_issues, nameof(CodeFixesResources.Suppress_or_configure_issues));
var wrappingSuggestedAction = new UnifiedSuggestedActionWithNestedActions(
workspace, codeAction: suppressOrConfigureCodeAction,
codeActionPriority: suppressOrConfigureCodeAction.Priority, provider: null,
nestedActionSets: suppressionSets.ToImmutable());
// Combine the spans and the category of each of the nested suggested actions
// to get the span and category for the new top level suggested action.
var (span, category) = CombineSpansAndCategory(suppressionSets);
var wrappingSet = new UnifiedSuggestedActionSet(
originalSolution,
category,
actions: [wrappingSuggestedAction],
title: CodeFixesResources.Suppress_or_configure_issues,
priority: CodeActionPriority.Lowest,
applicableToSpan: span);
sets = sets.Add(wrappingSet);
}
return sets;
// Local functions
static (TextSpan? span, string category) CombineSpansAndCategory(ArrayBuilder<UnifiedSuggestedActionSet> sets)
{
// We are combining the spans and categories of the given set of suggested action sets
// to generate a result span containing the spans of individual suggested action sets and
// a result category which is the maximum severity category amongst the set
var minStart = -1;
var maxEnd = -1;
var category = UnifiedPredefinedSuggestedActionCategoryNames.CodeFix;
foreach (var set in sets)
{
if (set.ApplicableToSpan.HasValue)
{
var currentStart = set.ApplicableToSpan.Value.Start;
var currentEnd = set.ApplicableToSpan.Value.End;
if (minStart == -1 || currentStart < minStart)
{
minStart = currentStart;
}
if (maxEnd == -1 || currentEnd > maxEnd)
{
maxEnd = currentEnd;
}
}
Debug.Assert(set.CategoryName is UnifiedPredefinedSuggestedActionCategoryNames.CodeFix or
UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix);
// If this set contains an error fix, then change the result category to ErrorFix
if (set.CategoryName == UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix)
{
category = UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix;
}
}
var combinedSpan = minStart >= 0 ? TextSpan.FromBounds(minStart, maxEnd) : (TextSpan?)null;
return (combinedSpan, category);
}
}
private static void AddUnifiedSuggestedActionsSet(
Solution originalSolution,
SourceText text,
ImmutableArray<IUnifiedSuggestedAction> actions,
CodeFixGroupKey groupKey,
ArrayBuilder<UnifiedSuggestedActionSet> sets)
{
foreach (var group in actions.GroupBy(a => a.CodeActionPriority))
{
var priority = group.Key;
// diagnostic from things like build shouldn't reach here since we don't support LB for those diagnostics
var category = GetFixCategory(groupKey.Item1.Severity);
sets.Add(new UnifiedSuggestedActionSet(
originalSolution,
category,
[.. group],
title: null,
priority,
applicableToSpan: groupKey.Item1.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text)));
}
}
private static string GetFixCategory(DiagnosticSeverity severity)
{
switch (severity)
{
case DiagnosticSeverity.Hidden:
case DiagnosticSeverity.Info:
case DiagnosticSeverity.Warning:
return UnifiedPredefinedSuggestedActionCategoryNames.CodeFix;
case DiagnosticSeverity.Error:
return UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix;
default:
throw ExceptionUtilities.Unreachable();
}
}
private static bool IsTopLevelSuppressionAction(CodeAction action)
=> action is AbstractConfigurationActionWithNestedActions;
private static bool IsBulkConfigurationAction(CodeAction action)
=> (action as AbstractConfigurationActionWithNestedActions)?.IsBulkConfigurationAction == true;
/// <summary>
/// Gets, filters, and orders code refactorings.
/// </summary>
public static async Task<ImmutableArray<UnifiedSuggestedActionSet>> GetFilterAndOrderCodeRefactoringsAsync(
Workspace workspace,
ICodeRefactoringService codeRefactoringService,
TextDocument document,
TextSpan selection,
CodeActionRequestPriority? priority,
bool filterOutsideSelection,
CancellationToken cancellationToken)
{
// Intentionally switch to a threadpool thread to compute fixes. We do not want to accidentally run any of
// this on the UI thread and potentially allow any code to take a dependency on that.
await TaskScheduler.Default;
var refactorings = await codeRefactoringService.GetRefactoringsAsync(
document, selection, priority,
cancellationToken).ConfigureAwait(false);
var filteredRefactorings = FilterOnAnyThread(refactorings, selection, filterOutsideSelection);
var orderedRefactorings = new FixedSizeArrayBuilder<UnifiedSuggestedActionSet>(filteredRefactorings.Length);
foreach (var refactoring in filteredRefactorings)
{
var orderedRefactoring = await OrganizeRefactoringsAsync(workspace, document, selection, refactoring, cancellationToken).ConfigureAwait(false);
orderedRefactorings.Add(orderedRefactoring);
}
return orderedRefactorings.MoveToImmutable();
}
private static ImmutableArray<CodeRefactoring> FilterOnAnyThread(
ImmutableArray<CodeRefactoring> refactorings,
TextSpan selection,
bool filterOutsideSelection)
=> refactorings.Select(r => FilterOnAnyThread(r, selection, filterOutsideSelection)).WhereNotNull().ToImmutableArray();
private static CodeRefactoring? FilterOnAnyThread(
CodeRefactoring refactoring,
TextSpan selection,
bool filterOutsideSelection)
{
var actions = refactoring.CodeActions.WhereAsArray(IsActionAndSpanApplicable);
return actions.Length == 0
? null
: actions.Length == refactoring.CodeActions.Length
? refactoring
: new CodeRefactoring(refactoring.Provider, actions, refactoring.FixAllProviderInfo);
bool IsActionAndSpanApplicable((CodeAction action, TextSpan? applicableSpan) actionAndSpan)
{
if (filterOutsideSelection)
{
// Filter out refactorings with applicable span outside the selection span.
if (!actionAndSpan.applicableSpan.HasValue ||
!selection.IntersectsWith(actionAndSpan.applicableSpan.Value))
{
return false;
}
}
return true;
}
}
/// <summary>
/// Arrange refactorings into groups.
/// </summary>
/// <remarks>
/// Refactorings are returned in priority order determined based on <see cref="ExtensionOrderAttribute"/>.
/// Priority for all <see cref="UnifiedSuggestedActionSet"/>s containing refactorings is set to
/// <see cref="CodeActionPriority.Low"/> and should show up after fixes but before
/// suppression fixes in the light bulb menu.
/// </remarks>
private static async Task<UnifiedSuggestedActionSet> OrganizeRefactoringsAsync(
Workspace workspace,
TextDocument document,
TextSpan selection,
CodeRefactoring refactoring,
CancellationToken cancellationToken)
{
var originalSolution = document.Project.Solution;
using var _ = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var refactoringSuggestedActions);
foreach (var (action, applicableToSpan) in refactoring.CodeActions)
{
var unifiedActionSet = await GetUnifiedSuggestedActionSetAsync(action, applicableToSpan, selection, cancellationToken).ConfigureAwait(false);
refactoringSuggestedActions.Add(unifiedActionSet);
}
var actions = refactoringSuggestedActions.ToImmutable();
// An action set:
// - gets the the same priority as the highest priority action within in.
// - gets `applicableToSpan` of the first action:
// - E.g. the `applicableToSpan` closest to current selection might be a more correct
// choice. All actions created by one Refactoring have usually the same `applicableSpan`
// and therefore the complexity of determining the closest one isn't worth the benefit
// of slightly more correct orderings in certain edge cases.
return new UnifiedSuggestedActionSet(
originalSolution,
UnifiedPredefinedSuggestedActionCategoryNames.Refactoring,
actions: actions,
title: null,
priority: actions.Max(a => a.CodeActionPriority),
applicableToSpan: refactoring.CodeActions.FirstOrDefault().applicableToSpan);
// Local functions
async Task<IUnifiedSuggestedAction> GetUnifiedSuggestedActionSetAsync(CodeAction codeAction, TextSpan? applicableToSpan, TextSpan selection, CancellationToken cancellationToken)
{
if (codeAction.NestedActions.Length > 0)
{
var nestedActions = new FixedSizeArrayBuilder<IUnifiedSuggestedAction>(codeAction.NestedActions.Length);
foreach (var nestedAction in codeAction.NestedActions)
{
var unifiedAction = await GetUnifiedSuggestedActionSetAsync(nestedAction, applicableToSpan, selection, cancellationToken).ConfigureAwait(false);
nestedActions.Add(unifiedAction);
}
var set = new UnifiedSuggestedActionSet(
originalSolution,
categoryName: null,
actions: nestedActions.MoveToImmutable(),
title: null,
priority: codeAction.Priority,
applicableToSpan: applicableToSpan);
return new UnifiedSuggestedActionWithNestedActions(
workspace, codeAction, codeAction.Priority, refactoring.Provider, [set]);
}
else
{
var fixAllSuggestedActionSet = await GetUnifiedFixAllSuggestedActionSetAsync(codeAction,
refactoring.CodeActions.Length, document as Document, selection, refactoring.Provider,
refactoring.FixAllProviderInfo,
workspace, cancellationToken).ConfigureAwait(false);
return new UnifiedCodeRefactoringSuggestedAction(
workspace, codeAction, codeAction.Priority, refactoring.Provider, fixAllSuggestedActionSet);
}
}
}
// If the provided fix all context is non-null and the context's code action Id matches
// the given code action's Id, returns the set of fix all occurrences actions associated
// with the code action.
private static async Task<UnifiedSuggestedActionSet?> GetUnifiedFixAllSuggestedActionSetAsync(
CodeAction action,
int actionCount,
Document? document,
TextSpan selection,
CodeRefactoringProvider provider,
FixAllProviderInfo? fixAllProviderInfo,
Workspace workspace,
CancellationToken cancellationToken)
{
if (fixAllProviderInfo == null || document == null)
{
return null;
}
// If the provider registered more than one code action, but provided a null equivalence key
// we have no way to distinguish between which registered actions to apply or ignore for FixAll.
// So, we just bail out for this case.
if (actionCount > 1 && action.EquivalenceKey == null)
{
return null;
}
var originalSolution = document.Project.Solution;
using var fixAllSuggestedActionsDisposer = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var fixAllSuggestedActions);
foreach (var scope in fixAllProviderInfo.SupportedScopes)
{
var fixAllState = new CodeRefactorings.FixAllState(
(CodeRefactorings.FixAllProvider)fixAllProviderInfo.FixAllProvider,
document, selection, provider, scope, action);
if (scope is FixAllScope.ContainingMember or FixAllScope.ContainingType)
{
// Skip showing ContainingMember and ContainingType FixAll scopes if the language
// does not implement 'IFixAllSpanMappingService' langauge service or
// we have no mapped FixAll spans to fix.
var documentsAndSpans = await fixAllState.GetFixAllSpansAsync(cancellationToken).ConfigureAwait(false);
if (documentsAndSpans.IsEmpty)
continue;
}
var fixAllSuggestedAction = new UnifiedFixAllCodeRefactoringSuggestedAction(
workspace, action, action.Priority, fixAllState);
fixAllSuggestedActions.Add(fixAllSuggestedAction);
}
return new UnifiedSuggestedActionSet(
originalSolution,
categoryName: null,
actions: fixAllSuggestedActions.ToImmutable(),
title: CodeFixesResources.Fix_all_occurrences_in,
priority: CodeActionPriority.Lowest,
applicableToSpan: null);
}
/// <summary>
/// Filters and orders the code fix sets and code refactoring sets amongst each other.
/// Should be called with the results from <see cref="GetFilterAndOrderCodeFixesAsync"/>
/// and <see cref="GetFilterAndOrderCodeRefactoringsAsync"/>.
/// </summary>
public static ImmutableArray<UnifiedSuggestedActionSet> FilterAndOrderActionSets(
ImmutableArray<UnifiedSuggestedActionSet> fixes,
ImmutableArray<UnifiedSuggestedActionSet> refactorings,
TextSpan? selectionOpt,
int currentActionCount)
{
// Get the initial set of action sets, with refactorings and fixes appropriately
// ordered against each other.
var result = GetInitiallyOrderedActionSets(selectionOpt, fixes, refactorings);
if (result.IsEmpty)
return [];
// Now that we have the entire set of action sets, inline, sort and filter
// them appropriately against each other.
var allActionSets = InlineActionSetsIfDesirable(result, currentActionCount);
var orderedActionSets = OrderActionSets(allActionSets, selectionOpt);
var filteredSets = FilterActionSetsByTitle(orderedActionSets);
return filteredSets;
}
private static ImmutableArray<UnifiedSuggestedActionSet> GetInitiallyOrderedActionSets(
TextSpan? selectionOpt,
ImmutableArray<UnifiedSuggestedActionSet> fixes,
ImmutableArray<UnifiedSuggestedActionSet> refactorings)
{
// First, order refactorings based on the order the providers actually gave for
// their actions. This way, a low pri refactoring always shows after a medium pri
// refactoring, no matter what we do below.
refactorings = OrderActionSets(refactorings, selectionOpt);
// If there's a selection, it's likely the user is trying to perform some operation
// directly on that operation (like 'extract method'). Prioritize refactorings over
// fixes in that case. Otherwise, it's likely that the user is just on some error
// and wants to fix it (in which case, prioritize fixes).
if (selectionOpt?.Length > 0)
{
// There was a selection. Treat refactorings as more important than fixes.
// Note: we still will sort after this. So any high pri fixes will come to the
// front. Any low-pri refactorings will go to the end.
return refactorings.Concat(fixes);
}
else
{
// No selection. Treat all medium and low pri refactorings as low priority, and
// place after fixes. Even a low pri fixes will be above what was *originally*
// a medium pri refactoring.
//
// Note: we do not do this for *high* pri refactorings (like 'rename'). These
// are still very important and need to stay at the top (though still after high
// pri fixes).
var highPriRefactorings = refactorings.WhereAsArray(
s => s.Priority == CodeActionPriority.High);
var nonHighPriRefactorings = refactorings.WhereAsArray(
s => s.Priority != CodeActionPriority.High)
.SelectAsArray(s => WithPriority(s, CodeActionPriority.Low));
var highPriFixes = fixes.WhereAsArray(s => s.Priority == CodeActionPriority.High);
var nonHighPriFixes = fixes.WhereAsArray(s => s.Priority != CodeActionPriority.High);
return highPriFixes.Concat(highPriRefactorings).Concat(nonHighPriFixes).Concat(nonHighPriRefactorings);
}
}
private static ImmutableArray<UnifiedSuggestedActionSet> OrderActionSets(
ImmutableArray<UnifiedSuggestedActionSet> actionSets, TextSpan? selectionOpt)
{
return [.. actionSets.OrderByDescending(s => s.Priority).ThenBy(s => s, new UnifiedSuggestedActionSetComparer(selectionOpt))];
}
private static UnifiedSuggestedActionSet WithPriority(
UnifiedSuggestedActionSet set, CodeActionPriority priority)
=> new(set.OriginalSolution, set.CategoryName, set.Actions, set.Title, priority, set.ApplicableToSpan);
private static ImmutableArray<UnifiedSuggestedActionSet> InlineActionSetsIfDesirable(
ImmutableArray<UnifiedSuggestedActionSet> actionSets,
int currentActionCount)
{
// 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).
return currentActionCount + actionSets.Sum(a => a.Actions.Count()) > 3
? actionSets
: actionSets.SelectAsArray(InlineActions);
}
private static UnifiedSuggestedActionSet InlineActions(UnifiedSuggestedActionSet actionSet)
{
using var newActionsDisposer = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var newActions);
foreach (var action in actionSet.Actions)
{
var actionWithNestedActions = action as UnifiedSuggestedActionWithNestedActions;
// Only inline if the underlying code action allows it.
if (actionWithNestedActions?.OriginalCodeAction.IsInlinable == true)
{
newActions.AddRange(actionWithNestedActions.NestedActionSets.SelectMany(set => set.Actions));
}
else
{
newActions.Add(action);
}
}
return new UnifiedSuggestedActionSet(
actionSet.OriginalSolution,
actionSet.CategoryName,
newActions.ToImmutable(),
actionSet.Title,
actionSet.Priority,
actionSet.ApplicableToSpan);
}
private static ImmutableArray<UnifiedSuggestedActionSet> FilterActionSetsByTitle(
ImmutableArray<UnifiedSuggestedActionSet> allActionSets)
{
using var resultDisposer = ArrayBuilder<UnifiedSuggestedActionSet>.GetInstance(out var result);
var seenTitles = new HashSet<string>();
foreach (var set in allActionSets)
{
var filteredSet = FilterActionSetByTitle(set, seenTitles);
if (filteredSet != null)
{
result.Add(filteredSet);
}
}
return result.ToImmutableAndClear();
}
private static UnifiedSuggestedActionSet? FilterActionSetByTitle(UnifiedSuggestedActionSet set, HashSet<string> seenTitles)
{
using var actionsDisposer = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var actions);
foreach (var action in set.Actions)
{
if (seenTitles.Add(action.OriginalCodeAction.Title))
{
actions.Add(action);
}
}
return actions.Count == 0
? null
: new UnifiedSuggestedActionSet(set.OriginalSolution, set.CategoryName, actions.ToImmutable(), set.Title, set.Priority, set.ApplicableToSpan);
}
}
}
|