Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 eng/targets/Services.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.CodeLensReferences" ClassName="Microsoft.CodeAnalysis.Remote.RemoteCodeLensReferencesService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.CompilationAvailable" ClassName="Microsoft.CodeAnalysis.Remote.RemoteCompilationAvailableService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.ConvertTupleToStructCodeRefactoring" ClassName="Microsoft.CodeAnalysis.Remote.RemoteConvertTupleToStructCodeRefactoringService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.CopilotChangeAnalysis" ClassName="Microsoft.CodeAnalysis.Remote.RemoteCopilotChangeAnalysisService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.DependentTypeFinder" ClassName="Microsoft.CodeAnalysis.Remote.RemoteDependentTypeFinderService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.DesignerAttributeDiscovery" ClassName="Microsoft.CodeAnalysis.Remote.RemoteDesignerAttributeDiscoveryService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.DiagnosticAnalyzer" ClassName="Microsoft.CodeAnalysis.Remote.RemoteDiagnosticAnalyzerService+Factory" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,9 @@ internal static bool SequenceEqual<TElement, TArg>(this ImmutableArray<TElement>
internal static int IndexOf<T>(this ImmutableArray<T> array, T item, IEqualityComparer<T> comparer)
=> array.IndexOf(item, startIndex: 0, comparer);

internal static bool IsSorted<T>(this ImmutableArray<T> array, Comparison<T> comparison)
=> IsSorted(array, Comparer<T>.Create(comparison));

