Skip to content

Commit 66049f5

Browse files
Automatically apply fixes to copilot changes (#79067)
2 parents fbb35c2 + 9c58af6 commit 66049f5

27 files changed

+786
-16
lines changed

eng/targets/Services.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.CompilationAvailable" ClassName="Microsoft.CodeAnalysis.Remote.RemoteCompilationAvailableService+Factory" />
1818
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.ConvertTupleToStructCodeRefactoring" ClassName="Microsoft.CodeAnalysis.Remote.RemoteConvertTupleToStructCodeRefactoringService+Factory" />
1919
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.CopilotChangeAnalysis" ClassName="Microsoft.CodeAnalysis.Remote.RemoteCopilotChangeAnalysisService+Factory" />
20+
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.CopilotProposalAdjuster" ClassName="Microsoft.CodeAnalysis.Remote.RemoteCopilotProposalAdjusterService+Factory" />
2021
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.DependentTypeFinder" ClassName="Microsoft.CodeAnalysis.Remote.RemoteDependentTypeFinderService+Factory" />
2122
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.DesignerAttributeDiscovery" ClassName="Microsoft.CodeAnalysis.Remote.RemoteDesignerAttributeDiscoveryService+Factory" />
2223
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.DiagnosticAnalyzer" ClassName="Microsoft.CodeAnalysis.Remote.RemoteDiagnosticAnalyzerService+Factory" />

src/EditorFeatures/Core/Copilot/CopilotEditorUtilities.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ internal static class CopilotEditorUtilities
2323
/// the same <see cref="ITextSnapshot"/>. Will also fail if this edits non-roslyn files, or files
2424
/// we cannot process semantics for (like non C#/VB files).
2525
/// </remarks>
26-
public static Solution? TryGetAffectedSolution(ProposalBase proposal)
26+
public static (Solution? affectedSolution, string? failureReason) TryGetAffectedSolution(ProposalBase proposal)
2727
{
2828
Solution? solution = null;
2929
foreach (var edit in proposal.Edits)
@@ -32,22 +32,22 @@ internal static class CopilotEditorUtilities
3232

3333
// Edit touches a file roslyn doesn't know about. Don't touch this.
3434
if (document is null)
35-
return null;
35+
return (null, "NonRoslynDocumentAffected");
3636

3737
// Only bother for languages we can actually process semantics for.
3838
if (document.SupportsSemanticModel)
39-
return null;
39+
return (null, "NonSemanticDocumentAffected");
4040

4141
var currentSolution = document.Project.Solution;
4242

4343
// Edit touches multiple solutions. Don't bother with this for now for simplicity's sake.
4444
if (solution != null && solution != currentSolution)
45-
return null;
45+
return (null, "MultipleSolutionsAffected");
4646

4747
solution = currentSolution;
4848
}
4949

50-
return solution;
50+
return (solution, null);
5151
}
5252

5353
/// <inheritdoc cref="CopilotUtilities.TryNormalizeCopilotTextChanges"/>

