Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Compilers/Test/Core/Traits/Traits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ public static class Features
public const string NetCore = nameof(NetCore);
public const string NormalizeModifiersOrOperators = nameof(NormalizeModifiersOrOperators);
public const string ObjectBrowser = nameof(ObjectBrowser);
public const string OnTheFlyDocs = nameof(OnTheFlyDocs);
public const string Options = nameof(Options);
public const string Organizing = nameof(Organizing);
public const string Outlining = nameof(Outlining);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.QuickInfo;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.OnTheFlyDocs;

[UseExportProvider]
[Trait(Traits.Feature, Traits.Features.OnTheFlyDocs)]
public sealed class OnTheFlyDocsUtilitiesTests
{
[Fact]
public async Task TestAdditionalContextNoContext()
{
var testCode = """
class C
{
void AddMethod(int a, int b)
{
return a + b;
}
}
""";

using var workspace = EditorTestWorkspace.CreateCSharp(testCode);
var solution = workspace.CurrentSolution;
var document = solution.Projects.First().Documents.First();

var syntaxTree = await document.GetSyntaxTreeAsync();
var semanticModel = await document.GetSemanticModelAsync();

var methodDeclaration = syntaxTree!.GetRoot()
.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.First();

var methodSymbol = semanticModel!.GetDeclaredSymbol(methodDeclaration);

var result = OnTheFlyDocsUtilities.GetAdditionalOnTheFlyDocsContext(solution, methodSymbol!);
Assert.True(result.All(item => item == null));
}

[Fact]
public async Task TestAdditionalContextWithTypeParameters()
{
var testCode = """
class C
{
int AddMethod(A a, int b)
{
return a.x + b;
}
}

class A
{
public int x;
}
""";

using var workspace = EditorTestWorkspace.CreateCSharp(testCode);
var solution = workspace.CurrentSolution;
var document = solution.Projects.First().Documents.First();

var syntaxTree = await document.GetSyntaxTreeAsync();
var semanticModel = await document.GetSemanticModelAsync();

var methodDeclaration = syntaxTree!.GetRoot()
.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.First();

var methodSymbol = semanticModel!.GetDeclaredSymbol(methodDeclaration);

var result = OnTheFlyDocsUtilities.GetAdditionalOnTheFlyDocsContext(solution, methodSymbol!);
Assert.NotNull(result.First());
Assert.Null(result.Last());
}

[Fact]
public async Task TestAdditionalContextWithTypeArguments()
{
var testCode = """
class C
{
void Method<T, U>(T t, U u) where T : class where U : struct
{
}

void CallMethod()
{
Method<CustomClass, CustomStruct>(new CustomClass(), new CustomStruct());
}
}

class CustomClass
{
public string Name { get; set; }
}

struct CustomStruct
{
public int Value { get; set; }
}
""";

using var workspace = EditorTestWorkspace.CreateCSharp(testCode);
var solution = workspace.CurrentSolution;
var document = solution.Projects.First().Documents.First();

var syntaxTree = await document.GetSyntaxTreeAsync();
var semanticModel = await document.GetSemanticModelAsync();

var methodInvocation = syntaxTree!.GetRoot()
.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.First();

var methodSymbol = semanticModel!.GetSymbolInfo(methodInvocation).Symbol;

var result = OnTheFlyDocsUtilities.GetAdditionalOnTheFlyDocsContext(solution, methodSymbol!);
Assert.True(result.All(item => item is not null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ private async Task SetResultTextAsync(ICopilotCodeAnalysisService copilotService

try
{
var (responseString, isQuotaExceeded) = await copilotService.GetOnTheFlyDocsAsync(_onTheFlyDocsInfo.SymbolSignature, _onTheFlyDocsInfo.DeclarationCode, _onTheFlyDocsInfo.Language, cancellationToken).ConfigureAwait(false);
var prompt = await copilotService.GetOnTheFlyDocsPromptAsync(_onTheFlyDocsInfo, cancellationToken).ConfigureAwait(false);
var (responseString, isQuotaExceeded) = await copilotService.GetOnTheFlyDocsResponseAsync(prompt, cancellationToken).ConfigureAwait(false);
var copilotRequestTime = stopwatch.Elapsed;

await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
Expand Down
13 changes: 9 additions & 4 deletions src/EditorFeatures/Test2/CodeFixes/CodeFixServiceTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Imports Microsoft.CodeAnalysis.ErrorLogger
Imports Microsoft.CodeAnalysis.FindSymbols
Imports Microsoft.CodeAnalysis.Host
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.QuickInfo
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.UnitTests
Imports Roslyn.Utilities
Expand Down Expand Up @@ -359,10 +360,6 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.CodeFixes.UnitTests
Return Task.CompletedTask
End Function

Public Function GetOnTheFlyDocsAsync(symbolSignature As String, declarationCode As ImmutableArray(Of String), language As String, cancellationToken As CancellationToken) As Task(Of (responseString As String, isQuotaExceeded As Boolean)) Implements ICopilotCodeAnalysisService.GetOnTheFlyDocsAsync
Return Task.FromResult(("", False))
End Function

Public Function IsFileExcludedAsync(filePath As String, cancellationToken As CancellationToken) As Task(Of Boolean) Implements ICopilotCodeAnalysisService.IsFileExcludedAsync
Return Task.FromResult(False)
End Function
Expand All @@ -371,6 +368,14 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.CodeFixes.UnitTests
Return Task.FromResult((New Dictionary(Of String, String), False))
End Function

Public Function GetOnTheFlyDocsPromptAsync(onTheFlyDocsInfo As OnTheFlyDocsInfo, cancellationToken As CancellationToken) As Task(Of String) Implements ICopilotCodeAnalysisService.GetOnTheFlyDocsPromptAsync
Return Task.FromResult(String.Empty)
End Function

Public Function GetOnTheFlyDocsResponseAsync(prompt As String, cancellationToken As CancellationToken) As Task(Of (responseString As String, isQuotaExceeded As Boolean)) Implements ICopilotCodeAnalysisService.GetOnTheFlyDocsResponseAsync
Return Task.FromResult((String.Empty, False))
End Function

Public Function IsImplementNotImplementedExceptionsAvailableAsync(cancellationToken As CancellationToken) As Task(Of Boolean) Implements ICopilotCodeAnalysisService.IsImplementNotImplementedExceptionsAvailableAsync
Return Task.FromResult(False)
End Function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,21 @@ protected override NullableFlowState GetNullabilityAnalysis(SemanticModel semant
}
}

var maxLength = 1000;
var symbolStrings = symbol.DeclaringSyntaxReferences.Select(reference =>
var solution = document.Project.Solution;
var declarationCode = symbol.DeclaringSyntaxReferences.Select(reference =>
{
var span = reference.Span;
var sourceText = reference.SyntaxTree.GetText(cancellationToken);
return sourceText.GetSubText(new Text.TextSpan(span.Start, Math.Min(maxLength, span.Length))).ToString();
var syntaxReferenceDocument = solution.GetDocument(reference.SyntaxTree);
if (syntaxReferenceDocument is not null)
{
return new OnTheFlyDocsRelevantFileInfo(syntaxReferenceDocument, span);
}

return null;
}).ToImmutableArray();

return new OnTheFlyDocsInfo(symbol.ToDisplayString(), symbolStrings, symbol.Language, hasContentExcluded);
var additionalContext = OnTheFlyDocsUtilities.GetAdditionalOnTheFlyDocsContext(solution, symbol);

return new OnTheFlyDocsInfo(symbol.ToDisplayString(), declarationCode, symbol.Language, hasContentExcluded, additionalContext);
}
}
50 changes: 50 additions & 0 deletions src/Features/CSharp/Portable/QuickInfo/OnTheFlyDocsUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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 Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.QuickInfo;

internal static class OnTheFlyDocsUtilities
{
public static ImmutableArray<OnTheFlyDocsRelevantFileInfo?> GetAdditionalOnTheFlyDocsContext(Solution solution, ISymbol symbol)
{
var parameters = symbol.GetParameters();
var typeArguments = symbol.GetTypeArguments();

var parameterStrings = parameters.Select(parameter =>
{
var typeSymbol = parameter.Type;
return GetOnTheFlyDocsRelevantFileInfo(typeSymbol);

}).ToImmutableArray();

var typeArgumentStrings = typeArguments.Select(typeArgument =>
{
return GetOnTheFlyDocsRelevantFileInfo(typeArgument);

}).ToImmutableArray();

return parameterStrings.AddRange(typeArgumentStrings);

OnTheFlyDocsRelevantFileInfo? GetOnTheFlyDocsRelevantFileInfo(ITypeSymbol typeSymbol)
{
var typeSyntaxReference = typeSymbol.DeclaringSyntaxReferences.FirstOrDefault();
if (typeSyntaxReference is not null)
{
var typeSpan = typeSyntaxReference.Span;
var syntaxReferenceDocument = solution.GetDocument(typeSyntaxReference.SyntaxTree);
if (syntaxReferenceDocument is not null)
{
return new OnTheFlyDocsRelevantFileInfo(syntaxReferenceDocument, typeSpan);
}
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
Expand Down Expand Up @@ -696,5 +697,15 @@ public Task<bool> IsImplementNotImplementedExceptionsAvailableAsync(Cancellation
{
return Task.FromResult(true);
}

public Task<string> GetOnTheFlyDocsPromptAsync(OnTheFlyDocsInfo onTheFlyDocsInfo, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

public Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsResponseAsync(string prompt, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Copilot;
Expand Down Expand Up @@ -65,15 +66,17 @@ internal interface ICopilotCodeAnalysisService : ILanguageService
Task StartRefinementSessionAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken);

/// <summary>
/// Method to fetch the on-the-fly documentation based on a a symbols <paramref name="symbolSignature"/>
/// and the code for the symbols in <paramref name="declarationCode"/>.
/// <para>
/// <paramref name="symbolSignature"/> is a formatted string representation of an <see cref="ISymbol"/>.<br/>
/// <paramref name="declarationCode"/> is a list of a code definitions from an <see cref="ISymbol"/>.
/// <paramref name="language"/> is the language of the originating <see cref="ISymbol"/>.
/// </para>
/// Retrieves the prompt
/// </summary>
Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray<string> declarationCode, string language, CancellationToken cancellationToken);
/// <param name="onTheFlyDocsInfo">Type containing code and other context about the symbol being examined.</param>
/// <returns></returns>
Task<string> GetOnTheFlyDocsPromptAsync(OnTheFlyDocsInfo onTheFlyDocsInfo, CancellationToken cancellationToken);

/// <summary>
/// Retrieves the response from Copilot summarizing what a symbol is being used for and whether or not the quota has exceeded.
/// </summary>
/// <param name="prompt">The input text used to generate the response.</param>
Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsResponseAsync(string prompt, CancellationToken cancellationToken);

/// <summary>
/// Determines if the given <paramref name="filePath"/> is excluded in the workspace.
Expand Down
5 changes: 3 additions & 2 deletions src/Features/Core/Portable/QuickInfo/OnTheFlyDocsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ namespace Microsoft.CodeAnalysis.QuickInfo;
/// <param name="declarationCode">the symbol's declaration code</param>
/// <param name="language">the language of the symbol</param>
/// <param name="hasComments">whether the symbol has comments</param>
internal sealed class OnTheFlyDocsInfo(string symbolSignature, ImmutableArray<string> declarationCode, string language, bool isContentExcluded, bool hasComments = false)
internal sealed class OnTheFlyDocsInfo(string symbolSignature, ImmutableArray<OnTheFlyDocsRelevantFileInfo?> declarationCode, string language, bool isContentExcluded, ImmutableArray<OnTheFlyDocsRelevantFileInfo?> additionalContext, bool hasComments = false)
{
public string SymbolSignature { get; } = symbolSignature;
public ImmutableArray<string> DeclarationCode { get; } = declarationCode;
public ImmutableArray<OnTheFlyDocsRelevantFileInfo?> DeclarationCode { get; } = declarationCode;
public string Language { get; } = language;
public bool IsContentExcluded { get; set; } = isContentExcluded;
public ImmutableArray<OnTheFlyDocsRelevantFileInfo?> AdditionalContext { get; } = additionalContext;

// Added for telemetry collection purposes.
public bool HasComments { get; set; } = hasComments;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.QuickInfo;

internal sealed record OnTheFlyDocsRelevantFileInfo
{
public Document Document { get; }
public TextSpan TextSpan { get; }

public OnTheFlyDocsRelevantFileInfo(Document document, TextSpan textSpan)
{
Document = document;
TextSpan = textSpan;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@ internal interface IExternalCSharpCopilotCodeAnalysisService
Task<ImmutableArray<Diagnostic>> AnalyzeDocumentAsync(Document document, TextSpan? span, string promptTitle, CancellationToken cancellationToken);
Task<ImmutableArray<Diagnostic>> GetCachedDiagnosticsAsync(Document document, string promptTitle, CancellationToken cancellationToken);
Task StartRefinementSessionAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken);
Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray<string> declarationCode, string language, CancellationToken cancellationToken);
Task<bool> IsFileExcludedAsync(string filePath, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

Expand Down Expand Up @@ -41,7 +42,8 @@ internal abstract class AbstractCopilotCodeAnalysisService(IDiagnosticsRefresher
protected abstract Task<ImmutableArray<Diagnostic>> AnalyzeDocumentCoreAsync(Document document, TextSpan? span, string promptTitle, CancellationToken cancellationToken);
protected abstract Task<ImmutableArray<Diagnostic>> GetCachedDiagnosticsCoreAsync(Document document, string promptTitle, CancellationToken cancellationToken);
protected abstract Task StartRefinementSessionCoreAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken);
protected abstract Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsCoreAsync(string symbolSignature, ImmutableArray<string> declarationCode, string language, CancellationToken cancellationToken);
protected abstract Task<string> GetOnTheFlyDocsPromptCoreAsync(OnTheFlyDocsInfo onTheFlyDocsInfo, CancellationToken cancellationToken);
protected abstract Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsResponseCoreAsync(string prompt, CancellationToken cancellationToken);
protected abstract Task<bool> IsFileExcludedCoreAsync(string filePath, CancellationToken cancellationToken);
protected abstract Task<(Dictionary<string, string>? responseDictionary, bool isQuotaExceeded)> GetDocumentationCommentCoreAsync(DocumentationCommentProposal proposal, CancellationToken cancellationToken);
protected abstract Task<ImmutableDictionary<MemberDeclarationSyntax, ImplementationDetails>> ImplementNotImplementedExceptionsCoreAsync(Document document, ImmutableDictionary<MemberDeclarationSyntax, ImmutableArray<ReferencedSymbol>> methodOrProperties, CancellationToken cancellationToken);
Expand Down Expand Up @@ -178,12 +180,16 @@ public async Task StartRefinementSessionAsync(Document oldDocument, Document new
await StartRefinementSessionCoreAsync(oldDocument, newDocument, primaryDiagnostic, cancellationToken).ConfigureAwait(false);
}

public async Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray<string> declarationCode, string language, CancellationToken cancellationToken)
public async Task<string> GetOnTheFlyDocsPromptAsync(OnTheFlyDocsInfo onTheFlyDocsInfo, CancellationToken cancellationToken)
{
return await GetOnTheFlyDocsPromptCoreAsync(onTheFlyDocsInfo, cancellationToken).ConfigureAwait(false);
}
public async Task<(string responseString, bool isQuotaExceeded)> GetOnTheFlyDocsResponseAsync(string prompt, CancellationToken cancellationToken)
{
if (!await IsAvailableAsync(cancellationToken).ConfigureAwait(false))
return (string.Empty, false);

return await GetOnTheFlyDocsCoreAsync(symbolSignature, declarationCode, language, cancellationToken).ConfigureAwait(false);
return await GetOnTheFlyDocsResponseCoreAsync(prompt, cancellationToken).ConfigureAwait(false);
}

public async Task<bool> IsFileExcludedAsync(string filePath, CancellationToken cancellationToken)
Expand Down
Loading