Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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 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,131 @@
// 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 IAsynchronousOperationListener _listener;

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;
_listener = listenerProvider.GetListener(FeatureAttribute.CopilotChangeAnalysis);
_workQueue = new AsyncBatchingWorkQueue<SuggestionAcceptedEventArgs>(
DelayTimeSpan.Idle,
ProcessEventsAsync,
_listener,
_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)
{
var token = _listener.BeginAsyncOperation(nameof(TextViewCreated));
Task.Run(() =>
{
var suggestionService = _suggestionServiceBase.Value;
suggestionService.SuggestionAccepted += OnSuggestionAccepted;
}).CompletesAsyncOperation(token);
}
}

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,84 @@
// 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);
}

[ExportWorkspaceService(typeof(ICopilotChangeAnalysisService)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class DefaultCopilotChangeAnalysisService(
[Import(AllowDefault = true)] ICodeFixService? codeFixService = null,
[Import(AllowDefault = true)] IDiagnosticAnalyzerService? diagnosticAnalyzerService = null) : ICopilotChangeAnalysisService
{
#pragma warning disable IDE0052 // Remove unread private members
private readonly ICodeFixService? _codeFixService = codeFixService;
private readonly IDiagnosticAnalyzerService? _diagnosticAnalyzerService = diagnosticAnalyzerService;
#pragma warning restore IDE0052 // Remove unread private members

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.");

var client = await RemoteHostClient.TryGetClientAsync(document.Project, 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);
}
}

#pragma warning disable CA1822 // Mark members as static
#pragma warning disable IDE0060 // Remove unused parameter
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
private async Task<CopilotChangeAnalysis> AnalyzeChangeInCurrentProcessAsync(
Document document,
ImmutableArray<TextChange> changes,
CancellationToken cancellationToken)
{
return default;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
#pragma warning restore IDE0060 // Remove unused parameter
#pragma warning restore CA1822 // Mark members as static
}
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Threading;
using Microsoft.CodeAnalysis.Threading;

namespace Microsoft.CodeAnalysis.Diagnostics;

Expand Down Expand Up @@ -88,7 +88,7 @@ public async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsForSpanAsync(
var analyzer = CreateIncrementalAnalyzer(document.Project.Solution.Workspace);

// always make sure that analyzer is called on background thread.
await TaskScheduler.Default;
await Task.Yield().ConfigureAwait(false);
priorityProvider ??= new DefaultCodeActionRequestPriorityProvider();

return await analyzer.GetDiagnosticsForSpanAsync(
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
Loading
Loading