diff --git a/src/Compilers/Core/Portable/Debugging/SourceHashAlgorithms.cs b/src/Compilers/Core/Portable/Debugging/SourceHashAlgorithms.cs
new file mode 100644
index 0000000000000..16df16e705cb1
--- /dev/null
+++ b/src/Compilers/Core/Portable/Debugging/SourceHashAlgorithms.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.CodeAnalysis.Text;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Debugging
+{
+ ///
+ /// Hash algorithms supported by the debugger used for source file checksums stored in the PDB.
+ ///
+ internal static class SourceHashAlgorithms
+ {
+ private static readonly Guid s_guidSha1 = unchecked(new Guid((int)0xff1816ec, (short)0xaa5e, 0x4d10, 0x87, 0xf7, 0x6f, 0x49, 0x63, 0x83, 0x34, 0x60));
+ private static readonly Guid s_guidSha256 = unchecked(new Guid((int)0x8829d00f, 0x11b8, 0x4213, 0x87, 0x8b, 0x77, 0x0e, 0x85, 0x97, 0xac, 0x16));
+
+ public static bool IsSupportedAlgorithm(SourceHashAlgorithm algorithm)
+ => algorithm switch
+ {
+ SourceHashAlgorithm.Sha1 => true,
+ SourceHashAlgorithm.Sha256 => true,
+ _ => false
+ };
+
+ public static Guid GetAlgorithmGuid(SourceHashAlgorithm algorithm)
+ => algorithm switch
+ {
+ SourceHashAlgorithm.Sha1 => s_guidSha1,
+ SourceHashAlgorithm.Sha256 => s_guidSha256,
+ _ => throw ExceptionUtilities.UnexpectedValue(algorithm),
+ };
+
+ public static SourceHashAlgorithm GetSourceHashAlgorithm(Guid guid)
+ => (guid == s_guidSha256) ? SourceHashAlgorithm.Sha256 :
+ (guid == s_guidSha1) ? SourceHashAlgorithm.Sha1 :
+ SourceHashAlgorithm.None;
+ }
+}
diff --git a/src/Compilers/Core/Portable/EmbeddedText.cs b/src/Compilers/Core/Portable/EmbeddedText.cs
index 54195029bdb92..698d308295b20 100644
--- a/src/Compilers/Core/Portable/EmbeddedText.cs
+++ b/src/Compilers/Core/Portable/EmbeddedText.cs
@@ -1,7 +1,5 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using Microsoft.CodeAnalysis.Text;
-using Roslyn.Utilities;
using System;
using System.Collections.Immutable;
using System.Diagnostics;
@@ -10,6 +8,9 @@
using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Debugging;
+using Microsoft.CodeAnalysis.Text;
+using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
@@ -50,7 +51,7 @@ public sealed class EmbeddedText
private EmbeddedText(string filePath, ImmutableArray checksum, SourceHashAlgorithm checksumAlgorithm, ImmutableArray blob)
{
Debug.Assert(filePath?.Length > 0);
- Debug.Assert(Cci.DebugSourceDocument.IsSupportedAlgorithm(checksumAlgorithm));
+ Debug.Assert(SourceHashAlgorithms.IsSupportedAlgorithm(checksumAlgorithm));
Debug.Assert(!blob.IsDefault && blob.Length >= sizeof(int));
FilePath = filePath;
diff --git a/src/Compilers/Core/Portable/PEWriter/DebugSourceDocument.cs b/src/Compilers/Core/Portable/PEWriter/DebugSourceDocument.cs
index 78414c2cd67ae..c26e70b23dd58 100644
--- a/src/Compilers/Core/Portable/PEWriter/DebugSourceDocument.cs
+++ b/src/Compilers/Core/Portable/PEWriter/DebugSourceDocument.cs
@@ -48,46 +48,6 @@ public DebugSourceDocument(string location, Guid language, ImmutableArray
_sourceInfo = Task.FromResult(new DebugSourceInfo(checksum, algorithm));
}
- internal static bool IsSupportedAlgorithm(SourceHashAlgorithm algorithm)
- {
- // Dev12 debugger supports MD5, SHA1.
- // Dev14 debugger supports MD5, SHA1, SHA256.
- // MD5 is obsolete.
-
- switch (algorithm)
- {
- case SourceHashAlgorithm.Sha1:
- case SourceHashAlgorithm.Sha256:
- return true;
- default:
- return false;
- }
- }
-
- internal static Guid GetAlgorithmGuid(SourceHashAlgorithm algorithm)
- {
- Debug.Assert(IsSupportedAlgorithm(algorithm));
-
- // Dev12 debugger supports MD5, SHA1.
- // Dev14 debugger supports MD5, SHA1, SHA256.
- // MD5 is obsolete.
-
- unchecked
- {
- switch (algorithm)
- {
- case SourceHashAlgorithm.Sha1:
- return new Guid((int)0xff1816ec, (short)0xaa5e, 0x4d10, 0x87, 0xf7, 0x6f, 0x49, 0x63, 0x83, 0x34, 0x60);
-
- case SourceHashAlgorithm.Sha256:
- return new Guid((int)0x8829d00f, 0x11b8, 0x4213, 0x87, 0x8b, 0x77, 0x0e, 0x85, 0x97, 0xac, 0x16);
-
- default:
- throw ExceptionUtilities.UnexpectedValue(algorithm);
- }
- }
- }
-
public Guid DocumentType
{
get { return s_corSymDocumentTypeText; }
diff --git a/src/Compilers/Core/Portable/PEWriter/DebugSourceInfo.cs b/src/Compilers/Core/Portable/PEWriter/DebugSourceInfo.cs
index e5925e04a70e8..5ccfdbde06601 100644
--- a/src/Compilers/Core/Portable/PEWriter/DebugSourceInfo.cs
+++ b/src/Compilers/Core/Portable/PEWriter/DebugSourceInfo.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Immutable;
@@ -30,15 +31,15 @@ internal struct DebugSourceInfo
public DebugSourceInfo(
ImmutableArray checksum,
SourceHashAlgorithm checksumAlgorithm,
- ImmutableArray embeddedTextBlob = default(ImmutableArray))
- : this(checksum, DebugSourceDocument.GetAlgorithmGuid(checksumAlgorithm), embeddedTextBlob)
+ ImmutableArray embeddedTextBlob = default)
+ : this(checksum, SourceHashAlgorithms.GetAlgorithmGuid(checksumAlgorithm), embeddedTextBlob)
{
}
public DebugSourceInfo(
ImmutableArray checksum,
Guid checksumAlgorithmId,
- ImmutableArray embeddedTextBlob = default(ImmutableArray))
+ ImmutableArray embeddedTextBlob = default)
{
ChecksumAlgorithmId = checksumAlgorithmId;
Checksum = checksum;
diff --git a/src/Compilers/Core/Portable/Text/SourceText.cs b/src/Compilers/Core/Portable/Text/SourceText.cs
index d5af9fe6113a5..54a4335d8f40a 100644
--- a/src/Compilers/Core/Portable/Text/SourceText.cs
+++ b/src/Compilers/Core/Portable/Text/SourceText.cs
@@ -9,6 +9,7 @@
using System.Linq;
using System.Text;
using System.Threading;
+using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
@@ -67,7 +68,7 @@ internal SourceText(ImmutableArray checksum, SourceHashAlgorithm checksumA
internal static void ValidateChecksumAlgorithm(SourceHashAlgorithm checksumAlgorithm)
{
- if (!Cci.DebugSourceDocument.IsSupportedAlgorithm(checksumAlgorithm))
+ if (!SourceHashAlgorithms.IsSupportedAlgorithm(checksumAlgorithm))
{
throw new ArgumentException(CodeAnalysisResources.UnsupportedHashAlgorithm, nameof(checksumAlgorithm));
}
diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/CSharpEditAndContinueAnalyzerTests.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/CSharpEditAndContinueAnalyzerTests.cs
index 0e33ff26414a9..bfa5987b8384b 100644
--- a/src/EditorFeatures/CSharpTest/EditAndContinue/CSharpEditAndContinueAnalyzerTests.cs
+++ b/src/EditorFeatures/CSharpTest/EditAndContinue/CSharpEditAndContinueAnalyzerTests.cs
@@ -261,13 +261,13 @@ public static void Main()
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source1);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var documentId = oldProject.Documents.First().Id;
var oldSolution = workspace.CurrentSolution;
- var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));
- var oldDocument = oldSolution.GetDocument(documentId);
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
var oldText = await oldDocument.GetTextAsync();
var oldSyntaxRoot = await oldDocument.GetSyntaxRootAsync();
+ var documentId = oldDocument.Id;
+ var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));
var newDocument = newSolution.GetDocument(documentId);
var newText = await newDocument.GetTextAsync();
var newSyntaxRoot = await newDocument.GetSyntaxRootAsync();
@@ -279,7 +279,7 @@ public static void Main()
var oldStatementSyntax = oldSyntaxRoot.FindNode(oldStatementTextSpan);
var baseActiveStatements = ImmutableArray.Create(ActiveStatementsDescription.CreateActiveStatement(ActiveStatementFlags.IsLeafFrame, oldStatementSpan, DocumentId.CreateNewId(ProjectId.CreateNewId())));
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newDocument, trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newDocument, trackingServiceOpt: null, CancellationToken.None);
Assert.True(result.HasChanges);
Assert.True(result.SemanticEdits[0].PreserveLocalVariables);
@@ -317,13 +317,14 @@ public static void Main()
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source1);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var documentId = oldProject.Documents.First().Id;
var oldSolution = workspace.CurrentSolution;
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
+ var documentId = oldDocument.Id;
var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
Assert.True(result.HasChanges);
Assert.True(result.HasChangesAndErrors);
@@ -345,10 +346,10 @@ public static void Main()
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var document = oldProject.Documents.First();
+ var oldProject = workspace.CurrentSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, document, trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, oldDocument, trackingServiceOpt: null, CancellationToken.None);
Assert.False(result.HasChanges);
Assert.False(result.HasChangesAndErrors);
@@ -379,13 +380,16 @@ public static void Main()
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source1);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var documentId = oldProject.Documents.First().Id;
+
var oldSolution = workspace.CurrentSolution;
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
+ var documentId = oldDocument.Id;
+
var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
Assert.False(result.HasChanges);
Assert.False(result.HasChangesAndErrors);
@@ -410,10 +414,14 @@ public static void Main()
using var workspace = TestWorkspace.CreateCSharp(
source, parseOptions: experimental, compilationOptions: null, exportProvider: null);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var document = oldProject.Documents.First();
+
+ var oldSolution = workspace.CurrentSolution;
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
+ var documentId = oldDocument.Id;
+
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, document, trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, oldDocument, trackingServiceOpt: null, CancellationToken.None);
Assert.False(result.HasChanges);
Assert.False(result.HasChangesAndErrors);
@@ -454,13 +462,16 @@ public static void Main()
using var workspace = TestWorkspace.CreateCSharp(
source1, parseOptions: experimental, compilationOptions: null, exportProvider: null);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var documentId = oldProject.Documents.First().Id;
+
var oldSolution = workspace.CurrentSolution;
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
+ var documentId = oldDocument.Id;
+
var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
Assert.True(result.HasChanges);
Assert.True(result.HasChangesAndErrors);
@@ -485,10 +496,14 @@ public static void Main()
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var document = oldProject.Documents.First();
+
+ var oldSolution = workspace.CurrentSolution;
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
+ var documentId = oldDocument.Id;
+
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, document, trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, oldDocument, trackingServiceOpt: null, CancellationToken.None);
Assert.False(result.HasChanges);
Assert.False(result.HasChangesAndErrors);
@@ -521,13 +536,16 @@ public static void Main()
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source1);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var documentId = oldProject.Documents.First().Id;
+
var oldSolution = workspace.CurrentSolution;
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
+ var documentId = oldDocument.Id;
+
var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
Assert.True(result.HasChanges);
@@ -560,13 +578,16 @@ public static void Main(Bar x)
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source1);
- var oldProject = workspace.CurrentSolution.Projects.First();
- var documentId = oldProject.Documents.First().Id;
+
var oldSolution = workspace.CurrentSolution;
+ var oldProject = oldSolution.Projects.Single();
+ var oldDocument = oldProject.Documents.Single();
+ var documentId = oldDocument.Id;
+
var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));
var baseActiveStatements = ImmutableArray.Create();
- var result = await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
+ var result = await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt: null, CancellationToken.None);
Assert.True(result.HasChanges);
Assert.True(result.HasChangesAndErrors);
@@ -619,7 +640,7 @@ public class D
var baseActiveStatements = ImmutableArray.Create();
foreach (var changedDocumentId in changedDocuments)
{
- result.Add(await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newProject.GetDocument(changedDocumentId), trackingServiceOpt: null, CancellationToken.None));
+ result.Add(await analyzer.AnalyzeDocumentAsync(oldProject.GetDocument(changedDocumentId), baseActiveStatements, newProject.GetDocument(changedDocumentId), trackingServiceOpt: null, CancellationToken.None));
}
Assert.True(result.IsSingle());
@@ -646,7 +667,7 @@ public static void Main()
var analyzer = new CSharpEditAndContinueAnalyzer();
using var workspace = TestWorkspace.CreateCSharp(source1);
- // fork the solution to introduce a change
+
var oldProject = workspace.CurrentSolution.Projects.Single();
var newDocId = DocumentId.CreateNewId(oldProject.Id);
var oldSolution = workspace.CurrentSolution;
@@ -667,7 +688,7 @@ public static void Main()
var baseActiveStatements = ImmutableArray.Create();
foreach (var changedDocumentId in changedDocuments)
{
- result.Add(await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newProject.GetDocument(changedDocumentId), trackingServiceOpt: null, CancellationToken.None));
+ result.Add(await analyzer.AnalyzeDocumentAsync(oldProject.GetDocument(changedDocumentId), baseActiveStatements, newProject.GetDocument(changedDocumentId), trackingServiceOpt: null, CancellationToken.None));
}
Assert.True(result.IsSingle());
diff --git a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs
index d7e78351d8986..8660ee69ace73 100644
--- a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs
+++ b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs
@@ -164,5 +164,10 @@ internal static class PredefinedCommandHandlerNames
/// Command handler name for Paste in Paste Tracking.
///
public const string PasteTrackingPaste = "Paste Tracking Paste Command Handler";
+
+ ///
+ /// Command handler name for Edit and Continue file save handler.
+ ///
+ public const string EditAndContinueFileSave = "Edit and Continue Save File Handler";
}
}
diff --git a/src/EditorFeatures/Core/Implementation/EditAndContinue/ActiveStatementTrackingService.cs b/src/EditorFeatures/Core/Implementation/EditAndContinue/ActiveStatementTrackingService.cs
index a0694957d06e4..775e0901a247c 100644
--- a/src/EditorFeatures/Core/Implementation/EditAndContinue/ActiveStatementTrackingService.cs
+++ b/src/EditorFeatures/Core/Implementation/EditAndContinue/ActiveStatementTrackingService.cs
@@ -1,16 +1,20 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.ErrorReporting;
+using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
@@ -28,15 +32,14 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.EditAndContinue
[Export(typeof(IActiveStatementTrackingService))]
internal sealed class ActiveStatementTrackingService : IActiveStatementTrackingService
{
- private TrackingSession _sessionOpt;
+ private TrackingSession? _session;
+ public event Action? TrackingSpansChanged;
[ImportingConstructor]
public ActiveStatementTrackingService()
{
}
- public event Action TrackingSpansChanged;
-
private void OnTrackingSpansChanged(bool leafChanged)
{
TrackingSpansChanged?.Invoke(leafChanged);
@@ -45,7 +48,7 @@ private void OnTrackingSpansChanged(bool leafChanged)
public void StartTracking(EditSession editSession)
{
var newSession = new TrackingSession(this, editSession);
- if (Interlocked.CompareExchange(ref _sessionOpt, newSession, null) != null)
+ if (Interlocked.CompareExchange(ref _session, newSession, null) != null)
{
newSession.EndTracking();
Contract.Fail("Can only track active statements for a single edit session.");
@@ -54,14 +57,14 @@ public void StartTracking(EditSession editSession)
public void EndTracking()
{
- var session = Interlocked.Exchange(ref _sessionOpt, null);
+ var session = Interlocked.Exchange(ref _session, null);
Contract.ThrowIfNull(session, "Active statement tracking not started.");
session.EndTracking();
}
public bool TryGetSpan(ActiveStatementId id, SourceText source, out TextSpan span)
{
- var session = _sessionOpt;
+ var session = _session;
if (session == null)
{
span = default;
@@ -73,12 +76,12 @@ public bool TryGetSpan(ActiveStatementId id, SourceText source, out TextSpan spa
public IEnumerable GetSpans(SourceText source)
{
- return _sessionOpt?.GetSpans(source) ?? SpecializedCollections.EmptyEnumerable();
+ return _session?.GetSpans(source) ?? SpecializedCollections.EmptyEnumerable();
}
public void UpdateActiveStatementSpans(SourceText source, IEnumerable<(ActiveStatementId, ActiveStatementTextSpan)> spans)
{
- _sessionOpt?.UpdateActiveStatementSpans(source, spans);
+ _session?.UpdateActiveStatementSpans(source, spans);
}
private sealed class TrackingSession
@@ -102,25 +105,22 @@ public ActiveStatementTrackingSpan(ITrackingSpan trackingSpan, ActiveStatementFl
// Spans that are tracking active statements contained in the specified document,
// or null if we lost track of them due to document being closed and reopened.
- private readonly Dictionary _trackingSpans;
+ private readonly Dictionary _trackingSpans;
#endregion
public TrackingSession(ActiveStatementTrackingService service, EditSession editSession)
{
- Debug.Assert(service != null);
- Debug.Assert(editSession != null);
-
_service = service;
_editSession = editSession;
- _trackingSpans = new Dictionary();
+ _trackingSpans = new Dictionary();
- editSession.BaseSolution.Workspace.DocumentOpened += DocumentOpened;
+ editSession.DebuggingSession.Workspace.DocumentOpened += DocumentOpened;
// fire and forget on a background thread:
try
{
- Task.Run(TrackActiveSpansAsync, _editSession.CancellationToken);
+ _ = Task.Run(TrackActiveSpansAsync, _editSession.CancellationToken);
}
catch (TaskCanceledException)
{
@@ -129,7 +129,7 @@ public TrackingSession(ActiveStatementTrackingService service, EditSession editS
public void EndTracking()
{
- _editSession.BaseSolution.Workspace.DocumentOpened -= DocumentOpened;
+ _editSession.DebuggingSession.Workspace.DocumentOpened -= DocumentOpened;
lock (_trackingSpans)
{
@@ -149,13 +149,15 @@ private async Task DocumentOpenedAsync(Document document)
try
{
var baseActiveStatements = await _editSession.BaseActiveStatements.GetValueAsync(_editSession.CancellationToken).ConfigureAwait(false);
+ var (baseDocument, _) = await _editSession.DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(document.Id, _editSession.CancellationToken).ConfigureAwait(false);
- if (baseActiveStatements.DocumentMap.TryGetValue(document.Id, out var documentActiveStatements) &&
+ if (baseDocument != null &&
+ baseActiveStatements.DocumentMap.TryGetValue(document.Id, out var documentActiveStatements) &&
TryGetSnapshot(document, out var snapshot))
{
lock (_trackingSpans)
{
- TrackActiveSpansNoLock(document, snapshot, documentActiveStatements);
+ TrackActiveSpansNoLock(baseDocument, document, snapshot, documentActiveStatements);
}
var leafChanged = documentActiveStatements.Contains(s => s.IsLeaf);
@@ -172,7 +174,7 @@ private async Task DocumentOpenedAsync(Document document)
}
}
- private static bool TryGetSnapshot(Document document, out ITextSnapshot snapshot)
+ private static bool TryGetSnapshot(Document document, [NotNullWhen(true)] out ITextSnapshot? snapshot)
{
if (!document.TryGetText(out var source))
{
@@ -188,20 +190,47 @@ private async Task TrackActiveSpansAsync()
{
try
{
- var baseActiveStatements = await _editSession.BaseActiveStatements.GetValueAsync(_editSession.CancellationToken).ConfigureAwait(false);
+ var cancellationToken = _editSession.CancellationToken;
+ var baseActiveStatements = await _editSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
+ var lastCommittedSolution = _editSession.DebuggingSession.LastCommittedSolution;
+ var currentSolution = _editSession.DebuggingSession.Workspace.CurrentSolution;
+ var activeSpansToTrack = ArrayBuilder<(Document, Document, ITextSnapshot, ImmutableArray)>.GetInstance();
+
+ foreach (var (documentId, documentActiveStatements) in baseActiveStatements.DocumentMap)
+ {
+ var document = currentSolution.GetDocument(documentId);
+ if (document == null)
+ {
+ // Document has been deleted.
+ continue;
+ }
+
+ var (baseDocument, _) = await lastCommittedSolution.GetDocumentAndStateAsync(documentId, cancellationToken).ConfigureAwait(false);
+ if (baseDocument == null)
+ {
+ // Document has been added, is out-of-sync or a design-time-only document.
+ continue;
+ }
+
+ if (!TryGetSnapshot(document, out var snapshot))
+ {
+ // Document is not open in an editor or a corresponding snapshot doesn't exist anymore.
+ continue;
+ }
+
+ activeSpansToTrack.Add((baseDocument, document, snapshot, documentActiveStatements));
+ }
lock (_trackingSpans)
{
- foreach (var (documentId, documentActiveStatements) in baseActiveStatements.DocumentMap)
+ foreach (var (baseDocument, document, snapshot, documentActiveStatements) in activeSpansToTrack)
{
- var document = _editSession.BaseSolution.GetDocument(documentId);
- if (TryGetSnapshot(document, out var snapshot))
- {
- TrackActiveSpansNoLock(document, snapshot, documentActiveStatements);
- }
+ TrackActiveSpansNoLock(baseDocument, document, snapshot, documentActiveStatements);
}
}
+ activeSpansToTrack.Free();
+
_service.OnTrackingSpansChanged(leafChanged: true);
}
catch (OperationCanceledException)
@@ -215,13 +244,14 @@ private async Task TrackActiveSpansAsync()
}
private void TrackActiveSpansNoLock(
+ Document baseDocument,
Document document,
ITextSnapshot snapshot,
ImmutableArray documentActiveStatements)
{
- if (!_trackingSpans.TryGetValue(document.Id, out var documentTrackingSpans))
+ if (!_trackingSpans.TryGetValue(baseDocument.Id, out var documentTrackingSpans))
{
- SetTrackingSpansNoLock(document.Id, CreateTrackingSpans(snapshot, documentActiveStatements));
+ SetTrackingSpansNoLock(baseDocument.Id, CreateTrackingSpans(snapshot, documentActiveStatements));
}
else if (documentTrackingSpans != null)
{
@@ -232,19 +262,25 @@ private void TrackActiveSpansNoLock(
// The underlying text buffer has changed - this means that our tracking spans
// are no longer useful, we need to refresh them. Refresh happens asynchronously
// as we calculate document delta.
- SetTrackingSpansNoLock(document.Id, null);
+ SetTrackingSpansNoLock(baseDocument.Id, null);
- // fire and forget:
- _ = RefreshTrackingSpansAsync(document, snapshot);
+ // fire and forget on a background thread:
+ try
+ {
+ _ = Task.Run(() => RefreshTrackingSpansAsync(baseDocument, document, snapshot), _editSession.CancellationToken);
+ }
+ catch (TaskCanceledException)
+ {
+ }
}
}
}
- private async Task RefreshTrackingSpansAsync(Document document, ITextSnapshot snapshot)
+ private async Task RefreshTrackingSpansAsync(Document baseDocument, Document document, ITextSnapshot snapshot)
{
try
{
- var documentAnalysis = await _editSession.GetDocumentAnalysis(document).GetValueAsync(_editSession.CancellationToken).ConfigureAwait(false);
+ var documentAnalysis = await _editSession.GetDocumentAnalysis(baseDocument, document).GetValueAsync(_editSession.CancellationToken).ConfigureAwait(false);
// Do nothing if the statements aren't available (in presence of compilation errors).
if (!documentAnalysis.ActiveStatements.IsDefault)
@@ -276,9 +312,9 @@ private void RefreshTrackingSpans(DocumentId documentId, ITextSnapshot snapshot,
}
}
- private void SetTrackingSpansNoLock(DocumentId documentId, ActiveStatementTrackingSpan[] spansOpt)
+ private void SetTrackingSpansNoLock(DocumentId documentId, ActiveStatementTrackingSpan[]? spans)
{
- _trackingSpans[documentId] = spansOpt;
+ _trackingSpans[documentId] = spans;
}
private static ActiveStatementTrackingSpan[] CreateTrackingSpans(ITextSnapshot snapshot, ImmutableArray documentActiveStatements)
@@ -329,12 +365,12 @@ public IEnumerable GetSpans(SourceText source)
// We might be asked for spans in a different workspace than
// the one we maintain tracking spans for (for example, a preview).
- if (document.Project.Solution.Workspace != _editSession.BaseSolution.Workspace)
+ if (document.Project.Solution.Workspace != _editSession.DebuggingSession.Workspace)
{
return SpecializedCollections.EmptyEnumerable();
}
- ActiveStatementTrackingSpan[] documentTrackingSpans;
+ ActiveStatementTrackingSpan[]? documentTrackingSpans;
lock (_trackingSpans)
{
if (!_trackingSpans.TryGetValue(document.Id, out documentTrackingSpans) || documentTrackingSpans == null)
diff --git a/src/EditorFeatures/Core/Implementation/EditAndContinue/EditAndContinueSaveFileCommandHandler.cs b/src/EditorFeatures/Core/Implementation/EditAndContinue/EditAndContinueSaveFileCommandHandler.cs
new file mode 100644
index 0000000000000..7082ae6e26d92
--- /dev/null
+++ b/src/EditorFeatures/Core/Implementation/EditAndContinue/EditAndContinueSaveFileCommandHandler.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.ComponentModel.Composition;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.VisualStudio.Utilities;
+using Microsoft.VisualStudio.Commanding;
+using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
+using VSCommanding = Microsoft.VisualStudio.Commanding;
+using Microsoft.CodeAnalysis.EditAndContinue;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.CodeAnalysis.Editor.Implementation.EditAndContinue
+{
+ [Export]
+ [Export(typeof(VSCommanding.ICommandHandler))]
+ [ContentType(ContentTypeNames.RoslynContentType)]
+ [Name(PredefinedCommandHandlerNames.EditAndContinueFileSave)]
+ internal sealed class EditAndContinueSaveFileCommandHandler : IChainedCommandHandler
+ {
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public EditAndContinueSaveFileCommandHandler()
+ {
+ }
+
+ public string DisplayName => PredefinedCommandHandlerNames.EditAndContinueFileSave;
+
+ void IChainedCommandHandler.ExecuteCommand(SaveCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
+ {
+ var textContainer = args.SubjectBuffer.AsTextContainer();
+
+ if (Workspace.TryGetWorkspace(textContainer, out var workspace))
+ {
+ var encService = workspace.Services.GetService();
+ if (encService != null)
+ {
+ var documentId = workspace.GetDocumentIdInCurrentContext(textContainer);
+ if (documentId != null)
+ {
+ encService.OnSourceFileUpdated(documentId);
+ }
+ }
+ }
+
+ nextCommandHandler();
+ }
+
+ public VSCommanding.CommandState GetCommandState(SaveCommandArgs args, Func nextCommandHandler)
+ => nextCommandHandler();
+ }
+}
+
+
diff --git a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs
index 4a9f608302f3e..06a9dc0af7921 100644
--- a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs
+++ b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs
@@ -88,6 +88,29 @@ private EditAndContinueWorkspaceService CreateEditAndContinueService(Workspace w
_mockDebugeeModuleMetadataProvider,
reportTelemetry: data => EditAndContinueWorkspaceService.LogDebuggingSessionTelemetry(data, (id, message) => _telemetryLog.Add($"{id}: {message.GetMessage()}"), () => ++_telemetryId));
+ private DebuggingSession StartDebuggingSession(EditAndContinueWorkspaceService service, CommittedSolution.DocumentState initialState = CommittedSolution.DocumentState.MatchesDebuggee)
+ {
+ service.StartDebuggingSession();
+ var session = service.Test_GetDebuggingSession();
+ if (initialState != CommittedSolution.DocumentState.None)
+ {
+ SetDocumentsState(session, session.Workspace.CurrentSolution, initialState);
+ }
+
+ return session;
+ }
+
+ internal static void SetDocumentsState(DebuggingSession session, Solution solution, CommittedSolution.DocumentState state)
+ {
+ foreach (var project in solution.Projects)
+ {
+ foreach (var document in project.Documents)
+ {
+ session.LastCommittedSolution.Test_SetDocumentState(document.Id, state);
+ }
+ }
+ }
+
private void VerifyReanalyzeInvocation(params object[] expectedArgs)
=> _mockDiagnosticService.Invocations.VerifyAndClear((nameof(IDiagnosticAnalyzerService.Reanalyze), expectedArgs));
@@ -114,6 +137,33 @@ TService IDocumentServiceProvider.GetService()
documentProperties : DefaultTextDocumentServiceProvider.Instance.GetService();
}
+ private (DebuggeeModuleInfo, Guid) EmitAndLoadLibraryToDebuggee(string source, ProjectId projectId, string assemblyName = "", string sourceFilePath = "test1.cs")
+ {
+ var sourceText = SourceText.From(new MemoryStream(Encoding.UTF8.GetBytes(source)), encoding: Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256);
+ var tree = SyntaxFactory.ParseSyntaxTree(sourceText, TestOptions.RegularPreview, sourceFilePath);
+ var compilation = CSharpTestBase.CreateCompilationWithMscorlib40(tree, options: TestOptions.DebugDll, assemblyName: assemblyName);
+ var (peImage, symReader) = SymReaderTestHelpers.EmitAndOpenDummySymReader(compilation, DebugInformationFormat.PortablePdb);
+
+ var moduleMetadata = ModuleMetadata.CreateFromImage(peImage);
+ var moduleId = moduleMetadata.GetModuleVersionId();
+ var debuggeeModuleInfo = new DebuggeeModuleInfo(moduleMetadata, symReader);
+
+ // "load" it to the debuggee:
+ _mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;
+
+ // associate the binaries with the project
+ _mockCompilationOutputsService.Outputs.Add(projectId, new MockCompilationOutputs(moduleId));
+
+ return (debuggeeModuleInfo, moduleId);
+ }
+
+ private SourceText CreateSourceTextFromFile(string path)
+ {
+ using var stream = File.OpenRead(path);
+ return SourceText.From(stream, Encoding.UTF8, SourceHashAlgorithm.Sha256);
+ }
+
+
[Fact]
public void ActiveStatementTracking()
{
@@ -121,7 +171,7 @@ public void ActiveStatementTracking()
{
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
_mockActiveStatementTrackingService.Verify(ts => ts.StartTracking(It.IsAny()), Times.Once());
@@ -151,7 +201,7 @@ public async Task RunMode_ProjectThatDoesNotSupportEnC()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
// no changes:
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
@@ -190,7 +240,7 @@ public async Task RunMode_DesignTimeOnlyDocument()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
// update a design-time-only source file:
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single(d => d.Id == documentInfo.Id);
@@ -224,7 +274,7 @@ public async Task RunMode_ProjectNotBuilt()
var project = workspace.CurrentSolution.Projects.Single();
_mockCompilationOutputsService.Outputs.Add(project.Id, new MockCompilationOutputs(Guid.Empty));
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
// no changes:
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
@@ -253,7 +303,7 @@ public async Task RunMode_ErrorReadingFile()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
// no changes:
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
@@ -278,6 +328,49 @@ public async Task RunMode_ErrorReadingFile()
}
}
+ [Fact]
+ public async Task RunMode_DocumentOutOfSync()
+ {
+ var moduleFile = Temp.CreateFile().WriteAllBytes(TestResources.Basic.Members);
+
+ using var workspace = TestWorkspace.CreateCSharp("class C1 { void M() { System.Console.WriteLine(1); } }");
+ var service = CreateEditAndContinueService(workspace);
+
+ var project = workspace.CurrentSolution.Projects.Single();
+ workspace.ChangeSolution(project.Solution.WithProjectOutputFilePath(project.Id, moduleFile.Path));
+ _mockCompilationOutputsService.Outputs.Add(project.Id, new CompilationOutputFiles(moduleFile.Path));
+
+ var document1 = project.Documents.Single();
+
+ var debuggingSession = StartDebuggingSession(service);
+ debuggingSession.LastCommittedSolution.Test_SetDocumentState(document1.Id, CommittedSolution.DocumentState.OutOfSync);
+
+ // no changes:
+ var diagnostics = await service.GetDocumentDiagnosticsAsync(document1, CancellationToken.None).ConfigureAwait(false);
+ Assert.Empty(diagnostics);
+
+ // change the source:
+ workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void RenamedMethod() { System.Console.WriteLine(1); } }"));
+ var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ // no Rude Edits, since the document is out-of-sync
+ diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ Assert.Empty(diagnostics);
+
+ // the document is now in-sync (a file watcher observed a change and updated the status):
+ debuggingSession.LastCommittedSolution.Test_SetDocumentState(document2.Id, CommittedSolution.DocumentState.MatchesDebuggee);
+
+ diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Equal(new[] { "ENC1003" }, diagnostics.Select(d => d.Id));
+
+ service.EndDebuggingSession();
+
+ AssertEx.Equal(new[]
+ {
+ "Debugging_EncSession: SessionId=1|SessionCount=0|EmptySessionCount=0"
+ }, _telemetryLog);
+ }
+
[Fact]
public async Task RunMode_FileAdded()
{
@@ -293,7 +386,7 @@ public async Task RunMode_FileAdded()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
// add a source file:
var document2 = project.AddDocument("file2.cs", SourceText.From("class C2 {}"));
@@ -337,7 +430,7 @@ public async Task RunMode_Diagnostics()
var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, solutionStatus);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
// no changes:
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
@@ -386,7 +479,7 @@ public async Task RunMode_DifferentDocumentWithSameContent()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
// update the document
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
@@ -426,7 +519,7 @@ public async Task BreakMode_ProjectThatDoesNotSupportEnC()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
// change the source:
@@ -445,7 +538,7 @@ public async Task BreakMode_ProjectThatDoesNotSupportEnC()
}
[Fact]
- public async Task BreakMode_DesignTimeOnlyDocument()
+ public async Task BreakMode_DesignTimeOnlyDocument_Dynamic()
{
var exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory(
TestExportProvider.MinimumCatalogWithCSharpAndVisualBasic.WithPart(typeof(DummyLanguageService)));
@@ -468,7 +561,7 @@ public async Task BreakMode_DesignTimeOnlyDocument()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
// change the source:
@@ -484,6 +577,63 @@ public async Task BreakMode_DesignTimeOnlyDocument()
Assert.Empty(deltas);
}
+ [Fact]
+ public async Task BreakMode_DesignTimeOnlyDocument_Wpf()
+ {
+ var sourceA = "class A { public void M() { } }";
+ var sourceB = "class B { public void M() { } }";
+ var sourceC = "class C { public void M() { } }";
+
+ var dir = Temp.CreateDirectory();
+ var sourceFile = dir.CreateFile("a.cs").WriteAllText(sourceA);
+
+ using var workspace = new TestWorkspace();
+
+ // the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build):
+ var documentA = workspace.CurrentSolution.
+ AddProject("test", "test", LanguageNames.CSharp).
+ AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)).
+ AddDocument("a.cs", SourceText.From(sourceA, Encoding.UTF8), filePath: sourceFile.Path);
+
+ var documentB = documentA.Project.
+ AddDocument("b.g.i.cs", SourceText.From(sourceB, Encoding.UTF8), filePath: "b.g.i.cs");
+
+ var documentC = documentB.Project.
+ AddDocument("c.g.i.vb", SourceText.From(sourceC, Encoding.UTF8), filePath: "c.g.i.vb");
+
+ workspace.ChangeSolution(documentC.Project.Solution);
+
+ // only compile A; B and C are design-time-only:
+ var (_, moduleId) = EmitAndLoadLibraryToDebuggee(sourceA, documentA.Project.Id, sourceFilePath: sourceFile.Path);
+
+ var service = CreateEditAndContinueService(workspace);
+
+ var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);
+
+ service.StartEditSession();
+
+ // change the source (rude edit):
+ workspace.ChangeDocument(documentB.Id, SourceText.From("class B { public void RenamedMethod() { } }"));
+ workspace.ChangeDocument(documentC.Id, SourceText.From("class C { public void RenamedMethod() { } }"));
+ var documentB2 = workspace.CurrentSolution.Projects.Single().Documents.Single(d => d.Id == documentB.Id);
+ var documentC2 = workspace.CurrentSolution.Projects.Single().Documents.Single(d => d.Id == documentC.Id);
+
+ // no Rude Edits reported:
+ Assert.Empty(await service.GetDocumentDiagnosticsAsync(documentB2, CancellationToken.None).ConfigureAwait(false));
+ Assert.Empty(await service.GetDocumentDiagnosticsAsync(documentC2, CancellationToken.None).ConfigureAwait(false));
+
+ // validate solution update status and emit:
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatus);
+
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit);
+ Assert.Empty(_emitDiagnosticsUpdated);
+
+ service.EndEditSession();
+ service.EndDebuggingSession();
+ }
+
[Fact]
public async Task BreakMode_ErrorReadingFile()
{
@@ -496,7 +646,7 @@ public async Task BreakMode_ErrorReadingFile()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
// change the source:
@@ -566,7 +716,7 @@ public async Task BreakMode_FileAdded()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
// add a source file:
@@ -647,7 +797,7 @@ string inspectDiagnostic(Diagnostic d)
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
@@ -709,7 +859,7 @@ public async Task BreakMode_RudeEdits()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
@@ -748,6 +898,119 @@ public async Task BreakMode_RudeEdits()
}
}
+ [Fact]
+ public async Task BreakMode_RudeEdits_DocumentOutOfSync()
+ {
+ var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }";
+
+ using var workspace = TestWorkspace.CreateCSharp(source1);
+
+ var project = workspace.CurrentSolution.Projects.Single();
+ var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source1, project.Id);
+ var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ var service = CreateEditAndContinueService(workspace);
+
+ var debuggingSession = StartDebuggingSession(service);
+ debuggingSession.LastCommittedSolution.Test_SetDocumentState(document1.Id, CommittedSolution.DocumentState.OutOfSync);
+
+ service.StartEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ // change the source (rude edit):
+ workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void RenamedMethod() { System.Console.WriteLine(1); } }"));
+ var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ // no Rude Edits, since the document is out-of-sync
+ var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ Assert.Empty(diagnostics);
+
+ // validate solution update status and emit:
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);
+
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
+ Assert.Empty(deltas);
+
+ AssertEx.Equal(
+ new[] { "ENC1005: " + string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, "test1.cs") },
+ _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => $"{d.Id}: {d.Message}"));
+
+ _emitDiagnosticsUpdated.Clear();
+ _emitDiagnosticsClearedCount = 0;
+
+ // the document is now in-sync (a file watcher observed a change and updated the status):
+ debuggingSession.LastCommittedSolution.Test_SetDocumentState(document1.Id, CommittedSolution.DocumentState.MatchesDebuggee);
+
+ diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Equal(
+ new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) },
+ diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}"));
+
+ // validate solution update status and emit:
+ solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);
+
+ (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
+ Assert.Empty(deltas);
+
+ service.EndEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Create(document2.Id), false);
+
+ service.EndDebuggingSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ AssertEx.Equal(new[] { moduleId }, _modulesPreparedForUpdate);
+
+ AssertEx.Equal(new[]
+ {
+ "Debugging_EncSession: SessionId=1|SessionCount=1|EmptySessionCount=0",
+ "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=1|EmitDeltaErrorIdCount=0",
+ "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=20|RudeEditSyntaxKind=8875|RudeEditBlocking=True"
+ }, _telemetryLog);
+ }
+
+ [Fact]
+ public async Task BreakMode_RudeEdits_DocumentWithoutSequencePoints()
+ {
+ var source1 = "abstract class C { public abstract void M(); }";
+
+ using var workspace = TestWorkspace.CreateCSharp(source1);
+
+ var project = workspace.CurrentSolution.Projects.Single();
+ var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source1, project.Id);
+ var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ var service = CreateEditAndContinueService(workspace);
+
+ var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);
+
+ service.StartEditSession();
+
+ // change the source (rude edit):
+ workspace.ChangeDocument(document1.Id, SourceText.From("abstract class C { public abstract void M(); public abstract void N(); }"));
+ var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ // Rude Edits reported:
+ var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Equal(
+ new[] { "ENC0023: " + string.Format(FeaturesResources.Adding_an_abstract_0_or_overriding_an_inherited_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) },
+ diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}"));
+
+ // validate solution update status and emit:
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);
+
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
+ Assert.Empty(deltas);
+
+ service.EndEditSession();
+ service.EndDebuggingSession();
+ }
+
[Fact]
public async Task BreakMode_SyntaxError()
{
@@ -760,7 +1023,7 @@ public async Task BreakMode_SyntaxError()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
@@ -802,66 +1065,58 @@ public async Task BreakMode_SyntaxError()
public async Task BreakMode_SemanticError()
{
var sourceV1 = "class C1 { void M() { System.Console.WriteLine(1); } }";
- var compilationV1 = CSharpTestBase.CreateCompilationWithMscorlib40(sourceV1, options: TestOptions.DebugDll);
- var (peImage, symReader) = SymReaderTestHelpers.EmitAndOpenDummySymReader(compilationV1, DebugInformationFormat.PortablePdb);
- var moduleMetadata = ModuleMetadata.CreateFromImage(peImage);
- var moduleId = moduleMetadata.GetModuleVersionId();
- var debuggeeModuleInfo = new DebuggeeModuleInfo(moduleMetadata, symReader);
+ using var workspace = TestWorkspace.CreateCSharp(sourceV1);
- using (var workspace = TestWorkspace.CreateCSharp(sourceV1))
- {
- var project = workspace.CurrentSolution.Projects.Single();
- _mockCompilationOutputsService.Outputs.Add(project.Id, new MockCompilationOutputs(moduleId));
- _mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;
+ var project = workspace.CurrentSolution.Projects.Single();
+ var (_, moduleId) = EmitAndLoadLibraryToDebuggee(sourceV1, project.Id);
- var service = CreateEditAndContinueService(workspace);
+ var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
- service.StartEditSession();
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ service.StartEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
- // change the source (compilation error):
- var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
- workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { int i = 0L; System.Console.WriteLine(i); } }", Encoding.UTF8));
- var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+ // change the source (compilation error):
+ var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+ workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { int i = 0L; System.Console.WriteLine(i); } }", Encoding.UTF8));
+ var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
- // compilation errors are not reported via EnC diagnostic analyzer:
- var diagnostics1 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
- AssertEx.Empty(diagnostics1);
+ // compilation errors are not reported via EnC diagnostic analyzer:
+ var diagnostics1 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Empty(diagnostics1);
- // The EnC analyzer does not check for and block on all semantic errors as they are already reported by diagnostic analyzer.
- // Blocking update on semantic errors would be possible, but the status check is only an optimization to avoid emitting.
- var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
- Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
+ // The EnC analyzer does not check for and block on all semantic errors as they are already reported by diagnostic analyzer.
+ // Blocking update on semantic errors would be possible, but the status check is only an optimization to avoid emitting.
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
- var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
- Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
- Assert.Empty(deltas);
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
+ Assert.Empty(deltas);
- // TODO: https://github.com/dotnet/roslyn/issues/36061
- // Semantic errors should not be reported in emit diagnostics.
- AssertEx.Equal(new[] { "CS0266" }, _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => d.Id));
- Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
- _emitDiagnosticsUpdated.Clear();
- _emitDiagnosticsClearedCount = 0;
+ // TODO: https://github.com/dotnet/roslyn/issues/36061
+ // Semantic errors should not be reported in emit diagnostics.
+ AssertEx.Equal(new[] { "CS0266" }, _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => d.Id));
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
+ _emitDiagnosticsUpdated.Clear();
+ _emitDiagnosticsClearedCount = 0;
- service.EndEditSession();
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ service.EndEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
- service.EndDebuggingSession();
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ service.EndDebuggingSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
- AssertEx.Equal(new[] { moduleId }, _modulesPreparedForUpdate);
+ AssertEx.Equal(new[] { moduleId }, _modulesPreparedForUpdate);
- AssertEx.Equal(new[]
- {
- "Debugging_EncSession: SessionId=1|SessionCount=1|EmptySessionCount=0",
- "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=True|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1",
- "Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId=CS0266"
- }, _telemetryLog);
- }
+ AssertEx.Equal(new[]
+ {
+ "Debugging_EncSession: SessionId=1|SessionCount=1|EmptySessionCount=0",
+ "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=True|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1",
+ "Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId=CS0266"
+ }, _telemetryLog);
}
[Fact]
@@ -882,7 +1137,7 @@ public async Task BreakMode_FileStatus_CompilationError()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
// change C.cs to have a compilation error:
@@ -911,83 +1166,309 @@ public async Task BreakMode_FileStatus_CompilationError()
public async Task BreakMode_ValidSignificantChange_EmitError()
{
var sourceV1 = "class C1 { void M() { System.Console.WriteLine(1); } }";
- var compilationV1 = CSharpTestBase.CreateCompilationWithMscorlib40(sourceV1, options: TestOptions.DebugDll);
- var (peImage, symReader) = SymReaderTestHelpers.EmitAndOpenDummySymReader(compilationV1, DebugInformationFormat.PortablePdb);
- var moduleMetadata = ModuleMetadata.CreateFromImage(peImage);
- var moduleFile = Temp.CreateFile().WriteAllBytes(peImage);
- var moduleId = moduleMetadata.GetModuleVersionId();
- var debuggeeModuleInfo = new DebuggeeModuleInfo(moduleMetadata, symReader);
+ using var workspace = TestWorkspace.CreateCSharp(sourceV1);
- using (var workspace = TestWorkspace.CreateCSharp(sourceV1))
+ var project = workspace.CurrentSolution.Projects.Single();
+ EmitAndLoadLibraryToDebuggee(sourceV1, project.Id);
+
+ var service = CreateEditAndContinueService(workspace);
+
+ StartDebuggingSession(service);
+
+ service.StartEditSession();
+ var editSession = service.Test_GetEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ // change the source (valid edit but passing no encoding to emulate emit error):
+ var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+ workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }", encoding: null));
+ var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ var diagnostics1 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Empty(diagnostics1);
+
+ // validate solution update status and emit:
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
+
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Equal(new[] { "CS8055" }, _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => d.Id));
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
+ _emitDiagnosticsUpdated.Clear();
+ _emitDiagnosticsClearedCount = 0;
+
+ // no emitted delta:
+ Assert.Empty(deltas);
+
+ // no pending update:
+ Assert.Null(service.Test_GetPendingSolutionUpdate());
+
+ Assert.Throws(() => service.CommitSolutionUpdate());
+ Assert.Throws(() => service.DiscardSolutionUpdate());
+
+ // no change in non-remappable regions since we didn't have any active statements:
+ Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);
+
+ // no open module readers since we didn't defer any module update:
+ Assert.Empty(editSession.DebuggingSession.GetBaselineModuleReaders());
+
+ // solution update status after discarding an update (still has update ready):
+ var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Ready, commitedUpdateSolutionStatus);
+
+ service.EndEditSession();
+ Assert.Empty(_emitDiagnosticsUpdated);
+ Assert.Equal(0, _emitDiagnosticsClearedCount);
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ service.EndDebuggingSession();
+ Assert.Empty(_emitDiagnosticsUpdated);
+ Assert.Equal(1, _emitDiagnosticsClearedCount);
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ AssertEx.Equal(new[]
{
- var project = workspace.CurrentSolution.Projects.Single();
- _mockCompilationOutputsService.Outputs.Add(project.Id, new CompilationOutputFiles(moduleFile.Path));
+ "Debugging_EncSession: SessionId=1|SessionCount=1|EmptySessionCount=0",
+ "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=True|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1",
+ "Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId=CS8055"
+ }, _telemetryLog);
+ }
- _mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task BreakMode_ValidSignificantChange_ApplyBeforeFileWatcherEvent(bool saveDocument)
+ {
+ // Scenarios tested:
+ //
+ // SaveDocument=true
+ // workspace: --V0-------------|--V2--------|------------|
+ // file system: --V0---------V1--|-----V2-----|------------|
+ // \--build--/ F5 ^ F10 ^ F10
+ // save file watcher: no-op
+ // SaveDocument=false
+ // workspace: --V0-------------|--V2--------|----V1------|
+ // file system: --V0---------V1--|------------|------------|
+ // \--build--/ F5 F10 ^ F10
+ // file watcher: workspace update
+
+ var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }";
- var service = CreateEditAndContinueService(workspace);
+ var dir = Temp.CreateDirectory();
+ var sourceFile = dir.CreateFile("test.cs").WriteAllText(source1);
- service.StartDebuggingSession();
+ using var workspace = new TestWorkspace();
- service.StartEditSession();
- var editSession = service.Test_GetEditSession();
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ // the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build):
+ var document1 = workspace.CurrentSolution.
+ AddProject("test", "test", LanguageNames.CSharp).
+ AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)).
+ AddDocument("test.cs", SourceText.From("class C1 { void M() { System.Console.WriteLine(0); } }", Encoding.UTF8), filePath: sourceFile.Path);
- // change the source (valid edit but passing no encoding to emulate emit error):
- var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
- workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }", encoding: null));
- var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+ var documentId = document1.Id;
- var diagnostics1 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
- AssertEx.Empty(diagnostics1);
+ var project = document1.Project;
+ workspace.ChangeSolution(project.Solution);
- // validate solution update status and emit:
- var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
- Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
+ var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source1, project.Id, sourceFilePath: sourceFile.Path);
- var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
- AssertEx.Equal(new[] { "CS8055" }, _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => d.Id));
- Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
- _emitDiagnosticsUpdated.Clear();
- _emitDiagnosticsClearedCount = 0;
+ var service = CreateEditAndContinueService(workspace);
+ var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);
- // no emitted delta:
- Assert.Empty(deltas);
+ service.StartEditSession();
- // no pending update:
- Assert.Null(service.Test_GetPendingSolutionUpdate());
+ // The user opens the source file and changes the source before Roslyn receives file watcher event.
+ var source2 = "class C1 { void M() { System.Console.WriteLine(2); } }";
+ workspace.ChangeDocument(documentId, SourceText.From(source2, Encoding.UTF8));
+ var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
- Assert.Throws(() => service.CommitSolutionUpdate());
- Assert.Throws(() => service.DiscardSolutionUpdate());
+ // Save the document:
+ if (saveDocument)
+ {
+ await debuggingSession.LastCommittedSolution.OnSourceFileUpdatedAsync(documentId, debuggingSession.CancellationToken).ConfigureAwait(false);
+ sourceFile.WriteAllText(source2);
+ }
- // no change in non-remappable regions since we didn't have any active statements:
- Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);
+ // EnC service queries for a document, which triggers read of the source file from disk.
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
- // no open module readers since we didn't defer any module update:
- Assert.Empty(editSession.DebuggingSession.GetBaselineModuleReaders());
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit);
+ service.CommitSolutionUpdate();
- // solution update status after discarding an update (still has update ready):
- var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
- Assert.Equal(SolutionUpdateStatus.Ready, commitedUpdateSolutionStatus);
+ service.EndEditSession();
- service.EndEditSession();
- Assert.Empty(_emitDiagnosticsUpdated);
- Assert.Equal(0, _emitDiagnosticsClearedCount);
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ service.StartEditSession();
- service.EndDebuggingSession();
- Assert.Empty(_emitDiagnosticsUpdated);
- Assert.Equal(1, _emitDiagnosticsClearedCount);
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ // file watcher updates the workspace:
+ workspace.ChangeDocument(documentId, CreateSourceTextFromFile(sourceFile.Path));
+ var document3 = workspace.CurrentSolution.Projects.Single().Documents.Single();
- AssertEx.Equal(new[]
- {
- "Debugging_EncSession: SessionId=1|SessionCount=1|EmptySessionCount=0",
- "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=True|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1",
- "Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId=CS8055"
- }, _telemetryLog);
+ solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+
+ if (saveDocument)
+ {
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatus);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit);
}
+ else
+ {
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit);
+ }
+
+ service.EndEditSession();
+ service.EndDebuggingSession();
+ }
+
+ [Fact]
+ public async Task BreakMode_ValidSignificantChange_FileUpdateBeforeDebuggingSessionStarts()
+ {
+ // workspace: --V0--------------V2-------|--------V3---------------V1--------------|
+ // file system: --V0---------V1-----V2-----|---------------------------V1------------|
+ // \--build--/ ^save F5 ^ ^F10 (blocked) ^save F10 (ok)
+ // file watcher: no-op
+
+ var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }";
+ var source2 = "class C1 { void M() { System.Console.WriteLine(2); } }";
+ var source3 = "class C1 { void M() { System.Console.WriteLine(3); } }";
+
+ var dir = Temp.CreateDirectory();
+ var sourceFile = dir.CreateFile("test.cs").WriteAllText(source2);
+
+ using var workspace = new TestWorkspace();
+
+ // the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build):
+ var document2 = workspace.CurrentSolution.
+ AddProject("test", "test", LanguageNames.CSharp).
+ AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)).
+ AddDocument("test.cs", SourceText.From(source2, Encoding.UTF8), filePath: sourceFile.Path);
+
+ var documentId = document2.Id;
+
+ var project = document2.Project;
+ workspace.ChangeSolution(project.Solution);
+
+ var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source1, project.Id, sourceFilePath: sourceFile.Path);
+
+ var service = CreateEditAndContinueService(workspace);
+ var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);
+
+ service.StartEditSession();
+
+ // user edits the file:
+ workspace.ChangeDocument(documentId, SourceText.From(source3, Encoding.UTF8));
+ var document3 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ // EnC service queries for a document, but the source file on disk doesn't match the PDB
+
+ // We don't report rude edits for out-of-sync documents:
+ var diagnostics = await service.GetDocumentDiagnosticsAsync(document3, CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Empty(diagnostics);
+
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);
+ Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
+
+ AssertEx.Equal(
+ new[] { "ENC1005: " + string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFile.Path) },
+ _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => $"{d.Id}: {d.Message}"));
+
+ _emitDiagnosticsUpdated.Clear();
+ _emitDiagnosticsClearedCount = 0;
+
+ // undo:
+ workspace.ChangeDocument(documentId, SourceText.From(source1, Encoding.UTF8));
+
+ // save (note that this call will fail to match the content with the PDB since it uses the content prior to the actual file write)
+ await debuggingSession.LastCommittedSolution.OnSourceFileUpdatedAsync(documentId, debuggingSession.CancellationToken).ConfigureAwait(false);
+ var (doc, state) = await debuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(documentId, CancellationToken.None).ConfigureAwait(false);
+ Assert.Null(doc);
+ Assert.Equal(CommittedSolution.DocumentState.OutOfSync, state);
+ sourceFile.WriteAllText(source1);
+
+ solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+
+ // the content actually hasn't changed:
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatus);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit);
+
+ service.EndEditSession();
+ service.EndDebuggingSession();
+ }
+
+ [Fact]
+ public async Task BreakMode_ValidSignificantChange_DocumentOutOfSync2()
+ {
+ var sourceOnDisk = "class C1 { void M() { System.Console.WriteLine(1); } }";
+
+ var dir = Temp.CreateDirectory();
+ var sourceFile = dir.CreateFile("test.cs").WriteAllText(sourceOnDisk);
+
+ using var workspace = new TestWorkspace();
+
+ // the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build):
+ var document1 = workspace.CurrentSolution.
+ AddProject("test", "test", LanguageNames.CSharp).
+ AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)).
+ AddDocument("test.cs", SourceText.From("class C1 { void M() { System.Console.WriteLine(0); } }", Encoding.UTF8), filePath: sourceFile.Path);
+
+ var project = document1.Project;
+ workspace.ChangeSolution(project.Solution);
+
+ var (_, moduleId) = EmitAndLoadLibraryToDebuggee(sourceOnDisk, project.Id, sourceFilePath: sourceFile.Path);
+
+ var service = CreateEditAndContinueService(workspace);
+
+ var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);
+
+ service.StartEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ // no changes have been made to the project
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatus);
+
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit);
+ Assert.Empty(deltas);
+
+ Assert.Empty(_emitDiagnosticsUpdated);
+
+ _emitDiagnosticsUpdated.Clear();
+ _emitDiagnosticsClearedCount = 0;
+
+ // a file watcher observed a change and updated the document, so it now reflects the content on disk (the code that we compiled):
+ workspace.ChangeDocument(document1.Id, SourceText.From(sourceOnDisk, Encoding.UTF8));
+ var document3 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+
+ var diagnostics2 = await service.GetDocumentDiagnosticsAsync(document3, CancellationToken.None).ConfigureAwait(false);
+ Assert.Empty(diagnostics2);
+
+ // the content of the file is now exactly the same as the compiled document, so there is no change to be applied:
+ var solutionStatus2 = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatus2);
+
+ var (solutionStatusEmit2, deltas2) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit2);
+
+ service.EndEditSession();
+
+ // no diagnostics reported via a document analyzer
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ service.EndDebuggingSession();
+
+ // no diagnostics reported via a document analyzer
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+
+ Assert.Empty(_modulesPreparedForUpdate);
}
[Theory]
@@ -996,112 +1477,102 @@ public async Task BreakMode_ValidSignificantChange_EmitError()
public async Task BreakMode_ValidSignificantChange_EmitSuccessful(bool commitUpdate)
{
var sourceV1 = "class C1 { void M() { System.Console.WriteLine(1); } }";
- var compilationV1 = CSharpTestBase.CreateCompilationWithMscorlib40(sourceV1, options: TestOptions.DebugDll);
- var (peImage, symReader) = SymReaderTestHelpers.EmitAndOpenDummySymReader(compilationV1, DebugInformationFormat.PortablePdb);
-
- var moduleMetadata = ModuleMetadata.CreateFromImage(peImage);
- var moduleFile = Temp.CreateFile().WriteAllBytes(peImage);
- var moduleId = moduleMetadata.GetModuleVersionId();
- var debuggeeModuleInfo = new DebuggeeModuleInfo(moduleMetadata, symReader);
- using (var workspace = TestWorkspace.CreateCSharp(sourceV1))
- {
- var project = workspace.CurrentSolution.Projects.Single();
- _mockCompilationOutputsService.Outputs.Add(project.Id, new CompilationOutputFiles(moduleFile.Path));
+ using var workspace = TestWorkspace.CreateCSharp(sourceV1);
- var diagnosticUpdateSource = new EditAndContinueDiagnosticUpdateSource();
- var emitDiagnosticsUpdated = new List();
- diagnosticUpdateSource.DiagnosticsUpdated += (object sender, DiagnosticsUpdatedArgs args) => emitDiagnosticsUpdated.Add(args);
+ var project = workspace.CurrentSolution.Projects.Single();
+ var (debuggeeModuleInfo, moduleId) = EmitAndLoadLibraryToDebuggee(sourceV1, project.Id);
- _mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;
+ var diagnosticUpdateSource = new EditAndContinueDiagnosticUpdateSource();
+ var emitDiagnosticsUpdated = new List();
+ diagnosticUpdateSource.DiagnosticsUpdated += (object sender, DiagnosticsUpdatedArgs args) => emitDiagnosticsUpdated.Add(args);
- var service = CreateEditAndContinueService(workspace);
+ var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
- service.StartEditSession();
- var editSession = service.Test_GetEditSession();
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ service.StartEditSession();
+ var editSession = service.Test_GetEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
- // change the source (valid edit):
- var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
- workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }", Encoding.UTF8));
- var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+ // change the source (valid edit):
+ var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
+ workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }", Encoding.UTF8));
+ var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();
- var diagnostics1 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
- AssertEx.Empty(diagnostics1);
+ var diagnostics1 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Empty(diagnostics1);
- // validate solution update status and emit:
- var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
- Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
+ // validate solution update status and emit:
+ var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
- var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
- AssertEx.Empty(emitDiagnosticsUpdated);
- Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit);
+ var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
+ AssertEx.Empty(emitDiagnosticsUpdated);
+ Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit);
- // check emitted delta:
- var delta = deltas.Single();
- Assert.Empty(delta.ActiveStatementsInUpdatedMethods);
- Assert.NotEmpty(delta.IL.Value);
- Assert.NotEmpty(delta.Metadata.Bytes);
- Assert.NotEmpty(delta.Pdb.Stream);
- Assert.Equal(0x06000001, delta.Pdb.UpdatedMethods.Single());
- Assert.Equal(moduleId, delta.Mvid);
- Assert.Empty(delta.NonRemappableRegions);
- Assert.Empty(delta.LineEdits);
+ // check emitted delta:
+ var delta = deltas.Single();
+ Assert.Empty(delta.ActiveStatementsInUpdatedMethods);
+ Assert.NotEmpty(delta.IL.Value);
+ Assert.NotEmpty(delta.Metadata.Bytes);
+ Assert.NotEmpty(delta.Pdb.Stream);
+ Assert.Equal(0x06000001, delta.Pdb.UpdatedMethods.Single());
+ Assert.Equal(moduleId, delta.Mvid);
+ Assert.Empty(delta.NonRemappableRegions);
+ Assert.Empty(delta.LineEdits);
- // the update should be stored on the service:
- var pendingUpdate = service.Test_GetPendingSolutionUpdate();
- var (baselineProjectId, newBaseline) = pendingUpdate.EmitBaselines.Single();
- AssertEx.Equal(deltas, pendingUpdate.Deltas);
- Assert.Empty(pendingUpdate.ModuleReaders);
- Assert.Equal(project.Id, baselineProjectId);
- Assert.Equal(moduleId, newBaseline.OriginalMetadata.GetModuleVersionId());
+ // the update should be stored on the service:
+ var pendingUpdate = service.Test_GetPendingSolutionUpdate();
+ var (baselineProjectId, newBaseline) = pendingUpdate.EmitBaselines.Single();
+ AssertEx.Equal(deltas, pendingUpdate.Deltas);
+ Assert.Empty(pendingUpdate.ModuleReaders);
+ Assert.Equal(project.Id, baselineProjectId);
+ Assert.Equal(moduleId, newBaseline.OriginalMetadata.GetModuleVersionId());
- if (commitUpdate)
- {
- // all update providers either provided updates or had no change to apply:
- service.CommitSolutionUpdate();
+ if (commitUpdate)
+ {
+ // all update providers either provided updates or had no change to apply:
+ service.CommitSolutionUpdate();
- Assert.Null(service.Test_GetPendingSolutionUpdate());
+ Assert.Null(service.Test_GetPendingSolutionUpdate());
- // no change in non-remappable regions since we didn't have any active statements:
- Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);
+ // no change in non-remappable regions since we didn't have any active statements:
+ Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);
- // no open module readers since we didn't defer any module update:
- Assert.Empty(editSession.DebuggingSession.GetBaselineModuleReaders());
+ // no open module readers since we didn't defer any module update:
+ Assert.Empty(editSession.DebuggingSession.GetBaselineModuleReaders());
- // verify that baseline is added:
- Assert.Same(newBaseline, editSession.DebuggingSession.Test_GetProjectEmitBaseline(project.Id));
+ // verify that baseline is added:
+ Assert.Same(newBaseline, editSession.DebuggingSession.Test_GetProjectEmitBaseline(project.Id));
- // solution update status after committing an update:
- var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
- Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus);
- }
- else
- {
- // another update provider blocked the update:
- service.DiscardSolutionUpdate();
+ // solution update status after committing an update:
+ var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus);
+ }
+ else
+ {
+ // another update provider blocked the update:
+ service.DiscardSolutionUpdate();
- Assert.Null(service.Test_GetPendingSolutionUpdate());
+ Assert.Null(service.Test_GetPendingSolutionUpdate());
- // solution update status after committing an update:
- var discardedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
- Assert.Equal(SolutionUpdateStatus.Ready, discardedUpdateSolutionStatus);
- }
+ // solution update status after committing an update:
+ var discardedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(SolutionUpdateStatus.Ready, discardedUpdateSolutionStatus);
+ }
- service.EndEditSession();
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ service.EndEditSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
- service.EndDebuggingSession();
- VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
+ service.EndDebuggingSession();
+ VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false);
- AssertEx.Equal(new[] { moduleId }, _modulesPreparedForUpdate);
- }
+ AssertEx.Equal(new[] { moduleId }, _modulesPreparedForUpdate);
// the debugger disposes the module metadata and SymReader:
debuggeeModuleInfo.Dispose();
- Assert.True(moduleMetadata.IsDisposed);
+ Assert.True(debuggeeModuleInfo.Metadata.IsDisposed);
Assert.Null(debuggeeModuleInfo.SymReader);
AssertEx.Equal(new[]
@@ -1152,7 +1623,7 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful_UpdateDeferred
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
var editSession = service.Test_GetEditSession();
@@ -1260,6 +1731,7 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()
var source1 = "class A { void M() { System.Console.WriteLine(1); } }";
var source2 = "class A { void M() { System.Console.WriteLine(2); } }";
var source3 = "class A { void M() { System.Console.WriteLine(3); } }";
+
var compilationA = CSharpTestBase.CreateCompilationWithMscorlib40(source1, options: TestOptions.DebugDll, assemblyName: "A");
var compilationB = CSharpTestBase.CreateCompilationWithMscorlib45(source1, options: TestOptions.DebugDll, assemblyName: "B");
@@ -1283,7 +1755,7 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()
{
var solution = workspace.CurrentSolution;
var projectA = solution.Projects.Single();
- var projectB = solution.AddProject("B", "A", "C#").AddMetadataReferences(projectA.MetadataReferences).AddDocument("DocB", source1).Project;
+ var projectB = solution.AddProject("B", "A", "C#").AddMetadataReferences(projectA.MetadataReferences).AddDocument("DocB", source1, filePath: "DocB.cs").Project;
workspace.ChangeSolution(projectB.Solution);
_mockCompilationOutputsService.Outputs.Add(projectA.Id, new CompilationOutputFiles(moduleFileA.Path));
@@ -1295,7 +1767,7 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
var editSession = service.Test_GetEditSession();
@@ -1500,7 +1972,7 @@ public async Task BreakMode_ValidSignificantChange_BaselineCreationFailed_NoStre
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
@@ -1538,7 +2010,7 @@ public async Task BreakMode_ValidSignificantChange_BaselineCreationFailed_Assemb
var service = CreateEditAndContinueService(workspace);
- service.StartDebuggingSession();
+ StartDebuggingSession(service);
service.StartEditSession();
diff --git a/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs
index 342e73e7a74b3..d5d9465a1beda 100644
--- a/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs
+++ b/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs
@@ -87,42 +87,51 @@ IEnumerable Enumerate()
return Enumerate().ToImmutableArray();
}
- internal static async Task<(ActiveStatementsMap, ImmutableArray, ImmutableArray)> GetBaseActiveStatementsAndExceptionRegions(
- string[] markedSource,
- ImmutableArray activeStatements,
- ImmutableDictionary> nonRemappableRegions = null,
- Func adjustSolution = null)
+ private sealed class Validator
{
- var exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory(
+ public readonly TestWorkspace Workspace;
+ public readonly EditSession EditSession;
+
+ public Validator(
+ string[] markedSource,
+ ImmutableArray activeStatements,
+ ImmutableDictionary> nonRemappableRegions = null,
+ Func adjustSolution = null,
+ CommittedSolution.DocumentState initialState = CommittedSolution.DocumentState.MatchesDebuggee)
+ {
+ var exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory(
TestExportProvider.MinimumCatalogWithCSharpAndVisualBasic.WithPart(typeof(CSharpEditAndContinueAnalyzer)).WithPart(typeof(DummyLanguageService)));
- var exportProvider = exportProviderFactory.CreateExportProvider();
+ var exportProvider = exportProviderFactory.CreateExportProvider();
- using var workspace = TestWorkspace.CreateCSharp(ActiveStatementsDescription.ClearTags(markedSource), exportProvider: exportProvider);
+ Workspace = TestWorkspace.CreateCSharp(ActiveStatementsDescription.ClearTags(markedSource), exportProvider: exportProvider);
- if (adjustSolution != null)
- {
- workspace.ChangeSolution(adjustSolution(workspace.CurrentSolution));
- }
+ if (adjustSolution != null)
+ {
+ Workspace.ChangeSolution(adjustSolution(Workspace.CurrentSolution));
+ }
- var docsIds = from p in workspace.CurrentSolution.Projects
- from d in p.DocumentIds
- select d;
+ var activeStatementProvider = new TestActiveStatementProvider(activeStatements);
+ var mockDebuggeModuleProvider = new Mock();
+ var mockCompilationOutputsProvider = new MockCompilationOutputsProviderService();
- var activeStatementProvider = new TestActiveStatementProvider(activeStatements);
- var mockDebuggeModuleProvider = new Mock();
- var mockCompilationOutputsProvider = new Mock();
+ var debuggingSession = new DebuggingSession(Workspace, mockDebuggeModuleProvider.Object, activeStatementProvider, mockCompilationOutputsProvider);
- var debuggingSession = new DebuggingSession(workspace, mockDebuggeModuleProvider.Object, activeStatementProvider, mockCompilationOutputsProvider.Object);
+ if (initialState != CommittedSolution.DocumentState.None)
+ {
+ EditAndContinueWorkspaceServiceTests.SetDocumentsState(debuggingSession, Workspace.CurrentSolution, initialState);
+ }
- debuggingSession.Test_SetNonRemappableRegions(nonRemappableRegions ?? ImmutableDictionary>.Empty);
+ debuggingSession.Test_SetNonRemappableRegions(nonRemappableRegions ?? ImmutableDictionary>.Empty);
- var telemetry = new EditSessionTelemetry();
- var editSession = new EditSession(debuggingSession, telemetry);
+ var telemetry = new EditSessionTelemetry();
+ EditSession = new EditSession(debuggingSession, telemetry);
+ }
- return (await editSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false),
- await editSession.BaseActiveExceptionRegions.GetValueAsync(CancellationToken.None).ConfigureAwait(false),
- docsIds.ToImmutableArray());
+ public ImmutableArray GetDocumentIds()
+ => (from p in Workspace.CurrentSolution.Projects
+ from d in p.DocumentIds
+ select d).ToImmutableArray();
}
private static string Delete(string src, string marker)
@@ -271,11 +280,13 @@ static void Main()
return project.Solution.AddDocument(DocumentId.CreateNewId(project.Id, DummyLanguageService.LanguageName), "a.dummy", "");
});
- var (baseActiveStatements, baseExceptionRegions, docs) = await GetBaseActiveStatementsAndExceptionRegions(markedSource, activeStatements, adjustSolution: adjustSolution).ConfigureAwait(false);
+ var validator = new Validator(markedSource, activeStatements, adjustSolution: adjustSolution);
+ var baseActiveStatementsMap = await validator.EditSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
+ var docs = validator.GetDocumentIds();
// Active Statements
- var statements = baseActiveStatements.InstructionMap.Values.OrderBy(v => v.Ordinal).ToArray();
+ var statements = baseActiveStatementsMap.InstructionMap.Values.OrderBy(v => v.Ordinal).ToArray();
AssertEx.Equal(new[]
{
"0: (9,14)-(9,35) flags=[IsLeafFrame, MethodUpToDate] pdid=test1.cs docs=[test1.cs] mvid=11111111-1111-1111-1111-111111111111 0x06000001 v1 IL_0001",
@@ -287,23 +298,25 @@ static void Main()
// Active Statements per document
- Assert.Equal(2, baseActiveStatements.DocumentMap.Count);
+ Assert.Equal(2, baseActiveStatementsMap.DocumentMap.Count);
AssertEx.Equal(new[]
{
"0: (9,14)-(9,35) flags=[IsLeafFrame, MethodUpToDate] pdid=test1.cs docs=[test1.cs]",
"1: (4,32)-(4,37) flags=[MethodUpToDate, IsNonLeafFrame] pdid=test1.cs docs=[test1.cs]"
- }, baseActiveStatements.DocumentMap[docs[0]].Select(InspectActiveStatement));
+ }, baseActiveStatementsMap.DocumentMap[docs[0]].Select(InspectActiveStatement));
AssertEx.Equal(new[]
{
"2: (21,14)-(21,24) flags=[MethodUpToDate, IsNonLeafFrame] pdid=test2.cs docs=[test2.cs]",
"3: (8,20)-(8,25) flags=[MethodUpToDate, IsNonLeafFrame] pdid=test2.cs docs=[test2.cs]",
"4: (26,20)-(26,25) flags=[MethodUpToDate, IsNonLeafFrame] pdid=test2.cs docs=[test2.cs]"
- }, baseActiveStatements.DocumentMap[docs[1]].Select(InspectActiveStatement));
+ }, baseActiveStatementsMap.DocumentMap[docs[1]].Select(InspectActiveStatement));
// Exception Regions
+ var baseExceptionRegions = await validator.EditSession.GetBaseActiveExceptionRegionsAsync(CancellationToken.None).ConfigureAwait(false);
+
AssertEx.Equal(new[]
{
"[]",
@@ -319,7 +332,7 @@ static void Main()
// Test2.M2: adding a line in front of try-catch.
// Test2.F2: moving the entire method 2 lines down.
- LinePositionSpan AddDelta(LinePositionSpan span, int lineDelta)
+ static LinePositionSpan AddDelta(LinePositionSpan span, int lineDelta)
=> new LinePositionSpan(new LinePosition(span.Start.Line + lineDelta, span.Start.Character), new LinePosition(span.End.Line + lineDelta, span.End.Character));
var newActiveStatementsInChangedDocuments = ImmutableArray.Create(
@@ -340,7 +353,7 @@ LinePositionSpan AddDelta(LinePositionSpan span, int lineDelta)
EditSession.GetActiveStatementAndExceptionRegionSpans(
module2,
- baseActiveStatements,
+ baseActiveStatementsMap,
baseExceptionRegions,
updatedMethodTokens: ImmutableArray.Create(0x06000004), // contains only recompiled methods in the project we are interested in (module2)
ImmutableDictionary>.Empty,
@@ -401,7 +414,9 @@ static void F2()
ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.IsLeafFrame, // F2
});
- var (baseActiveStatementMap, baseExceptionRegions, docs) = await GetBaseActiveStatementsAndExceptionRegions(new[] { baseSource }, baseActiveStatementInfos).ConfigureAwait(false);
+ var validator = new Validator(new[] { baseSource }, baseActiveStatementInfos);
+ var baseActiveStatementMap = await validator.EditSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
+ var docs = validator.GetDocumentIds();
// Active Statements
@@ -415,6 +430,8 @@ static void F2()
// Exception Regions
+ var baseExceptionRegions = await validator.EditSession.GetBaseActiveExceptionRegionsAsync(CancellationToken.None).ConfigureAwait(false);
+
// Note that the spans correspond to the base snapshot (V2).
AssertEx.Equal(new[]
{
@@ -461,6 +478,82 @@ static void F2()
}, activeStatementsInUpdatedMethods.Select(v => $"thread={v.ThreadId} {v.OldInstructionId.GetDebuggerDisplay()}: {v.NewSpan} '{GetFirstLineText(v.NewSpan, updatedText)}'"));
}
+ [Fact]
+ public async Task BaseActiveStatementsAndExceptionRegions_OutOfSyncDocuments()
+ {
+ var markedSource = new[]
+ {
+@"class C
+{
+ static void M()
+ {
+ try
+ {
+ M();
+ }
+ catch (Exception e)
+ {
+ }
+ }
+}"
+ };
+
+ var thread1 = Guid.NewGuid();
+
+ // Thread1 stack trace: F (AS:0 leaf)
+
+ var activeStatements = GetActiveStatementDebugInfos(
+ markedSource,
+ methodRowIds: new[] { 1 },
+ ilOffsets: new[] { 1 },
+ flags: new[]
+ {
+ ActiveStatementFlags.IsLeafFrame | ActiveStatementFlags.MethodUpToDate
+ },
+ threads: new[] { ImmutableArray.Create(thread1) });
+
+ var validator = new Validator(markedSource, activeStatements, initialState: CommittedSolution.DocumentState.OutOfSync);
+ var baseActiveStatementMap = await validator.EditSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
+ var docs = validator.GetDocumentIds();
+
+ // Active Statements - available in out-of-sync documents, as they reflect the state of the debuggee and not the base document content
+
+ Assert.Single(baseActiveStatementMap.DocumentMap);
+
+ AssertEx.Equal(new[]
+ {
+ "0: (6,18)-(6,22) flags=[IsLeafFrame, MethodUpToDate] pdid=test1.cs docs=[test1.cs]",
+ }, baseActiveStatementMap.DocumentMap[docs[0]].Select(InspectActiveStatement));
+
+ Assert.Equal(1, baseActiveStatementMap.InstructionMap.Count);
+
+ var s = baseActiveStatementMap.InstructionMap.Values.OrderBy(v => v.InstructionId.MethodId.Token).Single();
+ Assert.Equal(0x06000001, s.InstructionId.MethodId.Token);
+ Assert.Equal(0, s.PrimaryDocumentOrdinal);
+ Assert.Equal(docs[0], s.DocumentIds.Single());
+ Assert.True(s.IsLeaf);
+ AssertEx.Equal(new[] { thread1 }, s.ThreadIds);
+
+ // Exception Regions - not available in out-of-sync documents as we need the content of the base document to calculate them
+
+ var baseExceptionRegions = await validator.EditSession.GetBaseActiveExceptionRegionsAsync(CancellationToken.None).ConfigureAwait(false);
+
+ AssertEx.Equal(new[]
+ {
+ "out-of-sync"
+ }, baseExceptionRegions.Select(r => r.Spans.IsDefault ? "out-of-sync" : "[" + string.Join(",", r.Spans) + "]"));
+
+ // document got synchronized:
+ validator.EditSession.DebuggingSession.LastCommittedSolution.Test_SetDocumentState(docs[0], CommittedSolution.DocumentState.MatchesDebuggee);
+
+ baseExceptionRegions = await validator.EditSession.GetBaseActiveExceptionRegionsAsync(CancellationToken.None).ConfigureAwait(false);
+
+ AssertEx.Equal(new[]
+ {
+ "[]"
+ }, baseExceptionRegions.Select(r => r.Spans.IsDefault ? "out-of-sync" : "[" + string.Join(",", r.Spans) + "]"));
+ }
+
[Fact]
public async Task BaseActiveStatementsAndExceptionRegions_WithInitialNonRemappableRegions()
{
@@ -571,7 +664,9 @@ static void F4()
new NonRemappableRegion(erPreRemap31, lineDelta: +1, isExceptionRegion: true)) }
}.ToImmutableDictionary();
- var (baseActiveStatementMap, baseExceptionRegions, docs) = await GetBaseActiveStatementsAndExceptionRegions(new[] { markedSourceV2 }, activeStatementsPreRemap, initialNonRemappableRegions).ConfigureAwait(false);
+ var validator = new Validator(new[] { markedSourceV2 }, activeStatementsPreRemap, initialNonRemappableRegions);
+ var baseActiveStatementMap = await validator.EditSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
+ var docs = validator.GetDocumentIds();
// Active Statements
@@ -588,6 +683,8 @@ static void F4()
// Exception Regions
+ var baseExceptionRegions = await validator.EditSession.GetBaseActiveExceptionRegionsAsync(CancellationToken.None).ConfigureAwait(false);
+
// Note that the spans correspond to the base snapshot (V2).
AssertEx.Equal(new[]
{
@@ -692,21 +789,23 @@ static void F()
},
threads: new[] { ImmutableArray.Create(thread1, thread2), ImmutableArray.Create(thread1, thread2, thread2) });
- var (baseActiveStatements, baseExceptionRegions, docs) = await GetBaseActiveStatementsAndExceptionRegions(markedSource, activeStatements).ConfigureAwait(false);
+ var validator = new Validator(markedSource, activeStatements);
+ var baseActiveStatementMap = await validator.EditSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
+ var docs = validator.GetDocumentIds();
// Active Statements
- Assert.Equal(1, baseActiveStatements.DocumentMap.Count);
+ Assert.Equal(1, baseActiveStatementMap.DocumentMap.Count);
AssertEx.Equal(new[]
{
"0: (15,14)-(15,18) flags=[PartiallyExecuted, NonUserCode, MethodUpToDate, IsNonLeafFrame] pdid=test1.cs docs=[test1.cs]",
"1: (6,18)-(6,22) flags=[IsLeafFrame, MethodUpToDate, IsNonLeafFrame] pdid=test1.cs docs=[test1.cs]",
- }, baseActiveStatements.DocumentMap[docs[0]].Select(InspectActiveStatement));
+ }, baseActiveStatementMap.DocumentMap[docs[0]].Select(InspectActiveStatement));
- Assert.Equal(2, baseActiveStatements.InstructionMap.Count);
+ Assert.Equal(2, baseActiveStatementMap.InstructionMap.Count);
- var statements = baseActiveStatements.InstructionMap.Values.OrderBy(v => v.InstructionId.MethodId.Token).ToArray();
+ var statements = baseActiveStatementMap.InstructionMap.Values.OrderBy(v => v.InstructionId.MethodId.Token).ToArray();
var s = statements[0];
Assert.Equal(0x06000001, s.InstructionId.MethodId.Token);
Assert.Equal(0, s.PrimaryDocumentOrdinal);
@@ -724,6 +823,8 @@ static void F()
// Exception Regions
+ var baseExceptionRegions = await validator.EditSession.GetBaseActiveExceptionRegionsAsync(CancellationToken.None).ConfigureAwait(false);
+
AssertEx.Equal(new[]
{
"[]",
@@ -782,11 +883,13 @@ void AddProjectAndLinkDocument(string projectName, Document doc, SourceText text
return solution;
});
- var (baseActiveStatements, baseExceptionRegions, docs) = await GetBaseActiveStatementsAndExceptionRegions(markedSource, activeStatements, adjustSolution: adjustSolution).ConfigureAwait(false);
+ var validator = new Validator(markedSource, activeStatements, adjustSolution: adjustSolution);
+ var baseActiveStatementsMap = await validator.EditSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
+ var docs = validator.GetDocumentIds();
// Active Statements
- var documentMap = baseActiveStatements.DocumentMap;
+ var documentMap = baseActiveStatementsMap.DocumentMap;
Assert.Equal(5, docs.Length);
Assert.Equal(5, documentMap.Count);
@@ -822,9 +925,9 @@ void AddProjectAndLinkDocument(string projectName, Document doc, SourceText text
"0: (3,29)-(3,49) flags=[IsLeafFrame, MethodUpToDate] pdid=test2.cs docs=[test2.cs,Project4->test2.cs]",
}, documentMap[docs[4]].Select(InspectActiveStatement));
- Assert.Equal(3, baseActiveStatements.InstructionMap.Count);
+ Assert.Equal(3, baseActiveStatementsMap.InstructionMap.Count);
- var statements = baseActiveStatements.InstructionMap.Values.OrderBy(v => v.Ordinal).ToArray();
+ var statements = baseActiveStatementsMap.InstructionMap.Values.OrderBy(v => v.Ordinal).ToArray();
var s = statements[0];
Assert.Equal(0x06000001, s.InstructionId.MethodId.Token);
Assert.Equal(module4, s.InstructionId.MethodId.ModuleId);
diff --git a/src/EditorFeatures/Test/EditAndContinue/TraceLogTests.cs b/src/EditorFeatures/Test/EditAndContinue/TraceLogTests.cs
index 15f76c1a744ca..36528c6b75c32 100644
--- a/src/EditorFeatures/Test/EditAndContinue/TraceLogTests.cs
+++ b/src/EditorFeatures/Test/EditAndContinue/TraceLogTests.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.Linq;
using Roslyn.Test.Utilities;
using Xunit;
@@ -13,21 +14,24 @@ public void Write()
{
var log = new TraceLog(5, "log");
+ var projectId = ProjectId.CreateFromSerialized(Guid.Parse("5E40F37C-5AB3-495E-A3F2-4A244D177674"));
+ var diagnostic = Diagnostic.Create(EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile), Location.None, "file", "error");
+
log.Write("a");
log.Write("b {0} {1} {2}", 1, "x", 3);
log.Write("c");
- log.Write("d {0} {1}", (string)null, (string)null);
+ log.Write("d str={0} projectId={1} summary={2} diagnostic=`{3}`", (string)null, projectId, ProjectAnalysisSummary.RudeEdits, diagnostic);
log.Write("e");
log.Write("f");
AssertEx.Equal(new[]
{
- "f",
- "b 1 x 3",
- "c",
- "d ",
- "e"
- }, log.GetTestAccessor().Entries.Select(e => e.ToString()));
+ "f",
+ "b 1 x 3",
+ "c",
+ $"d str= projectId=1324595798 summary=RudeEdits diagnostic=`{diagnostic.ToString()}`",
+ "e"
+ }, log.GetTestAccessor().Entries.Select(e => e.GetDebuggerDisplay()));
}
}
}
diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/VisualBasicEditAndContinueAnalyzerTests.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/VisualBasicEditAndContinueAnalyzerTests.vb
index 1fce67cf54177..48b89ad8c42f1 100644
--- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/VisualBasicEditAndContinueAnalyzerTests.vb
+++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/VisualBasicEditAndContinueAnalyzerTests.vb
@@ -421,7 +421,7 @@ End Class
End Sub
- Public Async Function AnalyzeDocumentAsync_InsignificantChangesInMethodBody() As Threading.Tasks.Task
+ Public Async Function AnalyzeDocumentAsync_InsignificantChangesInMethodBody() As Task
Dim source1 = "
Class C
Sub Main()
@@ -441,11 +441,13 @@ End Class
Dim analyzer = New VisualBasicEditAndContinueAnalyzer()
Using workspace = TestWorkspace.CreateVisualBasic(source1)
- Dim oldProject = workspace.CurrentSolution.Projects.First()
- Dim documentId = oldProject.Documents.First().Id
+
Dim oldSolution = workspace.CurrentSolution
+ Dim oldProject = oldSolution.Projects.First()
+ Dim oldDocument = oldProject.Documents.Single()
+ Dim documentId = oldDocument.Id
+
Dim newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2))
- Dim oldDocument = oldSolution.GetDocument(documentId)
Dim oldText = Await oldDocument.GetTextAsync()
Dim oldSyntaxRoot = Await oldDocument.GetSyntaxRootAsync()
Dim newDocument = newSolution.GetDocument(documentId)
@@ -459,7 +461,7 @@ End Class
Dim oldStatementSyntax = oldSyntaxRoot.FindNode(oldStatementTextSpan)
Dim baseActiveStatements = ImmutableArray.Create(ActiveStatementsDescription.CreateActiveStatement(ActiveStatementFlags.IsLeafFrame, oldStatementSpan, DocumentId.CreateNewId(ProjectId.CreateNewId())))
- Dim result = Await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newDocument, trackingServiceOpt:=Nothing, CancellationToken.None)
+ Dim result = Await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newDocument, trackingServiceOpt:=Nothing, CancellationToken.None)
Assert.True(result.HasChanges)
Assert.True(result.SemanticEdits(0).PreserveLocalVariables)
@@ -486,10 +488,10 @@ End Class
Dim analyzer = New VisualBasicEditAndContinueAnalyzer()
Using workspace = TestWorkspace.CreateVisualBasic(source)
- Dim oldProject = workspace.CurrentSolution.Projects.First()
- Dim document = oldProject.Documents.First()
+ Dim oldProject = workspace.CurrentSolution.Projects.Single()
+ Dim oldDocument = oldProject.Documents.Single()
Dim baseActiveStatements = ImmutableArray.Create(Of ActiveStatement)()
- Dim result = Await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, document, trackingServiceOpt:=Nothing, CancellationToken.None)
+ Dim result = Await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, oldDocument, trackingServiceOpt:=Nothing, CancellationToken.None)
Assert.False(result.HasChanges)
Assert.False(result.HasChangesAndErrors)
@@ -498,7 +500,7 @@ End Class
End Function
- Public Async Function AnalyzeDocumentAsync_SyntaxError_NoChange2() As Threading.Tasks.Task
+ Public Async Function AnalyzeDocumentAsync_SyntaxError_NoChange2() As Task
Dim source1 = "
Class C
Public Shared Sub Main()
@@ -516,13 +518,14 @@ End Class
Dim analyzer = New VisualBasicEditAndContinueAnalyzer()
Using workspace = TestWorkspace.CreateVisualBasic(source1)
- Dim oldProject = workspace.CurrentSolution.Projects.First()
- Dim documentId = oldProject.Documents.First().Id
+ Dim oldProject = workspace.CurrentSolution.Projects.Single()
+ Dim oldDocument = oldProject.Documents.Single()
+ Dim documentId = oldDocument.Id
Dim oldSolution = workspace.CurrentSolution
Dim newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2))
Dim baseActiveStatements = ImmutableArray.Create(Of ActiveStatement)()
- Dim result = Await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt:=Nothing, CancellationToken.None)
+ Dim result = Await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt:=Nothing, CancellationToken.None)
Assert.False(result.HasChanges)
Assert.False(result.HasChangesAndErrors)
@@ -531,7 +534,7 @@ End Class
End Function
- Public Async Function AnalyzeDocumentAsync_SemanticError_NoChange() As Threading.Tasks.Task
+ Public Async Function AnalyzeDocumentAsync_SemanticError_NoChange() As Task
Dim source = "
Class C
Public Shared Sub Main()
@@ -543,10 +546,10 @@ End Class
Dim analyzer = New VisualBasicEditAndContinueAnalyzer()
Using workspace = TestWorkspace.CreateVisualBasic(source)
- Dim oldProject = workspace.CurrentSolution.Projects.First()
- Dim document = oldProject.Documents.First()
+ Dim oldProject = workspace.CurrentSolution.Projects.Single()
+ Dim oldDocument = oldProject.Documents.Single()
Dim baseActiveStatements = ImmutableArray.Create(Of ActiveStatement)()
- Dim result = Await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, document, trackingServiceOpt:=Nothing, CancellationToken.None)
+ Dim result = Await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, oldDocument, trackingServiceOpt:=Nothing, CancellationToken.None)
Assert.False(result.HasChanges)
Assert.False(result.HasChangesAndErrors)
@@ -575,13 +578,14 @@ End Class
Dim analyzer = New VisualBasicEditAndContinueAnalyzer()
Using workspace = TestWorkspace.CreateVisualBasic(source1)
- Dim oldProject = workspace.CurrentSolution.Projects.First()
- Dim documentId = oldProject.Documents.First().Id
+ Dim oldProject = workspace.CurrentSolution.Projects.Single()
+ Dim oldDocument = oldProject.Documents.Single()
+ Dim documentId = oldDocument.Id
Dim oldSolution = workspace.CurrentSolution
Dim newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2))
Dim baseActiveStatements = ImmutableArray.Create(Of ActiveStatement)()
- Dim result = Await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt:=Nothing, CancellationToken.None)
+ Dim result = Await analyzer.AnalyzeDocumentAsync(oldDocument, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt:=Nothing, CancellationToken.None)
' no declaration errors (error in method body is only reported when emitting)
Assert.False(result.HasChangesAndErrors)
@@ -608,13 +612,13 @@ End Class
Dim analyzer = New VisualBasicEditAndContinueAnalyzer()
Using workspace = TestWorkspace.CreateVisualBasic(source1)
- Dim oldProject = workspace.CurrentSolution.Projects.First()
- Dim documentId = oldProject.Documents.First().Id
Dim oldSolution = workspace.CurrentSolution
+ Dim oldProject = oldSolution.Projects.Single()
+ Dim documentId = oldProject.Documents.Single().Id
Dim newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2))
Dim baseActiveStatements = ImmutableArray.Create(Of ActiveStatement)()
- Dim result = Await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt:=Nothing, CancellationToken.None)
+ Dim result = Await analyzer.AnalyzeDocumentAsync(oldSolution.GetDocument(documentId), baseActiveStatements, newSolution.GetDocument(documentId), trackingServiceOpt:=Nothing, CancellationToken.None)
Assert.True(result.HasChanges)
Assert.True(result.HasChangesAndErrors)
@@ -641,12 +645,12 @@ End Class
Dim analyzer = New VisualBasicEditAndContinueAnalyzer()
Dim root = SyntaxFactory.ParseCompilationUnit(source)
- Assert.Null(analyzer.FindMemberDeclaration(root, Int32.MaxValue))
- Assert.Null(analyzer.FindMemberDeclaration(root, Int32.MinValue))
+ Assert.Null(analyzer.FindMemberDeclaration(root, Integer.MaxValue))
+ Assert.Null(analyzer.FindMemberDeclaration(root, Integer.MinValue))
End Sub
- Public Async Function AnalyzeDocumentAsync_Adding_A_New_File() As Threading.Tasks.Task
+ Public Async Function AnalyzeDocumentAsync_Adding_A_New_File() As Task
Dim source1 = "
Class C
Public Shared Sub Main()
@@ -661,9 +665,9 @@ End Class
Using workspace = TestWorkspace.CreateVisualBasic(source1)
' fork the solution to introduce a change
- Dim oldProject = workspace.CurrentSolution.Projects.Single()
- Dim newDocId = DocumentId.CreateNewId(oldProject.Id)
Dim oldSolution = workspace.CurrentSolution
+ Dim oldProject = oldSolution.Projects.Single()
+ Dim newDocId = DocumentId.CreateNewId(oldProject.Id)
Dim newSolution = oldSolution.AddDocument(newDocId, "goo.vb", SourceText.From(source2))
workspace.TryApplyChanges(newSolution)
@@ -680,7 +684,7 @@ End Class
Dim result = New List(Of DocumentAnalysisResults)()
Dim baseActiveStatements = ImmutableArray.Create(Of ActiveStatement)()
For Each changedDocumentId In changedDocuments
- result.Add(Await analyzer.AnalyzeDocumentAsync(oldProject, baseActiveStatements, newProject.GetDocument(changedDocumentId), trackingServiceOpt:=Nothing, CancellationToken.None))
+ result.Add(Await analyzer.AnalyzeDocumentAsync(oldProject.GetDocument(changedDocumentId), baseActiveStatements, newProject.GetDocument(changedDocumentId), trackingServiceOpt:=Nothing, CancellationToken.None))
Next
Assert.True(result.IsSingle())
diff --git a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs
index 1b8516314e0f7..fa77b15c3e11c 100644
--- a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs
+++ b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs
@@ -355,7 +355,7 @@ protected virtual string GetSuspensionPointDisplayName(SyntaxNode node, EditKind
#region Document Analysis
public async Task AnalyzeDocumentAsync(
- Project baseProjectOpt,
+ Document oldDocumentOpt,
ImmutableArray baseActiveStatements,
Document document,
IActiveStatementTrackingService trackingServiceOpt,
@@ -371,7 +371,6 @@ public async Task AnalyzeDocumentAsync(
SyntaxNode oldRoot;
SourceText oldText;
- var oldDocumentOpt = baseProjectOpt?.GetDocument(document.Id);
if (oldDocumentOpt != null)
{
oldTreeOpt = await oldDocumentOpt.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
@@ -1565,7 +1564,7 @@ private ImmutableArray GetExceptionRegions(List ex
if (exceptionHandlingAncestors.Count == 0)
{
- return ImmutableArray.Create();
+ return ImmutableArray.Empty;
}
var result = ArrayBuilder.GetInstance();
diff --git a/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs b/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs
new file mode 100644
index 0000000000000..bf0ac775904e0
--- /dev/null
+++ b/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs
@@ -0,0 +1,322 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#nullable enable
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.DiaSymReader;
+using Microsoft.CodeAnalysis.Debugging;
+using Microsoft.CodeAnalysis.Emit;
+using Microsoft.CodeAnalysis.ErrorReporting;
+using Microsoft.CodeAnalysis.Text;
+using Roslyn.Utilities;
+using System.IO;
+using System.Diagnostics;
+
+namespace Microsoft.CodeAnalysis.EditAndContinue
+{
+ ///
+ /// Encapsulates access to the last committed solution.
+ /// We don't want to expose the solution directly since access to documents must be gated by out-of-sync checks.
+ ///
+ internal sealed class CommittedSolution
+ {
+ private readonly DebuggingSession _debuggingSession;
+
+ private Solution _solution;
+
+ internal enum DocumentState
+ {
+ None = 0,
+ OutOfSync = 1,
+ MatchesDebuggee = 2,
+ DesignTimeOnly = 3,
+ }
+
+ ///
+ /// Implements workaround for https://github.com/dotnet/project-system/issues/5457.
+ ///
+ /// When debugging is started we capture the current solution snapshot.
+ /// The documents in this snapshot might not match exactly to those that the compiler used to build the module
+ /// that's currently loaded into the debuggee. This is because there is no reliable synchronization between
+ /// the (design-time) build and Roslyn workspace. Although Roslyn uses file-watchers to watch for changes in
+ /// the files on disk, the file-changed events raised by the build might arrive to Roslyn after the debugger
+ /// has attached to the debuggee and EnC service captured the solution.
+ ///
+ /// Ideally, the Project System would notify Roslyn at the end of each build what the content of the source
+ /// files generated by various targets is. Roslyn would then apply these changes to the workspace and
+ /// the EnC service would capture a solution snapshot that includes these changes.
+ ///
+ /// Since this notification is currently not available we check the current content of source files against
+ /// the corresponding checksums stored in the PDB. Documents for which we have not observed source file content
+ /// that maches the PDB checksum are considered .
+ ///
+ /// Some documents in the workspace are added for design-time-only purposes and are not part of the compilation
+ /// from which the assembly is built. These documents won't have a record in the PDB and will be tracked as
+ /// .
+ ///
+ /// A document state can only change from to .
+ /// Once a document state is or
+ /// it will never change.
+ ///
+ private readonly Dictionary _documentState;
+
+ private readonly object _guard = new object();
+
+ public CommittedSolution(DebuggingSession debuggingSession, Solution solution)
+ {
+ _solution = solution;
+ _debuggingSession = debuggingSession;
+ _documentState = new Dictionary();
+ }
+
+ // test only
+ internal void Test_SetDocumentState(DocumentId documentId, DocumentState state)
+ {
+ lock (_guard)
+ {
+ _documentState[documentId] = state;
+ }
+ }
+
+ public bool HasNoChanges(Solution solution)
+ => _solution == solution;
+
+ public Project? GetProject(ProjectId id)
+ => _solution.GetProject(id);
+
+ public ImmutableArray GetDocumentIdsWithFilePath(string path)
+ => _solution.GetDocumentIdsWithFilePath(path);
+
+ public bool ContainsDocument(DocumentId documentId)
+ => _solution.ContainsDocument(documentId);
+
+ ///
+ /// Captures the content of a file that is about to be overwritten by saving an open document,
+ /// if the document is currently out-of-sync and the content of the file matches the PDB.
+ /// If we didn't capture the content before the save we might never be able to find a document
+ /// snapshot that matches the PDB.
+ ///
+ public Task OnSourceFileUpdatedAsync(DocumentId documentId, CancellationToken cancellationToken)
+ => GetDocumentAndStateAsync(documentId, cancellationToken, reloadOutOfSyncDocument: true);
+
+ ///
+ /// Returns a document snapshot for given whose content exactly matches
+ /// the source file used to compile the binary currently loaded in the debuggee. Returns null
+ /// if it fails to find a document snapshot whose content hash maches the one recorded in the PDB.
+ ///
+ /// The result is cached and the next lookup uses the cached value, including failures unless is true.
+ ///
+ public async Task<(Document? Document, DocumentState State)> GetDocumentAndStateAsync(DocumentId documentId, CancellationToken cancellationToken, bool reloadOutOfSyncDocument = false)
+ {
+ Document? document;
+
+ lock (_guard)
+ {
+ document = _solution.GetDocument(documentId);
+ if (document == null)
+ {
+ return (null, DocumentState.None);
+ }
+
+ if (document.FilePath == null)
+ {
+ return (null, DocumentState.DesignTimeOnly);
+ }
+
+ if (_documentState.TryGetValue(documentId, out var documentState))
+ {
+ switch (documentState)
+ {
+ case DocumentState.MatchesDebuggee:
+ return (document, documentState);
+
+ case DocumentState.DesignTimeOnly:
+ return (null, documentState);
+
+ case DocumentState.OutOfSync:
+ if (reloadOutOfSyncDocument)
+ {
+ break;
+ }
+
+ return (null, documentState);
+
+ case DocumentState.None:
+ throw ExceptionUtilities.Unreachable;
+ }
+ }
+ }
+
+ var (matchingSourceText, isMissing) = await TryGetPdbMatchingSourceTextAsync(document.FilePath, document.Project.Id, cancellationToken).ConfigureAwait(false);
+
+ lock (_guard)
+ {
+ if (_documentState.TryGetValue(documentId, out var documentState) && documentState != DocumentState.OutOfSync)
+ {
+ return (document, documentState);
+ }
+
+ DocumentState newState;
+ Document? matchingDocument;
+
+ if (isMissing)
+ {
+ // Source file is not listed in the PDB. This may happen for a couple of reasons:
+ // 1) The source file contains no method bodies
+ // 2) The library wasn't built with that source file - the file has been added before debugging session started but after build captured it.
+ // This is the case for WPF .g.i.cs files.
+ //
+ // In case [1] we can't currently determine whether the document matches or not
+ // In case [2] there is no need to synchronize the document.
+ //
+ // TODO:
+ // Once https://github.com/dotnet/roslyn/issues/38954 is implemented we can consider all source files that are not present in the PDB design-time-only.
+
+ if (document.FilePath.EndsWith(".g.i.cs") || document.FilePath.EndsWith(".g.i.vb"))
+ {
+ matchingDocument = null;
+ newState = DocumentState.DesignTimeOnly;
+ }
+ else
+ {
+ matchingDocument = document;
+ newState = DocumentState.MatchesDebuggee;
+ }
+ }
+ else if (matchingSourceText != null)
+ {
+ if (document.TryGetText(out var sourceText) && sourceText.ContentEquals(matchingSourceText))
+ {
+ matchingDocument = document;
+ }
+ else
+ {
+ _solution = _solution.WithDocumentText(documentId, matchingSourceText, PreservationMode.PreserveValue);
+ matchingDocument = _solution.GetDocument(documentId);
+ }
+
+ newState = DocumentState.MatchesDebuggee;
+ }
+ else
+ {
+ matchingDocument = null;
+ newState = DocumentState.OutOfSync;
+ }
+
+ _documentState[documentId] = newState;
+ return (matchingDocument, newState);
+ }
+ }
+
+ public void CommitSolution(Solution solution, ImmutableArray updatedDocuments)
+ {
+ lock (_guard)
+ {
+ // Changes in the updated documents has just been applied to the debuggee process.
+ // Therefore, these documents now match exactly the state of the debuggee.
+ foreach (var document in updatedDocuments)
+ {
+ // Changes in design-time-only documents should have been ignored.
+ Debug.Assert(_documentState[document.Id] != DocumentState.DesignTimeOnly);
+
+ _documentState[document.Id] = DocumentState.MatchesDebuggee;
+ Debug.Assert(document.Project.Solution == solution);
+ }
+
+ _solution = solution;
+ }
+ }
+
+ private async Task<(SourceText? Source, bool IsMissing)> TryGetPdbMatchingSourceTextAsync(string sourceFilePath, ProjectId projectId, CancellationToken cancellationToken)
+ {
+ var (symChecksum, algorithm) = await TryReadSourceFileChecksumFromPdb(sourceFilePath, projectId, cancellationToken).ConfigureAwait(false);
+ if (symChecksum.IsDefault)
+ {
+ return (Source: null, IsMissing: true);
+ }
+
+ try
+ {
+ using var fileStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
+ var sourceText = SourceText.From(fileStream, checksumAlgorithm: algorithm);
+ return (sourceText.GetChecksum().SequenceEqual(symChecksum) ? sourceText : null, IsMissing: false);
+ }
+ catch (Exception e)
+ {
+ EditAndContinueWorkspaceService.Log.Write("Error calculating checksum for source file '{0}': '{1}'", sourceFilePath, e.Message);
+ return (Source: null, IsMissing: false);
+ }
+ }
+
+ private async Task<(ImmutableArray Checksum, SourceHashAlgorithm Algorithm)> TryReadSourceFileChecksumFromPdb(string sourceFilePath, ProjectId projectId, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var (mvid, mvidError) = await _debuggingSession.GetProjectModuleIdAsync(projectId, cancellationToken).ConfigureAwait(false);
+ if (mvid == Guid.Empty)
+ {
+ EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: can't read MVID ('{1}')", sourceFilePath, mvidError);
+ return default;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Dispatch to a background thread - reading symbols requires MTA thread.
+ if (Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA)
+ {
+ return await Task.Factory.SafeStartNew(ReadChecksum, cancellationToken, TaskScheduler.Default).ConfigureAwait(false);
+ }
+ else
+ {
+ return ReadChecksum();
+ }
+
+ (ImmutableArray Checksum, SourceHashAlgorithm Algorithm) ReadChecksum()
+ {
+ var moduleInfo = _debuggingSession.DebugeeModuleMetadataProvider.TryGetBaselineModuleInfo(mvid);
+ if (moduleInfo == null)
+ {
+ EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: can't get baseline SymReader", sourceFilePath);
+ return default;
+ }
+
+ try
+ {
+ var symDocument = moduleInfo.SymReader.GetDocument(sourceFilePath);
+ if (symDocument == null)
+ {
+ EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: no SymDocument", sourceFilePath);
+ return default;
+ }
+
+ var symAlgorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(symDocument.GetHashAlgorithm());
+ if (symAlgorithm == SourceHashAlgorithm.None)
+ {
+ // unknown algorithm:
+ EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath);
+ return default;
+ }
+
+ var symChecksum = symDocument.GetChecksum().ToImmutableArray();
+
+ return (symChecksum, symAlgorithm);
+ }
+ catch (Exception e)
+ {
+ EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: error reading symbols: {1}", sourceFilePath, e.Message);
+ return default;
+ }
+ }
+ }
+ catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
+ {
+ return default;
+ }
+ }
+ }
+}
diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
index 85fda8f2e3a6d..23afd62110e85 100644
--- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
+++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
@@ -11,6 +11,8 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Emit;
+using Microsoft.CodeAnalysis.ErrorReporting;
+using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.EditAndContinue
@@ -20,10 +22,13 @@ namespace Microsoft.CodeAnalysis.EditAndContinue
///
internal sealed class DebuggingSession : IDisposable
{
+ public readonly Workspace Workspace;
public readonly IActiveStatementProvider ActiveStatementProvider;
public readonly IDebuggeeModuleMetadataProvider DebugeeModuleMetadataProvider;
public readonly ICompilationOutputsProviderService CompilationOutputsProvider;
+ private readonly CancellationTokenSource _cancellationSource = new CancellationTokenSource();
+
///
/// MVIDs read from the assembly built for given project id.
///
@@ -78,7 +83,7 @@ internal sealed class DebuggingSession : IDisposable
/// or the solution which the last changes commited to the debuggee at the end of edit session were calculated from.
/// The solution reflecting the current state of the modules loaded in the debugee.
///
- internal Solution LastCommittedSolution { get; private set; }
+ internal readonly CommittedSolution LastCommittedSolution;
internal DebuggingSession(
Workspace workspace,
@@ -89,6 +94,7 @@ internal DebuggingSession(
Debug.Assert(workspace != null);
Debug.Assert(debugeeModuleMetadataProvider != null);
+ Workspace = workspace;
DebugeeModuleMetadataProvider = debugeeModuleMetadataProvider;
CompilationOutputsProvider = compilationOutputsProvider;
_projectModuleIds = new Dictionary();
@@ -97,7 +103,7 @@ internal DebuggingSession(
ActiveStatementProvider = activeStatementProvider;
- LastCommittedSolution = workspace.CurrentSolution;
+ LastCommittedSolution = new CommittedSolution(this, workspace.CurrentSolution);
NonRemappableRegions = ImmutableDictionary>.Empty;
}
@@ -125,12 +131,17 @@ internal ImmutableArray GetBaselineModuleReaders()
}
}
+ internal CancellationToken CancellationToken => _cancellationSource.Token;
+ internal void Cancel() => _cancellationSource.Cancel();
+
public void Dispose()
{
foreach (var reader in GetBaselineModuleReaders())
{
reader.Dispose();
}
+
+ _cancellationSource.Dispose();
}
internal void PrepareModuleForUpdate(Guid mvid)
@@ -180,7 +191,7 @@ from region in delta.NonRemappableRegions
}
}
- LastCommittedSolution = update.Solution;
+ LastCommittedSolution.CommitSolution(update.Solution, update.ChangedDocuments);
}
///
diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs
index b9e112d8421c3..b666725936d42 100644
--- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs
+++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs
@@ -153,6 +153,7 @@ void AddGeneralDiagnostic(EditAndContinueErrorCode code, string resourceName, Di
AddGeneralDiagnostic(EditAndContinueErrorCode.CannotApplyChangesUnexpectedError, nameof(FeaturesResources.CannotApplyChangesUnexpectedError));
AddGeneralDiagnostic(EditAndContinueErrorCode.ChangesDisallowedWhileStoppedAtException, nameof(FeaturesResources.ChangesDisallowedWhileStoppedAtException));
AddGeneralDiagnostic(EditAndContinueErrorCode.ChangesNotAppliedWhileRunning, nameof(FeaturesResources.ChangesNotAppliedWhileRunning), DiagnosticSeverity.Warning);
+ AddGeneralDiagnostic(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee, nameof(FeaturesResources.DocumentIsOutOfSyncWithDebuggee), DiagnosticSeverity.Warning);
s_descriptors = builder.ToImmutable();
}
diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs
index 6379531e67d42..0ffd4b8d79f87 100644
--- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs
+++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs
@@ -8,5 +8,6 @@ internal enum EditAndContinueErrorCode
CannotApplyChangesUnexpectedError = 2,
ChangesNotAppliedWhileRunning = 3,
ChangesDisallowedWhileStoppedAtException = 4,
+ DocumentIsOutOfSyncWithDebuggee = 5,
}
}
diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs
index f8f61236031b2..cd964b3903a80 100644
--- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs
+++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs
@@ -75,12 +75,23 @@ internal EditAndContinueWorkspaceService(
}
// test only:
+ internal DebuggingSession? Test_GetDebuggingSession() => _debuggingSession;
internal EditSession? Test_GetEditSession() => _editSession;
internal PendingSolutionUpdate? Test_GetPendingSolutionUpdate() => _pendingUpdate;
public bool IsDebuggingSessionInProgress
=> _debuggingSession != null;
+ public void OnSourceFileUpdated(DocumentId documentId)
+ {
+ var debuggingSession = _debuggingSession;
+ if (debuggingSession != null)
+ {
+ // fire and forget
+ _ = Task.Run(() => debuggingSession.LastCommittedSolution.OnSourceFileUpdatedAsync(documentId, debuggingSession.CancellationToken));
+ }
+ }
+
///
/// Invoked whenever a module instance is loaded to a process being debugged.
///
@@ -139,6 +150,9 @@ public void EndDebuggingSession()
var debuggingSession = Interlocked.Exchange(ref _debuggingSession, null);
Contract.ThrowIfNull(debuggingSession, "Debugging session has not started.");
+ // cancel all ongoing work bound to the session:
+ debuggingSession.Cancel();
+
_reportTelemetry(_debuggingSessionTelemetry.GetDataAndClear());
// clear emit/apply diagnostics reported previously:
@@ -166,14 +180,6 @@ public async Task> GetDocumentDiagnosticsAsync(Docume
return ImmutableArray.Empty;
}
- // The document has not changed while the application is running since the last changes were committed:
- var oldDocument = debuggingSession.LastCommittedSolution.GetDocument(document.Id);
- var editSession = _editSession;
- if (editSession == null && document == oldDocument)
- {
- return ImmutableArray.Empty;
- }
-
// Not a C# or VB project.
var project = document.Project;
if (!SupportsEditAndContinue(project))
@@ -197,8 +203,24 @@ public async Task> GetDocumentDiagnosticsAsync(Docume
return ImmutableArray.Empty;
}
+ var (oldDocument, oldDocumentState) = await debuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(document.Id, cancellationToken).ConfigureAwait(false);
+ if (oldDocumentState == CommittedSolution.DocumentState.OutOfSync ||
+ oldDocumentState == CommittedSolution.DocumentState.DesignTimeOnly)
+ {
+ // Do not report diagnostics for existing out-of-sync documents or design-time-only documents.
+ return ImmutableArray.Empty;
+ }
+
+ // The document has not changed while the application is running since the last changes were committed:
+ var editSession = _editSession;
+
if (editSession == null)
{
+ if (document == oldDocument)
+ {
+ return ImmutableArray.Empty;
+ }
+
// Any changes made in loaded, built projects outside of edit session are rude edits (the application is running):
var newSyntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(newSyntaxTree);
@@ -207,13 +229,7 @@ public async Task> GetDocumentDiagnosticsAsync(Docume
return GetRunModeDocumentDiagnostics(document, newSyntaxTree, changedSpans);
}
- // No changes made in the entire workspace since the edit session started:
- if (editSession.BaseSolution.WorkspaceVersion == project.Solution.WorkspaceVersion)
- {
- return ImmutableArray.Empty;
- }
-
- var analysis = await editSession.GetDocumentAnalysis(document).GetValueAsync(cancellationToken).ConfigureAwait(false);
+ var analysis = await editSession.GetDocumentAnalysis(oldDocument, document).GetValueAsync(cancellationToken).ConfigureAwait(false);
if (analysis.HasChanges)
{
// Once we detected a change in a document let the debugger know that the corresponding loaded module
@@ -412,7 +428,8 @@ public Task GetSolutionUpdateStatusAsync(string sourceFile
solution,
solutionUpdate.EmitBaselines,
solutionUpdate.Deltas,
- solutionUpdate.ModuleReaders));
+ solutionUpdate.ModuleReaders,
+ solutionUpdate.ChangedDocuments));
// commit/discard was not called:
Contract.ThrowIfFalse(previousPendingUpdate == null);
@@ -479,8 +496,15 @@ public void DiscardSolutionUpdate()
return null;
}
+ var (oldPrimaryDocument, _) = await editSession.DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(baseActiveStatement.PrimaryDocumentId, cancellationToken).ConfigureAwait(false);
+ if (oldPrimaryDocument == null)
+ {
+ // Can't determine position of an active statement if the document is out-of-sync with loaded module debug information.
+ return null;
+ }
+
var primaryDocument = _workspace.CurrentSolution.GetDocument(baseActiveStatement.PrimaryDocumentId);
- var documentAnalysis = await editSession.GetDocumentAnalysis(primaryDocument).GetValueAsync(cancellationToken).ConfigureAwait(false);
+ var documentAnalysis = await editSession.GetDocumentAnalysis(oldPrimaryDocument, primaryDocument).GetValueAsync(cancellationToken).ConfigureAwait(false);
var currentActiveStatements = documentAnalysis.ActiveStatements;
if (currentActiveStatements.IsDefault)
{
@@ -496,6 +520,15 @@ public void DiscardSolutionUpdate()
}
}
+ ///
+ /// Called by the debugger to determine whether an active statement is in an exception region,
+ /// so it can determine whether the active statement can be remapped. This only happens when the EnC is about to apply changes.
+ /// If the debugger determines we can remap active statements, the application of changes proceeds.
+ ///
+ ///
+ /// True if the instruction is located within an exception region, false if it is not, null if the instruction isn't an active statement
+ /// or the exception regions can't be determined.
+ ///
public async Task IsActiveStatementInExceptionRegionAsync(ActiveInstructionId instructionId, CancellationToken cancellationToken)
{
try
@@ -506,19 +539,20 @@ public void DiscardSolutionUpdate()
return null;
}
- // TODO: Avoid enumerating active statements for unchanged documents.
- // We would need to add a document path parameter to be able to find the document we need to check for changes.
- // https://github.com/dotnet/roslyn/issues/24324
+ // This method is only called when the EnC is about to apply changes, at which point all active statements and
+ // their exception regions will be needed. Hence it's not neccessary to scope this query down to just the instruction
+ // the debugger is interested at this point while not calculating the others.
+
var baseActiveStatements = await editSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
if (!baseActiveStatements.InstructionMap.TryGetValue(instructionId, out var baseActiveStatement))
{
return null;
}
- // TODO: avoid waiting for ERs of all active statements to be calculated and just calculate the one we are interested in at this moment:
- // https://github.com/dotnet/roslyn/issues/24324
- var baseExceptionRegions = await editSession.BaseActiveExceptionRegions.GetValueAsync(cancellationToken).ConfigureAwait(false);
- return baseExceptionRegions[baseActiveStatement.Ordinal].IsActiveStatementCovered;
+ var baseExceptionRegions = (await editSession.GetBaseActiveExceptionRegionsAsync(cancellationToken).ConfigureAwait(false))[baseActiveStatement.Ordinal];
+
+ // If the document is out-of-sync the exception regions can't be determined.
+ return baseExceptionRegions.Spans.IsDefault ? (bool?)null : baseExceptionRegions.IsActiveStatementCovered;
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
{
diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs
index b5ad217e4a2d9..70d7a8fb01b6c 100644
--- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs
+++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs
@@ -26,11 +26,6 @@ internal sealed class EditSession : IDisposable
internal readonly DebuggingSession DebuggingSession;
internal readonly EditSessionTelemetry Telemetry;
- ///
- /// The solution captured when entering the break state.
- ///
- internal readonly Solution BaseSolution;
-
private readonly ImmutableDictionary> _nonRemappableRegions;
///
@@ -41,7 +36,7 @@ internal sealed class EditSession : IDisposable
///
/// For each base active statement the exception regions around that statement.
///
- internal readonly AsyncLazy> BaseActiveExceptionRegions;
+ internal ImmutableArray _lazyBaseActiveExceptionRegions;
///
/// Results of changed documents analysis.
@@ -76,18 +71,11 @@ private readonly Dictionary> _modul
internal EditSession(DebuggingSession debuggingSession, EditSessionTelemetry telemetry)
{
- Debug.Assert(debuggingSession != null);
- Debug.Assert(telemetry != null);
-
DebuggingSession = debuggingSession;
-
- _nonRemappableRegions = debuggingSession.NonRemappableRegions;
-
Telemetry = telemetry;
- BaseSolution = debuggingSession.LastCommittedSolution;
+ _nonRemappableRegions = debuggingSession.NonRemappableRegions;
BaseActiveStatements = new AsyncLazy(GetBaseActiveStatementsAsync, cacheResult: true);
- BaseActiveExceptionRegions = new AsyncLazy>(GetBaseActiveExceptionRegionsAsync, cacheResult: true);
}
internal CancellationToken CancellationToken => _cancellationSource.Token;
@@ -136,14 +124,12 @@ public ImmutableArray GetModuleDiagnostics(Guid mvid, st
return result;
}
- private Project GetBaseProject(ProjectId id)
- => BaseSolution.GetProject(id);
-
private async Task GetBaseActiveStatementsAsync(CancellationToken cancellationToken)
{
try
{
- return CreateActiveStatementsMap(BaseSolution, await DebuggingSession.ActiveStatementProvider.GetActiveStatementsAsync(cancellationToken).ConfigureAwait(false));
+ // Last committed solution reflects the state of the source that is in sync with the binaries that are loaded in the debuggee.
+ return CreateActiveStatementsMap(await DebuggingSession.ActiveStatementProvider.GetActiveStatementsAsync(cancellationToken).ConfigureAwait(false));
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
{
@@ -153,13 +139,13 @@ private async Task GetBaseActiveStatementsAsync(Cancellatio
}
}
- private ActiveStatementsMap CreateActiveStatementsMap(Solution solution, ImmutableArray debugInfos)
+ private ActiveStatementsMap CreateActiveStatementsMap(ImmutableArray debugInfos)
{
var byDocument = PooledDictionary>.GetInstance();
var byInstruction = PooledDictionary.GetInstance();
bool supportsEditAndContinue(DocumentId documentId)
- => EditAndContinueWorkspaceService.SupportsEditAndContinue(solution.GetProject(documentId.ProjectId));
+ => EditAndContinueWorkspaceService.SupportsEditAndContinue(DebuggingSession.LastCommittedSolution.GetProject(documentId.ProjectId));
foreach (var debugInfo in debugInfos)
{
@@ -170,7 +156,7 @@ bool supportsEditAndContinue(DocumentId documentId)
continue;
}
- var documentIds = solution.GetDocumentIdsWithFilePath(documentName);
+ var documentIds = DebuggingSession.LastCommittedSolution.GetDocumentIdsWithFilePath(documentName);
var firstDocumentId = documentIds.FirstOrDefault(supportsEditAndContinue);
if (firstDocumentId == null)
{
@@ -251,29 +237,62 @@ private LinePositionSpan GetUpToDateSpan(ActiveStatementDebugInfo activeStatemen
return activeStatementInfo.LinePositionSpan;
}
- private async Task> GetBaseActiveExceptionRegionsAsync(CancellationToken cancellationToken)
+ ///
+ /// Calculates exception regions for all active statements.
+ /// If an active statement is in a document that's out-of-sync returns default() for that statement.
+ ///
+ internal async Task> GetBaseActiveExceptionRegionsAsync(CancellationToken cancellationToken)
{
try
{
+ if (!_lazyBaseActiveExceptionRegions.IsDefault)
+ {
+ return _lazyBaseActiveExceptionRegions;
+ }
+
var baseActiveStatements = await BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
var instructionMap = baseActiveStatements.InstructionMap;
using var builderDisposer = ArrayBuilder.GetInstance(instructionMap.Count, out var builder);
builder.Count = instructionMap.Count;
+ bool hasOutOfSyncDocuments = false;
+
foreach (var activeStatement in instructionMap.Values)
{
- var document = BaseSolution.GetDocument(activeStatement.PrimaryDocumentId);
+ bool isCovered;
+ ImmutableArray exceptionRegions;
- var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
- var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ // Can't calculate exception regions for active statements in out-of-sync documents.
+ var (document, _) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(activeStatement.PrimaryDocumentId, cancellationToken).ConfigureAwait(false);
+ if (document != null)
+ {
+ var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
+ var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
- var analyzer = document.Project.LanguageServices.GetService();
- var exceptionRegions = analyzer.GetExceptionRegions(sourceText, syntaxRoot, activeStatement.Span, activeStatement.IsNonLeaf, out var isCovered);
+ var analyzer = document.Project.LanguageServices.GetService();
+ exceptionRegions = analyzer.GetExceptionRegions(sourceText, syntaxRoot, activeStatement.Span, activeStatement.IsNonLeaf, out isCovered);
+ }
+ else
+ {
+ // Document is either out-of-sync, design-time-only or missing from the baseline.
+ // If it's missing or design-time-only it can't have active statements.
+ hasOutOfSyncDocuments = true;
+ isCovered = false;
+ exceptionRegions = default;
+ }
builder[activeStatement.Ordinal] = new ActiveStatementExceptionRegions(exceptionRegions, isCovered);
}
- return builder.ToImmutable();
+ var result = builder.ToImmutable();
+
+ // Only cache results if no active statements are in out-of-sync documents.
+ if (!hasOutOfSyncDocuments)
+ {
+ ImmutableInterlocked.InterlockedInitialize(ref _lazyBaseActiveExceptionRegions, result);
+ }
+
+ return result;
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
{
@@ -281,80 +300,76 @@ private async Task> GetBaseActiv
}
}
- private ImmutableArray<(DocumentId DocumentId, AsyncLazy Results)> GetChangedDocumentsAnalyses(Project baseProject, Project project)
+ private async Task<(ImmutableArray<(Document Document, AsyncLazy Results)>, ImmutableArray Diagnostics)> GetChangedDocumentsAnalysesAsync(
+ Project baseProject, Project project, CancellationToken cancellationToken)
{
+ var changedDocuments = ArrayBuilder<(Document Old, Document New)>.GetInstance();
+ var outOfSyncDiagnostics = ArrayBuilder.GetInstance();
+
var changes = project.GetChanges(baseProject);
+ foreach (var documentId in changes.GetChangedDocuments())
+ {
+ var document = project.GetDocument(documentId);
+ if (EditAndContinueWorkspaceService.IsDesignTimeOnlyDocument(document))
+ {
+ continue;
+ }
- var changedDocuments =
- changes.GetChangedDocuments().
- Concat(changes.GetAddedDocuments()).
- Select(id => project.GetDocument(id)).
- Where(d => !EditAndContinueWorkspaceService.IsDesignTimeOnlyDocument(d)).
- ToArray();
+ var (oldDocument, oldDocumentState) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(documentId, cancellationToken, reloadOutOfSyncDocument: true).ConfigureAwait(false);
+ switch (oldDocumentState)
+ {
+ case CommittedSolution.DocumentState.DesignTimeOnly:
+ continue;
- if (changedDocuments.Length == 0)
- {
- return ImmutableArray<(DocumentId, AsyncLazy)>.Empty;
- }
+ case CommittedSolution.DocumentState.OutOfSync:
+ var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee);
+ outOfSyncDiagnostics.Add(Diagnostic.Create(descriptor, Location.Create(document.FilePath, textSpan: default, lineSpan: default), new[] { document.FilePath }));
+ continue;
- lock (_analysesGuard)
- {
- return changedDocuments.SelectAsArray(d => (d.Id, GetDocumentAnalysisNoLock(d)));
+ default:
+ changedDocuments.Add((oldDocument, document));
+ break;
+ }
}
- }
- private async Task> GetAllAddedSymbolsAsync(Project project, CancellationToken cancellationToken)
- {
- try
+ foreach (var documentId in changes.GetAddedDocuments())
{
- (Document Document, AsyncLazy Results)[] analyses;
- lock (_analysesGuard)
- {
- analyses = _analyses.Values.ToArray();
- }
-
- HashSet addedSymbols = null;
- foreach (var (document, lazyResults) in analyses)
+ var document = project.GetDocument(documentId);
+ if (EditAndContinueWorkspaceService.IsDesignTimeOnlyDocument(document))
{
- // Only consider analyses for documents that belong the currently analyzed project.
- if (document.Project == project)
- {
- var results = await lazyResults.GetValueAsync(cancellationToken).ConfigureAwait(false);
- if (!results.HasChangesAndErrors)
- {
- foreach (var edit in results.SemanticEdits)
- {
- if (edit.Kind == SemanticEditKind.Insert)
- {
- if (addedSymbols == null)
- {
- addedSymbols = new HashSet();
- }
-
- addedSymbols.Add(edit.NewSymbol);
- }
- }
- }
- }
+ continue;
}
- return addedSymbols;
+ changedDocuments.Add((null, document));
}
- catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e))
+
+ var result = ImmutableArray<(Document, AsyncLazy)>.Empty;
+ if (changedDocuments.Count != 0)
{
- throw ExceptionUtilities.Unreachable;
+ lock (_analysesGuard)
+ {
+ result = changedDocuments.SelectAsArray(change => (change.New, GetDocumentAnalysisNoLock(change.Old, change.New)));
+ }
}
+
+ changedDocuments.Free();
+ return (result, outOfSyncDiagnostics.ToImmutableAndFree());
}
- public AsyncLazy GetDocumentAnalysis(Document document)
+ public AsyncLazy GetDocumentAnalysis(Document baseDocument, Document document)
{
lock (_analysesGuard)
{
- return GetDocumentAnalysisNoLock(document);
+ return GetDocumentAnalysisNoLock(baseDocument, document);
}
}
- private AsyncLazy GetDocumentAnalysisNoLock(Document document)
+ ///
+ /// Returns a document analysis or kicks off a new one if one is not available for the specified document snapshot.
+ ///
+ /// Base document or null if the document did not exist in the baseline.
+ /// Document snapshot to analyze.
+ private AsyncLazy GetDocumentAnalysisNoLock(Document baseDocumentOpt, Document document)
{
if (_analyses.TryGetValue(document.Id, out var analysis) && analysis.Document == document)
{
@@ -374,10 +389,9 @@ private AsyncLazy GetDocumentAnalysisNoLock(Document do
documentBaseActiveStatements = ImmutableArray.Empty;
}
- var trackingService = BaseSolution.Workspace.Services.GetService();
+ var trackingService = DebuggingSession.Workspace.Services.GetService();
- var baseProject = GetBaseProject(document.Project.Id);
- return await analyzer.AnalyzeDocumentAsync(baseProject, documentBaseActiveStatements, document, trackingService, cancellationToken).ConfigureAwait(false);
+ return await analyzer.AnalyzeDocumentAsync(baseDocumentOpt, documentBaseActiveStatements, document, trackingService, cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (FatalError.ReportUnlessCanceled(e))
{
@@ -386,6 +400,8 @@ private AsyncLazy GetDocumentAnalysisNoLock(Document do
},
cacheResult: true);
+ // TODO: this will replace potentially running analysis with another one.
+ // Consider cancelling the replaced one.
_analyses[document.Id] = (document, lazyResults);
return lazyResults;
}
@@ -419,7 +435,7 @@ public async Task GetSolutionUpdateStatusAsync(Solution so
return SolutionUpdateStatus.None;
}
- if (BaseSolution == solution)
+ if (DebuggingSession.LastCommittedSolution.HasNoChanges(solution))
{
return SolutionUpdateStatus.None;
}
@@ -436,7 +452,7 @@ from documentId in solution.GetDocumentIdsWithFilePath(sourceFilePath)
continue;
}
- var baseProject = GetBaseProject(project.Id);
+ var baseProject = DebuggingSession.LastCommittedSolution.GetProject(project.Id);
// When debugging session is started some projects might not have been loaded to the workspace yet.
// We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied
@@ -451,7 +467,15 @@ from documentId in solution.GetDocumentIdsWithFilePath(sourceFilePath)
continue;
}
- var changedDocumentAnalyses = GetChangedDocumentsAnalyses(baseProject, project);
+ var (changedDocumentAnalyses, diagnostics) = await GetChangedDocumentsAnalysesAsync(baseProject, project, cancellationToken).ConfigureAwait(false);
+ if (diagnostics.Any())
+ {
+ EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: out-of-sync documents present (diagnostic: '{2}')",
+ project.Id.DebugName, project.Id, diagnostics[0]);
+
+ return SolutionUpdateStatus.Blocked;
+ }
+
if (changedDocumentAnalyses.Length == 0)
{
continue;
@@ -505,7 +529,7 @@ from documentId in solution.GetDocumentIdsWithFilePath(sourceFilePath)
}
private async Task GetProjectAnalysisSymmaryAsync(
- ImmutableArray<(DocumentId DocumentId, AsyncLazy Results)> documentAnalyses,
+ ImmutableArray<(Document Document, AsyncLazy Results)> documentAnalyses,
CancellationToken cancellationToken)
{
bool hasChanges = false;
@@ -551,15 +575,16 @@ private async Task GetProjectAnalysisSymmaryAsync(
return ProjectAnalysisSummary.ValidChanges;
}
- private static async Task GetProjectChangesAsync(ImmutableArray<(DocumentId Document, AsyncLazy Results)> changedDocumentAnalyses, CancellationToken cancellationToken)
+ private static async Task GetProjectChangesAsync(ImmutableArray<(Document Document, AsyncLazy Results)> changedDocumentAnalyses, CancellationToken cancellationToken)
{
try
{
var allEdits = ArrayBuilder.GetInstance();
var allLineEdits = ArrayBuilder<(DocumentId, ImmutableArray)>.GetInstance();
var activeStatementsInChangedDocuments = ArrayBuilder<(DocumentId, ImmutableArray, ImmutableArray>)>.GetInstance();
+ var allAddedSymbols = ArrayBuilder.GetInstance();
- foreach (var (documentId, asyncResult) in changedDocumentAnalyses)
+ foreach (var (document, asyncResult) in changedDocumentAnalyses)
{
var result = await asyncResult.GetValueAsync(cancellationToken).ConfigureAwait(false);
@@ -572,18 +597,37 @@ private static async Task GetProjectChangesAsync(ImmutableArray<
Debug.Assert(!result.HasChangesAndErrors);
allEdits.AddRange(result.SemanticEdits);
+
+ if (!result.HasChangesAndErrors)
+ {
+ foreach (var edit in result.SemanticEdits)
+ {
+ if (edit.Kind == SemanticEditKind.Insert)
+ {
+ allAddedSymbols.Add(edit.NewSymbol);
+ }
+ }
+ }
+
if (result.LineEdits.Length > 0)
{
- allLineEdits.Add((documentId, result.LineEdits));
+ allLineEdits.Add((document.Id, result.LineEdits));
}
if (result.ActiveStatements.Length > 0)
{
- activeStatementsInChangedDocuments.Add((documentId, result.ActiveStatements, result.ExceptionRegions));
+ activeStatementsInChangedDocuments.Add((document.Id, result.ActiveStatements, result.ExceptionRegions));
}
}
- return new ProjectChanges(allEdits.ToImmutableAndFree(), allLineEdits.ToImmutableAndFree(), activeStatementsInChangedDocuments.ToImmutableAndFree());
+ var allAddedSymbolResult = allAddedSymbols.ToImmutableHashSet();
+ allAddedSymbols.Free();
+
+ return new ProjectChanges(
+ allEdits.ToImmutableAndFree(),
+ allLineEdits.ToImmutableAndFree(),
+ allAddedSymbolResult,
+ activeStatementsInChangedDocuments.ToImmutableAndFree());
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e))
{
@@ -602,6 +646,7 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can
var emitBaselines = ArrayBuilder<(ProjectId, EmitBaseline)>.GetInstance();
var readers = ArrayBuilder.GetInstance();
var diagnostics = ArrayBuilder<(ProjectId, ImmutableArray)>.GetInstance();
+ var changedDocuments = ArrayBuilder.GetInstance();
try
{
@@ -614,7 +659,7 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can
continue;
}
- var baseProject = GetBaseProject(project.Id);
+ var baseProject = DebuggingSession.LastCommittedSolution.GetProject(project.Id);
// TODO (https://github.com/dotnet/roslyn/issues/1204):
// When debugging session is started some projects might not have been loaded to the workspace yet.
@@ -635,7 +680,32 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can
continue;
}
- var changedDocumentAnalyses = GetChangedDocumentsAnalyses(baseProject, project);
+ // Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
+ // Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
+ // incoming events updating the content of out-of-sync documents.
+ //
+ // If in past we concluded that a document is out-of-sync, attempt to check one more time before we block apply.
+ // The source file content might have been updated since the last time we checked.
+ //
+ // TODO (investigate): https://github.com/dotnet/roslyn/issues/38866
+ // It is possible that the result of Rude Edit semantic analysis of an unchanged document will change if there
+ // another document is updated. If we encounter a significant case of this we should consider caching such a result per project,
+ // rather then per document. Also, we might be observing an older semantics if the document that is causing the change is out-of-sync --
+ // e.g. the binary was built with an overload C.M(object), but a generator updated class C to also contain C.M(string),
+ // which change we have not observed yet. Then call-sites of C.M in a changed document observed by the analysis will be seen as C.M(object)
+ // instead of the true C.M(string).
+ var (changedDocumentAnalyses, outOfSyncDiagnostics) = await GetChangedDocumentsAnalysesAsync(baseProject, project, cancellationToken).ConfigureAwait(false);
+ if (outOfSyncDiagnostics.Any())
+ {
+ // The error hasn't been reported by GetDocumentDiagnosticsAsync since out-of-sync documents are likely to be synchronized
+ // before the changes are attempted to be applied. If they are not the project changes can't be applied.
+ diagnostics.Add((project.Id, outOfSyncDiagnostics));
+
+ Telemetry.LogProjectAnalysisSummary(ProjectAnalysisSummary.RudeEdits, outOfSyncDiagnostics);
+ isBlocked = true;
+ continue;
+ }
+
var projectSummary = await GetProjectAnalysisSymmaryAsync(changedDocumentAnalyses, cancellationToken).ConfigureAwait(false);
if (projectSummary != ProjectAnalysisSummary.ValidChanges)
@@ -671,9 +741,12 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can
var projectChanges = await GetProjectChangesAsync(changedDocumentAnalyses, cancellationToken).ConfigureAwait(false);
var currentCompilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
- var allAddedSymbols = await GetAllAddedSymbolsAsync(project, cancellationToken).ConfigureAwait(false);
var baseActiveStatements = await BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
- var baseActiveExceptionRegions = await BaseActiveExceptionRegions.GetValueAsync(cancellationToken).ConfigureAwait(false);
+
+ // Exception regions of active statements in changed documents are calculated (non-default),
+ // since we already checked that no changed document is out-of-sync above.
+ var baseActiveExceptionRegions = await GetBaseActiveExceptionRegionsAsync(cancellationToken).ConfigureAwait(false);
+
var lineEdits = projectChanges.LineChanges.SelectAsArray((lineChange, p) => (p.GetDocument(lineChange.DocumentId).FilePath, lineChange.Changes), project);
// Dispatch to a background thread - the compiler reads symbols and ISymUnmanagedReader requires MTA thread.
@@ -687,6 +760,11 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can
Emit();
}
+ if (!isBlocked)
+ {
+ changedDocuments.AddRange(changedDocumentAnalyses.Select(a => a.Document));
+ }
+
void Emit()
{
Debug.Assert(Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA, "SymReader requires MTA");
@@ -725,7 +803,7 @@ void Emit()
var emitResult = currentCompilation.EmitDifference(
baseline,
projectChanges.SemanticEdits,
- s => allAddedSymbols?.Contains(s) ?? false,
+ projectChanges.AddedSymbols.Contains,
metadataStream,
ilStream,
pdbStream,
@@ -788,6 +866,7 @@ void Emit()
}
readers.Free();
+ changedDocuments.Free();
return SolutionUpdate.Blocked(diagnostics.ToImmutableAndFree());
}
@@ -797,6 +876,7 @@ void Emit()
deltas.ToImmutableAndFree(),
readers.ToImmutableAndFree(),
emitBaselines.ToImmutableAndFree(),
+ changedDocuments.ToImmutableAndFree(),
diagnostics.ToImmutableAndFree());
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e))
@@ -937,6 +1017,10 @@ void AddNonRemappableRegion(LinePositionSpan oldSpan, LinePositionSpan newSpan,
AddNonRemappableRegion(oldActiveStatement.Span, newActiveStatement.Span, isExceptionRegion: false);
+ // The spans of the exception regions are known (non-default) for active statements in changed documents
+ // as we ensured earlier that all changed documents are in-sync. The outer loop only enumerates active
+ // statements of changed documents, so the corresponding exception regions are initialized.
+
var j = 0;
foreach (var oldSpan in baseActiveExceptionRegions[oldActiveStatement.Ordinal].Spans)
{
diff --git a/src/Features/Core/Portable/EditAndContinue/IDebuggeeModuleMetadataProvider.cs b/src/Features/Core/Portable/EditAndContinue/IDebuggeeModuleMetadataProvider.cs
index 535156fa2cba5..ba11ff1b69a78 100644
--- a/src/Features/Core/Portable/EditAndContinue/IDebuggeeModuleMetadataProvider.cs
+++ b/src/Features/Core/Portable/EditAndContinue/IDebuggeeModuleMetadataProvider.cs
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+#nullable enable
+
using System;
-using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
namespace Microsoft.CodeAnalysis.EditAndContinue
{
@@ -15,12 +17,12 @@ internal interface IDebuggeeModuleMetadataProvider
/// Shall only be called while in debug mode.
/// Shall only be called on MTA thread.
///
- DebuggeeModuleInfo TryGetBaselineModuleInfo(Guid mvid);
+ DebuggeeModuleInfo? TryGetBaselineModuleInfo(Guid mvid);
///
/// Returns an error message when any instance of a module with given disallows EnC.
///
- bool IsEditAndContinueAvailable(Guid mvid, out int errorCode, out string localizedMessage);
+ bool IsEditAndContinueAvailable(Guid mvid, out int errorCode, [NotNullWhen(true)]out string localizedMessage);
///
/// Notifies the debugger that a document changed that may affect the given module when the change is applied.
diff --git a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs
index 5b7517677fd7e..8d38e0440dba6 100644
--- a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs
+++ b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs
@@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.EditAndContinue
{
internal interface IEditAndContinueAnalyzer : ILanguageService
{
- Task AnalyzeDocumentAsync(Project baseProjectOpt, ImmutableArray activeStatements, Document document, IActiveStatementTrackingService trackingService, CancellationToken cancellationToken);
+ Task AnalyzeDocumentAsync(Document oldDocumentOpt, ImmutableArray activeStatements, Document document, IActiveStatementTrackingService trackingService, CancellationToken cancellationToken);
ImmutableArray GetExceptionRegions(SourceText text, SyntaxNode syntaxRoot, LinePositionSpan activeStatementSpan, bool isLeaf, out bool isCovered);
}
}
diff --git a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs
index c83381e6aa5cb..223135931c3f9 100644
--- a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs
+++ b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs
@@ -22,6 +22,7 @@ internal interface IEditAndContinueWorkspaceService : IWorkspaceService
void OnManagedModuleInstanceUnloaded(Guid mvid);
bool IsDebuggingSessionInProgress { get; }
+ void OnSourceFileUpdated(DocumentId documentId);
void StartDebuggingSession();
void StartEditSession();
diff --git a/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs b/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs
index 153d2ba4c3948..c891b8409a3bd 100644
--- a/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs
+++ b/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs
@@ -11,17 +11,20 @@ internal sealed class PendingSolutionUpdate
public readonly ImmutableArray<(ProjectId ProjectId, EmitBaseline Baseline)> EmitBaselines;
public readonly ImmutableArray Deltas;
public readonly ImmutableArray ModuleReaders;
+ public readonly ImmutableArray ChangedDocuments;
public PendingSolutionUpdate(
Solution solution,
ImmutableArray<(ProjectId ProjectId, EmitBaseline Baseline)> emitBaselines,
ImmutableArray deltas,
- ImmutableArray moduleReaders)
+ ImmutableArray moduleReaders,
+ ImmutableArray changedDocuments)
{
Solution = solution;
EmitBaselines = emitBaselines;
Deltas = deltas;
ModuleReaders = moduleReaders;
+ ChangedDocuments = changedDocuments;
}
}
}
diff --git a/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs b/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs
index ad36d6a74fc1c..875fb63e40ded 100644
--- a/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs
+++ b/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs
@@ -19,6 +19,11 @@ internal readonly struct ProjectChanges
///
public readonly ImmutableArray<(DocumentId DocumentId, ImmutableArray Changes)> LineChanges;
+ ///
+ /// All symbols added in changed documents.
+ ///
+ public readonly ImmutableHashSet AddedSymbols;
+
///
/// All active statements and the corresponding exception regions in changed documents.
///
@@ -27,14 +32,17 @@ internal readonly struct ProjectChanges
public ProjectChanges(
ImmutableArray semanticEdits,
ImmutableArray<(DocumentId, ImmutableArray)> lineChanges,
+ ImmutableHashSet addedSymbols,
ImmutableArray<(DocumentId, ImmutableArray, ImmutableArray>)> newActiveStatements)
{
Debug.Assert(!semanticEdits.IsDefault);
Debug.Assert(!lineChanges.IsDefault);
+ Debug.Assert(addedSymbols != null);
Debug.Assert(!newActiveStatements.IsDefault);
SemanticEdits = semanticEdits;
LineChanges = lineChanges;
+ AddedSymbols = addedSymbols;
NewActiveStatements = newActiveStatements;
}
}
diff --git a/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs b/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs
index 033ced9df8bc3..9f040b051ff47 100644
--- a/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs
+++ b/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs
@@ -12,18 +12,21 @@ internal readonly struct SolutionUpdate
public readonly ImmutableArray ModuleReaders;
public readonly ImmutableArray<(ProjectId ProjectId, EmitBaseline Baseline)> EmitBaselines;
public readonly ImmutableArray<(ProjectId ProjectId, ImmutableArray Diagnostic)> Diagnostics;
+ public readonly ImmutableArray ChangedDocuments;
public SolutionUpdate(
SolutionUpdateStatus summary,
ImmutableArray deltas,
ImmutableArray moduleReaders,
ImmutableArray<(ProjectId, EmitBaseline)> emitBaselines,
+ ImmutableArray changedDocuments,
ImmutableArray<(ProjectId ProjectId, ImmutableArray Diagnostics)> diagnostics)
{
Summary = summary;
Deltas = deltas;
EmitBaselines = emitBaselines;
ModuleReaders = moduleReaders;
+ ChangedDocuments = changedDocuments;
Diagnostics = diagnostics;
}
@@ -35,6 +38,7 @@ public static SolutionUpdate Blocked()
ImmutableArray.Empty,
ImmutableArray.Empty,
ImmutableArray<(ProjectId, EmitBaseline)>.Empty,
+ ImmutableArray.Empty,
diagnostics);
}
}
diff --git a/src/Features/Core/Portable/EditAndContinue/TraceLog.cs b/src/Features/Core/Portable/EditAndContinue/TraceLog.cs
index 76588bce1a3ef..fb521abfde176 100644
--- a/src/Features/Core/Portable/EditAndContinue/TraceLog.cs
+++ b/src/Features/Core/Portable/EditAndContinue/TraceLog.cs
@@ -19,27 +19,28 @@ internal sealed class TraceLog
{
internal readonly struct Arg
{
- public readonly string String;
+ public readonly object Object;
public readonly int Int32;
- public Arg(string value)
+ public Arg(object value)
{
Int32 = 0;
- String = value ?? "";
+ Object = value ?? "";
}
public Arg(int value)
{
Int32 = value;
- String = null;
+ Object = null;
}
- public override string ToString() => String ?? Int32.ToString();
+ public override string ToString() => (Object is null) ? Int32.ToString() : Object.ToString();
public static implicit operator Arg(string value) => new Arg(value);
public static implicit operator Arg(int value) => new Arg(value);
public static implicit operator Arg(ProjectId value) => new Arg(value.Id.GetHashCode());
public static implicit operator Arg(ProjectAnalysisSummary value) => new Arg(ToString(value));
+ public static implicit operator Arg(Diagnostic value) => new Arg(value);
private static string ToString(ProjectAnalysisSummary summary)
{
@@ -55,6 +56,7 @@ private static string ToString(ProjectAnalysisSummary summary)
}
}
+ [DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
internal readonly struct Entry
{
public readonly string MessageFormat;
@@ -66,8 +68,8 @@ public Entry(string format, Arg[] argsOpt)
ArgsOpt = argsOpt;
}
- public override string ToString() =>
- string.Format(MessageFormat, ArgsOpt?.Select(a => (object)a).ToArray() ?? Array.Empty