internal static bool IsSorted<T>(this ImmutableArray<T> array, IComparer<T>? comparer = null)
{
comparer ??= Comparer<T>.Default;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Threading;
using Microsoft.VisualStudio.Language.Proposals;
using Microsoft.VisualStudio.Language.Suggestions;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;

namespace Microsoft.CodeAnalysis.Copilot;

[Export(typeof(IWpfTextViewCreationListener))]
[ContentType(ContentTypeNames.RoslynContentType)]
[TextViewRole(PredefinedTextViewRoles.Document)]
internal sealed class CopilotWpfTextViewCreationListener : IWpfTextViewCreationListener
{
private readonly IThreadingContext _threadingContext;
private readonly Lazy<SuggestionServiceBase> _suggestionServiceBase;

private readonly AsyncBatchingWorkQueue<SuggestionAcceptedEventArgs> _workQueue;

private int _started;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CopilotWpfTextViewCreationListener(
IThreadingContext threadingContext,
Lazy<SuggestionServiceBase> suggestionServiceBase,
IAsynchronousOperationListenerProvider listenerProvider)
{
_threadingContext = threadingContext;
_suggestionServiceBase = suggestionServiceBase;
_workQueue = new AsyncBatchingWorkQueue<SuggestionAcceptedEventArgs>(
DelayTimeSpan.Idle,
ProcessEventsAsync,
listenerProvider.GetListener(FeatureAttribute.CopilotChangeAnalysis),
_threadingContext.DisposalToken);
}

public void TextViewCreated(IWpfTextView textView)
{
// On the first roslyn text view created, kick off work to hydrate the suggestion service and register to events
// from it.
if (Interlocked.CompareExchange(ref _started, 1, 0) == 0)
{
_ = Task.Run(() =>
{
var suggestionService = _suggestionServiceBase.Value;
suggestionService.SuggestionAccepted += OnSuggestionAccepted;
});
}
}

private void OnSuggestionAccepted(object sender, SuggestionAcceptedEventArgs e)
{
if (e.FinalProposal.Edits.Count == 0)
return;

_workQueue.AddWork(e);
}

private async ValueTask ProcessEventsAsync(
ImmutableSegmentedList<SuggestionAcceptedEventArgs> list, CancellationToken cancellationToken)
{
foreach (var eventArgs in list)
await ProcessEventAsync(eventArgs, cancellationToken).ConfigureAwait(false);
}

private static async ValueTask ProcessEventAsync(
SuggestionAcceptedEventArgs eventArgs, CancellationToken cancellationToken)
{
foreach (var editGroup in eventArgs.FinalProposal.Edits.GroupBy(e => e.Span.Snapshot))
{
cancellationToken.ThrowIfCancellationRequested();

var snapshot = editGroup.Key;
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();

if (document is null)
continue;

var normalizedEdits = Normalize(editGroup);
if (normalizedEdits.IsDefaultOrEmpty)
continue;

var changeAnalysisService = document.Project.Solution.Services.GetRequiredService<ICopilotChangeAnalysisService>();
await changeAnalysisService.AnalyzeChangeAsync(document, normalizedEdits, cancellationToken).ConfigureAwait(false);
}
}

private static ImmutableArray<TextChange> Normalize(IEnumerable<ProposedEdit> editGroup)
{
using var _ = PooledObjects.ArrayBuilder<TextChange>.GetInstance(out var builder);
foreach (var edit in editGroup)
builder.Add(new TextChange(edit.Span.Span.ToTextSpan(), edit.ReplacementText));

// Ensure everything is sorted.
builder.Sort(static (c1, c2) => c1.Span.Start - c2.Span.Start);

// Now, go through and make sure no edit overlaps another.
for (int i = 1, n = builder.Count; i < n; i++)
{
var lastEdit = builder[i - 1];
var currentEdit = builder[i];

if (lastEdit.Span.OverlapsWith(currentEdit.Span))
return default;
}

// Things look good. Can process these sorted edits.
return builder.ToImmutableAndClear();
}
}
49 changes: 49 additions & 0 deletions src/Features/Core/Portable/Copilot/CopilotChangeAnalysis.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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.Runtime.Serialization;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeAnalysis.Copilot;

/// <param name="TotalAnalysisTime">Total time to do all analysis (including diagnostics, code fixes, and application).</param>
/// <param name="TotalDiagnosticComputationTime">Total time to do all diagnostic computation over all diagnostic kinds.</param>
[DataContract]
internal readonly record struct CopilotChangeAnalysis(
[property: DataMember(Order = 0)] bool Succeeded,
[property: DataMember(Order = 1)] TimeSpan TotalAnalysisTime,
[property: DataMember(Order = 2)] TimeSpan TotalDiagnosticComputationTime,
[property: DataMember(Order = 3)] ImmutableArray<CopilotDiagnosticAnalysis> DiagnosticAnalyses,
[property: DataMember(Order = 4)] CopilotCodeFixAnalysis CodeFixAnalysis);

/// <param name="Kind">What diagnostic kind this is analysis data for.</param>
/// <param name="ComputationTime">How long it took to produce the diagnostics for this diagnostic kind.</param>
/// <param name="IdToCount">Mapping from <see cref="Diagnostic.Id"/> to the number of diagnostics produced for that id.</param>
/// <param name="CategoryToCount">Mapping from <see cref="Diagnostic.Category"/> to the number of diagnostics produced for that category.</param>
/// <param name="SeverityToCount">Mapping from <see cref="Diagnostic.Severity"/> to the number of diagnostics produced for that severity.</param>
[DataContract]
internal readonly record struct CopilotDiagnosticAnalysis(
[property: DataMember(Order = 0)] DiagnosticKind Kind,
[property: DataMember(Order = 1)] TimeSpan ComputationTime,
[property: DataMember(Order = 2)] Dictionary<string, int> IdToCount,
[property: DataMember(Order = 3)] Dictionary<string, int> CategoryToCount,
[property: DataMember(Order = 4)] Dictionary<DiagnosticSeverity, int> SeverityToCount);

/// <param name="TotalComputationTime">Total time to compute code fixes for the changed regions.</param>
/// <param name="TotalApplicationTime">Total time to apply code fixes for the changed regions.</param>
/// <param name="DiagnosticIdToCount">Mapping from diagnostic id to to how many diagnostics with that id had fixes.</param>
/// <param name="DiagnosticIdToApplicationTime">Mapping from diagnostic id to the total time taken to fix diagnostics with that id.</param>
/// <param name="DiagnosticIdToProviderName">Mapping from diagnostic id to the name of the provider that provided the fix.</param>
/// <param name="ProviderNameToApplicationTime">Mapping from provider name to the total time taken to fix diagnostics with that provider.</param>
[DataContract]
internal readonly record struct CopilotCodeFixAnalysis(
[property: DataMember(Order = 0)] TimeSpan TotalComputationTime,
[property: DataMember(Order = 1)] TimeSpan TotalApplicationTime,
[property: DataMember(Order = 2)] Dictionary<string, int> DiagnosticIdToCount,
[property: DataMember(Order = 3)] Dictionary<string, TimeSpan> DiagnosticIdToApplicationTime,
[property: DataMember(Order = 4)] Dictionary<string, HashSet<string>> DiagnosticIdToProviderName,
[property: DataMember(Order = 5)] Dictionary<string, TimeSpan> ProviderNameToApplicationTime);
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Copilot;

internal interface ICopilotChangeAnalysisService : IWorkspaceService
{
/// <summary>
/// Kicks of work to analyze a change that copilot suggested making to a document. <paramref name="document"/> is
/// the state of the document prior to the edits, and <paramref name="changes"/> are the changes Copilot wants to
/// make to it. <paramref name="changes"/> must be sorted and normalized before calling this.
/// </summary>
Task<CopilotChangeAnalysis> AnalyzeChangeAsync(Document document, ImmutableArray<TextChange> changes, CancellationToken cancellationToken);
}

[ExportWorkspaceServiceFactory(typeof(ICopilotChangeAnalysisService)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class DefaultCopilotChangeAnalysisServiceFactory(
ICodeFixService codeFixService,
IDiagnosticAnalyzerService diagnosticAnalyzerService) : IWorkspaceServiceFactory
{
public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
=> new DefaultCopilotChangeAnalysisService(codeFixService, diagnosticAnalyzerService, workspaceServices);

private sealed class DefaultCopilotChangeAnalysisService(
ICodeFixService codeFixService,
IDiagnosticAnalyzerService diagnosticAnalyzerService,
HostWorkspaceServices workspaceServices) : ICopilotChangeAnalysisService
{
private readonly ICodeFixService _codeFixService = codeFixService;
private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService = diagnosticAnalyzerService;
private readonly HostWorkspaceServices _workspaceServices = workspaceServices;

public async Task<CopilotChangeAnalysis> AnalyzeChangeAsync(
Document document,
ImmutableArray<TextChange> changes,
CancellationToken cancellationToken)
{
if (!document.SupportsSemanticModel)
return default;

Contract.ThrowIfTrue(!changes.IsSorted(static (c1, c2) => c1.Span.Start - c2.Span.Start), "'changes' was not sorted.");
Contract.ThrowIfTrue(new NormalizedTextSpanCollection(changes.Select(c => c.Span)).Count != changes.Length, "'changes' was not normalized.");
Contract.ThrowIfTrue(document.Project.Solution.Workspace != _workspaceServices.Workspace);

var client = await RemoteHostClient.TryGetClientAsync(
_workspaceServices.Workspace, cancellationToken).ConfigureAwait(false);

if (client != null)
{
var value = await client.TryInvokeAsync<IRemoteCopilotChangeAnalysisService, CopilotChangeAnalysis>(
// Don't need to sync the entire solution over. Just the cone of projects this document it contained within.
document.Project,
(service, checksum, cancellationToken) => service.AnalyzeChangeAsync(checksum, document.Id, changes, cancellationToken),
cancellationToken).ConfigureAwait(false);
return value.HasValue ? value.Value : default;
}
else
{
return await AnalyzeChangeInCurrentProcessAsync(document, changes, cancellationToken).ConfigureAwait(false);
}
}

private async Task<CopilotChangeAnalysis> AnalyzeChangeInCurrentProcessAsync(
Document document,
ImmutableArray<TextChange> changes,
CancellationToken cancellationToken)
{
return default;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be implemented in a followup PR today.

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.Host;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Copilot;

/// <summary>Remote version of <see cref="ICopilotChangeAnalysisService"/></summary>
internal interface IRemoteCopilotChangeAnalysisService : IWorkspaceService
{
/// <inheritdoc cref="ICopilotChangeAnalysisService.AnalyzeChangeAsync"/>
ValueTask<CopilotChangeAnalysis> AnalyzeChangeAsync(
Checksum solutionChecksum, DocumentId documentId, ImmutableArray<TextChange> edits, CancellationToken cancellationToken);
}
1 change: 1 addition & 0 deletions src/Workspaces/Core/Portable/CodeActions/CodeAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeCleanup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal static class FeatureAttribute
public const string CompletionSet = nameof(CompletionSet);
public const string CopilotImplementNotImplementedException = nameof(CopilotImplementNotImplementedException);
public const string CopilotSuggestions = nameof(CopilotSuggestions);
public const string CopilotChangeAnalysis = nameof(CopilotChangeAnalysis);
public const string DesignerAttributes = nameof(DesignerAttributes);
public const string DiagnosticService = nameof(DiagnosticService);
public const string DocumentOutline = nameof(DocumentOutline);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public InProcRemoteServices(SolutionServices workspaceServices, TraceListener? t
RegisterRemoteBrokeredService(new RemoteAsynchronousOperationListenerService.Factory());
RegisterRemoteBrokeredService(new RemoteCodeLensReferencesService.Factory());
RegisterRemoteBrokeredService(new RemoteConvertTupleToStructCodeRefactoringService.Factory());
RegisterRemoteBrokeredService(new RemoteCopilotChangeAnalysisService.Factory());
RegisterRemoteBrokeredService(new RemoteDependentTypeFinderService.Factory());
RegisterRemoteBrokeredService(new RemoteDesignerAttributeDiscoveryService.Factory());
RegisterRemoteBrokeredService(new RemoteDiagnosticAnalyzerService.Factory());
Expand Down
2 changes: 2 additions & 0 deletions src/Workspaces/Remote/Core/ServiceDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.CodeLens;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.ConvertTupleToStruct;
using Microsoft.CodeAnalysis.Copilot;
using Microsoft.CodeAnalysis.DesignerAttribute;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.DocumentHighlighting;
Expand Down Expand Up @@ -58,6 +59,7 @@ internal sealed class ServiceDescriptors
(typeof(IRemoteAsynchronousOperationListenerService), null),
(typeof(IRemoteCodeLensReferencesService), null),
(typeof(IRemoteConvertTupleToStructCodeRefactoringService), null),
(typeof(IRemoteCopilotChangeAnalysisService), null),
(typeof(IRemoteDependentTypeFinderService), null),
(typeof(IRemoteDesignerAttributeDiscoveryService), typeof(IRemoteDesignerAttributeDiscoveryService.ICallback)),
(typeof(IRemoteDiagnosticAnalyzerService), null),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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.Copilot;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Remote;

internal sealed partial class RemoteCopilotChangeAnalysisService(
in BrokeredServiceBase.ServiceConstructionArguments arguments)
: BrokeredServiceBase(arguments), IRemoteCopilotChangeAnalysisService
{
internal sealed class Factory : FactoryBase<IRemoteCopilotChangeAnalysisService>
{
protected override IRemoteCopilotChangeAnalysisService CreateService(in ServiceConstructionArguments arguments)
=> new RemoteCopilotChangeAnalysisService(arguments);
}

public ValueTask AnalyzeChangeAsync(
Checksum solutionChecksum,
DocumentId documentId,
ImmutableArray<TextChange> edits,
CancellationToken cancellationToken)
{
return RunServiceAsync(solutionChecksum, async solution =>
{
var document = await solution.GetRequiredDocumentAsync(
documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);

var service = solution.Services.GetRequiredService<ICopilotChangeAnalysisService>();
await service.AnalyzeChangeAsync(
document, edits, cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Copilot;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

Expand Down
Loading