diff --git a/eng/Packages.props b/eng/Packages.props
index 79e87dbd81d6f..358d86df78b58 100644
--- a/eng/Packages.props
+++ b/eng/Packages.props
@@ -131,7 +131,7 @@
-
+
diff --git a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs
index 42cc6155d1722..aaa78055241fb 100644
--- a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs
+++ b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs
@@ -33,21 +33,30 @@ public static InternalContracts.ManagedHotReloadAvailability ToContract(this Man
=> new((InternalContracts.ManagedHotReloadAvailabilityStatus)value.Status, value.LocalizedMessage);
public static ManagedHotReloadUpdate FromContract(this InternalContracts.ManagedHotReloadUpdate update)
- => new(
- module: update.Module,
- moduleName: update.ModuleName,
- ilDelta: update.ILDelta,
- metadataDelta: update.MetadataDelta,
- pdbDelta: update.PdbDelta,
- updatedTypes: update.UpdatedTypes,
- requiredCapabilities: update.RequiredCapabilities,
- updatedMethods: update.UpdatedMethods,
- sequencePoints: update.SequencePoints.SelectAsArray(FromContract),
- activeStatements: update.ActiveStatements.SelectAsArray(FromContract),
- exceptionRegions: update.ExceptionRegions.SelectAsArray(FromContract));
+ => new()
+ {
+ Module = update.Module,
+ ModuleName = update.ModuleName,
+ ILDelta = update.ILDelta,
+ MetadataDelta = update.MetadataDelta,
+ PdbDelta = update.PdbDelta,
+ UpdatedTypes = update.UpdatedTypes,
+ RequiredCapabilities = update.RequiredCapabilities,
+ UpdatedMethods = update.UpdatedMethods,
+ SequencePoints = update.SequencePoints.SelectAsArray(FromContract),
+ ActiveStatements = update.ActiveStatements.SelectAsArray(FromContract),
+ ExceptionRegions = update.ExceptionRegions.SelectAsArray(FromContract)
+ };
public static ManagedHotReloadUpdates FromContract(this InternalContracts.ManagedHotReloadUpdates updates)
- => new(updates.Updates.FromContract(), updates.Diagnostics.FromContract(), updates.ProjectsToRebuild.SelectAsArray(FromContract), updates.ProjectsToRestart.SelectAsArray(FromContract));
+ => new()
+ {
+ Updates = updates.Updates.FromContract(),
+ Diagnostics = updates.Diagnostics.FromContract(),
+ ProjectInstancesToRebuild = updates.ProjectsToRebuild.SelectAsArray(FromContract),
+ ProjectInstancesToRestart = updates.ProjectsToRestart.SelectAsArray(FromContract),
+ HasPendingUpdates = updates.HasPendingUpdates,
+ };
public static ImmutableArray FromContract(this ImmutableArray diagnostics)
=> diagnostics.SelectAsArray(FromContract);
diff --git a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs
index 56714364e9a7f..40a2cd6b6ce39 100644
--- a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs
+++ b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs
@@ -7,7 +7,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Debugger.Contracts.HotReload;
-using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue;
namespace Microsoft.CodeAnalysis.EditAndContinue;
@@ -61,4 +60,8 @@ public ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken)
public ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken)
=> impl.HasChangesAsync(sourceFilePath, cancellationToken);
+
+ // internal for testing:
+ internal ManagedHotReloadLanguageServiceImpl Impl
+ => impl;
}
diff --git a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs
index 91ee6d2baf950..5154c15303def 100644
--- a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs
+++ b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs
@@ -45,7 +45,20 @@ public NoSessionException()
private DebuggingSessionProxy? _debuggingSession;
private Solution? _pendingUpdatedSolution;
- private Solution? _committedSolution;
+
+ private Solution? CommittedSolution
+ {
+ get;
+ set
+ {
+ field = value;
+
+ if (value != null)
+ {
+ SolutionCommitted?.Invoke(value);
+ }
+ }
+ }
public event Action? SolutionCommitted;
@@ -84,7 +97,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
sourceTextProvider.Activate();
var currentSolution = await solutionSnapshotProvider.GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false);
- _committedSolution = currentSolution;
+ CommittedSolution = currentSolution;
sourceTextProvider.SetBaseline(currentSolution);
@@ -162,15 +175,7 @@ public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken)
var committedSolution = Interlocked.Exchange(ref _pendingUpdatedSolution, null);
Contract.ThrowIfNull(committedSolution);
- try
- {
- SolutionCommitted?.Invoke(committedSolution);
- }
- catch (Exception e) when (FatalError.ReportAndCatch(e))
- {
- }
-
- _committedSolution = committedSolution;
+ CommittedSolution = committedSolution;
try
{
@@ -226,7 +231,7 @@ public async ValueTask EndSessionAsync(CancellationToken cancellationToken)
sourceTextProvider.Deactivate();
_debuggingSession = null;
- _committedSolution = null;
+ CommittedSolution = null;
_pendingUpdatedSolution = null;
}
@@ -252,8 +257,8 @@ public async ValueTask HasChangesAsync(string? sourceFilePath, Cancellatio
return false;
}
- Contract.ThrowIfNull(_committedSolution);
- var oldSolution = _committedSolution;
+ Contract.ThrowIfNull(CommittedSolution);
+ var oldSolution = CommittedSolution;
var newSolution = await solutionSnapshotProvider.GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false);
return (sourceFilePath != null)
@@ -285,7 +290,14 @@ public async ValueTask GetUpdatesAsync(ImmutableArray GetUpdatesAsync(ImmutableArray ToProjectIntanceIds(IEnumerable ids)
=> ids.SelectAsArray(id =>
@@ -342,4 +357,13 @@ ImmutableArray ToProjectIntanceIds(IEnumerable ids
return new ProjectInstanceId(project.FilePath!, project.State.NameAndFlavor.flavor ?? "");
});
}
+
+ internal TestAccessor GetTestAccessor()
+ => new(this);
+
+ internal readonly struct TestAccessor(ManagedHotReloadLanguageServiceImpl instance)
+ {
+ public Solution? PendingUpdatedSolution => instance._pendingUpdatedSolution;
+ public Solution? CommittedSolution => instance.CommittedSolution;
+ }
}
diff --git a/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs
index 9fd927bcdb044..57d647f0465e0 100644
--- a/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs
+++ b/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs
@@ -111,7 +111,7 @@ private static string Inspect(DiagnosticData d)
(!string.IsNullOrWhiteSpace(d.DataLocation.UnmappedFileSpan.Path) ? $" {d.DataLocation.UnmappedFileSpan.Path}({d.DataLocation.UnmappedFileSpan.StartLinePosition.Line}, {d.DataLocation.UnmappedFileSpan.StartLinePosition.Character}, {d.DataLocation.UnmappedFileSpan.EndLinePosition.Line}, {d.DataLocation.UnmappedFileSpan.EndLinePosition.Character}):" : "") +
$" {d.Message}";
- private static string Inspect(Microsoft.VisualStudio.Debugger.Contracts.HotReload.ManagedHotReloadDiagnostic d)
+ private static string Inspect(DebuggerContracts.ManagedHotReloadDiagnostic d)
=> $"{d.Severity} {d.Id}:" +
(!string.IsNullOrWhiteSpace(d.FilePath) ? $" {d.FilePath}({d.Span.StartLine}, {d.Span.StartColumn}, {d.Span.EndLine}, {d.Span.EndColumn}):" : "") +
$" {d.Message}";
@@ -129,7 +129,7 @@ private TestWorkspace CreateEditorWorkspace(out Solution solution, out EditAndCo
((MockServiceBroker)workspace.Services.GetRequiredService().ServiceBroker).CreateService = t => t switch
{
- _ when t == typeof(Microsoft.VisualStudio.Debugger.Contracts.HotReload.IHotReloadLogger) => new MockHotReloadLogger(),
+ _ when t == typeof(DebuggerContracts.IHotReloadLogger) => new MockHotReloadLogger(),
_ => throw ExceptionUtilities.UnexpectedValue(t)
};
@@ -147,39 +147,60 @@ private TestWorkspace CreateEditorWorkspace(out Solution solution, out EditAndCo
return workspace;
}
- [Theory, CombinatorialData]
- public async Task Test(bool commitChanges)
+ private class TestContext : IDisposable
{
- var localComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures
- .AddExcludedPartTypes(
- typeof(EditAndContinueService.WorkspaceServiceFactory))
- .AddParts(
- typeof(NoCompilationLanguageService),
- typeof(MockHostWorkspaceProvider),
- typeof(MockServiceBrokerProvider),
- typeof(MockEditAndContinueServiceFactory),
- typeof(MockManagedHotReloadService));
+ public readonly TestWorkspace LocalWorkspace;
+ public readonly PdbMatchingSourceTextProvider PdbMatchingSourceTextProvider;
+ public readonly MockEditAndContinueService MockEncService;
+ public readonly ManagedHotReloadLanguageService LocalService;
- using var localWorkspace = new TestWorkspace(composition: localComposition);
+ public TestContext()
+ {
+ var localComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures
+ .AddExcludedPartTypes(
+ typeof(EditAndContinueService.WorkspaceServiceFactory))
+ .AddParts(
+ typeof(NoCompilationLanguageService),
+ typeof(MockHostWorkspaceProvider),
+ typeof(MockServiceBrokerProvider),
+ typeof(MockEditAndContinueServiceFactory),
+ typeof(MockManagedHotReloadService));
+
+ LocalWorkspace = new TestWorkspace(composition: localComposition);
+
+ var globalOptions = LocalWorkspace.GetService();
+ ((MockHostWorkspaceProvider)LocalWorkspace.GetService()).Workspace = LocalWorkspace;
+
+ ((MockServiceBroker)LocalWorkspace.Services.GetRequiredService().ServiceBroker).CreateService = t => t switch
+ {
+ _ when t == typeof(DebuggerContracts.IHotReloadLogger) => new MockHotReloadLogger(),
+ _ => throw ExceptionUtilities.UnexpectedValue(t)
+ };
- var globalOptions = localWorkspace.GetService();
- ((MockHostWorkspaceProvider)localWorkspace.GetService()).Workspace = localWorkspace;
+ MockEncService = (MockEditAndContinueService)LocalWorkspace.Services.GetRequiredService().Service;
- ((MockServiceBroker)localWorkspace.Services.GetRequiredService().ServiceBroker).CreateService = t => t switch
- {
- _ when t == typeof(DebuggerContracts.IHotReloadLogger) => new MockHotReloadLogger(),
- _ => throw ExceptionUtilities.UnexpectedValue(t)
- };
+ var localFactory = LocalWorkspace.GetService();
+ var localBroker = LocalWorkspace.Services.GetRequiredService().ServiceBroker;
+ var localSnapshotProvider = LocalWorkspace.GetService();
+ PdbMatchingSourceTextProvider = new PdbMatchingSourceTextProvider(LocalWorkspace);
+ LocalService = localFactory.Create(localBroker, localSnapshotProvider, LocalWorkspace.GetService(), PdbMatchingSourceTextProvider);
+ }
- MockEditAndContinueService mockEncService;
+ public void Dispose()
+ {
+ LocalWorkspace.Dispose();
+ PdbMatchingSourceTextProvider.Dispose();
+ }
+ }
- mockEncService = (MockEditAndContinueService)localWorkspace.Services.GetRequiredService().Service;
+ [Theory, CombinatorialData]
+ public async Task Test(bool commitChanges)
+ {
+ using var context = new TestContext();
- var localFactory = localWorkspace.GetService();
- var localBroker = localWorkspace.Services.GetRequiredService().ServiceBroker;
- var localSnapshotProvider = localWorkspace.GetService();
- using var pdbMatchingSourceTextProvider = new PdbMatchingSourceTextProvider(localWorkspace);
- var localService = localFactory.Create(localBroker, localSnapshotProvider, localWorkspace.GetService(), pdbMatchingSourceTextProvider);
+ var localWorkspace = context.LocalWorkspace;
+ var mockEncService = context.MockEncService;
+ var localService = context.LocalService;
await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
.AddTestProject("proj", out var projectId)
@@ -250,6 +271,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
return new()
{
+ SolutionAction = SolutionAction.PendingUpdate,
Solution = solution,
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []),
Diagnostics =
@@ -279,8 +301,8 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
};
};
- var runningProjectInfo = new Microsoft.VisualStudio.Debugger.Contracts.HotReload.RunningProjectInfo(
- new Microsoft.VisualStudio.Debugger.Contracts.HotReload.ProjectInstanceId(project.FilePath, "net10.0"),
+ var runningProjectInfo = new DebuggerContracts.RunningProjectInfo(
+ new DebuggerContracts.ProjectInstanceId(project.FilePath, "net10.0"),
restartAutomatically: false);
var updates = await localService.GetUpdatesAsync([runningProjectInfo], CancellationToken.None);
@@ -322,6 +344,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
return new()
{
+ SolutionAction = SolutionAction.PendingUpdate,
Solution = solution,
ModuleUpdates = new ModuleUpdates(
ModuleUpdateStatus.Ready,
@@ -416,6 +439,84 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
Assert.False(sessionState.IsSessionActive);
}
+ [Theory]
+ [InlineData(SolutionAction.None, null)]
+ [InlineData(SolutionAction.Committed, null)]
+ [InlineData(SolutionAction.PendingUpdate, true)]
+ [InlineData(SolutionAction.PendingUpdate, false)]
+ internal async Task SolutionActions(SolutionAction solutionAction, bool? commit)
+ {
+ using var context = new TestContext();
+
+ var localService = context.LocalService;
+ var localWorkspace = context.LocalWorkspace;
+ var serviceImpl = localService.Impl.GetTestAccessor();
+
+ context.MockEncService.StartDebuggingSessionImpl = (_, _, _, _) => new DebuggingSessionId(1);
+
+ context.MockEncService.EmitSolutionUpdateImpl = (solution, _, _) => new()
+ {
+ SolutionAction = solutionAction,
+ Solution = solution,
+ ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []),
+ Diagnostics = [],
+ SyntaxError = null,
+ ProjectsToRebuild = [],
+ ProjectsToRestart = [],
+ ProjectsToRedeploy = [],
+ };
+
+ await localService.StartSessionAsync(CancellationToken.None);
+
+ var initialSolution = localWorkspace.CurrentSolution;
+ Assert.Same(initialSolution, serviceImpl.CommittedSolution);
+
+ await localWorkspace.ChangeSolutionAsync(initialSolution.AddTestProject("proj", out var projectId).Solution);
+
+ var updatedSolution = localWorkspace.CurrentSolution;
+
+ await localService.GetUpdatesAsync(runningProjects: ImmutableArray.Empty, CancellationToken.None);
+
+ switch (solutionAction)
+ {
+ case SolutionAction.None:
+ Assert.Null(serviceImpl.PendingUpdatedSolution);
+ Assert.Same(initialSolution, serviceImpl.CommittedSolution);
+ break;
+
+ case SolutionAction.Committed:
+ Assert.Null(serviceImpl.PendingUpdatedSolution);
+ Assert.Same(updatedSolution, serviceImpl.CommittedSolution);
+ break;
+
+ case SolutionAction.PendingUpdate:
+ Assert.Same(updatedSolution, serviceImpl.PendingUpdatedSolution);
+ Assert.Same(initialSolution, serviceImpl.CommittedSolution);
+
+ if (commit.Value)
+ {
+ await localService.CommitUpdatesAsync(CancellationToken.None);
+
+ Assert.Null(serviceImpl.PendingUpdatedSolution);
+ Assert.Same(updatedSolution, serviceImpl.CommittedSolution);
+ }
+ else
+ {
+ await localService.DiscardUpdatesAsync(CancellationToken.None);
+
+ Assert.Null(serviceImpl.PendingUpdatedSolution);
+ Assert.Same(initialSolution, serviceImpl.CommittedSolution);
+ }
+
+ break;
+
+ default:
+ throw ExceptionUtilities.UnexpectedValue(solutionAction);
+ }
+
+ await localService.EndSessionAsync(CancellationToken.None);
+ }
+
[Fact]
public async Task DefaultPdbMatchingSourceTextProvider()
{
diff --git a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs
index 04149dfbb14d1..10b82919e80d3 100644
--- a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs
+++ b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs
@@ -9361,6 +9361,60 @@ public void PartialMember_DeleteInsert_AddFieldInitializer()
])
]);
+ [Fact]
+ public void PartialMember_DeleteInsert_AddFieldInitializer2()
+ => EditAndContinueValidation.VerifySemantics(
+ editScripts:
+ [
+ GetTopEdits("""
+ partial class C
+ {
+ public C() => M();
+
+ private partial void M();
+ }
+ """,
+ """
+ partial class C
+ {
+ public C() => M();
+
+ private partial void M();
+ }
+ """),
+ GetTopEdits("""
+ partial class C
+ {
+ private partial void M()
+ {
+ _ = 1;
+ }
+ }
+ """,
+ """
+ partial class C
+ {
+ private int f = 0;
+
+ private partial void M()
+ {
+ _ = 2;
+ }
+ }
+ """)
+ ],
+ results:
+ [
+ DocumentResults(),
+ DocumentResults(semanticEdits:
+ [
+ SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.f")),
+ SemanticEdit(SemanticEditKind.Update, c => c.GetParameterlessConstructor("C"), partialType: "C", preserveLocalVariables: true),
+ SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.M"), partialType: "C"),
+ ])
+ ],
+ capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType);
+
[Fact]
public void PartialMember_DeleteInsert_RemoveFieldInitializer()
=> EditAndContinueValidation.VerifySemantics(
diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs
index e8f0b167f8815..dd70554517faf 100644
--- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs
+++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs
@@ -8,17 +8,20 @@
namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue;
[DataContract]
-internal readonly struct ManagedHotReloadUpdates(ImmutableArray updates, ImmutableArray diagnostics, ImmutableArray projectsToRebuild, ImmutableArray projectsToRestart)
+internal readonly struct ManagedHotReloadUpdates
{
- [DataMember(Name = "updates")]
- public ImmutableArray Updates { get; } = updates;
+ [DataMember]
+ public ImmutableArray Updates { get; init; }
- [DataMember(Name = "diagnostics")]
- public ImmutableArray Diagnostics { get; } = diagnostics;
+ [DataMember]
+ public ImmutableArray Diagnostics { get; init; }
- [DataMember(Name = "projectsToRebuild")]
- public ImmutableArray ProjectsToRebuild { get; } = projectsToRebuild;
+ [DataMember]
+ public ImmutableArray ProjectsToRebuild { get; init; }
- [DataMember(Name = "projectsToRestart")]
- public ImmutableArray ProjectsToRestart { get; } = projectsToRestart;
+ [DataMember]
+ public ImmutableArray ProjectsToRestart { get; init; }
+
+ [DataMember]
+ public bool HasPendingUpdates { get; init; }
}
diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
index 811035619a1dc..7cedeb6f7a7fd 100644
--- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
+++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
@@ -35,6 +35,7 @@ internal sealed class DebuggingSession : IDisposable
private readonly CancellationTokenSource _cancellationSource = new();
internal readonly IPdbMatchingSourceTextProvider SourceTextProvider;
+ internal readonly EditAndContinueDiagnosticLevel DiagnosticLevel;
///
/// Logs debugging session events.
@@ -134,6 +135,7 @@ internal DebuggingSession(
IPdbMatchingSourceTextProvider sourceTextProvider,
TraceLog sessionLog,
TraceLog analysisLog,
+ EditAndContinueDiagnosticLevel diagnosticLevel,
bool reportDiagnostics)
{
sessionLog.Write($"Debugging session started: #{id}");
@@ -157,6 +159,7 @@ internal DebuggingSession(
inBreakState: false);
ReportDiagnostics = reportDiagnostics;
+ DiagnosticLevel = diagnosticLevel;
}
public void Dispose()
@@ -540,6 +543,8 @@ public async ValueTask EmitSolutionUpdateAsync(
solutionUpdate.Log(SessionLog, updateId);
_lastModuleUpdatesLog = solutionUpdate.ModuleUpdates.Updates;
+ SolutionAction solutionAction;
+
switch (solutionUpdate.ModuleUpdates.Status)
{
case ModuleUpdateStatus.Ready:
@@ -556,6 +561,7 @@ public async ValueTask EmitSolutionUpdateAsync(
solutionUpdate.ModuleUpdates.Updates,
solutionUpdate.NonRemappableRegions));
+ solutionAction = SolutionAction.PendingUpdate;
break;
case ModuleUpdateStatus.None:
@@ -569,6 +575,12 @@ public async ValueTask EmitSolutionUpdateAsync(
// No significant changes have been made.
// Commit the solution to apply any insignificant changes that do not generate updates.
LastCommittedSolution.CommitChanges(solution, solutionUpdate.StaleProjects);
+
+ solutionAction = SolutionAction.Committed;
+ break;
+
+ default:
+ solutionAction = SolutionAction.None;
break;
}
@@ -577,6 +589,7 @@ public async ValueTask EmitSolutionUpdateAsync(
return new EmitSolutionUpdateResults()
{
Solution = solution,
+ SolutionAction = solutionAction,
ModuleUpdates = solutionUpdate.ModuleUpdates,
Diagnostics = solutionUpdate.Diagnostics,
SyntaxError = solutionUpdate.SyntaxError,
diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticLevel.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticLevel.cs
new file mode 100644
index 0000000000000..6bc35e8236d99
--- /dev/null
+++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticLevel.cs
@@ -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.
+
+namespace Microsoft.CodeAnalysis.EditAndContinue;
+
+internal enum EditAndContinueDiagnosticLevel
+{
+ ///
+ /// No extra validation.
+ ///
+ None = 0,
+
+ ///
+ /// Adds extra validation that is normally not performed due to its impact on performance
+ /// and should only be used when diagnosing issues with EnC.
+ ///
+ Debug = 1,
+}
diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs
index 6dacf0f6fe69e..868511abf786e 100644
--- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs
+++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs
@@ -11,7 +11,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
-using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
@@ -44,6 +43,11 @@ internal sealed class WorkspaceService(IEditAndContinueService service) : IEditA
public IEditAndContinueService Service { get; } = service;
}
+ private static EditAndContinueDiagnosticLevel DiagnosticLevel
+ => byte.TryParse(Environment.GetEnvironmentVariable("Microsoft_CodeAnalysis_EditAndContinue_DiagnosticLevel"), out var level)
+ ? (EditAndContinueDiagnosticLevel)level
+ : EditAndContinueDiagnosticLevel.None;
+
private static readonly string? s_logDir = GetLogDirectory();
internal readonly TraceLog Log;
@@ -156,7 +160,7 @@ public DebuggingSessionId StartDebuggingSession(
solution = solution.WithUpToDateSourceGeneratorDocuments(solution.ProjectIds);
var sessionId = new DebuggingSessionId(Interlocked.Increment(ref s_debuggingSessionId));
- var session = new DebuggingSession(sessionId, solution, debuggerService, _compilationOutputsProvider, sourceTextProvider, Log, AnalysisLog, reportDiagnostics);
+ var session = new DebuggingSession(sessionId, solution, debuggerService, _compilationOutputsProvider, sourceTextProvider, Log, AnalysisLog, DiagnosticLevel, reportDiagnostics);
lock (_debuggingSessions)
{
diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs
index 55d9d652091dc..33ac3276fbbbb 100644
--- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs
+++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs
@@ -340,7 +340,11 @@ internal static async ValueTask HasDifferencesAsync(Project oldProject, Pr
return false;
}
- if (AbstractEditAndContinueAnalyzer.EnableProjectLevelAnalysis && HasProjectLevelDifferences(oldProject, newProject, differences) && differences == null)
+ // Check for project differences even when AbstractEditAndContinueAnalyzer.EnableProjectLevelAnalysis is false.
+ // A project-level change may be used by a source generator to produce different outputs.
+ // We assume that a source generator will not produce different outputs if the project state
+ // and content of all input documents are the same.
+ if (HasProjectLevelDifferences(oldProject, newProject, differences) && differences == null)
{
return true;
}
@@ -494,7 +498,14 @@ internal static bool HasDifferences(CompilationOptions oldOptions, CompilationOp
.WithXmlReferenceResolver(newOptions.XmlReferenceResolver)
.Equals(newOptions);
- internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? oldProject, Project newProject, ProjectDifferences documentDifferences, ArrayBuilder diagnostics, CancellationToken cancellationToken)
+ internal static async Task GetProjectDifferencesAsync(
+ TraceLog log,
+ Project? oldProject,
+ Project newProject,
+ ProjectDifferences documentDifferences,
+ ArrayBuilder diagnostics,
+ EditAndContinueDiagnosticLevel diagnosticLevel,
+ CancellationToken cancellationToken)
{
documentDifferences.Clear();
@@ -503,8 +514,12 @@ internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? old
return;
}
- if (!await HasDifferencesAsync(oldProject, newProject, documentDifferences, cancellationToken).ConfigureAwait(false))
+ var hasNonGeneratedDifferences = await HasDifferencesAsync(oldProject, newProject, documentDifferences, cancellationToken).ConfigureAwait(false);
+ if (!hasNonGeneratedDifferences && diagnosticLevel == EditAndContinueDiagnosticLevel.None)
{
+ // When not running in diagnostic mode we expect source generators to be deterministic
+ // and not produce any changes if the project state and content of all input documents are the same.
+ // Therefore, we can avoid computing the source generated document states.
return;
}
@@ -546,6 +561,25 @@ internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? old
documentDifferences.DeletedDocuments.Add(oldProject.GetOrCreateSourceGeneratedDocument(oldState));
}
+
+ if (!hasNonGeneratedDifferences)
+ {
+ Contract.ThrowIfTrue(diagnosticLevel == EditAndContinueDiagnosticLevel.None);
+
+ foreach (var newDocument in documentDifferences.ChangedOrAddedDocuments)
+ {
+ log.Write($"Source-generated document '{newDocument.FilePath}' changed even though there are no differences in the project. The generator is faulty.", LogMessageSeverity.Warning);
+ }
+
+ foreach (var document in documentDifferences.DeletedDocuments)
+ {
+ log.Write($"Source-generated document '{document.FilePath}' has been deleted even though there are no differences in the project. The generator is faulty.", LogMessageSeverity.Warning);
+ }
+
+ // Keep the changed documents in the result.
+ // This modifies the behavior compared to non-diagnostic mode,
+ // but it allows to work around the issue with the source generator.
+ }
}
private static async ValueTask> GetSourceGeneratedDocumentStatesAsync(TraceLog log, Project project, ArrayBuilder? diagnostics, CancellationToken cancellationToken)
@@ -1156,7 +1190,7 @@ void UpdateChangedDocumentsStaleness(DocumentStalenessReason? staleness)
continue;
}
- await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, cancellationToken).ConfigureAwait(false);
+ await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, DebuggingSession.DiagnosticLevel, cancellationToken).ConfigureAwait(false);
projectDifferences.Log(Log, newProject);
if (projectDifferences.IsEmpty)
@@ -1597,7 +1631,11 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
bool LogException(Exception e)
{
- Log.Write($"Exception while emitting update: {e}", LogMessageSeverity.Error);
+ if (e is not OperationCanceledException)
+ {
+ Log.Write($"Exception while emitting update: {e}", LogMessageSeverity.Error);
+ }
+
return true;
}
}
diff --git a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs
index 34ac6f9574295..c7a3c306930e5 100644
--- a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs
+++ b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs
@@ -12,7 +12,6 @@
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
-using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
@@ -23,6 +22,9 @@ internal readonly struct EmitSolutionUpdateResults
[DataContract]
internal readonly struct Data
{
+ [DataMember]
+ public required SolutionAction SolutionAction { get; init; }
+
[DataMember]
public required ModuleUpdates ModuleUpdates { get; init; }
@@ -89,8 +91,12 @@ public static Data CreateFromInternalError(Solution solution, string errorMessag
Location.None,
errorMessage);
+ // An internal error should be treated as a blocking rude edit in all running projects
+ // since restarting all projects will apply any changes that were made and allow the user to continue debugging.
+
return new()
{
+ SolutionAction = SolutionAction.None,
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []),
Diagnostics = [DiagnosticData.Create(diagnostic, firstProject)],
SyntaxError = null,
@@ -104,6 +110,7 @@ public static Data CreateFromInternalError(Solution solution, string errorMessag
public static readonly EmitSolutionUpdateResults Empty = new()
{
Solution = null,
+ SolutionAction = SolutionAction.None,
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.None, []),
Diagnostics = [],
SyntaxError = null,
@@ -121,6 +128,11 @@ public static Data CreateFromInternalError(Solution solution, string errorMessag
///
public required Solution? Solution { get; init; }
+ ///
+ /// Action taken on the solution.
+ ///
+ public required SolutionAction SolutionAction { get; init; }
+
public required ModuleUpdates ModuleUpdates { get; init; }
///
@@ -153,6 +165,7 @@ public Data Dehydrate()
=> Solution == null
? new()
{
+ SolutionAction = SolutionAction,
ModuleUpdates = ModuleUpdates,
Diagnostics = [],
SyntaxError = null,
@@ -162,6 +175,7 @@ public Data Dehydrate()
}
: new()
{
+ SolutionAction = SolutionAction,
ModuleUpdates = ModuleUpdates,
Diagnostics = Diagnostics.ToDiagnosticData(Solution),
SyntaxError = GetSyntaxErrorData(),
diff --git a/src/Features/Core/Portable/EditAndContinue/SolutionAction.cs b/src/Features/Core/Portable/EditAndContinue/SolutionAction.cs
new file mode 100644
index 0000000000000..5dc18ce6d46a0
--- /dev/null
+++ b/src/Features/Core/Portable/EditAndContinue/SolutionAction.cs
@@ -0,0 +1,23 @@
+// 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.
+
+namespace Microsoft.CodeAnalysis.EditAndContinue;
+
+internal enum SolutionAction
+{
+ ///
+ /// No action should be taken on the solution.
+ ///
+ None,
+
+ ///
+ /// The solution has been committed.
+ ///
+ Committed,
+
+ ///
+ /// Pending solution updates have been stored and will need to be committed or discarded.
+ ///
+ PendingUpdate,
+}
diff --git a/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs b/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs
index ec917a27bfea9..2d02441ccc86a 100644
--- a/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs
+++ b/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs
@@ -87,7 +87,7 @@ public readonly struct Updates
///
/// Status of the updates.
///
- public readonly Status Status { get; init; }
+ public required Status Status { get; init; }
///
/// Returns all diagnostics that can't be addressed by rebuilding/restarting the project.
diff --git a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs
index 070117c2d7925..d5820825f3c2d 100644
--- a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs
+++ b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs
@@ -2435,7 +2435,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind)
await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));
var diagnostics = new ArrayBuilder();
- await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None);
+ await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None);
Assert.Empty(diagnostics);
Assert.Empty(projectDifferences.DeletedDocuments);
AssertEx.Equal(documentKind == TextDocumentKind.Document ? [documentId, generatedDocumentId] : [generatedDocumentId], projectDifferences.ChangedOrAddedDocuments.Select(d => d.Id));
@@ -2465,7 +2465,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind)
AssertEx.Equal(documentKind == TextDocumentKind.Document ? new[] { documentId } : [],
await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));
- await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None);
+ await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None);
Assert.Empty(diagnostics);
Assert.True(projectDifferences.IsEmpty);
@@ -2490,7 +2490,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind)
AssertEx.Equal(documentKind == TextDocumentKind.Document ? [documentId, generatedDocumentId] : [generatedDocumentId],
await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));
- await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None);
+ await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None);
Assert.Empty(diagnostics);
Assert.Empty(projectDifferences.DeletedDocuments);
AssertEx.Equal(documentKind == TextDocumentKind.Document ? [documentId, generatedDocumentId] : [generatedDocumentId], projectDifferences.ChangedOrAddedDocuments.Select(d => d.Id));
@@ -2518,7 +2518,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind)
AssertEx.Equal([generatedDocumentId],
await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));
- await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None);
+ await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None);
Assert.Empty(diagnostics);
if (documentKind == TextDocumentKind.Document)
@@ -2601,7 +2601,7 @@ public async Task HasChanges_SourceGeneratorFailure()
AssertEx.Empty(await EditSession.GetChangedDocumentsAsync(log, oldProject, project, CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));
var diagnostics = new ArrayBuilder();
- await EditSession.GetProjectDifferencesAsync(log, oldProject, project, diffences, diagnostics, CancellationToken.None);
+ await EditSession.GetProjectDifferencesAsync(log, oldProject, project, diffences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None);
Assert.Contains("System.InvalidOperationException: Source generator failed", diagnostics.Single().GetMessage());
AssertEx.Empty(diffences.ChangedOrAddedDocuments);
AssertEx.Equal(["generated.cs"], diffences.DeletedDocuments.Select(d => d.Name));
diff --git a/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs b/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs
index 38f306fce25e1..8fac9b0eed2c3 100644
--- a/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs
+++ b/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs
@@ -53,6 +53,7 @@ private static EditSession CreateEditSession(
NullPdbMatchingSourceTextProvider.Instance,
log,
log,
+ EditAndContinueDiagnosticLevel.None,
reportDiagnostics: true);
if (initialState != CommittedSolution.DocumentState.None)
diff --git a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs
index d488b6254c34c..bf4e52d32b4a1 100644
--- a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs
+++ b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs
@@ -137,6 +137,7 @@ public async Task GetHotReloadDiagnostics()
var data = new EmitSolutionUpdateResults.Data()
{
+ SolutionAction = SolutionAction.None,
Diagnostics = [.. diagnostics, .. rudeEdits],
SyntaxError = syntaxError,
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Blocked, Updates: []),
diff --git a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs
index 574994e52993c..539cfac9ed3ff 100644
--- a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs
+++ b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs
@@ -209,6 +209,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
return new()
{
+ SolutionAction = SolutionAction.None,
Solution = solution,
ModuleUpdates = updates,
Diagnostics = diagnostics,
diff --git a/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs b/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs
index e47e89f1ed1ce..ff63c71f865d9 100644
--- a/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs
+++ b/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs
@@ -62,6 +62,33 @@ internal abstract class EditAndContinueTestVerifier
public const EditAndContinueCapabilities AllRuntimeCapabilities =
Net10RuntimeCapabilities;
+ internal static readonly SymbolDisplayFormat TestFormat =
+ new SymbolDisplayFormat(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor,
+ localOptions: SymbolDisplayLocalOptions.IncludeType,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance,
+ memberOptions:
+ SymbolDisplayMemberOptions.IncludeParameters |
+ SymbolDisplayMemberOptions.IncludeContainingType |
+ SymbolDisplayMemberOptions.IncludeType |
+ SymbolDisplayMemberOptions.IncludeRef |
+ SymbolDisplayMemberOptions.IncludeExplicitInterface,
+ kindOptions:
+ SymbolDisplayKindOptions.IncludeMemberKeyword,
+ parameterOptions:
+ SymbolDisplayParameterOptions.IncludeOptionalBrackets |
+ SymbolDisplayParameterOptions.IncludeDefaultValue |
+ SymbolDisplayParameterOptions.IncludeParamsRefOut |
+ SymbolDisplayParameterOptions.IncludeExtensionThis |
+ SymbolDisplayParameterOptions.IncludeType |
+ SymbolDisplayParameterOptions.IncludeName,
+ miscellaneousOptions:
+ SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
+ SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName |
+ SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
+
public AbstractEditAndContinueAnalyzer Analyzer { get; }
protected EditAndContinueTestVerifier(Action? faultInjector)
@@ -352,12 +379,12 @@ static int CompareEdits(SymbolKey leftKey, SemanticEditKind leftKind, SymbolKey
=> leftKey.ToString().CompareTo(rightKey.ToString()) is not 0 and var result ? result : leftKind.CompareTo(rightKind);
SymbolKey CreateSymbolKey(SemanticEditDescription edit)
- => SymbolKey.Create(edit.SymbolProvider((edit.Kind == SemanticEditKind.Delete) ? oldCompilation : newCompilation));
+ => SymbolKey.Create(edit.GetSymbol((edit.Kind == SemanticEditKind.Delete) ? oldCompilation : newCompilation));
// string comparison to simplify understanding why a test failed:
AssertEx.Equal(
- expectedSemanticEdits.Select(e => $"{e.Kind}: {e.SymbolProvider((e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation))}"),
- actualSemanticEdits.Select(e => $"{e.Kind}: {e.Symbol.Resolve(e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation).Symbol}"),
+ expectedSemanticEdits.Select(e => $"{e.Kind}: {Inspect(e.GetSymbol((e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation)))}"),
+ actualSemanticEdits.Select(e => $"{e.Kind}: {Inspect(e.Symbol.Resolve(e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation).Symbol)}"),
message: message);
for (var i = 0; i < actualSemanticEdits.Length; i++)
@@ -374,23 +401,23 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit)
switch (editKind)
{
case SemanticEditKind.Update:
- expectedOldSymbol = expectedSemanticEdit.SymbolProvider(oldCompilation);
- expectedNewSymbol = expectedSemanticEdit.SymbolProvider(newCompilation);
+ expectedOldSymbol = expectedSemanticEdit.GetSymbol(oldCompilation);
+ expectedNewSymbol = expectedSemanticEdit.GetSymbol(newCompilation);
- Assert.Equal(expectedOldSymbol, symbolKey.Resolve(oldCompilation).Symbol);
- Assert.Equal(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol);
+ VerifySymbolsEqual(expectedOldSymbol, symbolKey.Resolve(oldCompilation).Symbol);
+ VerifySymbolsEqual(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol);
break;
case SemanticEditKind.Delete:
- expectedOldSymbol = expectedSemanticEdit.SymbolProvider(oldCompilation);
+ expectedOldSymbol = expectedSemanticEdit.GetSymbol(oldCompilation);
// Symbol key will happily resolve to a definition part that has no implementation, so we validate that
// differently
if (expectedOldSymbol.IsPartialDefinition() &&
symbolKey.Resolve(oldCompilation).Symbol is ISymbol resolvedSymbol)
{
- Assert.Equal(expectedOldSymbol, resolvedSymbol.PartialDefinitionPart());
- Assert.Equal(null, resolvedSymbol.PartialImplementationPart());
+ VerifySymbolsEqual(expectedOldSymbol, resolvedSymbol.PartialDefinitionPart());
+ Assert.Null(resolvedSymbol.PartialImplementationPart());
}
else
{
@@ -401,7 +428,7 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit)
// represented in the symbol key, so the check below would fail, so we skip it.
if (expectedSemanticEdit.DeletedSymbolContainerProvider is null)
{
- Assert.Equal(null, symbolKey.Resolve(newCompilation).Symbol);
+ Assert.Null(symbolKey.Resolve(newCompilation).Symbol);
}
}
@@ -409,13 +436,13 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit)
AssertEx.AreEqual(
deletedSymbolContainer,
expectedSemanticEdit.DeletedSymbolContainerProvider?.Invoke(newCompilation),
- message: $"{message}, {editKind}({expectedNewSymbol ?? expectedOldSymbol}): Incorrect deleted container");
+ message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Incorrect deleted container");
break;
case SemanticEditKind.Insert or SemanticEditKind.Replace:
- expectedNewSymbol = expectedSemanticEdit.SymbolProvider(newCompilation);
- Assert.Equal(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol);
+ expectedNewSymbol = expectedSemanticEdit.GetSymbol(newCompilation);
+ VerifySymbolsEqual(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol);
break;
default:
@@ -426,7 +453,7 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit)
AssertEx.AreEqual(
expectedSemanticEdit.PartialType?.Invoke(newCompilation),
actualSemanticEdit.PartialType?.Resolve(newCompilation).Symbol,
- message: $"{message}, {editKind}({expectedNewSymbol ?? expectedOldSymbol}): Partial types do not match");
+ message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Partial types do not match");
var expectedSyntaxMap = expectedSemanticEdit.GetSyntaxMap();
@@ -435,7 +462,7 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit)
AssertEx.AreEqual(
expectedSyntaxMap != null,
actualSyntaxMaps.HasMap,
- message: $"{message}, {editKind}({expectedNewSymbol ?? expectedOldSymbol}): Incorrect syntax map");
+ message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Incorrect syntax map");
// If expected map is specified validate its mappings with the actual one:
if (expectedSyntaxMap != null)
@@ -445,6 +472,35 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit)
}
}
+ public static void VerifySymbolsEqual(ISymbol expectedSymbol, ISymbol? actualSymbol)
+ {
+ if (expectedSymbol != actualSymbol)
+ {
+ Assert.Fail($"Expected: {Inspect(expectedSymbol)}; actual: {Inspect(actualSymbol)}");
+ }
+ }
+
+ private static string Inspect(ISymbol? symbol)
+ {
+ if (symbol is null)
+ {
+ return "";
+ }
+
+ var display = $"'{symbol.ToDisplayString(TestFormat)}'";
+
+ if (symbol.IsPartialDefinition())
+ {
+ display += " (partial def)";
+ }
+ else if (symbol.IsPartialImplementation())
+ {
+ display += " (partial impl)";
+ }
+
+ return display;
+ }
+
public static SyntaxNode FindNode(SyntaxNode root, TextSpan span)
{
var result = root.FindToken(span.Start).Parent;
diff --git a/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs b/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs
index b8c9b4d57b7e2..6e5c2e36d2bec 100644
--- a/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs
+++ b/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs
@@ -20,7 +20,6 @@ internal sealed class SemanticEditDescription(
Func? deletedSymbolContainerProvider)
{
public readonly SemanticEditKind Kind = kind;
- public readonly Func SymbolProvider = symbolProvider;
public readonly Func? PartialType = partialType;
public readonly Func? DeletedSymbolContainerProvider = deletedSymbolContainerProvider;
@@ -32,6 +31,9 @@ internal sealed class SemanticEditDescription(
public readonly bool HasSyntaxMap = hasSyntaxMap;
+ public ISymbol GetSymbol(Compilation compilation)
+ => symbolProvider(compilation).PartialAsImplementation();
+
private static IEnumerable<(TextSpan oldSpan, TextSpan newSpan, RuntimeRudeEditDescription? runtimeRudeEdit)> GetSyntaxMapWithRudeEdits(IEnumerable<(TextSpan, TextSpan)>? syntaxMap, IEnumerable? rudeEdits)
{
if (syntaxMap == null)
diff --git a/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs b/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs
index 8b1a66e8430bc..a57b5492e5581 100644
--- a/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs
+++ b/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs
@@ -7,6 +7,7 @@
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.EditAndContinue;
+using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.VisualStudio.LanguageServices.Xaml;
@@ -43,6 +44,12 @@ public void Dispose()
private void OnEditAndContinueSolutionCommitted(Solution solution)
{
- SolutionCommitted?.Invoke(solution);
+ try
+ {
+ SolutionCommitted?.Invoke(solution);
+ }
+ catch (Exception e) when (FatalError.ReportAndCatch(e))
+ {
+ }
}
}