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, } }