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()); + internal string GetDebuggerDisplay() => + (MessageFormat == null) ? "" : string.Format(MessageFormat, ArgsOpt?.Select(a => (object)a).ToArray() ?? Array.Empty()); } private readonly Entry[] _log; diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 2cf9f1dedc22e..93509d3e5ad45 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -1328,6 +1328,15 @@ internal static string Document { } } + /// + /// Looks up a localized string similar to The current content of source file '{0}' does not match the built source. The file is likely being updated by a background build. The debug session can't continue until the update is completed.. + /// + internal static string DocumentIsOutOfSyncWithDebuggee { + get { + return ResourceManager.GetString("DocumentIsOutOfSyncWithDebuggee", resourceCulture); + } + } + /// /// Looks up a localized string similar to Edit and Continue. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index dfcc5c1931cd2..5676d173b301f 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1671,6 +1671,9 @@ This version used in: {2} Changes made in project '{0}' will not be applied while the application is running + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + Changes are not allowed while stopped at exception diff --git a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj index b19a4d78b06cd..dc7a5a26ee87d 100644 --- a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj +++ b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj @@ -99,6 +99,7 @@ + True True diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index 312c1a842a796..2cd33052cb339 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -187,6 +187,11 @@ Uvolňujte objekty před ztrátou oboru + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Upravit a pokračovat diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index f39f9cacb6660..83c2b91868178 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -187,6 +187,11 @@ Objekte verwerfen, bevor Bereich verloren geht + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Bearbeiten und Fortfahren diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index 23548abe92589..4054a45bc013a 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -187,6 +187,11 @@ Desechar (Dispose) objetos antes de perder el ámbito + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Editar y continuar diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index daca22e8740a1..eda737dbe1d20 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -187,6 +187,11 @@ Supprimer les objets avant la mise hors de portée + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Modifier et continuer diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index 6eaa4004d0a67..6e846085e7a8f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -187,6 +187,11 @@ Elimina gli oggetti prima che siano esterni all'ambito + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Modifica e continuazione diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index b80284dd29f8f..75235ab5dca4c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -187,6 +187,11 @@ スコープを失う前にオブジェクトを破棄 + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue エディット コンティニュ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index b934e00fdcd06..4ac6d7c20cdf6 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -187,6 +187,11 @@ 범위를 벗어나기 전에 개체를 삭제하십시오. + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue 편집하며 계속하기 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index 97c6657c748a0..cc52875853c42 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -187,6 +187,11 @@ Likwiduj obiekty przed utratą zakresu + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Edytuj i kontynuuj diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index a374c839446f8..24c1a33ee36ef 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -187,6 +187,11 @@ Descartar objetos antes de perder o escopo + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Editar e Continuar diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 89ee62371a3b9..f45285158da5d 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -187,6 +187,11 @@ Ликвидировать объекты перед потерей области + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Операция "Изменить и продолжить" diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index 8817a490f5473..4a2b3d59c5f48 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -187,6 +187,11 @@ Kapsamı kaybetmeden çnce nesneleri bırakın + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue Düzenle ve Devam Et diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index f2ec672ef215a..72bda1c7a046d 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -187,6 +187,11 @@ 丢失范围之前释放对象 + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue 编辑并继续 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index f71d1d47469da..dab34dc966f10 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -187,6 +187,11 @@ 必須在超出範圍前處置物件 + + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + + Edit and Continue 編輯並繼續