Skip to content

Commit

Permalink
Merge pull request dotnet#51911 from dibarbet/intents
Browse files Browse the repository at this point in the history
Add intellicode API for generating code action edits from intents and
  • Loading branch information
dibarbet authored Mar 24, 2021
2 parents 1756346 + 80dc0c2 commit 064bf65
Show file tree
Hide file tree
Showing 20 changed files with 662 additions and 49 deletions.
3 changes: 2 additions & 1 deletion eng/targets/Settings.props
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
<VisualStudioKey>002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293</VisualStudioKey>
<MonoDevelopKey>002400000c800000940000000602000000240000525341310004000001000100e1290d741888d13312c0cd1f72bb843236573c80158a286f11bb98de5ee8acc3142c9c97b472684e521ae45125d7414558f2e70ac56504f3e8fe80830da2cdb1cda8504e8d196150d05a214609234694ec0ebf4b37fc7537e09d877c3e65000f7467fa3adb6e62c82b10ada1af4a83651556c7d949959817fed97480839dd39b</MonoDevelopKey>
<RazorKey>0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb</RazorKey>
<IntelliCodeKey>0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9</IntelliCodeKey>
<IntelliCodeCSharpKey>0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9</IntelliCodeCSharpKey>
<IntelliCodeKey>002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293</IntelliCodeKey>
<FSharpKey>$(VisualStudioKey)</FSharpKey>
<TypeScriptKey>$(VisualStudioKey)</TypeScriptKey>
<VisualStudioDebuggerKey>$(VisualStudioKey)</VisualStudioDebuggerKey>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IIntentSourceProvider>();

// 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());
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// For an input intent, computes the edits required to apply that intent and returns them.
/// </summary>
/// <param name="context">the intents with the context in which the intent was found.</param>
/// <returns>the edits that should be applied to the current snapshot.</returns>
Task<ImmutableArray<IntentSource>> ComputeIntentsAsync(IntentRequestContext context, CancellationToken cancellationToken = default);
}

/// <summary>
/// Defines the data needed to compute the code action edits from an intent.
/// </summary>
internal readonly struct IntentRequestContext
{
/// <summary>
/// The intent name. <see cref="WellKnownIntents"/> contains all intents roslyn knows how to handle.
/// </summary>
public string IntentName { get; }

/// <summary>
/// JSON formatted data specific to the intent that must be deserialized into the appropriate object.
/// </summary>
public string? IntentData { get; }

/// <summary>
/// The text snapshot and selection when <see cref="IIntentSourceProvider.ComputeIntentsAsync"/>
/// was called to compute the text edits and against which the resulting text edits will be calculated.
/// </summary>
public SnapshotSpan CurrentSnapshotSpan { get; }

/// <summary>
/// The text edits that should be applied to the <see cref="CurrentSnapshotSpan"/> to calculate
/// a prior text snapshot before the intent happened. The snapshot is used to calculate the actions.
/// </summary>
public ImmutableArray<TextChange> PriorTextEdits { get; }

/// <summary>
/// The caret position / selection in the snapshot calculated by applying
/// <see cref="PriorTextEdits"/> to the <see cref="CurrentSnapshotSpan"/>
/// </summary>
public TextSpan PriorSelection { get; }

public IntentRequestContext(string intentName, SnapshotSpan currentSnapshotSpan, ImmutableArray<TextChange> textEditsToPrior, TextSpan priorSelection, string? intentData)
{
IntentName = intentName ?? throw new ArgumentNullException(nameof(intentName));
IntentData = intentData;
CurrentSnapshotSpan = currentSnapshotSpan;
PriorTextEdits = textEditsToPrior;
PriorSelection = priorSelection;
}
}

/// <summary>
/// Defines the text changes needed to apply an intent.
/// </summary>
internal readonly struct IntentSource
{
/// <summary>
/// The title associated with this intent result.
/// </summary>
public readonly string Title { get; }

/// <summary>
/// The text changes that should be applied to the <see cref="IntentRequestContext.CurrentSnapshotSpan"/>
/// </summary>
public readonly ImmutableArray<TextChange> TextChanges { get; }

/// <summary>
/// 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.
/// </summary>
public readonly string ActionName { get; }

public IntentSource(string title, ImmutableArray<TextChange> textChanges, string actionName)
{
TextChanges = textChanges;
Title = title ?? throw new ArgumentNullException(nameof(title));
ActionName = actionName ?? throw new ArgumentNullException(nameof(actionName));
}
}
}
Loading

0 comments on commit 064bf65

Please sign in to comment.