File: Copilot\CopilotWpfTextCreationListener.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures_l5l0octc_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.Options;
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 IGlobalOptionService _globalOptions;
    private readonly IThreadingContext _threadingContext;
    private readonly Lazy<SuggestionServiceBase> _suggestionServiceBase;
    private readonly IAsynchronousOperationListener _listener;
 
    private readonly AsyncBatchingWorkQueue<(bool accepted, ProposalBase proposal)> _completionWorkQueue;
 
    private int _started;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public CopilotWpfTextViewCreationListener(
        IGlobalOptionService globalOptions,
        IThreadingContext threadingContext,
        Lazy<SuggestionServiceBase> suggestionServiceBase,
        IAsynchronousOperationListenerProvider listenerProvider)
    {
        _globalOptions = globalOptions;
        _threadingContext = threadingContext;
        _suggestionServiceBase = suggestionServiceBase;
        _listener = listenerProvider.GetListener(FeatureAttribute.CopilotChangeAnalysis);
 
        _completionWorkQueue = new AsyncBatchingWorkQueue<(bool accepted, ProposalBase proposal)>(
            DelayTimeSpan.Idle,
            ProcessCompletionEventsAsync,
            _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 += OnCompletionSuggestionAccepted;
                suggestionService.SuggestionDismissed += OnCompletionSuggestionDismissed;
            }).CompletesAsyncOperation(token);
        }
    }
 
    private void OnCompletionSuggestionAccepted(object sender, SuggestionAcceptedEventArgs e)
        => OnCompletionSuggestionEvent(accepted: true, e.FinalProposal);
 
    private void OnCompletionSuggestionDismissed(object sender, SuggestionDismissedEventArgs e)
        => OnCompletionSuggestionEvent(accepted: false, e.FinalProposal);
 
    private void OnCompletionSuggestionEvent(bool accepted, ProposalBase? proposal)
    {
        if (proposal is not { Edits.Count: > 0 })
            return;
 
        _completionWorkQueue.AddWork((accepted, proposal));
    }
 
    private async ValueTask ProcessCompletionEventsAsync(
        ImmutableSegmentedList<(bool accepted, ProposalBase proposal)> list, CancellationToken cancellationToken)
    {
        // Ignore if analyzing changes is disabled for this user.
        if (!_globalOptions.GetOption(CopilotOptions.AnalyzeCopilotChanges))
            return;
 
        foreach (var (accepted, proposal) in list)
            await ProcessCompletionEventAsync(accepted, proposal, cancellationToken).ConfigureAwait(false);
    }
 
    private static async ValueTask ProcessCompletionEventAsync(
        bool accepted, ProposalBase proposal, CancellationToken cancellationToken)
    {
        const string featureId = "Completion";
        var proposalId = proposal.ProposalId;
 
        foreach (var editGroup in proposal.Edits.GroupBy(e => e.Span.Snapshot))
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            var snapshot = editGroup.Key;
            var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
 
            if (document is null)
                continue;
 
            // Currently we do not support analyzing languges other than C# and VB.  This is because we only want to do
            // this analsis in our OOP process to avoid perf impact on the VS process.  And we don't have OOP for other
            // languages yet.
            if (!document.SupportsSemanticModel)
                continue;
 
            var normalizedEdits = Normalize(editGroup);
            if (normalizedEdits.IsDefaultOrEmpty)
                continue;
 
            var changeAnalysisService = document.Project.Solution.Services.GetRequiredService<ICopilotChangeAnalysisService>();
            var analysisResult = await changeAnalysisService.AnalyzeChangeAsync(
                document, normalizedEdits, cancellationToken).ConfigureAwait(false);
 
            CopilotChangeAnalysisUtilities.LogCopilotChangeAnalysis(
                featureId, accepted, proposalId, analysisResult, cancellationToken).Dispose();
        }
    }
 
    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();
    }
}