File: Suggestions\RefineUsingCopilot\RefineUsingCopilotCodeAction.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_tpal30ww_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures.Wpf)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Copilot;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions;
 
internal partial class SuggestedActionWithNestedFlavors
{
    private partial class RefineUsingCopilotSuggestedAction
    {
        /// <summary>
        /// Code action that triggers Copilot refinement session to add further
        /// code changes on top of the changes from the wrapped <paramref name="originalCodeAction"/>.
        /// </summary>
        private sealed class RefineUsingCopilotCodeAction(
            Solution originalSolution,
            CodeAction originalCodeAction,
            DiagnosticData? primaryDiagnostic,
            ICopilotCodeAnalysisService copilotCodeAnalysisService) : CodeAction
        {
            public override string Title => EditorFeaturesResources.Refine_using_Copilot;
 
            protected override Task<IEnumerable<CodeActionOperation>> ComputePreviewOperationsAsync(CancellationToken cancellationToken)
            {
                // Make sure we don't trigger the refinement session for preview operation
                return Task.FromResult(SpecializedCollections.EmptyEnumerable<CodeActionOperation>());
            }
 
            protected override async Task<ImmutableArray<CodeActionOperation>> ComputeOperationsAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
            {
                // This method is called when the user has clicked on the 'Refine using Copilot'
                // hyperlink in the lightbulb preview.
                // We want to bring up Copilot refinement session on top of the code changes
                // from the underlying code action. Additionally, if the underlying code action
                // came from a prior Copilot code fix suggestion, we also want to pass in the Copilot
                // diagnostic to the refinement session. This diagnostic would be mapped to the prior
                // Copilot session id to ensure that the Copilot refinement session has the historical
                // context on the Copilot conversation that produce the underlying diagnostic/code action.
                // 
                // We have a bunch of checks upfront before we bring up the Copilot refinement:
                //  - Applying the underlying code action produces a non-null newSolution.
                //  - The underlying code action produces change(s) to exactly one source document.
                //
                // TODO: Currently, we start a task to spawn a new Copilot refinement session
                //       at the end of this method, without waiting for the refinement session to complete.
                //       Consider if there could be better UX/platform support for such flavored actions
                //       where clicking on the hyperlink needs to bring up another unrelated UI.
 
                var newSolution = await originalCodeAction.GetChangedSolutionInternalAsync(originalSolution, progress, cancellationToken: cancellationToken).ConfigureAwait(false);
                if (newSolution == null)
                    return [];
 
                var changes = newSolution.GetChanges(originalSolution);
                var changeSummary = new SolutionChangeSummary(originalSolution, newSolution, changes);
                if (changeSummary.TotalFilesAffected != 1
                    || changeSummary.TotalProjectsAffected != 1
                    || changeSummary.NewSolution.GetChangedDocuments(changeSummary.OldSolution).FirstOrDefault() is not { } changedDocumentId)
                {
                    return [];
                }
 
                var oldDocument = changeSummary.OldSolution.GetRequiredDocument(changedDocumentId);
                var newDocument = changeSummary.NewSolution.GetRequiredDocument(changedDocumentId);
 
                var convertedPrimaryDiagnostic = primaryDiagnostic != null
                    ? await primaryDiagnostic.ToDiagnosticAsync(oldDocument.Project, cancellationToken).ConfigureAwait(false)
                    : null;
 
                cancellationToken.ThrowIfCancellationRequested();
                return [new OpenRefinementSessionOperation(oldDocument, newDocument, convertedPrimaryDiagnostic, copilotCodeAnalysisService)];
            }
 
            /// <summary>
            /// A code action operation for trigger Copilot Chat inline refinement session.
            /// </summary>
            private sealed class OpenRefinementSessionOperation(
                Document oldDocument,
                Document newDocument,
                Diagnostic? convertedPrimaryDiagnostic,
                ICopilotCodeAnalysisService copilotCodeAnalysisService) : CodeActionOperation
            {
                internal override async Task<bool> TryApplyAsync(Workspace workspace, Solution originalSolution, IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
                {
                    // Trigger the Copilot refinement session in background, passing in the old and new document for
                    // the base code changes on top of which we want to perform further refinement.
                    // Note that we do not pass in our cancellation token to the StartRefinementSessionAsync
                    // call as bringing up the refinement session is a quick operation and the refinement session
                    // has it's own cancellation token source to allow users to dismiss the session.
                    // Additionally, we do not want cancellation triggered on the token passed into
                    // GetChangedSolutionAsync to suddenly dismiss the refinement session UI without user explicitly
                    // dismissing the session.
                    await copilotCodeAnalysisService.StartRefinementSessionAsync(oldDocument, newDocument, convertedPrimaryDiagnostic, CancellationToken.None).ConfigureAwait(false);
                    return true;
                }
            }
        }
    }
}