|
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CaseCorrection;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Tags;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CodeActions
{
/// <summary>
/// An action produced by a <see cref="CodeFixProvider"/> or a <see cref="CodeRefactoringProvider"/>.
/// </summary>
public abstract class CodeAction
{
/// <summary>
/// Tag we use to convey that this code action should only be shown if it's in a host that allows for
/// non-document changes. For example if it needs to make project changes, or if will show host-specific UI.
/// <para>
/// Note: if the bulk of code action is just document changes, and it does some optional things beyond that
/// (like navigating the user somewhere) this should not be set. Such a code action is still usable in all
/// hosts and should be shown to the user. It's only if the code action can truly not function should this
/// tag be provided.
/// </para>
/// <para>
/// Currently, this also means that we presume that all 3rd party code actions do not require non-document
/// changes and we will show them all in all hosts.
/// </para>
/// </summary>
internal const string RequiresNonDocumentChange = nameof(RequiresNonDocumentChange);
private protected static ImmutableArray<string> RequiresNonDocumentChangeTags = ImmutableArray.Create(RequiresNonDocumentChange);
/// <summary>
/// A short title describing the action that may appear in a menu.
/// </summary>
public abstract string Title { get; }
internal virtual string Message => Title;
/// <summary>
/// Two code actions are treated as equivalent if they have equal non-null <see cref="EquivalenceKey"/> values and were generated
/// by the same <see cref="CodeFixProvider"/> or <see cref="CodeRefactoringProvider"/>.
/// </summary>
/// <remarks>
/// Equivalence of code actions affects some Visual Studio behavior. For example, if multiple equivalent
/// code actions result from code fixes or refactorings for a single Visual Studio light bulb instance,
/// the light bulb UI will present only one code action from each set of equivalent code actions.
/// Additionally, a Fix All operation will apply only code actions that are equivalent to the original code action.
///
/// If two code actions that could be treated as equivalent do not have equal <see cref="EquivalenceKey"/> values, Visual Studio behavior
/// may be less helpful than would be optimal. If two code actions that should be treated as distinct have
/// equal <see cref="EquivalenceKey"/> values, Visual Studio behavior may appear incorrect.
/// </remarks>
public virtual string? EquivalenceKey => null;
internal virtual bool IsInlinable => false;
internal virtual CodeActionPriority Priority => CodeActionPriority.Default;
/// <summary>
/// Descriptive tags from <see cref="WellKnownTags"/>.
/// These tags may influence how the item is displayed.
/// </summary>
public virtual ImmutableArray<string> Tags => ImmutableArray<string>.Empty;
internal virtual ImmutableArray<CodeAction> NestedCodeActions
=> ImmutableArray<CodeAction>.Empty;
/// <summary>
/// Gets custom tags for the CodeAction.
/// </summary>
internal ImmutableArray<string> CustomTags { get; private set; } = ImmutableArray<string>.Empty;
/// <summary>
/// Lazily set provider type that registered this code action.
/// Used for telemetry purposes only.
/// </summary>
private Type? _providerTypeForTelemetry;
/// <summary>
/// Used by the CodeFixService and CodeRefactoringService to add the Provider Name as a CustomTag.
/// </summary>
internal void AddCustomTagAndTelemetryInfo(CodeChangeProviderMetadata? providerMetadata, object provider)
{
Contract.ThrowIfFalse(provider is CodeFixProvider or CodeRefactoringProvider);
// Add the provider name to the parent CodeAction's CustomTags.
// Always add a name even in cases of 3rd party fixers/refactorings that do not export
// name metadata.
var tag = providerMetadata?.Name ?? provider.GetTypeDisplayName();
CustomTags = CustomTags.Add(tag);
// Set the provider type to use for logging telemetry.
_providerTypeForTelemetry = provider.GetType();
}
internal Guid GetTelemetryId(FixAllScope? fixAllScope = null)
{
// We need to identify the type name to use for CodeAction's telemetry ID.
// For code actions created from 'CodeAction.Create' factory methods,
// we use the provider type for telemetry. For the rest of the code actions
// created by sub-typing CodeAction type, we use the code action type for telemetry.
// For the former case, if the provider type is not set, we fallback to the CodeAction type instead.
var isFactoryGenerated = this is SimpleCodeAction { CreatedFromFactoryMethod: true };
var type = isFactoryGenerated && _providerTypeForTelemetry != null
? _providerTypeForTelemetry
: this.GetType();
// Additionally, we also add the equivalence key and fixAllScope ID (if non-null)
// to the telemetry ID.
var scope = fixAllScope?.GetScopeIdForTelemetry() ?? 0;
return type.GetTelemetryId(scope, EquivalenceKey);
}
/// <summary>
/// The sequence of operations that define the code action.
/// </summary>
public Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync(CancellationToken cancellationToken)
=> GetOperationsAsync(originalSolution: null!, new ProgressTracker(), cancellationToken);
internal Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync(
Solution originalSolution, IProgressTracker progressTracker, CancellationToken cancellationToken)
{
return GetOperationsCoreAsync(originalSolution, progressTracker, cancellationToken);
}
/// <summary>
/// The sequence of operations that define the code action.
/// </summary>
internal virtual async Task<ImmutableArray<CodeActionOperation>> GetOperationsCoreAsync(
Solution originalSolution, IProgressTracker progressTracker, CancellationToken cancellationToken)
{
var operations = await this.ComputeOperationsAsync(progressTracker, cancellationToken).ConfigureAwait(false);
if (operations != null)
{
return await this.PostProcessAsync(originalSolution, operations, cancellationToken).ConfigureAwait(false);
}
return ImmutableArray<CodeActionOperation>.Empty;
}
/// <summary>
/// The sequence of operations used to construct a preview.
/// </summary>
public Task<ImmutableArray<CodeActionOperation>> GetPreviewOperationsAsync(CancellationToken cancellationToken)
=> GetPreviewOperationsAsync(originalSolution: null!, cancellationToken);
internal async Task<ImmutableArray<CodeActionOperation>> GetPreviewOperationsAsync(
Solution originalSolution, CancellationToken cancellationToken)
{
var operations = await this.ComputePreviewOperationsAsync(cancellationToken).ConfigureAwait(false);
if (operations != null)
{
return await this.PostProcessAsync(originalSolution, operations, cancellationToken).ConfigureAwait(false);
}
return ImmutableArray<CodeActionOperation>.Empty;
}
/// <summary>
/// Override this method if you want to implement a <see cref="CodeAction"/> subclass that includes custom <see cref="CodeActionOperation"/>'s.
/// </summary>
protected virtual async Task<IEnumerable<CodeActionOperation>> ComputeOperationsAsync(CancellationToken cancellationToken)
{
var changedSolution = await GetChangedSolutionAsync(cancellationToken).ConfigureAwait(false);
if (changedSolution == null)
{
return Array.Empty<CodeActionOperation>();
}
return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) };
}
internal virtual async Task<ImmutableArray<CodeActionOperation>> ComputeOperationsAsync(
IProgressTracker progressTracker, CancellationToken cancellationToken)
{
var operations = await ComputeOperationsAsync(cancellationToken).ConfigureAwait(false);
return operations.ToImmutableArrayOrEmpty();
}
/// <summary>
/// Override this method if you want to implement a <see cref="CodeAction"/> that has a set of preview operations that are different
/// than the operations produced by <see cref="ComputeOperationsAsync(CancellationToken)"/>.
/// </summary>
protected virtual Task<IEnumerable<CodeActionOperation>> ComputePreviewOperationsAsync(CancellationToken cancellationToken)
=> ComputeOperationsAsync(cancellationToken);
/// <summary>
/// Computes all changes for an entire solution.
/// Override this method if you want to implement a <see cref="CodeAction"/> subclass that changes more than one document.
/// </summary>
protected virtual async Task<Solution?> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
var changedDocument = await GetChangedDocumentAsync(cancellationToken).ConfigureAwait(false);
if (changedDocument == null)
{
return null;
}
return changedDocument.Project.Solution;
}
internal virtual Task<Solution?> GetChangedSolutionAsync(
IProgressTracker progressTracker, CancellationToken cancellationToken)
{
return GetChangedSolutionAsync(cancellationToken);
}
/// <summary>
/// Computes changes for a single document. Override this method if you want to implement a
/// <see cref="CodeAction"/> subclass that changes a single document.
/// </summary>
/// <remarks>
/// All code actions are expected to operate on solutions. This method is a helper to simplify the
/// implementation of <see cref="GetChangedSolutionAsync(CancellationToken)"/> for code actions that only need
/// to change one document.
/// </remarks>
/// <exception cref="NotSupportedException">If this code action does not support changing a single document.</exception>
protected virtual Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
=> throw new NotSupportedException(GetType().FullName);
/// <summary>
/// used by batch fixer engine to get new solution
/// </summary>
internal async Task<Solution?> GetChangedSolutionInternalAsync(Solution originalSolution, bool postProcessChanges = true, CancellationToken cancellationToken = default)
{
var solution = await GetChangedSolutionAsync(new ProgressTracker(), cancellationToken).ConfigureAwait(false);
if (solution == null || !postProcessChanges)
{
return solution;
}
return await this.PostProcessChangesAsync(originalSolution, solution, cancellationToken).ConfigureAwait(false);
}
internal Task<Document> GetChangedDocumentInternalAsync(CancellationToken cancellation)
=> GetChangedDocumentAsync(cancellation);
/// <summary>
/// Apply post processing steps to any <see cref="ApplyChangesOperation"/>'s.
/// </summary>
/// <param name="operations">A list of operations.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A new list of operations with post processing steps applied to any <see cref="ApplyChangesOperation"/>'s.</returns>
protected Task<ImmutableArray<CodeActionOperation>> PostProcessAsync(IEnumerable<CodeActionOperation> operations, CancellationToken cancellationToken)
=> PostProcessAsync(originalSolution: null!, operations, cancellationToken);
internal async Task<ImmutableArray<CodeActionOperation>> PostProcessAsync(
Solution originalSolution, IEnumerable<CodeActionOperation> operations, CancellationToken cancellationToken)
{
using var result = TemporaryArray<CodeActionOperation>.Empty;
foreach (var op in operations)
{
if (op is ApplyChangesOperation ac)
{
result.Add(new ApplyChangesOperation(await this.PostProcessChangesAsync(originalSolution, ac.ChangedSolution, cancellationToken).ConfigureAwait(false)));
}
else
{
result.Add(op);
}
}
return result.ToImmutableAndClear();
}
/// <summary>
/// Apply post processing steps to solution changes, like formatting and simplification.
/// </summary>
/// <param name="changedSolution">The solution changed by the <see cref="CodeAction"/>.</param>
/// <param name="cancellationToken">A cancellation token</param>
protected Task<Solution> PostProcessChangesAsync(Solution changedSolution, CancellationToken cancellationToken)
=> PostProcessChangesAsync(originalSolution: null!, changedSolution, cancellationToken);
internal async Task<Solution> PostProcessChangesAsync(
Solution originalSolution,
Solution changedSolution,
CancellationToken cancellationToken)
{
// originalSolution is only null on backward compatible codepaths. In that case, we get the workspace's
// current solution. This is not ideal (as that is a mutable field that could be changing out from
// underneath us). But it's the only option we have for the compat case with existing public extension
// points.
originalSolution ??= changedSolution.Workspace.CurrentSolution;
var solutionChanges = changedSolution.GetChanges(originalSolution);
var processedSolution = changedSolution;
// process changed projects
foreach (var projectChanges in solutionChanges.GetProjectChanges())
{
var documentsToProcess = projectChanges.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true).Concat(
projectChanges.GetAddedDocuments());
foreach (var documentId in documentsToProcess)
{
var document = processedSolution.GetRequiredDocument(documentId);
var processedDocument = await PostProcessChangesAsync(document, cancellationToken).ConfigureAwait(false);
processedSolution = processedDocument.Project.Solution;
}
}
// process completely new projects too
foreach (var addedProject in solutionChanges.GetAddedProjects())
{
var documentsToProcess = addedProject.DocumentIds;
foreach (var documentId in documentsToProcess)
{
var document = processedSolution.GetRequiredDocument(documentId);
var processedDocument = await PostProcessChangesAsync(document, cancellationToken).ConfigureAwait(false);
processedSolution = processedDocument.Project.Solution;
}
}
return processedSolution;
}
/// <summary>
/// Apply post processing steps to a single document:
/// Reducing nodes annotated with <see cref="Simplifier.Annotation"/>
/// Formatting nodes annotated with <see cref="Formatter.Annotation"/>
/// </summary>
/// <param name="document">The document changed by the <see cref="CodeAction"/>.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A document with the post processing changes applied.</returns>
protected virtual async Task<Document> PostProcessChangesAsync(Document document, CancellationToken cancellationToken)
{
if (document.SupportsSyntaxTree)
{
// TODO: avoid ILegacyGlobalCodeActionOptionsWorkspaceService https://github.com/dotnet/roslyn/issues/60777
var globalOptions = document.Project.Solution.Services.GetService<ILegacyGlobalCleanCodeGenerationOptionsWorkspaceService>();
var fallbackOptions = globalOptions?.Provider ?? CodeActionOptions.DefaultProvider;
var options = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false);
return await CleanupDocumentAsync(document, options, cancellationToken).ConfigureAwait(false);
}
return document;
}
internal static async Task<Document> CleanupDocumentAsync(
Document document, CodeCleanupOptions options, CancellationToken cancellationToken)
{
document = await ImportAdder.AddImportsFromSymbolAnnotationAsync(
document, Simplifier.AddImportsAnnotation, options.AddImportOptions, cancellationToken).ConfigureAwait(false);
document = await Simplifier.ReduceAsync(document, Simplifier.Annotation, options.SimplifierOptions, cancellationToken).ConfigureAwait(false);
// format any node with explicit formatter annotation
document = await Formatter.FormatAsync(document, Formatter.Annotation, options.FormattingOptions, cancellationToken).ConfigureAwait(false);
// format any elastic whitespace
document = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation, options.FormattingOptions, cancellationToken).ConfigureAwait(false);
document = await CaseCorrector.CaseCorrectAsync(document, CaseCorrector.Annotation, cancellationToken).ConfigureAwait(false);
return document;
}
#region Factories for standard code actions
/// <summary>
/// Creates a <see cref="CodeAction"/> for a change to a single <see cref="Document"/>.
/// Use this factory when the change is expensive to compute and should be deferred until requested.
/// </summary>
/// <param name="title">Title of the <see cref="CodeAction"/>.</param>
/// <param name="createChangedDocument">Function to create the <see cref="Document"/>.</param>
/// <param name="equivalenceKey">Optional value used to determine the equivalence of the <see cref="CodeAction"/> with other <see cref="CodeAction"/>s. See <see cref="CodeAction.EquivalenceKey"/>.</param>
[SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Preserving existing public API")]
public static CodeAction Create(string title, Func<CancellationToken, Task<Document>> createChangedDocument, string? equivalenceKey = null)
{
if (title == null)
{
throw new ArgumentNullException(nameof(title));
}
if (createChangedDocument == null)
{
throw new ArgumentNullException(nameof(createChangedDocument));
}
return DocumentChangeAction.Create(title, createChangedDocument, equivalenceKey);
}
/// <summary>
/// Creates a <see cref="CodeAction"/> for a change to more than one <see cref="Document"/> within a <see cref="Solution"/>.
/// Use this factory when the change is expensive to compute and should be deferred until requested.
/// </summary>
/// <param name="title">Title of the <see cref="CodeAction"/>.</param>
/// <param name="createChangedSolution">Function to create the <see cref="Solution"/>.</param>
/// <param name="equivalenceKey">Optional value used to determine the equivalence of the <see cref="CodeAction"/> with other <see cref="CodeAction"/>s. See <see cref="CodeAction.EquivalenceKey"/>.</param>
[SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Preserving existing public API")]
public static CodeAction Create(string title, Func<CancellationToken, Task<Solution>> createChangedSolution, string? equivalenceKey = null)
{
if (title == null)
{
throw new ArgumentNullException(nameof(title));
}
if (createChangedSolution == null)
{
throw new ArgumentNullException(nameof(createChangedSolution));
}
return SolutionChangeAction.Create(title, createChangedSolution, equivalenceKey);
}
/// <summary>
/// Creates a <see cref="CodeAction"/> representing a group of code actions.
/// </summary>
/// <param name="title">Title of the <see cref="CodeAction"/> group.</param>
/// <param name="nestedActions">The code actions within the group.</param>
/// <param name="isInlinable"><see langword="true"/> to allow inlining the members of the group into the parent;
/// otherwise, <see langword="false"/> to require that this group appear as a group with nested actions.</param>
public static CodeAction Create(string title, ImmutableArray<CodeAction> nestedActions, bool isInlinable)
=> Create(title, nestedActions, isInlinable, priority: CodeActionPriority.Default);
internal static CodeAction Create(string title, ImmutableArray<CodeAction> nestedActions, bool isInlinable, CodeActionPriority priority)
{
if (title is null)
throw new ArgumentNullException(nameof(title));
if (nestedActions == null)
throw new ArgumentNullException(nameof(nestedActions));
return CodeActionWithNestedActions.Create(title, nestedActions, isInlinable, priority);
}
internal static CodeAction CreateWithPriority(CodeActionPriority priority, string title, Func<CancellationToken, Task<Document>> createChangedDocument, string equivalenceKey)
=> DocumentChangeAction.Create(
title ?? throw new ArgumentNullException(nameof(title)),
createChangedDocument ?? throw new ArgumentNullException(nameof(createChangedDocument)),
equivalenceKey ?? throw new ArgumentNullException(nameof(equivalenceKey)),
priority);
internal static CodeAction CreateWithPriority(CodeActionPriority priority, string title, Func<CancellationToken, Task<Solution>> createChangedSolution, string equivalenceKey)
=> SolutionChangeAction.Create(
title ?? throw new ArgumentNullException(nameof(title)),
createChangedSolution ?? throw new ArgumentNullException(nameof(createChangedSolution)),
equivalenceKey ?? throw new ArgumentNullException(nameof(equivalenceKey)),
priority);
internal static CodeAction CreateWithPriority(CodeActionPriority priority, string title, ImmutableArray<CodeAction> nestedActions, bool isInlinable)
=> CodeActionWithNestedActions.Create(
title ?? throw new ArgumentNullException(nameof(title)), nestedActions, isInlinable, priority);
internal abstract class SimpleCodeAction : CodeAction
{
protected SimpleCodeAction(
string title,
string? equivalenceKey,
CodeActionPriority priority,
bool createdFromFactoryMethod)
{
Title = title;
EquivalenceKey = equivalenceKey;
Priority = priority;
CreatedFromFactoryMethod = createdFromFactoryMethod;
}
public sealed override string Title { get; }
public sealed override string? EquivalenceKey { get; }
internal sealed override CodeActionPriority Priority { get; }
/// <summary>
/// Indicates if this CodeAction was created using one of the 'CodeAction.Create' factory methods.
/// This is used in <see cref="GetTelemetryId(FixAllScope?)"/> to determine the appropriate type
/// name to log in the CodeAction telemetry.
/// </summary>
public bool CreatedFromFactoryMethod { get; }
}
internal class CodeActionWithNestedActions : SimpleCodeAction
{
private CodeActionWithNestedActions(
string title,
ImmutableArray<CodeAction> nestedActions,
bool isInlinable,
CodeActionPriority priority,
bool createdFromFactoryMethod)
: base(title, ComputeEquivalenceKey(nestedActions), priority, createdFromFactoryMethod)
{
Debug.Assert(nestedActions.Length > 0);
NestedCodeActions = nestedActions;
IsInlinable = isInlinable;
}
protected CodeActionWithNestedActions(
string title,
ImmutableArray<CodeAction> nestedActions,
bool isInlinable,
CodeActionPriority priority = CodeActionPriority.Default)
: this(title, nestedActions, isInlinable, priority, createdFromFactoryMethod: false)
{
}
public static new CodeActionWithNestedActions Create(
string title,
ImmutableArray<CodeAction> nestedActions,
bool isInlinable,
CodeActionPriority priority = CodeActionPriority.Default)
=> new(title, nestedActions, isInlinable, priority, createdFromFactoryMethod: true);
internal sealed override bool IsInlinable { get; }
internal sealed override ImmutableArray<CodeAction> NestedCodeActions { get; }
private static string? ComputeEquivalenceKey(ImmutableArray<CodeAction> nestedActions)
{
var equivalenceKey = StringBuilderPool.Allocate();
try
{
foreach (var action in nestedActions)
{
equivalenceKey.Append((action.EquivalenceKey ?? action.GetHashCode().ToString()) + ";");
}
return equivalenceKey.Length > 0 ? equivalenceKey.ToString() : null;
}
finally
{
StringBuilderPool.ReturnAndFree(equivalenceKey);
}
}
}
internal class DocumentChangeAction : SimpleCodeAction
{
private readonly Func<CancellationToken, Task<Document>> _createChangedDocument;
private DocumentChangeAction(
string title,
Func<CancellationToken, Task<Document>> createChangedDocument,
string? equivalenceKey,
CodeActionPriority priority,
bool createdFromFactoryMethod)
: base(title, equivalenceKey, priority, createdFromFactoryMethod)
{
_createChangedDocument = createChangedDocument;
}
protected DocumentChangeAction(
string title,
Func<CancellationToken, Task<Document>> createChangedDocument,
string? equivalenceKey,
CodeActionPriority priority = CodeActionPriority.Default)
: this(title, createChangedDocument, equivalenceKey, priority, createdFromFactoryMethod: false)
{
}
public static DocumentChangeAction Create(
string title,
Func<CancellationToken, Task<Document>> createChangedDocument,
string? equivalenceKey,
CodeActionPriority priority = CodeActionPriority.Default)
=> new(title, createChangedDocument, equivalenceKey, priority, createdFromFactoryMethod: true);
protected sealed override Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
=> _createChangedDocument(cancellationToken);
}
internal class SolutionChangeAction : SimpleCodeAction
{
private readonly Func<CancellationToken, Task<Solution>> _createChangedSolution;
protected SolutionChangeAction(
string title,
Func<CancellationToken, Task<Solution>> createChangedSolution,
string? equivalenceKey,
CodeActionPriority priority,
bool createdFromFactoryMethod)
: base(title, equivalenceKey, priority, createdFromFactoryMethod)
{
_createChangedSolution = createChangedSolution;
}
protected SolutionChangeAction(
string title,
Func<CancellationToken, Task<Solution>> createChangedSolution,
string? equivalenceKey,
CodeActionPriority priority = CodeActionPriority.Default)
: this(title, createChangedSolution, equivalenceKey, priority, createdFromFactoryMethod: false)
{
}
public static SolutionChangeAction Create(
string title,
Func<CancellationToken, Task<Solution>> createChangedSolution,
string? equivalenceKey,
CodeActionPriority priority = CodeActionPriority.Default)
=> new(title, createChangedSolution, equivalenceKey, priority, createdFromFactoryMethod: true);
protected sealed override Task<Solution?> GetChangedSolutionAsync(CancellationToken cancellationToken)
=> _createChangedSolution(cancellationToken).AsNullable();
}
internal sealed class NoChangeAction : SimpleCodeAction
{
private NoChangeAction(
string title,
string? equivalenceKey,
CodeActionPriority priority,
bool createdFromFactoryMethod)
: base(title, equivalenceKey, priority, createdFromFactoryMethod)
{
}
public static NoChangeAction Create(
string title,
string? equivalenceKey,
CodeActionPriority priority = CodeActionPriority.Default)
=> new(title, equivalenceKey, priority, createdFromFactoryMethod: true);
protected sealed override Task<Solution?> GetChangedSolutionAsync(CancellationToken cancellationToken)
=> SpecializedTasks.Null<Solution>();
}
#endregion
}
}
|