File: EditAndContinue\PdbMatchingSourceTextProvider.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue;
 
/// <summary>
/// Notifies EnC service of host workspace events.
/// </summary>
[ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared]
[Export(typeof(PdbMatchingSourceTextProvider))]
internal sealed class PdbMatchingSourceTextProvider : IEventListener<object>, IEventListenerStoppable, IPdbMatchingSourceTextProvider
{
    private readonly object _guard = new();
 
    private bool _isActive;
    private int _baselineSolutionVersion;
    private readonly Dictionary<string, (DocumentState state, int solutionVersion)> _documentsWithChangedLoaderByPath = [];
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public PdbMatchingSourceTextProvider()
    {
    }
 
    public void StartListening(Workspace workspace, object serviceOpt)
    {
        workspace.WorkspaceChanged += WorkspaceChanged;
    }
 
    public void StopListening(Workspace workspace)
    {
        workspace.WorkspaceChanged -= WorkspaceChanged;
    }
 
    private void WorkspaceChanged(object? sender, WorkspaceChangeEventArgs e)
    {
        if (!_isActive)
        {
            // Not capturing document states because debugging session isn't active.
            return;
        }
 
        if (e.DocumentId == null)
        {
            return;
        }
 
        var oldDocument = e.OldSolution.GetDocument(e.DocumentId);
        if (oldDocument == null)
        {
            // document added
            return;
        }
 
        var newDocument = e.NewSolution.GetDocument(e.DocumentId);
        if (newDocument == null)
        {
            // document removed
            return;
        }
 
        if (!oldDocument.State.SupportsEditAndContinue())
        {
            return;
        }
 
        Contract.ThrowIfNull(oldDocument.FilePath);
 
        // When a document is open its loader transitions from file-based loader to text buffer based.
        // The file checksum is no longer available from the latter, so capture it at this moment.
        if (oldDocument.State.TextAndVersionSource.CanReloadText && !newDocument.State.TextAndVersionSource.CanReloadText)
        {
            var oldSolutionVersion = oldDocument.Project.Solution.WorkspaceVersion;
 
            lock (_guard)
            {
                // ignore updates to a document that we have already seen this session:
                if (_isActive && oldSolutionVersion >= _baselineSolutionVersion && !_documentsWithChangedLoaderByPath.ContainsKey(oldDocument.FilePath))
                {
                    _documentsWithChangedLoaderByPath.Add(oldDocument.FilePath, (oldDocument.DocumentState, oldSolutionVersion));
                }
            }
        }
    }
 
    /// <summary>
    /// Establish a baseline snapshot. The listener will ignore all document snapshots that are older.
    /// </summary>
    public void SetBaseline(Solution solution)
    {
        lock (_guard)
        {
            _baselineSolutionVersion = solution.WorkspaceVersion;
        }
    }
 
    public void Activate()
    {
        lock (_guard)
        {
            _isActive = true;
        }
    }
 
    public void Deactivate()
    {
        lock (_guard)
        {
            _isActive = false;
            _documentsWithChangedLoaderByPath.Clear();
        }
    }
 
    public async ValueTask<string?> TryGetMatchingSourceTextAsync(string filePath, ImmutableArray<byte> requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken)
    {
        DocumentState? state;
        lock (_guard)
        {
            if (!_documentsWithChangedLoaderByPath.TryGetValue(filePath, out var stateAndVersion))
            {
                return null;
            }
 
            state = stateAndVersion.state;
        }
 
        if (state.LoadTextOptions.ChecksumAlgorithm != checksumAlgorithm)
        {
            return null;
        }
 
        var text = await state.GetTextAsync(cancellationToken).ConfigureAwait(false);
        if (!text.GetChecksum().SequenceEqual(requiredChecksum))
        {
            return null;
        }
 
        return text.ToString();
    }
 
    internal TestAccessor GetTestAccessor()
        => new(this);
 
    internal readonly struct TestAccessor
    {
        private readonly PdbMatchingSourceTextProvider _instance;
 
        internal TestAccessor(PdbMatchingSourceTextProvider instance)
            => _instance = instance;
 
        public ImmutableDictionary<string, (DocumentState state, int solutionVersion)> GetDocumentsWithChangedLoaderByPath()
        {
            lock (_instance._guard)
            {
                return _instance._documentsWithChangedLoaderByPath.ToImmutableDictionary();
            }
        }
    }
}