diff --git a/src/EditorFeatures/CSharpTest/Intents/AddConstructorParameterIntentTests.cs b/src/EditorFeatures/CSharpTest/Intents/AddConstructorParameterIntentTests.cs new file mode 100644 index 0000000000000..cf6160606f439 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Intents/AddConstructorParameterIntentTests.cs @@ -0,0 +1,273 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.Features.Intents; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Intents; + +[UseExportProvider] +public sealed class AddConstructorParameterIntentTests : IntentTestsBase +{ + [Fact] + public async Task AddConstructorParameterWithField() + { + var initialText = + """ + class C + { + private readonly int _someInt;{|priorSelection:|} + + public C() + { + } + } + """; + + var currentText = + """ + class C + { + private readonly int _someInt; + + public C(int som) + { + } + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + public C(int someInt) + { + _someInt = someInt; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.AddConstructorParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task AddConstructorParameterWithProperty() + { + var initialText = + """ + class C + { + public int SomeInt { get; }{|priorSelection:|} + + public C() + { + } + } + """; + var currentText = + """ + class C + { + public int SomeInt { get; } + + public C(int som) + { + } + } + """; + var expectedText = + """ + class C + { + public int SomeInt { get; } + + public C(int someInt) + { + SomeInt = someInt; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.AddConstructorParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task AddMultipleConstructorParameters() + { + var initialText = + """ + class C + { + {|priorSelection:private readonly int _someInt; + private readonly string _someString;|} + + public C() + { + } + } + """; + var currentText = + """ + class C + { + {|priorSelection:private readonly int _someInt; + private readonly string _someString;|} + + public C(int som) + { + } + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + private readonly string _someString; + + public C(int someInt, string someString) + { + _someInt = someInt; + _someString = someString; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.AddConstructorParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task AddConstructorParameterOnlyAddsSelected() + { + var initialText = + """ + class C + { + private readonly int _someInt;{|priorSelection:|} + private readonly string _someString; + + public C() + { + } + } + """; + var currentText = + """ + class C + { + private readonly int _someInt;{|priorSelection:|} + private readonly string _someString; + + public C(int som) + { + } + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + private readonly string _someString; + + public C(int someInt) + { + _someInt = someInt; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.AddConstructorParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task AddConstructorParameterUsesCodeStyleOption() + { + var initialText = + """ + class C + { + private readonly int _someInt;{|priorSelection:|} + + public C() + { + } + } + """; + var currentText = + """ + class C + { + private readonly int _someInt;{|priorSelection:|} + + public C(int som) + { + } + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + public C(int someInt) + { + this._someInt = someInt; + } + } + """; + await VerifyExpectedTextAsync(WellKnownIntents.AddConstructorParameter, initialText, currentText, expectedText, + options: new OptionsCollection(LanguageNames.CSharp) + { + { CodeStyleOptions2.QualifyFieldAccess, true } + }).ConfigureAwait(false); + } + + [Fact] + public async Task AddConstructorParameterUsesExistingAccessibility() + { + var initialText = + """ + class C + { + private readonly int _someInt;{|priorSelection:|} + + protected C() + { + } + } + """; + var currentText = + """ + class C + { + private readonly int _someInt;{|priorSelection:|} + + protected C(int som) + { + } + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + protected C(int someInt) + { + _someInt = someInt; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.AddConstructorParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } +} diff --git a/src/EditorFeatures/CSharpTest/Intents/DeleteParameterIntentTests.cs b/src/EditorFeatures/CSharpTest/Intents/DeleteParameterIntentTests.cs new file mode 100644 index 0000000000000..a75ce38942c48 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Intents/DeleteParameterIntentTests.cs @@ -0,0 +1,589 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.Features.Intents; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Intents; + +[UseExportProvider] +public sealed class DeleteParameterIntentTests : IntentTestsBase +{ + [Fact] + public async Task TestDeleteParameterIntentAsync() + { + var initialText = + """ + class C + { + void M() + { + Method({|priorSelection:1|}); + } + + void Method(int value) + { + } + } + """; + var currentText = + """ + class C + { + void M() + { + Method(); + } + + void Method(int value) + { + } + } + """; + var expectedText = + """ + class C + { + void M() + { + Method(); + } + + void Method() + { + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteParameterOnDefinitionIntentAsync() + { + var initialText = + """ + class C + { + void M() + { + Method(1); + } + + void Method(int {|priorSelection:value|}) + { + } + } + """; + var currentText = + """ + class C + { + void M() + { + Method(); + } + + void Method(int value) + { + } + } + """; + var expectedText = + """ + class C + { + void M() + { + Method(); + } + + void Method() + { + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteSecondParameterIntentAsync() + { + var initialText = """ + class C + { + void M() + { + Method(1, {|priorSelection:2|}, 3); + } + + void Method(int value1, int value2, int value3) + { + } + } + """; + var currentText = + """ + class C + { + void M() + { + Method(1, 3); + } + + void Method(int value1, int value2, int value3) + { + } + } + """; + var expectedText = + """ + class C + { + void M() + { + Method(1, 3); + } + + void Method(int value1, int value3) + { + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteLastParameterAsync() + { + var initialText = """ + class C + { + void M() + { + Method(1, 2, {|priorSelection:3|}); + } + + void Method(int value1, int value2, int value3) + { + } + } + """; + var currentText = + """ + class C + { + void M() + { + Method(1, 2); + } + + void Method(int value1, int value2, int value3) + { + } + } + """; + var expectedText = + """ + class C + { + void M() + { + Method(1, 2); + } + + void Method(int value1, int value2) + { + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteThisParameterAsync() + { + var initialText = """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo(); + } + } + + public static class FooExtensions + { + public static void DoFoo(this {|priorSelection:Foo|} foo) + { + + } + } + """; + var currentText = + """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo(); + } + } + + public static class FooExtensions + { + public static void DoFoo() + { + + } + } + """; + + await VerifyIntentMissingAsync(WellKnownIntents.DeleteParameter, initialText, currentText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteParameterInExtensionMethodAsync() + { + var initialText = """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo({|priorSelection:1|}, 2); + } + } + + public static class FooExtensions + { + public static void DoFoo(this Foo foo, int value1, int value2) + { + + } + } + """; + var currentText = + """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo(2); + } + } + + public static class FooExtensions + { + public static void DoFoo(this Foo foo, int value1, int value2) + { + + } + } + """; + var expectedText = + """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo(2); + } + } + + public static class FooExtensions + { + public static void DoFoo(this Foo foo, int value2) + { + + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteParameterOnDefinitionAsync() + { + var initialText = """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo(1, 2); + } + } + + public static class FooExtensions + { + public static void DoFoo(this Foo foo, int {|priorSelection:value1|}, int value2) + { + + } + } + """; + var currentText = + """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo(2); + } + } + + public static class FooExtensions + { + public static void DoFoo(this Foo foo, int value1, int value2) + { + + } + } + """; + var expectedText = + """ + public class Foo + { + static void Bar() + { + var f = new Foo(); + f.DoFoo(2); + } + } + + public static class FooExtensions + { + public static void DoFoo(this Foo foo, int value2) + { + + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteParamsParameterAsync() + { + var initialText = + """ + class C + { + void M() + { + Method(new C(), {|priorSelection:1|}, 2, 3); + } + + void Method(C c, params int[] values) + { + } + } + """; + var currentText = + """ + class C + { + void M() + { + Method(new C(), 2, 3); + } + + void Method(C c, params int[] values) + { + } + } + """; + var expectedText = + """ + class C + { + void M() + { + Method(new C()); + } + + void Method(C c) + { + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteParameterBeforeParamsAsync() + { + var initialText = + """ + class C + { + void M() + { + Method(1.0f, 1, 2, 3); + } + + void Method(float {|priorSelection:f|}, params int[] values) + { + } + } + """; + var currentText = + """ + class C + { + void M() + { + Method(1, 2, 3); + } + + void Method(float f, params int[] values) + { + } + } + """; + var expectedText = + """ + class C + { + void M() + { + Method(1, 2, 3); + } + + void Method(params int[] values) + { + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteParameterOnStaticExtensionInvocationAsync() + { + var initialText = + """ + public static class AExtension + { + public static void Method(this A c, int i) + { + + } + } + + public class A + { + void M() + { + AExtension.Method(new A(), {|priorSelection:1|}); + } + } + """; + var currentText = + """ + public static class AExtension + { + public static void Method(this A c, int i) + { + + } + } + + public class A + { + void M() + { + AExtension.Method(new A()); + } + } + """; + var expectedText = + """ + public static class AExtension + { + public static void Method(this A c) + { + + } + } + + public class A + { + void M() + { + AExtension.Method(new A()); + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task TestDeleteParameterOnConstructorInvocationAsync() + { + var initialText = + """ + public class A + { + public A(int i, string s) + { + + } + + static A M() + { + return new A(1, {|priorSelection:"hello"|}); + } + } + """; + var currentText = + """ + public class A + { + public A(int i, string s) + { + + } + + static A M() + { + return new A(1); + } + } + """; + var expectedText = + """ + public class A + { + public A(int i) + { + + } + + static A M() + { + return new A(1); + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.DeleteParameter, initialText, currentText, expectedText).ConfigureAwait(false); + } +} diff --git a/src/EditorFeatures/CSharpTest/Intents/GenerateConstructorIntentTests.cs b/src/EditorFeatures/CSharpTest/Intents/GenerateConstructorIntentTests.cs new file mode 100644 index 0000000000000..745a09589f14f --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Intents/GenerateConstructorIntentTests.cs @@ -0,0 +1,285 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.CodeStyle; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.Features.Intents; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Intents; + +[UseExportProvider] +public sealed class GenerateConstructorIntentTests : IntentTestsBase +{ + [Fact] + public async Task GenerateConstructorSimpleResult() + { + var initialText = + """ + class C + { + private readonly int _someInt; + + {|priorSelection:|} + } + """; + var currentText = + """ + class C + { + private readonly int _someInt; + + public C + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + public C(int someInt) + { + _someInt = someInt; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.GenerateConstructor, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task GenerateConstructorTypedPrivateWithoutIntentData() + { + var initialText = + """ + class C + { + private readonly int _someInt; + + {|priorSelection:|} + } + """; + var currentText = + """ + class C + { + private readonly int _someInt; + + private C + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + public C(int someInt) + { + _someInt = someInt; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.GenerateConstructor, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task GenerateConstructorTypedPrivateWithIntentData() + { + var initialText = + """ + class C + { + private readonly int _someInt; + + {|priorSelection:|} + } + """; + var currentText = + """ + class C + { + private readonly int _someInt; + + private C + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + private C(int someInt) + { + _someInt = someInt; + } + } + """; + + // lang=json + var intentData = @"{ ""accessibility"": ""Private""}"; + + await VerifyExpectedTextAsync(WellKnownIntents.GenerateConstructor, initialText, currentText, expectedText, intentData: intentData).ConfigureAwait(false); + } + + [Fact] + public async Task GenerateConstructorTypedPrivateProtectedWithIntentData() + { + var initialText = + """ + class C + { + private readonly int _someInt; + + {|priorSelection:|} + } + """; + var currentText = + """ + class C + { + private readonly int _someInt; + + private protected C + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + private protected C(int someInt) + { + _someInt = someInt; + } + } + """; + + // lang=json + var intentData = @"{ ""accessibility"": ""ProtectedAndInternal""}"; + + await VerifyExpectedTextAsync(WellKnownIntents.GenerateConstructor, initialText, currentText, expectedText, intentData: intentData).ConfigureAwait(false); + } + + [Fact] + public async Task GenerateConstructorWithFieldsInPartial() + { + var initialText = + """ + partial class C + { + {|priorSelection:|} + } + """; + var currentText = + """ + partial class C + { + 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(WellKnownIntents.GenerateConstructor, initialText, currentText, additionalDocuments, [expectedText]).ConfigureAwait(false); + } + + [Fact] + public async Task GenerateConstructorWithReferenceType() + { + var initialText = + """ + class C + { + private readonly object _someObject; + + {|priorSelection:|} + } + """; + var currentText = + """ + class C + { + private readonly object _someObject; + + public C + } + """; + var expectedText = + """ + class C + { + private readonly object _someObject; + + public C(object someObject) + { + _someObject = someObject; + } + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.GenerateConstructor, initialText, currentText, expectedText).ConfigureAwait(false); + } + + [Fact] + public async Task GenerateConstructorWithExpressionBodyOption() + { + var initialText = + """ + class C + { + private readonly int _someInt; + + {|priorSelection:|} + } + """; + var currentText = + """ + class C + { + private readonly int _someInt; + + public C + } + """; + var expectedText = + """ + class C + { + private readonly int _someInt; + + public C(int someInt) => _someInt = someInt; + } + """; + + await VerifyExpectedTextAsync(WellKnownIntents.GenerateConstructor, initialText, currentText, expectedText, + options: new OptionsCollection(LanguageNames.CSharp) + { + { CSharpCodeStyleOptions.PreferExpressionBodiedConstructors, CSharpCodeStyleOptions.WhenPossibleWithSilentEnforcement } + }).ConfigureAwait(false); + } +} diff --git a/src/EditorFeatures/CSharpTest/Intents/IntentTestsBase.cs b/src/EditorFeatures/CSharpTest/Intents/IntentTestsBase.cs new file mode 100644 index 0000000000000..e1f1635e7463c --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Intents/IntentTestsBase.cs @@ -0,0 +1,132 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.UnitTests; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Text.Shared.Extensions; +using Microsoft.VisualStudio.Text; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Intents; + +public abstract class IntentTestsBase +{ + internal static async Task VerifyIntentMissingAsync( + string intentName, + string priorDocumentText, + string currentDocumentText, + OptionsCollection? options = null, + string? intentData = null) + { + using var workspace = EditorTestWorkspace.CreateCSharp(priorDocumentText, composition: EditorTestCompositions.EditorFeatures); + var results = await GetIntentsAsync(workspace, intentName, currentDocumentText, options, intentData).ConfigureAwait(false); + Assert.Empty(results); + } + + internal static Task VerifyExpectedTextAsync( + string intentName, + string priorDocumentText, + string currentDocumentText, + string expectedText, + OptionsCollection? options = null, + string? intentData = null) + { + return VerifyExpectedTextAsync(intentName, priorDocumentText, currentDocumentText, [], [expectedText], options, intentData); + } + + internal static async Task VerifyExpectedTextAsync( + string intentName, + string priorDocumentText, + string currentDocumentText, + string[] additionalDocuments, + string[] expectedTexts, + OptionsCollection? options = null, + string? intentData = null) + { + // Create the workspace from the prior document + any additional documents. + var documentSet = additionalDocuments.Prepend(priorDocumentText).ToArray(); + using var workspace = EditorTestWorkspace.CreateCSharp(documentSet, composition: EditorTestCompositions.EditorFeatures); + var results = await GetIntentsAsync(workspace, intentName, currentDocumentText, options, intentData).ConfigureAwait(false); + + // For now, we're just taking the first result to match intellicode behavior. + var result = results.First(); + + var actualDocumentTexts = new List(); + foreach (var documentChange in result.DocumentChanges) + { + // Get the document and open it. Since we're modifying the text buffer we don't care about linked documents. + var documentBuffer = workspace.GetTestDocument(documentChange.Key)!.GetTextBuffer(); + + using var edit = documentBuffer.CreateEdit(); + foreach (var change in documentChange.Value) + { + edit.Replace(change.Span.ToSpan(), change.NewText); + } + + edit.Apply(); + + actualDocumentTexts.Add(documentBuffer.CurrentSnapshot.GetText()); + } + + actualDocumentTexts.Sort(); + Array.Sort(expectedTexts); + + Assert.Equal(expectedTexts.Length, actualDocumentTexts.Count); + for (var i = 0; i < actualDocumentTexts.Count; i++) + { + AssertEx.EqualOrDiff(expectedTexts[i], actualDocumentTexts[i]); + } + } + + internal static async Task> GetIntentsAsync( + EditorTestWorkspace workspace, + string intentName, + string currentDocumentText, + OptionsCollection? options = null, + string? intentData = null) + { + workspace.SetAnalyzerFallbackOptions(options); + + var intentSource = workspace.ExportProvider.GetExportedValue(); + + // Get the prior test document from the workspace. + var testDocument = workspace.Documents.Single(d => d.Name == "test1.cs"); + var priorDocument = workspace.CurrentSolution.GetRequiredDocument(testDocument.Id); + + // Extract the prior selection annotated region from the prior document. + var priorSelection = testDocument.AnnotatedSpans["priorSelection"].Single(); + + // Move the test document buffer forward to the current document. + testDocument.Update(SourceText.From(currentDocumentText)); + var currentTextBuffer = testDocument.GetTextBuffer(); + + // Get the text change to pass into the API that rewinds the current document to the prior document. + var currentDocument = currentTextBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + var textDiffService = workspace.CurrentSolution.Services.GetRequiredService(); + var changes = await textDiffService.GetTextChangesAsync(currentDocument!, priorDocument, CancellationToken.None).ConfigureAwait(false); + + // Get the current snapshot span to pass in. + var currentSnapshot = new SnapshotSpan(currentTextBuffer.CurrentSnapshot, new Span(0, currentTextBuffer.CurrentSnapshot.Length)); + + var intentContext = new IntentRequestContext( + intentName, + currentSnapshot, + changes, + priorSelection, + intentData: intentData); + var results = await intentSource.ComputeIntentsAsync(intentContext, CancellationToken.None).ConfigureAwait(false); + return results; + } +} diff --git a/src/EditorFeatures/CSharpTest/Intents/RenameIntentTests.cs b/src/EditorFeatures/CSharpTest/Intents/RenameIntentTests.cs new file mode 100644 index 0000000000000..5f6aa21a67ee8 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Intents/RenameIntentTests.cs @@ -0,0 +1,226 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.Features.Intents; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Intents; + +[UseExportProvider] +public sealed class RenameIntentTests : IntentTestsBase +{ + [Fact] + public async Task TestRenameIntentAsync() + { + var initialText = + """ + class C + { + void M() + { + var thing = 1; + {|priorSelection:thing|}.ToString(); + } + } + """; + var currentText = + """ + class C + { + void M() + { + var thing = 1; + something.ToString(); + } + } + """; + var expectedText = + """ + class C + { + void M() + { + var something = 1; + something.ToString(); + } + } + """; + + await VerifyExpectedRenameAsync(initialText, currentText, expectedText, "something").ConfigureAwait(false); + } + + [Fact] + public async Task TestRenameIntentAsync_Insert() + { + var initialText = + """ + class C + { + void M() + { + var thing = 1; + {|priorSelection:|}thing.ToString(); + } + } + """; + var currentText = + """ + class C + { + void M() + { + var thing = 1; + something.ToString(); + } + } + """; + var expectedText = + """ + class C + { + void M() + { + var something = 1; + something.ToString(); + } + } + """; + + await VerifyExpectedRenameAsync(initialText, currentText, expectedText, "something").ConfigureAwait(false); + } + + [Fact] + public async Task TestRenameIntentAsync_Delete() + { + var initialText = + """ + class C + { + void M() + { + var something = 1; + {|priorSelection:some|}thing.ToString(); + } + } + """; + var currentText = + """ + class C + { + void M() + { + var something = 1; + thing.ToString(); + } + } + """; + var expectedText = + """ + class C + { + void M() + { + var thing = 1; + thing.ToString(); + } + } + """; + + await VerifyExpectedRenameAsync(initialText, currentText, expectedText, "thing").ConfigureAwait(false); + } + + [Fact] + public async Task TestRenameIntentAsync_MultipleFiles() + { + var initialText = + """ + namespace M + { + public class C + { + public static string {|priorSelection:SomeString|} = string.Empty; + + void M() + { + var m = SomeString; + } + } + } + """; + var currentText = + """ + namespace M + { + public class C + { + public static string BetterString = string.Empty; + + void M() + { + var m = SomeString; + } + } + } + """; + var additionalDocuments = new string[] + { + """ + namespace M + { + public class D + { + void M() + { + var m = C.SomeString; + } + } + } + """ + }; + + var expectedTexts = new string[] + { + """ + namespace M + { + public class C + { + public static string BetterString = string.Empty; + + void M() + { + var m = BetterString; + } + } + } + """, + """ + namespace M + { + public class D + { + void M() + { + var m = C.BetterString; + } + } + } + """ + }; + + await VerifyExpectedRenameAsync(initialText, currentText, additionalDocuments, expectedTexts, "BetterString").ConfigureAwait(false); + } + + private static Task VerifyExpectedRenameAsync(string initialText, string currentText, string expectedText, string newName) + { + return VerifyExpectedTextAsync(WellKnownIntents.Rename, initialText, currentText, expectedText, intentData: $"{{ \"newName\": \"{newName}\" }}"); + } + + private static Task VerifyExpectedRenameAsync(string initialText, string currentText, string[] additionalText, string[] expectedTexts, string newName) + { + return VerifyExpectedTextAsync(WellKnownIntents.Rename, initialText, currentText, additionalText, expectedTexts, intentData: $"{{ \"newName\": \"{newName}\" }}"); + } +} diff --git a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs index 8345677d895b0..2389d70300c63 100644 --- a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs +++ b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Features.Intents; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; @@ -27,7 +28,7 @@ internal interface IIntentSourceProvider internal readonly struct IntentRequestContext(string intentName, SnapshotSpan currentSnapshotSpan, ImmutableArray textEditsToPrior, TextSpan priorSelection, string? intentData) { /// - /// The intent name. WellKnownIntents contains all intents roslyn knows how to handle. + /// The intent name. contains all intents roslyn knows how to handle. /// public string IntentName { get; } = intentName ?? throw new ArgumentNullException(nameof(intentName)); diff --git a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs index 42a996e3345ca..4a9565d257094 100644 --- a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs +++ b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs @@ -3,21 +3,126 @@ // 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 Roslyn.Utilities; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode; [Export(typeof(IIntentSourceProvider)), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal class IntentSourceProvider() : IIntentSourceProvider +internal class IntentSourceProvider( + [ImportMany] IEnumerable> lazyIntentProviders) : IIntentSourceProvider { - public Task> ComputeIntentsAsync(IntentRequestContext intentRequestContext, CancellationToken cancellationToken) - => SpecializedTasks.EmptyImmutableArray(); + private readonly ImmutableDictionary<(string LanguageName, string IntentName), Lazy> _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, static (m, args) => + { + var (intentRequestContext, languageName) = args; + m["intent"] = intentRequestContext.IntentName; + m["language"] = languageName; + }, (intentRequestContext, languageName))); + + return []; + } + + var currentText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var originalDocument = currentDocument.WithText(currentText.WithChanges(intentRequestContext.PriorTextEdits)); + + var selectionTextSpan = intentRequestContext.PriorSelection; + + var results = await provider.Value.ComputeIntentAsync( + originalDocument, + selectionTextSpan, + currentDocument, + new IntentDataProvider(intentRequestContext.IntentData), + cancellationToken).ConfigureAwait(false); + + if (results.IsDefaultOrEmpty) + { + return []; + } + + 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.ToImmutableAndClear(); + } + + 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); + + using var _ = PooledDictionary>.GetInstance(out var results); + foreach (var changedDocumentId in processorResult.ChangedDocuments) + { + // Calculate the text changes by comparing the solution with intent applied to the current solution (not to be confused with the original solution, the one prior to intent detection). + var docChanges = await GetTextChangesForDocumentAsync(newSolution, currentDocument.Project.Solution, changedDocumentId, cancellationToken).ConfigureAwait(false); + if (docChanges != null) + { + results[changedDocumentId] = docChanges.Value; + } + } + + return new IntentSource(processorResult.Title, processorResult.ActionName, results.ToImmutableDictionary()); + } + + private static async Task?> GetTextChangesForDocumentAsync( + Solution changedSolution, + Solution currentSolution, + DocumentId changedDocumentId, + CancellationToken cancellationToken) + { + var changedDocument = changedSolution.GetRequiredDocument(changedDocumentId); + var currentDocument = currentSolution.GetRequiredDocument(changedDocumentId); + + var textDiffService = changedSolution.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 textDiffs; + } } diff --git a/src/EditorFeatures/Core/Intents/DeleteParameterIntentProvider.cs b/src/EditorFeatures/Core/Intents/DeleteParameterIntentProvider.cs new file mode 100644 index 0000000000000..1b685db55cba0 --- /dev/null +++ b/src/EditorFeatures/Core/Intents/DeleteParameterIntentProvider.cs @@ -0,0 +1,64 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ChangeSignature; +using Microsoft.CodeAnalysis.Features.Intents; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.EditorFeatures.Intents; + +[IntentProvider(WellKnownIntents.DeleteParameter, LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DeleteParameterIntentProvider() : IIntentProvider +{ + public async Task> ComputeIntentAsync( + Document priorDocument, + TextSpan priorSelection, + Document currentDocument, + IntentDataProvider intentDataProvider, + CancellationToken cancellationToken) + { + var changeSignatureService = priorDocument.GetRequiredLanguageService(); + var contextResult = await changeSignatureService.GetChangeSignatureContextAsync( + priorDocument, priorSelection.Start, restrictToDeclarations: false, cancellationToken).ConfigureAwait(false); + + if (contextResult is not ChangeSignatureAnalysisSucceededContext context) + { + return []; + } + + var parameterIndexToDelete = context.ParameterConfiguration.SelectedIndex; + var parameters = context.ParameterConfiguration.ToListOfParameters(); + var isExtensionMethod = context.ParameterConfiguration.ThisParameter != null; + + if (isExtensionMethod && parameterIndexToDelete == 0) + { + // We can't delete the 'this' parameter of an extension method. + return []; + } + + var newParameters = parameters.RemoveAt(parameterIndexToDelete); + + var signatureChange = new SignatureChange(context.ParameterConfiguration, ParameterConfiguration.Create(newParameters, isExtensionMethod, selectedIndex: 0)); + var changeSignatureOptionResult = new ChangeSignatureOptionsResult(signatureChange, previewChanges: false); + + var changeSignatureResult = await changeSignatureService.ChangeSignatureWithContextAsync(context, changeSignatureOptionResult, cancellationToken).ConfigureAwait(false); + if (!changeSignatureResult.Succeeded) + { + return []; + } + + var changedDocuments = changeSignatureResult.UpdatedSolution.GetChangedDocuments(priorDocument.Project.Solution).ToImmutableArray(); + return [new IntentProcessorResult(changeSignatureResult.UpdatedSolution, changedDocuments, EditorFeaturesResources.Change_Signature, WellKnownIntents.DeleteParameter)]; + } +} diff --git a/src/EditorFeatures/Core/Intents/RenameIntentProvider.cs b/src/EditorFeatures/Core/Intents/RenameIntentProvider.cs new file mode 100644 index 0000000000000..7cfc38df565b4 --- /dev/null +++ b/src/EditorFeatures/Core/Intents/RenameIntentProvider.cs @@ -0,0 +1,58 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor; +using Microsoft.CodeAnalysis.Features.Intents; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Rename; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.EditorFeatures.Intents; + +[IntentProvider(WellKnownIntents.Rename, LanguageNames.CSharp), Shared] +internal sealed class RenameIntentProvider : IIntentProvider +{ + private sealed record RenameIntentData(string NewName); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RenameIntentProvider() + { + } + + public async Task> ComputeIntentAsync( + Document priorDocument, + TextSpan priorSelection, + Document currentDocument, + IntentDataProvider intentDataProvider, + CancellationToken cancellationToken) + { + var renameIntentData = intentDataProvider.GetIntentData(); + Contract.ThrowIfNull(renameIntentData); + + var renameService = priorDocument.GetRequiredLanguageService(); + var renameInfo = await renameService.GetRenameInfoAsync(priorDocument, priorSelection.Start, cancellationToken).ConfigureAwait(false); + if (!renameInfo.CanRename) + { + return []; + } + + var options = new SymbolRenameOptions( + RenameOverloads: false, + RenameInStrings: false, + RenameInComments: false, + RenameFile: false); + + var renameLocationSet = await renameInfo.FindRenameLocationsAsync(options, cancellationToken).ConfigureAwait(false); + var renameReplacementInfo = await renameLocationSet.GetReplacementsAsync(renameIntentData.NewName, options, cancellationToken).ConfigureAwait(false); + + return [new IntentProcessorResult(renameReplacementInfo.NewSolution, [.. renameReplacementInfo.DocumentIds], EditorFeaturesResources.Rename, WellKnownIntents.Rename)]; + } +} diff --git a/src/Features/CSharp/Portable/GenerateConstructors/CSharpGenerateConstructorsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/GenerateConstructors/CSharpGenerateConstructorsCodeRefactoringProvider.cs index 0c3552411581e..871064e1ddb43 100644 --- a/src/Features/CSharp/Portable/GenerateConstructors/CSharpGenerateConstructorsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/GenerateConstructors/CSharpGenerateConstructorsCodeRefactoringProvider.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Simplification; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Features.Intents; using Microsoft.CodeAnalysis.GenerateConstructors; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PickMembers; @@ -21,6 +22,7 @@ namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructors; [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers), Shared] [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.GenerateEqualsAndGetHashCodeFromMembers)] +[IntentProvider(WellKnownIntents.GenerateConstructor, LanguageNames.CSharp)] internal sealed class CSharpGenerateConstructorsCodeRefactoringProvider : AbstractGenerateConstructorsCodeRefactoringProvider { diff --git a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs index d3082b632a28d..f5e063a49567d 100644 --- a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs @@ -11,6 +11,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.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; @@ -25,10 +26,11 @@ namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers; Name = PredefinedCodeRefactoringProviderNames.AddConstructorParametersFromMembers), Shared] [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers, Before = PredefinedCodeRefactoringProviderNames.GenerateOverrides)] +[IntentProvider(WellKnownIntents.AddConstructorParameter, LanguageNames.CSharp)] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed partial class AddConstructorParametersFromMembersCodeRefactoringProvider() - : CodeRefactoringProvider + : CodeRefactoringProvider, IIntentProvider { public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { @@ -155,4 +157,37 @@ static AddConstructorParametersCodeAction GetOptionalContructorParametersCodeAct document, info, constructorCandidate, containingType, missingOptionalParameters, useSubMenuName); } } + + public async Task> ComputeIntentAsync( + Document priorDocument, + TextSpan priorSelection, + Document currentDocument, + IntentDataProvider intentDataProvider, + CancellationToken cancellationToken) + { + var addConstructorParametersResult = await AddConstructorParametersFromMembersAsync(priorDocument, priorSelection, cancellationToken).ConfigureAwait(false); + if (addConstructorParametersResult == null) + { + return []; + } + + var actions = addConstructorParametersResult.Value.RequiredParameterActions.Concat(addConstructorParametersResult.Value.OptionalParameterActions); + if (actions.IsEmpty) + { + return []; + } + + var results = new FixedSizeArrayBuilder(actions.Length); + foreach (var action in actions) + { + // Intents currently have no way to report progress. + var changedSolution = await action.GetChangedSolutionInternalAsync( + priorDocument.Project.Solution, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(changedSolution); + var intent = new IntentProcessorResult(changedSolution, [priorDocument.Id], action.Title, action.ActionName); + results.Add(intent); + } + + return results.MoveToImmutable(); + } } diff --git a/src/Features/Core/Portable/GenerateConstructors/AbstractGenerateConstructorsCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateConstructors/AbstractGenerateConstructorsCodeRefactoringProvider.cs index 0df7dba3b84f7..67e88355afbd3 100644 --- a/src/Features/Core/Portable/GenerateConstructors/AbstractGenerateConstructorsCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/GenerateConstructors/AbstractGenerateConstructorsCodeRefactoringProvider.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Features.Intents; using Microsoft.CodeAnalysis.GenerateDefaultConstructors; using Microsoft.CodeAnalysis.GenerateFromMembers; using Microsoft.CodeAnalysis.Internal.Log; @@ -45,7 +46,7 @@ namespace Microsoft.CodeAnalysis.GenerateConstructors; /// For testing purposes only. /// internal abstract partial class AbstractGenerateConstructorsCodeRefactoringProvider(IPickMembersService? pickMembersService_forTesting) - : CodeRefactoringProvider + : CodeRefactoringProvider, IIntentProvider { public sealed record GenerateConstructorIntentData(Accessibility? Accessibility); @@ -73,6 +74,80 @@ public override Task ComputeRefactoringsAsync(CodeRefactoringContext context) context.CancellationToken); } + public async Task> ComputeIntentAsync( + Document priorDocument, TextSpan priorSelection, Document currentDocument, IntentDataProvider intentDataProvider, CancellationToken cancellationToken) + { + var accessibility = intentDataProvider.GetIntentData()?.Accessibility; + + using var _1 = ArrayBuilder.GetInstance(out var actions); + await ComputeRefactoringsAsync( + priorDocument, + priorSelection, + (singleAction, applicableToSpan) => actions.Add(singleAction), + actions.AddRange, + desiredAccessibility: accessibility, + cancellationToken).ConfigureAwait(false); + + if (actions.IsEmpty) + { + return []; + } + + // The refactorings returned will be in the following order (if available) + // FieldDelegatingCodeAction, ConstructorDelegatingCodeAction, GenerateConstructorWithDialogCodeAction + using var _2 = ArrayBuilder.GetInstance(out var results); + foreach (var action in actions) + { + // Intents don't current support progress. + var intentResult = await GetIntentProcessorResultAsync( + priorDocument, action, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false); + results.AddIfNotNull(intentResult); + } + + return results.ToImmutableAndClear(); + + static async Task GetIntentProcessorResultAsync( + Document priorDocument, CodeAction codeAction, IProgress progressTracker, CancellationToken cancellationToken) + { + var operations = await GetCodeActionOperationsAsync( + priorDocument.Project.Solution, codeAction, progressTracker, 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, [priorDocument.Id], codeAction.Title, type.Name); + } + + static async Task> GetCodeActionOperationsAsync( + Solution originalSolution, + CodeAction action, + IProgress progressTracker, + 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, + selectedAll: true); + var operations = await dialogAction.GetOperationsAsync(originalSolution, options, progressTracker, cancellationToken).ConfigureAwait(false); + return operations == null ? [] : [.. operations]; + } + else + { + return await action.GetOperationsAsync(originalSolution, progressTracker, cancellationToken).ConfigureAwait(false); + } + } + } + private async Task ComputeRefactoringsAsync( Document document, TextSpan textSpan, diff --git a/src/Features/Core/Portable/Intents/IIntentProvider.cs b/src/Features/Core/Portable/Intents/IIntentProvider.cs new file mode 100644 index 0000000000000..8cc4bdf29b414 --- /dev/null +++ b/src/Features/Core/Portable/Intents/IIntentProvider.cs @@ -0,0 +1,20 @@ +// 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, + IntentDataProvider intentDataProvider, + 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..9a62d58225514 --- /dev/null +++ b/src/Features/Core/Portable/Intents/IIntentProviderMetadata.cs @@ -0,0 +1,11 @@ +// 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/IntentDataProvider.cs b/src/Features/Core/Portable/Intents/IntentDataProvider.cs new file mode 100644 index 0000000000000..737a21de6ccfd --- /dev/null +++ b/src/Features/Core/Portable/Intents/IntentDataProvider.cs @@ -0,0 +1,42 @@ +// 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.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis.ErrorReporting; + +namespace Microsoft.CodeAnalysis.Features.Intents; + +internal sealed class IntentDataProvider( + string? serializedIntentData) +{ + private static readonly Lazy s_serializerOptions = new Lazy(() => + { + var serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + serializerOptions.Converters.Add(new JsonStringEnumConverter()); + return serializerOptions; + }); + + private readonly string? _serializedIntentData = serializedIntentData; + + public T? GetIntentData() where T : class + { + if (_serializedIntentData != null) + { + try + { + return JsonSerializer.Deserialize(_serializedIntentData, s_serializerOptions.Value); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.General)) + { + } + } + + return null; + } +} diff --git a/src/Features/Core/Portable/Intents/IntentProviderAttribute.cs b/src/Features/Core/Portable/Intents/IntentProviderAttribute.cs new file mode 100644 index 0000000000000..9c1d71ea00f24 --- /dev/null +++ b/src/Features/Core/Portable/Intents/IntentProviderAttribute.cs @@ -0,0 +1,16 @@ +// 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 sealed class IntentProviderAttribute(string intentName, string languageName) : ExportAttribute(typeof(IIntentProvider)), IIntentProviderMetadata +{ + public string IntentName { get; } = intentName; + public string LanguageName { get; } = 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..f462c15d2afee --- /dev/null +++ b/src/Features/Core/Portable/Intents/IntentResult.cs @@ -0,0 +1,35 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Features.Intents; + +/// +/// Defines the text changes needed to apply an intent. +/// +internal readonly struct IntentProcessorResult(Solution solution, ImmutableArray changedDocuments, string title, string actionName) +{ + /// + /// The changed solution for this intent result. + /// + public readonly Solution Solution = solution; + + /// + /// The set of documents that have changed for this intent result. + /// + public readonly ImmutableArray ChangedDocuments = changedDocuments; + + /// + /// The title associated with this intent result. + /// + public readonly string Title = title ?? throw new ArgumentNullException(nameof(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 = 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..46bfdcd2adb2f --- /dev/null +++ b/src/Features/Core/Portable/Intents/WellKnownIntents.cs @@ -0,0 +1,16 @@ +// 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); + public const string AddConstructorParameter = nameof(AddConstructorParameter); + public const string Rename = nameof(Rename); + public const string DeleteParameter = nameof(DeleteParameter); +}