// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#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;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Implementation.EditAndContinue
{
///
/// Tracks active statements for the debugger during an edit session.
///
///
/// An active statement is a source statement that occurs in a stack trace of a thread.
/// Active statements are visualized via a gray marker in the text editor.
///
[Export(typeof(IActiveStatementTrackingService))]
internal sealed class ActiveStatementTrackingService : IActiveStatementTrackingService
{
private TrackingSession? _session;
public event Action? TrackingSpansChanged;
[ImportingConstructor]
public ActiveStatementTrackingService()
{
}
private void OnTrackingSpansChanged(bool leafChanged)
{
TrackingSpansChanged?.Invoke(leafChanged);
}
public void StartTracking(EditSession editSession)
{
var newSession = new TrackingSession(this, editSession);
if (Interlocked.CompareExchange(ref _session, newSession, null) != null)
{
newSession.EndTracking();
Contract.Fail("Can only track active statements for a single edit session.");
}
}
public void EndTracking()
{
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 = _session;
if (session == null)
{
span = default;
return false;
}
return session.TryGetSpan(id, source, out span);
}
public IEnumerable GetSpans(SourceText source)
{
return _session?.GetSpans(source) ?? SpecializedCollections.EmptyEnumerable();
}
public void UpdateActiveStatementSpans(SourceText source, IEnumerable<(ActiveStatementId, ActiveStatementTextSpan)> spans)
{
_session?.UpdateActiveStatementSpans(source, spans);
}
private sealed class TrackingSession
{
private struct ActiveStatementTrackingSpan
{
public readonly ITrackingSpan Span;
public readonly ActiveStatementFlags Flags;
public ActiveStatementTrackingSpan(ITrackingSpan trackingSpan, ActiveStatementFlags flags)
{
Span = trackingSpan;
Flags = flags;
}
}
private readonly ActiveStatementTrackingService _service;
private readonly EditSession _editSession;
#region lock(_trackingSpans)
// 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;
#endregion
public TrackingSession(ActiveStatementTrackingService service, EditSession editSession)
{
_service = service;
_editSession = editSession;
_trackingSpans = new Dictionary();
editSession.DebuggingSession.Workspace.DocumentOpened += DocumentOpened;
// fire and forget on a background thread:
try
{
_ = Task.Run(TrackActiveSpansAsync, _editSession.CancellationToken);
}
catch (TaskCanceledException)
{
}
}
public void EndTracking()
{
_editSession.DebuggingSession.Workspace.DocumentOpened -= DocumentOpened;
lock (_trackingSpans)
{
_trackingSpans.Clear();
}
_service.OnTrackingSpansChanged(leafChanged: true);
}
private void DocumentOpened(object sender, DocumentEventArgs e)
{
_ = DocumentOpenedAsync(e.Document);
}
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 (baseDocument != null &&
baseActiveStatements.DocumentMap.TryGetValue(document.Id, out var documentActiveStatements) &&
TryGetSnapshot(document, out var snapshot))
{
lock (_trackingSpans)
{
TrackActiveSpansNoLock(baseDocument, document, snapshot, documentActiveStatements);
}
var leafChanged = documentActiveStatements.Contains(s => s.IsLeaf);
_service.OnTrackingSpansChanged(leafChanged);
}
}
catch (OperationCanceledException)
{
// nop
}
catch (Exception e) when (FatalError.ReportWithoutCrash(e))
{
// nop
}
}
private static bool TryGetSnapshot(Document document, [NotNullWhen(true)] out ITextSnapshot? snapshot)
{
if (!document.TryGetText(out var source))
{
snapshot = null;
return false;
}
snapshot = source.FindCorrespondingEditorTextSnapshot();
return snapshot != null;
}
private async Task TrackActiveSpansAsync()
{
try
{
var cancellationToken = _editSession.CancellationToken;
var baseActiveStatements = await _editSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
var lastCommittedSolution = _editSession.DebuggingSession.LastCommittedSolution;
var currentSolution = _editSession.DebuggingSession.Workspace.CurrentSolution;
using var _ = ArrayBuilder<(Document, Document, ITextSnapshot, ImmutableArray)>.GetInstance(out var activeSpansToTrack);
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 (baseDocument, document, snapshot, documentActiveStatements) in activeSpansToTrack)
{
TrackActiveSpansNoLock(baseDocument, document, snapshot, documentActiveStatements);
}
}
_service.OnTrackingSpansChanged(leafChanged: true);
}
catch (OperationCanceledException)
{
// nop
}
catch (Exception e) when (FatalError.ReportWithoutCrash(e))
{
// nop
}
}
private void TrackActiveSpansNoLock(
Document baseDocument,
Document document,
ITextSnapshot snapshot,
ImmutableArray documentActiveStatements)
{
if (!_trackingSpans.TryGetValue(baseDocument.Id, out var documentTrackingSpans))
{
SetTrackingSpansNoLock(baseDocument.Id, CreateTrackingSpans(snapshot, documentActiveStatements));
}
else if (documentTrackingSpans != null)
{
Debug.Assert(documentTrackingSpans.Length > 0);
if (documentTrackingSpans[0].Span.TextBuffer != snapshot.TextBuffer)
{
// 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(baseDocument.Id, null);
// fire and forget on a background thread:
try
{
_ = Task.Run(() => RefreshTrackingSpansAsync(baseDocument, document, snapshot), _editSession.CancellationToken);
}
catch (TaskCanceledException)
{
}
}
}
}
private async Task RefreshTrackingSpansAsync(Document baseDocument, Document document, ITextSnapshot snapshot)
{
try
{
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)
{
RefreshTrackingSpans(document.Id, snapshot, documentAnalysis.ActiveStatements);
}
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
{
// nop
}
}
private void RefreshTrackingSpans(DocumentId documentId, ITextSnapshot snapshot, ImmutableArray documentActiveStatements)
{
var updated = false;
lock (_trackingSpans)
{
if (_trackingSpans.TryGetValue(documentId, out var documentTrackingSpans) && documentTrackingSpans == null)
{
SetTrackingSpansNoLock(documentId, CreateTrackingSpans(snapshot, documentActiveStatements));
updated = true;
}
}
if (updated)
{
_service.OnTrackingSpansChanged(leafChanged: true);
}
}
private void SetTrackingSpansNoLock(DocumentId documentId, ActiveStatementTrackingSpan[]? spans)
{
_trackingSpans[documentId] = spans;
}
private static ActiveStatementTrackingSpan[] CreateTrackingSpans(ITextSnapshot snapshot, ImmutableArray documentActiveStatements)
{
var result = new ActiveStatementTrackingSpan[documentActiveStatements.Length];
for (var i = 0; i < result.Length; i++)
{
var span = snapshot.GetTextSpan(documentActiveStatements[i].Span).ToSpan();
result[i] = CreateTrackingSpan(snapshot, span, documentActiveStatements[i].Flags);
}
return result;
}
private static ActiveStatementTrackingSpan CreateTrackingSpan(ITextSnapshot snapshot, Span span, ActiveStatementFlags flags)
{
return new ActiveStatementTrackingSpan(snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeExclusive), flags);
}
public bool TryGetSpan(ActiveStatementId id, SourceText source, out TextSpan span)
{
lock (_trackingSpans)
{
if (_trackingSpans.TryGetValue(id.DocumentId, out var documentSpans) && documentSpans != null)
{
var trackingSpan = documentSpans[id.Ordinal].Span;
var snapshot = source.FindCorrespondingEditorTextSnapshot();
if (snapshot != null && snapshot.TextBuffer == trackingSpan.TextBuffer)
{
span = trackingSpan.GetSpan(snapshot).Span.ToTextSpan();
return true;
}
}
}
span = default;
return false;
}
public IEnumerable GetSpans(SourceText source)
{
var document = source.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
{
return SpecializedCollections.EmptyEnumerable();
}
// 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.DebuggingSession.Workspace)
{
return SpecializedCollections.EmptyEnumerable();
}
ActiveStatementTrackingSpan[]? documentTrackingSpans;
lock (_trackingSpans)
{
if (!_trackingSpans.TryGetValue(document.Id, out documentTrackingSpans) || documentTrackingSpans == null)
{
return SpecializedCollections.EmptyEnumerable();
}
}
Debug.Assert(documentTrackingSpans.Length > 0);
var snapshot = source.FindCorrespondingEditorTextSnapshot();
// The document might have been reopened with a new text buffer
// and we haven't created tracking spans for the new text buffer yet.
if (snapshot == null || snapshot.TextBuffer != documentTrackingSpans[0].Span.TextBuffer)
{
return SpecializedCollections.EmptyEnumerable();
}
var result = new ActiveStatementTextSpan[documentTrackingSpans.Length];
for (var i = 0; i < documentTrackingSpans.Length; i++)
{
Debug.Assert(documentTrackingSpans[i].Span.TextBuffer == snapshot.TextBuffer);
result[i] = new ActiveStatementTextSpan(
documentTrackingSpans[i].Flags,
documentTrackingSpans[i].Span.GetSpan(snapshot).Span.ToTextSpan());
}
return result;
}
public void UpdateActiveStatementSpans(SourceText source, IEnumerable<(ActiveStatementId, ActiveStatementTextSpan)> spans)
{
var leafUpdated = false;
var updated = false;
lock (_trackingSpans)
{
foreach (var (id, span) in spans)
{
if (_trackingSpans.TryGetValue(id.DocumentId, out var documentSpans) && documentSpans != null)
{
var snapshot = source.FindCorrespondingEditorTextSnapshot();
// Avoid updating spans if the buffer has changed.
// Buffer change is handled by DocumentOpened event.
if (snapshot != null && snapshot.TextBuffer == documentSpans[id.Ordinal].Span.TextBuffer)
{
documentSpans[id.Ordinal] = new ActiveStatementTrackingSpan(snapshot.CreateTrackingSpan(span.Span.ToSpan(), SpanTrackingMode.EdgeExclusive), span.Flags);
if (!leafUpdated)
{
leafUpdated = span.IsLeaf;
}
updated = true;
}
}
}
}
if (updated)
{
_service.OnTrackingSpansChanged(leafUpdated);
}
}
}
}
}