File: EditAndContinue\EditAndContinueLanguageService.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.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.BrokeredServices;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Debugger.Contracts.HotReload;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue;
 
[Shared]
[Export(typeof(IManagedHotReloadLanguageService))]
[Export(typeof(IManagedHotReloadLanguageService2))]
[Export(typeof(IEditAndContinueSolutionProvider))]
[Export(typeof(EditAndContinueLanguageService))]
[ExportMetadata("UIContext", EditAndContinueUIContext.EncCapableProjectExistsInWorkspaceUIContextString)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class EditAndContinueLanguageService(
    IServiceBrokerProvider serviceBrokerProvider,
    EditAndContinueSessionState sessionState,
    Lazy<IHostWorkspaceProvider> workspaceProvider,
    Lazy<IManagedHotReloadService> debuggerService,
    PdbMatchingSourceTextProvider sourceTextProvider,
    IDiagnosticsRefresher diagnosticRefresher,
    IAsynchronousOperationListenerProvider listenerProvider) : IManagedHotReloadLanguageService2, IEditAndContinueSolutionProvider
{
    private sealed class NoSessionException : InvalidOperationException
    {
        public NoSessionException()
            : base("Internal error: no session.")
        {
            // unique enough HResult to distinguish from other exceptions
            HResult = unchecked((int)0x801315087);
        }
    }
 
    private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.EditAndContinue);
    private readonly HotReloadLoggerProxy _logger = new(serviceBrokerProvider.ServiceBroker);
 
    private bool _disabled;
    private RemoteDebuggingSessionProxy? _debuggingSession;
 
    private Solution? _pendingUpdatedDesignTimeSolution;
    private Solution? _committedDesignTimeSolution;
 
    public event Action<Solution>? SolutionCommitted;
 
    public void SetFileLoggingDirectory(string? logDirectory)
    {
        _ = Task.Run(async () =>
        {
            try
            {
                var proxy = new RemoteEditAndContinueServiceProxy(Services);
                await proxy.SetFileLoggingDirectoryAsync(logDirectory, CancellationToken.None).ConfigureAwait(false);
            }
            catch
            {
                // ignore
            }
        });
    }
 
    private SolutionServices Services
        => workspaceProvider.Value.Workspace.Services.SolutionServices;
 
    private Solution GetCurrentDesignTimeSolution()
        => workspaceProvider.Value.Workspace.CurrentSolution;
 
    private Solution GetCurrentCompileTimeSolution(Solution currentDesignTimeSolution)
        => Services.GetRequiredService<ICompileTimeSolutionProvider>().GetCompileTimeSolution(currentDesignTimeSolution);
 
    private RemoteDebuggingSessionProxy GetDebuggingSession()
        => _debuggingSession ?? throw new NoSessionException();
 
    private IActiveStatementTrackingService GetActiveStatementTrackingService()
        => Services.GetRequiredService<IActiveStatementTrackingService>();
 
    internal void Disable(Exception e)
    {
        _disabled = true;
 
        var token = _asyncListener.BeginAsyncOperation(nameof(EditAndContinueLanguageService) + ".LogToOutput");
 
        _ = _logger.LogAsync(new HotReloadLogMessage(HotReloadVerbosity.Diagnostic, e.ToString(), errorLevel: HotReloadDiagnosticErrorLevel.Error), CancellationToken.None).AsTask()
            .ReportNonFatalErrorAsync().CompletesAsyncOperation(token);
    }
 
    private void UpdateApplyChangesDiagnostics(ImmutableArray<DiagnosticData> diagnostics)
    {
        sessionState.ApplyChangesDiagnostics = diagnostics;
        diagnosticRefresher.RequestWorkspaceRefresh();
    }
 
    /// <summary>
    /// Called by the debugger when a debugging session starts and managed debugging is being used.
    /// </summary>
    public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
    {
        sessionState.IsSessionActive = true;
 
        if (_disabled)
        {
            return;
        }
 
        try
        {
            // Activate listener before capturing the current solution snapshot,
            // so that we don't miss any pertinent workspace update events.
            sourceTextProvider.Activate();
 
            var currentSolution = GetCurrentDesignTimeSolution();
            _committedDesignTimeSolution = currentSolution;
            var solution = GetCurrentCompileTimeSolution(currentSolution);
 
            sourceTextProvider.SetBaseline(currentSolution);
 
            var proxy = new RemoteEditAndContinueServiceProxy(Services);
 
            _debuggingSession = await proxy.StartDebuggingSessionAsync(
                solution,
                new ManagedHotReloadServiceBridge(debuggerService.Value),
                sourceTextProvider,
                captureMatchingDocuments: [],
                captureAllMatchingDocuments: false,
                reportDiagnostics: true,
                cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
            // the service failed, error has been reported - disable further operations
            Disable(e);
        }
    }
 
    public ValueTask EnterBreakStateAsync(CancellationToken cancellationToken)
        => BreakStateOrCapabilitiesChangedAsync(inBreakState: true, cancellationToken);
 
    public ValueTask ExitBreakStateAsync(CancellationToken cancellationToken)
        => BreakStateOrCapabilitiesChangedAsync(inBreakState: false, cancellationToken);
 
    public ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken)
        => BreakStateOrCapabilitiesChangedAsync(inBreakState: null, cancellationToken);
 
    private async ValueTask BreakStateOrCapabilitiesChangedAsync(bool? inBreakState, CancellationToken cancellationToken)
    {
        if (_disabled)
        {
            return;
        }
 
        try
        {
            var session = GetDebuggingSession();
            var solution = (inBreakState == true) ? GetCurrentCompileTimeSolution(GetCurrentDesignTimeSolution()) : null;
 
            await session.BreakStateOrCapabilitiesChangedAsync(inBreakState, cancellationToken).ConfigureAwait(false);
 
            if (inBreakState == false)
            {
                GetActiveStatementTrackingService().EndTracking();
            }
            else if (inBreakState == true)
            {
                // Start tracking after we entered break state so that break-state session is active.
                // This is potentially costly operation as source generators might get invoked in OOP
                // to determine the spans of all active statements.
                // We start the operation but do not wait for it to complete.
                // The tracking session is cancelled when we exit the break state.
 
                Debug.Assert(solution != null);
                GetActiveStatementTrackingService().StartTracking(solution, session);
            }
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
            Disable(e);
        }
 
        // clear diagnostics reported previously:
        UpdateApplyChangesDiagnostics([]);
    }
 
    public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken)
    {
        if (_disabled)
        {
            return;
        }
 
        var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null);
        Contract.ThrowIfNull(committedDesignTimeSolution);
 
        try
        {
            SolutionCommitted?.Invoke(committedDesignTimeSolution);
        }
        catch (Exception e) when (FatalError.ReportAndCatch(e))
        {
        }
 
        _committedDesignTimeSolution = committedDesignTimeSolution;
 
        try
        {
            await GetDebuggingSession().CommitSolutionUpdateAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
        }
 
        workspaceProvider.Value.Workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false);
    }
 
    public async ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken)
    {
        if (_disabled)
        {
            return;
        }
 
        Contract.ThrowIfNull(Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null));
 
        try
        {
            await GetDebuggingSession().DiscardSolutionUpdateAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
        }
    }
 
    public async ValueTask UpdateBaselinesAsync(ImmutableArray<string> projectPaths, CancellationToken cancellationToken)
    {
        if (_disabled)
        {
            return;
        }
 
        var currentDesignTimeSolution = GetCurrentDesignTimeSolution();
        var currentCompileTimeSolution = GetCurrentCompileTimeSolution(currentDesignTimeSolution);
 
        try
        {
            SolutionCommitted?.Invoke(currentDesignTimeSolution);
        }
        catch (Exception e) when (FatalError.ReportAndCatch(e))
        {
        }
 
        _committedDesignTimeSolution = currentDesignTimeSolution;
        var projectIds = await GetProjectIdsAsync(projectPaths, currentCompileTimeSolution, cancellationToken).ConfigureAwait(false);
 
        try
        {
            await GetDebuggingSession().UpdateBaselinesAsync(currentCompileTimeSolution, projectIds, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
        }
 
        foreach (var projectId in projectIds)
        {
            workspaceProvider.Value.Workspace.EnqueueUpdateSourceGeneratorVersion(projectId, forceRegeneration: false);
        }
    }
 
    private async ValueTask<ImmutableArray<ProjectId>> GetProjectIdsAsync(ImmutableArray<string> projectPaths, Solution solution, CancellationToken cancellationToken)
    {
        using var _ = ArrayBuilder<ProjectId>.GetInstance(out var projectIds);
        foreach (var path in projectPaths)
        {
            var projectId = solution.Projects.FirstOrDefault(project => project.FilePath == path)?.Id;
            if (projectId != null)
            {
                projectIds.Add(projectId);
            }
            else
            {
                await _logger.LogAsync(new HotReloadLogMessage(
                    HotReloadVerbosity.Diagnostic,
                    $"Project with path '{path}' not found in the current solution.",
                    errorLevel: HotReloadDiagnosticErrorLevel.Warning),
                    cancellationToken).ConfigureAwait(false);
            }
        }
 
        return projectIds.ToImmutable();
    }
 
    public async ValueTask EndSessionAsync(CancellationToken cancellationToken)
    {
        sessionState.IsSessionActive = false;
 
        if (!_disabled)
        {
            try
            {
                await GetDebuggingSession().EndDebuggingSessionAsync(cancellationToken).ConfigureAwait(false);
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
                Disable(e);
            }
        }
 
        // clear diagnostics reported previously:
        UpdateApplyChangesDiagnostics([]);
 
        sourceTextProvider.Deactivate();
        _debuggingSession = null;
        _committedDesignTimeSolution = null;
        _pendingUpdatedDesignTimeSolution = null;
    }
 
    private ActiveStatementSpanProvider GetActiveStatementSpanProvider(Solution solution)
    {
        var service = GetActiveStatementTrackingService();
        return new((documentId, filePath, cancellationToken) => service.GetSpansAsync(solution, documentId, filePath, cancellationToken));
    }
 
    /// <summary>
    /// Returns true if any changes have been made to the source since the last changes had been applied.
    /// For performance reasons it only implements a heuristic and may return both false positives and false negatives.
    /// If the result is a false negative the debugger will not apply the changes unless the user explicitly triggers apply change command.
    /// The background diagnostic analysis will still report rude edits for these ignored changes. It may also happen that these rude edits 
    /// will disappear once the debuggee is resumed - if they are caused by presence of active statements around the change.
    /// If the result is a false positive the debugger attempts to apply the changes, which will result in a delay but will correctly end up
    /// with no actual deltas to be applied.
    /// 
    /// If <paramref name="sourceFilePath"/> is specified checks for changes only in a document of the given path.
    /// This is not supported (returns false) for source-generated documents.
    /// </summary>
    public async ValueTask<bool> HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken)
    {
        try
        {
            var debuggingSession = _debuggingSession;
            if (debuggingSession == null)
            {
                return false;
            }
 
            Contract.ThrowIfNull(_committedDesignTimeSolution);
            var oldSolution = _committedDesignTimeSolution;
            var newSolution = GetCurrentDesignTimeSolution();
 
            return (sourceFilePath != null)
                ? await EditSession.HasChangesAsync(oldSolution, newSolution, sourceFilePath, cancellationToken).ConfigureAwait(false)
                : await EditSession.HasChangesAsync(oldSolution, newSolution, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
            return true;
        }
    }
 
    [Obsolete]
    public ValueTask<ManagedHotReloadUpdates> GetUpdatesAsync(CancellationToken cancellationToken)
        => GetUpdatesAsync(runningProjects: [], cancellationToken);
 
    public async ValueTask<ManagedHotReloadUpdates> GetUpdatesAsync(ImmutableArray<string> runningProjects, CancellationToken cancellationToken)
    {
        if (_disabled)
        {
            return new ManagedHotReloadUpdates([], []);
        }
 
        var designTimeSolution = GetCurrentDesignTimeSolution();
        var solution = GetCurrentCompileTimeSolution(designTimeSolution);
        var activeStatementSpanProvider = GetActiveStatementSpanProvider(solution);
 
        using var _ = PooledHashSet<string>.GetInstance(out var runningProjectPaths);
        runningProjectPaths.AddAll(runningProjects);
 
        var runningProjectIds = solution.Projects.Where(p => p.FilePath != null && runningProjectPaths.Contains(p.FilePath)).Select(static p => p.Id).ToImmutableHashSet();
        var result = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, runningProjectIds, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false);
 
        // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called.
        if (result.ModuleUpdates.Status == ModuleUpdateStatus.Ready)
        {
            _pendingUpdatedDesignTimeSolution = designTimeSolution;
        }
 
        UpdateApplyChangesDiagnostics(result.Diagnostics);
 
        return new ManagedHotReloadUpdates(
            result.ModuleUpdates.Updates.FromContract(),
            result.GetAllDiagnostics().FromContract(),
            GetProjectPaths(result.ProjectsToRebuild),
            GetProjectPaths(result.ProjectsToRestart));
 
        ImmutableArray<string> GetProjectPaths(ImmutableArray<ProjectId> ids)
            => ids.SelectAsArray(static (id, solution) => solution.GetRequiredProject(id).FilePath!, solution);
    }
}