diff --git a/eng/targets/Settings.props b/eng/targets/Settings.props
index f349d270ffa63..126846e38f04b 100644
--- a/eng/targets/Settings.props
+++ b/eng/targets/Settings.props
@@ -70,7 +70,8 @@
002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293
002400000c800000940000000602000000240000525341310004000001000100e1290d741888d13312c0cd1f72bb843236573c80158a286f11bb98de5ee8acc3142c9c97b472684e521ae45125d7414558f2e70ac56504f3e8fe80830da2cdb1cda8504e8d196150d05a214609234694ec0ebf4b37fc7537e09d877c3e65000f7467fa3adb6e62c82b10ada1af4a83651556c7d949959817fed97480839dd39b
0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb
- 0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9
+ 0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9
+ 002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293
$(VisualStudioKey)
$(VisualStudioKey)
$(VisualStudioKey)
diff --git a/src/EditorFeatures/CSharpTest/Intents/GenerateConstructorIntentTests.cs b/src/EditorFeatures/CSharpTest/Intents/GenerateConstructorIntentTests.cs
new file mode 100644
index 0000000000000..72a336cf782bb
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/Intents/GenerateConstructorIntentTests.cs
@@ -0,0 +1,205 @@
+// 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.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp.CodeStyle;
+using Microsoft.CodeAnalysis.Editor.UnitTests;
+using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
+using Microsoft.CodeAnalysis.Editor.UnitTests.Extensions;
+using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
+using Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api;
+using Microsoft.CodeAnalysis.Features.Intents;
+using Microsoft.CodeAnalysis.Test.Utilities;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis.Text.Shared.Extensions;
+using Microsoft.VisualStudio.Text;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Intents
+{
+ [UseExportProvider]
+ public class GenerateConstructorIntentTests
+ {
+ [Fact]
+ public async Task GenerateConstructorSimpleResult()
+ {
+ var initialText =
+@"class C
+{
+ private readonly int _someInt;
+
+ {|typed:public C|}
+}";
+ var expectedText =
+@"class C
+{
+ private readonly int _someInt;
+
+ public C(int someInt)
+ {
+ _someInt = someInt;
+ }
+}";
+
+ await VerifyExpectedTextAsync(initialText, expectedText).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async Task GenerateConstructorTypedPrivate()
+ {
+ var initialText =
+@"class C
+{
+ private readonly int _someInt;
+
+ {|typed:private C|}
+}";
+ var expectedText =
+@"class C
+{
+ private readonly int _someInt;
+
+ public C(int someInt)
+ {
+ _someInt = someInt;
+ }
+}";
+
+ await VerifyExpectedTextAsync(initialText, expectedText).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async Task GenerateConstructorWithFieldsInPartial()
+ {
+ var initialText =
+@"partial class C
+{
+ {|typed:public C|}
+}";
+ var additionalDocuments = new string[]
+ {
+@"partial class C
+{
+ private readonly int _someInt;
+}"
+ };
+ var expectedText =
+@"partial class C
+{
+ public C(int someInt)
+ {
+ _someInt = someInt;
+ }
+}";
+
+ await VerifyExpectedTextAsync(initialText, additionalDocuments, expectedText).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async Task GenerateConstructorWithReferenceType()
+ {
+ var initialText =
+@"class C
+{
+ private readonly object _someObject;
+
+ {|typed:public C|}
+}";
+ var expectedText =
+@"class C
+{
+ private readonly object _someObject;
+
+ public C(object someObject)
+ {
+ _someObject = someObject;
+ }
+}";
+
+ await VerifyExpectedTextAsync(initialText, expectedText).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async Task GenerateConstructorWithExpressionBodyOption()
+ {
+ var initialText =
+@"class C
+{
+ private readonly int _someInt;
+
+ {|typed:public C|}
+}";
+ var expectedText =
+@"class C
+{
+ private readonly int _someInt;
+
+ public C(int someInt) => _someInt = someInt;
+}";
+
+ await VerifyExpectedTextAsync(initialText, expectedText,
+ options: new OptionsCollection(LanguageNames.CSharp)
+ {
+ { CSharpCodeStyleOptions.PreferExpressionBodiedConstructors, CSharpCodeStyleOptions.WhenPossibleWithSilentEnforcement }
+ }).ConfigureAwait(false);
+ }
+
+ private static Task VerifyExpectedTextAsync(string markup, string expectedText, OptionsCollection? options = null)
+ {
+ return VerifyExpectedTextAsync(markup, new string[] { }, expectedText, options);
+ }
+
+ private static async Task VerifyExpectedTextAsync(string activeDocument, string[] additionalDocuments, string expectedText, OptionsCollection? options = null)
+ {
+ var documentSet = additionalDocuments.Prepend(activeDocument).ToArray();
+ using var workspace = TestWorkspace.CreateCSharp(documentSet, exportProvider: EditorTestCompositions.EditorFeatures.ExportProviderFactory.CreateExportProvider());
+ if (options != null)
+ {
+ workspace.ApplyOptions(options!);
+ }
+
+ var intentSource = workspace.ExportProvider.GetExportedValue();
+
+ // The first document will be the active document.
+ var document = workspace.Documents.Single(d => d.Name == "test1.cs");
+ var textBuffer = document.GetTextBuffer();
+ var annotatedSpan = document.AnnotatedSpans["typed"].Single();
+
+ // Get the current snapshot span and selection.
+ var currentSelectedSpan = document.SelectedSpans.FirstOrDefault();
+ if (currentSelectedSpan.IsEmpty)
+ {
+ currentSelectedSpan = TextSpan.FromBounds(annotatedSpan.End, annotatedSpan.End);
+ }
+
+ var currentSnapshotSpan = new SnapshotSpan(textBuffer.CurrentSnapshot, currentSelectedSpan.ToSpan());
+
+ // Determine the edits to rewind to the prior snapshot by removing the changes in the annotated span.
+ var rewindTextChange = new TextChange(annotatedSpan, "");
+
+ var intentContext = new IntentRequestContext(
+ WellKnownIntents.GenerateConstructor,
+ currentSnapshotSpan,
+ ImmutableArray.Create(rewindTextChange),
+ TextSpan.FromBounds(rewindTextChange.Span.Start, rewindTextChange.Span.Start),
+ intentData: null);
+ var results = await intentSource.ComputeIntentsAsync(intentContext, CancellationToken.None).ConfigureAwait(false);
+
+ // For now, we're just taking the first result to match intellicode behavior.
+ var result = results.First();
+
+ using var edit = textBuffer.CreateEdit();
+ foreach (var change in result.TextChanges)
+ {
+ edit.Replace(change.Span.ToSpan(), change.NewText);
+ }
+ edit.Apply();
+
+ Assert.Equal(expectedText, textBuffer.CurrentSnapshot.GetText());
+ }
+ }
+}
diff --git a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs
new file mode 100644
index 0000000000000..2734f46275bc0
--- /dev/null
+++ b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs
@@ -0,0 +1,97 @@
+// 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.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Features.Intents;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api
+{
+ internal interface IIntentSourceProvider
+ {
+ ///
+ /// For an input intent, computes the edits required to apply that intent and returns them.
+ ///
+ /// the intents with the context in which the intent was found.
+ /// the edits that should be applied to the current snapshot.
+ Task> ComputeIntentsAsync(IntentRequestContext context, CancellationToken cancellationToken = default);
+ }
+
+ ///
+ /// Defines the data needed to compute the code action edits from an intent.
+ ///
+ internal readonly struct IntentRequestContext
+ {
+ ///
+ /// The intent name. contains all intents roslyn knows how to handle.
+ ///
+ public string IntentName { get; }
+
+ ///
+ /// JSON formatted data specific to the intent that must be deserialized into the appropriate object.
+ ///
+ public string? IntentData { get; }
+
+ ///
+ /// The text snapshot and selection when
+ /// was called to compute the text edits and against which the resulting text edits will be calculated.
+ ///
+ public SnapshotSpan CurrentSnapshotSpan { get; }
+
+ ///
+ /// The text edits that should be applied to the to calculate
+ /// a prior text snapshot before the intent happened. The snapshot is used to calculate the actions.
+ ///
+ public ImmutableArray PriorTextEdits { get; }
+
+ ///
+ /// The caret position / selection in the snapshot calculated by applying
+ /// to the
+ ///
+ public TextSpan PriorSelection { get; }
+
+ public IntentRequestContext(string intentName, SnapshotSpan currentSnapshotSpan, ImmutableArray textEditsToPrior, TextSpan priorSelection, string? intentData)
+ {
+ IntentName = intentName ?? throw new ArgumentNullException(nameof(intentName));
+ IntentData = intentData;
+ CurrentSnapshotSpan = currentSnapshotSpan;
+ PriorTextEdits = textEditsToPrior;
+ PriorSelection = priorSelection;
+ }
+ }
+
+ ///
+ /// Defines the text changes needed to apply an intent.
+ ///
+ internal readonly struct IntentSource
+ {
+ ///
+ /// The title associated with this intent result.
+ ///
+ public readonly string Title { get; }
+
+ ///
+ /// The text changes that should be applied to the
+ ///
+ public readonly ImmutableArray TextChanges { get; }
+
+ ///
+ /// Contains metadata that can be used to identify the kind of sub-action these edits
+ /// apply to for the requested intent. Used for telemetry purposes only.
+ /// For example, the code action type name like FieldDelegatingCodeAction.
+ ///
+ public readonly string ActionName { get; }
+
+ public IntentSource(string title, ImmutableArray textChanges, string actionName)
+ {
+ TextChanges = textChanges;
+ Title = title ?? throw new ArgumentNullException(nameof(title));
+ ActionName = actionName ?? throw new ArgumentNullException(nameof(actionName));
+ }
+ }
+}
diff --git a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs
new file mode 100644
index 0000000000000..276fd632b39cd
--- /dev/null
+++ b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs
@@ -0,0 +1,112 @@
+// 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.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api;
+using Microsoft.CodeAnalysis.Features.Intents;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Internal.Log;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Microsoft.CodeAnalysis.Shared.Extensions;
+using Microsoft.CodeAnalysis.Text;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode
+{
+ [Export(typeof(IIntentSourceProvider)), Shared]
+ internal class IntentSourceProvider : IIntentSourceProvider
+ {
+ private readonly ImmutableDictionary<(string LanguageName, string IntentName), Lazy> _lazyIntentProviders;
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public IntentSourceProvider([ImportMany] IEnumerable> lazyIntentProviders)
+ {
+ _lazyIntentProviders = CreateProviderMap(lazyIntentProviders);
+ }
+
+ private static ImmutableDictionary<(string LanguageName, string IntentName), Lazy> CreateProviderMap(
+ IEnumerable> lazyIntentProviders)
+ {
+ return lazyIntentProviders.ToImmutableDictionary(
+ provider => (provider.Metadata.LanguageName, provider.Metadata.IntentName),
+ provider => provider);
+ }
+
+ public async Task> ComputeIntentsAsync(IntentRequestContext intentRequestContext, CancellationToken cancellationToken)
+ {
+ var currentDocument = intentRequestContext.CurrentSnapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
+ if (currentDocument == null)
+ {
+ throw new ArgumentException("could not retrieve document for request snapshot");
+ }
+
+ var languageName = currentDocument.Project.Language;
+ if (!_lazyIntentProviders.TryGetValue((LanguageName: languageName, IntentName: intentRequestContext.IntentName), out var provider))
+ {
+ Logger.Log(FunctionId.Intellicode_UnknownIntent, KeyValueLogMessage.Create(LogType.UserAction, m =>
+ {
+ m["intent"] = intentRequestContext.IntentName;
+ m["language"] = languageName;
+ }));
+
+ return ImmutableArray.Empty;
+ }
+
+ var currentText = await currentDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
+ var originalDocument = currentDocument.WithText(currentText.WithChanges(intentRequestContext.PriorTextEdits));
+
+ var selectionTextSpan = intentRequestContext.PriorSelection;
+ var results = await provider.Value.ComputeIntentAsync(
+ originalDocument,
+ selectionTextSpan,
+ currentDocument,
+ intentRequestContext.IntentData,
+ cancellationToken).ConfigureAwait(false);
+ if (results.IsDefaultOrEmpty)
+ {
+ return ImmutableArray.Empty;
+ }
+
+ using var _ = ArrayBuilder.GetInstance(out var convertedResults);
+ foreach (var result in results)
+ {
+ var convertedIntent = await ConvertToIntelliCodeResultAsync(result, originalDocument, currentDocument, cancellationToken).ConfigureAwait(false);
+ convertedResults.AddIfNotNull(convertedIntent);
+ }
+
+ return convertedResults.ToImmutable();
+ }
+
+ private static async Task ConvertToIntelliCodeResultAsync(
+ IntentProcessorResult processorResult,
+ Document originalDocument,
+ Document currentDocument,
+ CancellationToken cancellationToken)
+ {
+ var newSolution = processorResult.Solution;
+
+ // Merge linked file changes so all linked files have the same text changes.
+ newSolution = await newSolution.WithMergedLinkedFileChangesAsync(originalDocument.Project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ // For now we only support changes to the current document. Everything else is dropped.
+ var changedDocument = newSolution.GetRequiredDocument(currentDocument.Id);
+
+ var textDiffService = newSolution.Workspace.Services.GetRequiredService();
+ // Compute changes against the current version of the document.
+ var textDiffs = await textDiffService.GetTextChangesAsync(currentDocument, changedDocument, cancellationToken).ConfigureAwait(false);
+ if (textDiffs.IsEmpty)
+ {
+ return null;
+ }
+
+ return new IntentSource(processorResult.Title, textDiffs, processorResult.ActionName);
+ }
+ }
+}
diff --git a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
index 9fa289dd1b748..57a5ebbdd94bb 100644
--- a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
+++ b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
@@ -95,6 +95,7 @@
+
diff --git a/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs
index 3ae504ef9bce6..e4b33eb706dd6 100644
--- a/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs
+++ b/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs
@@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
+using Microsoft.CodeAnalysis.Features.Intents;
using Microsoft.CodeAnalysis.GenerateConstructorFromMembers;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
@@ -19,6 +20,7 @@ namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructorFromMembers
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers), Shared]
[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.GenerateEqualsAndGetHashCodeFromMembers)]
+ [IntentProvider(WellKnownIntents.GenerateConstructor, LanguageNames.CSharp)]
internal sealed class CSharpGenerateConstructorFromMembersCodeRefactoringProvider
: AbstractGenerateConstructorFromMembersCodeRefactoringProvider
{
diff --git a/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj b/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj
index c3e5bdb1cfd9e..47d616b13cae2 100644
--- a/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj
+++ b/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj
@@ -49,9 +49,9 @@
-
-
-
+
+
+
diff --git a/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.GenerateConstructorWithDialogCodeAction.cs b/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.GenerateConstructorWithDialogCodeAction.cs
index 3be4a981120f9..71dc6e7c71dc1 100644
--- a/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.GenerateConstructorWithDialogCodeAction.cs
+++ b/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.GenerateConstructorWithDialogCodeAction.cs
@@ -24,11 +24,12 @@ private class GenerateConstructorWithDialogCodeAction : CodeActionWithOptions
private readonly INamedTypeSymbol _containingType;
private readonly AbstractGenerateConstructorFromMembersCodeRefactoringProvider _service;
private readonly TextSpan _textSpan;
- private readonly ImmutableArray _viableMembers;
- private readonly ImmutableArray _pickMembersOptions;
private bool? _addNullCheckOptionValue;
+ internal ImmutableArray ViableMembers { get; }
+ internal ImmutableArray PickMembersOptions { get; }
+
public override string Title => FeaturesResources.Generate_constructor;
public GenerateConstructorWithDialogCodeAction(
@@ -42,8 +43,8 @@ public GenerateConstructorWithDialogCodeAction(
_document = document;
_textSpan = textSpan;
_containingType = containingType;
- _viableMembers = viableMembers;
- _pickMembersOptions = pickMembersOptions;
+ ViableMembers = viableMembers;
+ PickMembersOptions = pickMembersOptions;
}
public override object GetOptions(CancellationToken cancellationToken)
@@ -53,7 +54,7 @@ public override object GetOptions(CancellationToken cancellationToken)
return service.PickMembers(
FeaturesResources.Pick_members_to_be_used_as_constructor_parameters,
- _viableMembers, _pickMembersOptions);
+ ViableMembers, PickMembersOptions);
}
protected override async Task> ComputeOperationsAsync(
diff --git a/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.cs
index 7947bff47d048..8484f82d7b7e2 100644
--- a/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.cs
+++ b/src/Features/Core/Portable/GenerateConstructorFromMembers/AbstractGenerateConstructorFromMembersCodeRefactoringProvider.cs
@@ -2,8 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-#nullable disable
-
+using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
@@ -11,6 +10,7 @@
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
+using Microsoft.CodeAnalysis.Features.Intents;
using Microsoft.CodeAnalysis.GenerateFromMembers;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageServices;
@@ -18,7 +18,9 @@
using Microsoft.CodeAnalysis.PickMembers;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
+using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
+using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.GenerateConstructorFromMembers
{
@@ -34,11 +36,11 @@ namespace Microsoft.CodeAnalysis.GenerateConstructorFromMembers
/// something like "new MyType(x, y, z)", nor is it responsible for generating constructors
/// in a derived type that delegate to a base type. Both of those are handled by other services.
///
- internal abstract partial class AbstractGenerateConstructorFromMembersCodeRefactoringProvider : AbstractGenerateFromMembersCodeRefactoringProvider
+ internal abstract partial class AbstractGenerateConstructorFromMembersCodeRefactoringProvider : AbstractGenerateFromMembersCodeRefactoringProvider, IIntentProvider
{
private const string AddNullChecksId = nameof(AddNullChecksId);
- private readonly IPickMembersService _pickMembersService_forTesting;
+ private readonly IPickMembersService? _pickMembersService_forTesting;
protected AbstractGenerateConstructorFromMembersCodeRefactoringProvider() : this(null)
{
@@ -47,16 +49,93 @@ protected AbstractGenerateConstructorFromMembersCodeRefactoringProvider() : this
///
/// For testing purposes only.
///
- protected AbstractGenerateConstructorFromMembersCodeRefactoringProvider(IPickMembersService pickMembersService_forTesting)
+ protected AbstractGenerateConstructorFromMembersCodeRefactoringProvider(IPickMembersService? pickMembersService_forTesting)
=> _pickMembersService_forTesting = pickMembersService_forTesting;
protected abstract bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType);
protected abstract string ToDisplayString(IParameterSymbol parameter, SymbolDisplayFormat format);
protected abstract bool PrefersThrowExpression(DocumentOptionSet options);
- public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
+ public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
+ {
+ return ComputeRefactoringsAsync(context.Document, context.Span,
+ (action, applicableToSpan) => context.RegisterRefactoring(action, applicableToSpan),
+ (actions) => context.RegisterRefactorings(actions), context.CancellationToken);
+ }
+
+ public async Task> ComputeIntentAsync(
+ Document priorDocument,
+ TextSpan priorSelection,
+ Document currentDocument,
+ string? serializedIntentData,
+ CancellationToken cancellationToken)
+ {
+ using var _ = ArrayBuilder.GetInstance(out var actions);
+ await ComputeRefactoringsAsync(
+ priorDocument,
+ priorSelection,
+ (singleAction, applicableToSpan) => actions.Add(singleAction),
+ (multipleActions) => actions.AddRange(multipleActions),
+ cancellationToken).ConfigureAwait(false);
+
+ if (actions.IsEmpty())
+ {
+ return ImmutableArray.Empty;
+ }
+
+ // The refactorings returned will be in the following order (if available)
+ // FieldDelegatingCodeAction, ConstructorDelegatingCodeAction, GenerateConstructorWithDialogCodeAction
+ using var resultsBuilder = ArrayBuilder.GetInstance(out var results);
+ foreach (var action in actions)
+ {
+ var intentResult = await GetIntentProcessorResultAsync(action, cancellationToken).ConfigureAwait(false);
+ results.AddIfNotNull(intentResult);
+ }
+
+ return results.ToImmutable();
+
+ static async Task GetIntentProcessorResultAsync(CodeAction codeAction, CancellationToken cancellationToken)
+ {
+ var operations = await GetCodeActionOperationsAsync(codeAction, cancellationToken).ConfigureAwait(false);
+
+ // Generate ctor will only return an ApplyChangesOperation or potentially document navigation actions.
+ // We can only return edits, so we only care about the ApplyChangesOperation.
+ var applyChangesOperation = operations.OfType().SingleOrDefault();
+ if (applyChangesOperation == null)
+ {
+ return null;
+ }
+
+ var type = codeAction.GetType();
+ return new IntentProcessorResult(applyChangesOperation.ChangedSolution, codeAction.Title, type.Name);
+ }
+
+ static async Task> GetCodeActionOperationsAsync(
+ CodeAction action,
+ CancellationToken cancellationToken)
+ {
+ if (action is GenerateConstructorWithDialogCodeAction dialogAction)
+ {
+ // Usually applying this code action pops up a dialog allowing the user to choose which options.
+ // We can't do that here, so instead we just take the defaults until we have more intent data.
+ var options = new PickMembersResult(dialogAction.ViableMembers, dialogAction.PickMembersOptions);
+ var operations = await dialogAction.GetOperationsAsync(options: options, cancellationToken).ConfigureAwait(false);
+ return operations == null ? ImmutableArray.Empty : operations.ToImmutableArray();
+ }
+ else
+ {
+ return await action.GetOperationsAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private async Task ComputeRefactoringsAsync(
+ Document document,
+ TextSpan textSpan,
+ Action registerSingleAction,
+ Action> registerMultipleActions,
+ CancellationToken cancellationToken)
{
- var (document, textSpan, cancellationToken) = context;
if (document.Project.Solution.Workspace.Kind == WorkspaceKind.MiscellaneousFiles)
{
return;
@@ -64,43 +143,51 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
var actions = await GenerateConstructorFromMembersAsync(
document, textSpan, addNullChecks: false, cancellationToken: cancellationToken).ConfigureAwait(false);
- context.RegisterRefactorings(actions);
+ if (!actions.IsDefault)
+ {
+ registerMultipleActions(actions);
+ }
if (actions.IsDefaultOrEmpty && textSpan.IsEmpty)
{
- await HandleNonSelectionAsync(context).ConfigureAwait(false);
+ var nonSelectionAction = await HandleNonSelectionAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
+ if (nonSelectionAction != null)
+ {
+ registerSingleAction(nonSelectionAction.Value.CodeAction, nonSelectionAction.Value.ApplicableToSpan);
+ }
}
}
- private async Task HandleNonSelectionAsync(CodeRefactoringContext context)
+ private async Task<(CodeAction CodeAction, TextSpan ApplicableToSpan)?> HandleNonSelectionAsync(
+ Document document,
+ TextSpan textSpan,
+ CancellationToken cancellationToken)
{
- var (document, textSpan, cancellationToken) = context;
-
- var syntaxFacts = document.GetLanguageService();
+ var syntaxFacts = document.GetRequiredLanguageService();
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
- var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
// We offer the refactoring when the user is either on the header of a class/struct,
// or if they're between any members of a class/struct and are on a blank line.
if (!syntaxFacts.IsOnTypeHeader(root, textSpan.Start, out var typeDeclaration) &&
!syntaxFacts.IsBetweenTypeMembers(sourceText, root, textSpan.Start, out typeDeclaration))
{
- return;
+ return null;
}
- var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
+ var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
// Only supported on classes/structs.
var containingType = semanticModel.GetDeclaredSymbol(typeDeclaration) as INamedTypeSymbol;
if (containingType?.TypeKind != TypeKind.Class && containingType?.TypeKind != TypeKind.Struct)
{
- return;
+ return null;
}
// No constructors for static classes.
if (containingType.IsStatic)
{
- return;
+ return null;
}
// Find all the possible writable instance fields/properties. If there are any, then
@@ -109,7 +196,7 @@ private async Task HandleNonSelectionAsync(CodeRefactoringContext context)
var viableMembers = containingType.GetMembers().WhereAsArray(IsWritableInstanceFieldOrProperty);
if (viableMembers.Length == 0)
{
- return;
+ return null;
}
using var _ = ArrayBuilder.GetInstance(out var pickMemberOptions);
@@ -127,11 +214,9 @@ private async Task HandleNonSelectionAsync(CodeRefactoringContext context)
optionValue));
}
- context.RegisterRefactoring(
- new GenerateConstructorWithDialogCodeAction(
+ return (new GenerateConstructorWithDialogCodeAction(
this, document, textSpan, containingType, viableMembers,
- pickMemberOptions.ToImmutable()),
- typeDeclaration.Span);
+ pickMemberOptions.ToImmutable()), typeDeclaration.Span);
}
public async Task> GenerateConstructorFromMembersAsync(
@@ -166,10 +251,10 @@ private ImmutableArray GetCodeActions(Document document, State state
private static async Task AddNavigationAnnotationAsync(Document document, CancellationToken cancellationToken)
{
- var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var nodes = root.GetAnnotatedNodes(CodeGenerator.Annotation);
- var syntaxFacts = document.GetLanguageService();
+ var syntaxFacts = document.GetRequiredLanguageService();
foreach (var node in nodes)
{
diff --git a/src/Features/Core/Portable/Intents/IIntentProvider.cs b/src/Features/Core/Portable/Intents/IIntentProvider.cs
new file mode 100644
index 0000000000000..b5107838bd103
--- /dev/null
+++ b/src/Features/Core/Portable/Intents/IIntentProvider.cs
@@ -0,0 +1,21 @@
+// 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.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.CodeAnalysis.Features.Intents
+{
+ internal interface IIntentProvider
+ {
+ Task> ComputeIntentAsync(
+ Document priorDocument,
+ TextSpan priorSelection,
+ Document currentDocument,
+ string? serializedIntentData,
+ CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Features/Core/Portable/Intents/IIntentProviderMetadata.cs b/src/Features/Core/Portable/Intents/IIntentProviderMetadata.cs
new file mode 100644
index 0000000000000..51291ff3d6d65
--- /dev/null
+++ b/src/Features/Core/Portable/Intents/IIntentProviderMetadata.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.CodeAnalysis.Features.Intents
+{
+ internal interface IIntentProviderMetadata
+ {
+ public string IntentName { get; }
+ public string LanguageName { get; }
+ }
+}
diff --git a/src/Features/Core/Portable/Intents/IntentProviderAttribute.cs b/src/Features/Core/Portable/Intents/IntentProviderAttribute.cs
new file mode 100644
index 0000000000000..44e9a88a9c339
--- /dev/null
+++ b/src/Features/Core/Portable/Intents/IntentProviderAttribute.cs
@@ -0,0 +1,23 @@
+// 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.Composition;
+
+namespace Microsoft.CodeAnalysis.Features.Intents
+{
+ [MetadataAttribute]
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ internal class IntentProviderAttribute : ExportAttribute, IIntentProviderMetadata
+ {
+ public string IntentName { get; }
+ public string LanguageName { get; }
+
+ public IntentProviderAttribute(string intentName, string languageName) : base(typeof(IIntentProvider))
+ {
+ IntentName = intentName;
+ LanguageName = languageName;
+ }
+ }
+}
diff --git a/src/Features/Core/Portable/Intents/IntentResult.cs b/src/Features/Core/Portable/Intents/IntentResult.cs
new file mode 100644
index 0000000000000..3f56c09397da5
--- /dev/null
+++ b/src/Features/Core/Portable/Intents/IntentResult.cs
@@ -0,0 +1,37 @@
+// 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;
+
+namespace Microsoft.CodeAnalysis.Features.Intents
+{
+ ///
+ /// Defines the text changes needed to apply an intent.
+ ///
+ internal struct IntentProcessorResult
+ {
+ ///
+ /// The changed solution for this intent result.
+ ///
+ public readonly Solution Solution;
+
+ ///
+ /// The title associated with this intent result.
+ ///
+ public readonly string Title;
+
+ ///
+ /// Contains metadata that can be used to identify the kind of sub-action these edits
+ /// apply to for the requested intent.
+ ///
+ public readonly string ActionName;
+
+ public IntentProcessorResult(Solution solution, string title, string actionName)
+ {
+ Solution = solution;
+ Title = title ?? throw new ArgumentNullException(nameof(title));
+ ActionName = actionName ?? throw new ArgumentNullException(nameof(actionName));
+ }
+ }
+}
diff --git a/src/Features/Core/Portable/Intents/WellKnownIntents.cs b/src/Features/Core/Portable/Intents/WellKnownIntents.cs
new file mode 100644
index 0000000000000..127e093d0e2a7
--- /dev/null
+++ b/src/Features/Core/Portable/Intents/WellKnownIntents.cs
@@ -0,0 +1,14 @@
+// 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.
+
+namespace Microsoft.CodeAnalysis.Features.Intents
+{
+ ///
+ /// The set of well known intents that Roslyn can calculate edits for.
+ ///
+ internal static class WellKnownIntents
+ {
+ public const string GenerateConstructor = nameof(GenerateConstructor);
+ }
+}
diff --git a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj
index bf59d330743a2..1334a5b79194c 100644
--- a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj
+++ b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj
@@ -95,9 +95,9 @@
-
-
-
+
+
+
diff --git a/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj b/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj
index 97ee5cf043d15..fad332e9ccc56 100644
--- a/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj
+++ b/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj
@@ -47,9 +47,9 @@
-
-
-
+
+
+
diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj
index a2b6290c21aa0..88a57c943ccb0 100644
--- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj
+++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj
@@ -124,9 +124,9 @@
-
-
-
+
+
+
diff --git a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj
index 16cd31984b6e3..1e6a39c0aa780 100644
--- a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj
+++ b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj
@@ -56,10 +56,10 @@
-
+
-
-
+
+
diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs
index 38654f12bd3f8..081b1e25bba38 100644
--- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs
+++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs
@@ -510,5 +510,7 @@ internal enum FunctionId
LSP_RequestCounter = 482,
LSP_RequestDuration = 483,
LSP_TimeInQueue = 484,
+
+ Intellicode_UnknownIntent = 485,
}
}