src/EditorFeatures/Core/Copilot/CopilotWpfTextCreationListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private static async ValueTask ProcessCompletionEventAsync(
107107
const string featureId = "Completion";
108108
var proposalId = proposal.ProposalId;
109109

110-
var solution = CopilotEditorUtilities.TryGetAffectedSolution(proposal);
110+
var (solution, _) = CopilotEditorUtilities.TryGetAffectedSolution(proposal);
111111
if (solution is null)
112112
return;
113113

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.ComponentModel.Composition;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis.Editor;
12+
using Microsoft.CodeAnalysis.ErrorReporting;
13+
using Microsoft.CodeAnalysis.Host.Mef;
14+
using Microsoft.CodeAnalysis.Internal.Log;
15+
using Microsoft.CodeAnalysis.Remote;
16+
using Microsoft.CodeAnalysis.Text;
17+
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
18+
using Microsoft.VisualStudio.Language.Proposals;
19+
using Microsoft.VisualStudio.Utilities;
20+
using Roslyn.Utilities;
21+
22+
namespace Microsoft.CodeAnalysis.Copilot;
23+
24+
// The entire AdjusterProvider api is marked as obsolete since this is a preview API. So we do the same here as well.
25+
[Obsolete("This is a preview api and subject to change")]
26+
[ContentType(ContentTypeNames.CSharpContentType)]
27+
[ContentType(ContentTypeNames.VisualBasicContentType)]
28+
[method: ImportingConstructor]
29+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
30+
internal sealed class RoslynProposalAdjusterProvider() : ProposalAdjusterProviderBase
31+
{
32+
public override Task<ProposalBase> AdjustProposalBeforeDisplayAsync(ProposalBase proposal, string providerName, CancellationToken cancellationToken)
33+
=> AdjustProposalAsync(proposal, providerName, before: true, cancellationToken);
34+
35+
public override Task<ProposalBase> AdjustProposalAfterAcceptAsync(ProposalBase proposal, string providerName, CancellationToken cancellationToken)
36+
=> AdjustProposalAsync(proposal, providerName, before: false, cancellationToken);
37+
38+
private static void SetDefaultTelemetryProperties(Dictionary<string, object?> map, string providerName, bool before, TimeSpan elapsedTime)
39+
{
40+
// Common properties that all adjustments will log.
41+
map["ProviderName"] = providerName;
42+
map["AdjustProposalBeforeDisplay"] = before;
43+
map["ComputationTime"] = elapsedTime.TotalMilliseconds.ToString("G17");
44+
}
45+
46+
private async Task<ProposalBase> AdjustProposalAsync(
47+
ProposalBase proposal, string providerName, bool before, CancellationToken cancellationToken)
48+
{
49+
var stopwatch = SharedStopwatch.StartNew();
50+
51+
// Ensure we're only operating on one solution. It makes the logic much simpler, as we don't have to
52+
// worry about edits that touch multiple solutions.
53+
var (solution, failureReason) = CopilotEditorUtilities.TryGetAffectedSolution(proposal);
54+
if (solution is null)
55+
{
56+
// If we can't find a solution, then we can't adjust the proposal. Log telemetry and return the original proposal.
57+
Logger.LogBlock(FunctionId.Copilot_AdjustProposal, KeyValueLogMessage.Create(static (d, args) =>
58+
{
59+
var (providerName, before, failureReason, elapsedTime) = args;
60+
SetDefaultTelemetryProperties(d, providerName, before, elapsedTime);
61+
d["SolutionAcquisitionFailure"] = failureReason;
62+
},
63+
args: (providerName, before, failureReason, stopwatch.Elapsed)),
64+
cancellationToken).Dispose();
65+
66+
return proposal;
67+
}
68+
69+
return await AdjustProposalAsync(
70+
solution, proposal, providerName, before, stopwatch, cancellationToken).ConfigureAwait(false);
71+
}
72+
73+
private async Task<ProposalBase> AdjustProposalAsync(
74+
Solution solution,
75+
ProposalBase proposal,
76+
string providerName,
77+
bool before,
78+
SharedStopwatch stopwatch,
79+
CancellationToken cancellationToken)
80+
{
81+
try
82+
{
83+
var (newProposal, adjustmentsProposed) = await AdjustProposalAsync(
84+
solution, proposal, cancellationToken).ConfigureAwait(false);
85+
var adjustmentsAccepted = newProposal != proposal;
86+
87+
// Report telemetry if we were or were not able to adjust the proposal.
88+
Logger.LogBlock(FunctionId.Copilot_AdjustProposal, KeyValueLogMessage.Create(static (d, args) =>
89+
{
90+
var (providerName, before, adjustmentsProposed, adjustmentsAccepted, elapsedTime) = args;
91+
SetDefaultTelemetryProperties(d, providerName, before, elapsedTime);
92+
d["AdjustmentsProposed"] = adjustmentsProposed;
93+
d["AdjustmentsAccepted"] = adjustmentsAccepted;
94+
},
95+
args: (providerName, before, adjustmentsProposed, adjustmentsAccepted, stopwatch.Elapsed)),
96+
cancellationToken).Dispose();
97+
98+
return newProposal;
99+
}
100+
catch (Exception ex) when (ReportFailureTelemetry(ex))
101+
{
102+
// Don't leak out any exceptions. We report them as telemetry (and/or NFW), but we don't want to block the
103+
// user getting the original proposal.
104+
return proposal;
105+
}
106+
107+
bool ReportFailureTelemetry(Exception ex)
108+
{
109+
// If it's not cancellation, report an NFW to track down our bug.
110+
if (ex is not OperationCanceledException)
111+
FatalError.ReportAndCatch(ex);
112+
113+
Logger.LogBlock(FunctionId.Copilot_AdjustProposal, KeyValueLogMessage.Create(static (d, args) =>
114+
{
115+
var (providerName, before, ex, elapsedTime) = args;
116+
SetDefaultTelemetryProperties(d, providerName, before, elapsedTime);
117+
d["AdjustmentsProposed"] = false;
118+
119+
if (ex is OperationCanceledException)
120+
{
121+
d["Canceled"] = true;
122+
}
123+
else
124+
{
125+
// Will be able to get a count of how often this happens. NFWs can be used to track down the issue.
126+
d["Failed"] = true;
127+
}
128+
},
129+
args: (providerName, before, ex, stopwatch.Elapsed)),
130+
cancellationToken).Dispose();
131+
132+
return true;
133+
}
134+
}
135+
136+
private async Task<(ProposalBase newProposal, bool adjustmentsProposed)> AdjustProposalAsync(
137+
Solution solution, ProposalBase proposal, CancellationToken cancellationToken)
138+
{
139+
// We're potentially making multiple calls to oop here. So keep a session alive to avoid
140+
// resyncing the solution and recomputing compilations.
141+
using var _1 = await RemoteKeepAliveSession.CreateAsync(solution, cancellationToken).ConfigureAwait(false);
142+
using var _2 = PooledObjects.ArrayBuilder<ProposedEdit>.GetInstance(out var finalEdits);
143+
144+
var adjustmentsProposed = false;
145+
foreach (var editGroup in proposal.Edits.GroupBy(e => e.Span.Snapshot))
146+
{
147+
cancellationToken.ThrowIfCancellationRequested();
148+
149+
var snapshot = editGroup.Key;
150+
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
151+
152+
// Checked in TryGetAffectedSolution
153+
Contract.ThrowIfNull(document);
154+
155+
var proposalAdjusterService = document.Project.Solution.Services.GetRequiredService<ICopilotProposalAdjusterService>();
156+
var proposedEdits = await proposalAdjusterService.TryAdjustProposalAsync(
157+
document, CopilotEditorUtilities.TryGetNormalizedTextChanges(editGroup), cancellationToken).ConfigureAwait(false);
158+
159+
if (proposedEdits.IsDefault)
160+
{
161+
// No changes were made to the proposal. Just add the original edits.
162+
finalEdits.AddRange(editGroup);
163+
}
164+
else
165+
{
166+
// Changes were made to the proposal. Add the new edits.
167+
adjustmentsProposed = true;
168+
foreach (var proposedEdit in proposedEdits)
169+
{
170+
finalEdits.Add(new ProposedEdit(
171+
new(snapshot, proposedEdit.Span.ToSpan()),
172+
proposedEdit.NewText!));
173+
}
174+
}
175+
}
176+
177+
// No adjustments were made. Don't touch anything.
178+
if (!adjustmentsProposed)
179+
return (proposal, adjustmentsProposed: false);
180+
181+
// We have some changes we want to to make to the proposal. See if the proposal system allows us merging
182+
// those changes in.
183+
var newProposal = Proposal.TryCreateProposal(proposal, finalEdits);
184+
return (newProposal ?? proposal, adjustmentsProposed: true);
185+
}
186+
}

0 commit comments

Comments
 (0)