File: EditAndContinue\EditAndContinueLanguageService.cs
Web Access
Project: ..\..\..\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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Debugger.Contracts.EditAndContinue;
using Microsoft.VisualStudio.Debugger.Contracts.HotReload;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue
{
    [Shared]
    [Export(typeof(IManagedHotReloadLanguageService))]
    [Export(typeof(IEditAndContinueSolutionProvider))]
    [Export(typeof(EditAndContinueLanguageService))]
    [ExportMetadata("UIContext", EditAndContinueUIContext.EncCapableProjectExistsInWorkspaceUIContextString)]
    internal sealed class EditAndContinueLanguageService : IManagedHotReloadLanguageService, IEditAndContinueSolutionProvider
    {
        private readonly PdbMatchingSourceTextProvider _sourceTextProvider;
        private readonly Lazy<IManagedHotReloadService> _debuggerService;
        private readonly IDiagnosticAnalyzerService _diagnosticService;
        private readonly EditAndContinueDiagnosticUpdateSource _diagnosticUpdateSource;
 
        public readonly Lazy<IHostWorkspaceProvider> WorkspaceProvider;
 
        public bool IsSessionActive { get; private set; }
 
        private bool _disabled;
        private RemoteDebuggingSessionProxy? _debuggingSession;
 
        private Solution? _pendingUpdatedDesignTimeSolution;
        private Solution? _committedDesignTimeSolution;
 
        public event Action<Solution>? SolutionCommitted;
 
        /// <summary>
        /// Import <see cref="IHostWorkspaceProvider"/> lazily so that the host does not need to implement it 
        /// unless the host implements debugger components.
        /// </summary>
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public EditAndContinueLanguageService(
            Lazy<IHostWorkspaceProvider> workspaceProvider,
            Lazy<IManagedHotReloadService> debuggerService,
            IDiagnosticAnalyzerService diagnosticService,
            EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource,
            PdbMatchingSourceTextProvider sourceTextProvider)
        {
            WorkspaceProvider = workspaceProvider;
            _debuggerService = debuggerService;
            _diagnosticService = diagnosticService;
            _diagnosticUpdateSource = diagnosticUpdateSource;
            _sourceTextProvider = sourceTextProvider;
        }
 
        public void SetFileLoggingDirectory(string? logDirectory)
        {
            _ = Task.Run(async () =>
            {
                try
                {
                    var proxy = new RemoteEditAndContinueServiceProxy(WorkspaceProvider.Value.Workspace);
                    await proxy.SetFileLoggingDirectoryAsync(logDirectory, CancellationToken.None).ConfigureAwait(false);
                }
                catch
                {
                    // ignore
                }
            });
        }
 
        private Solution GetCurrentCompileTimeSolution(Solution? currentDesignTimeSolution = null)
        {
            var workspace = WorkspaceProvider.Value.Workspace;
            return workspace.Services.GetRequiredService<ICompileTimeSolutionProvider>().GetCompileTimeSolution(currentDesignTimeSolution ?? workspace.CurrentSolution);
        }
 
        private RemoteDebuggingSessionProxy GetDebuggingSession()
        {
            var debuggingSession = _debuggingSession;
            Contract.ThrowIfNull(debuggingSession);
            return debuggingSession;
        }
 
        private IActiveStatementTrackingService GetActiveStatementTrackingService()
            => WorkspaceProvider.Value.Workspace.Services.GetRequiredService<IActiveStatementTrackingService>();
 
        internal void Disable()
            => _disabled = true;
 
        /// <summary>
        /// Called by the debugger when a debugging session starts and managed debugging is being used.
        /// </summary>
        public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
        {
            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 workspace = WorkspaceProvider.Value.Workspace;
                var currentSolution = workspace.CurrentSolution;
                _committedDesignTimeSolution = currentSolution;
                var solution = GetCurrentCompileTimeSolution(currentSolution);
 
                _sourceTextProvider.SetBaseline(currentSolution);
 
                var proxy = new RemoteEditAndContinueServiceProxy(workspace);
 
                _debuggingSession = await proxy.StartDebuggingSessionAsync(
                    solution,
                    new ManagedHotReloadServiceImpl(_debuggerService.Value),
                    _sourceTextProvider,
                    captureMatchingDocuments: ImmutableArray<DocumentId>.Empty,
                    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
            _disabled = _debuggingSession == null;
        }
 
        public async ValueTask EnterBreakStateAsync(CancellationToken cancellationToken)
        {
            if (_disabled)
            {
                return;
            }
 
            var solution = GetCurrentCompileTimeSolution();
            var session = GetDebuggingSession();
 
            try
            {
                await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: true, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
                _disabled = true;
                return;
            }
 
            // 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.
 
            GetActiveStatementTrackingService().StartTracking(solution, session);
        }
 
        public async ValueTask ExitBreakStateAsync(CancellationToken cancellationToken)
        {
            if (_disabled)
            {
                return;
            }
 
            var session = GetDebuggingSession();
 
            try
            {
                await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: false, cancellationToken).ConfigureAwait(false);
                GetActiveStatementTrackingService().EndTracking();
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
                _disabled = true;
                return;
            }
        }
 
        public async ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken)
        {
            if (_disabled)
            {
                return;
            }
 
            try
            {
                await GetDebuggingSession().BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: null, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
                _disabled = true;
            }
        }
 
        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(_diagnosticService, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
            }
        }
 
        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 EndSessionAsync(CancellationToken cancellationToken)
        {
            IsSessionActive = false;
 
            if (!_disabled)
            {
                try
                {
                    var solution = GetCurrentCompileTimeSolution();
                    await GetDebuggingSession().EndDebuggingSessionAsync(solution, _diagnosticUpdateSource, _diagnosticService, cancellationToken).ConfigureAwait(false);
                }
                catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
                {
                    _disabled = true;
                }
            }
 
            _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 = WorkspaceProvider.Value.Workspace.CurrentSolution;
 
                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;
            }
        }
 
        public async ValueTask<ManagedHotReloadUpdates> GetUpdatesAsync(CancellationToken cancellationToken)
        {
            if (_disabled)
            {
                return new ManagedHotReloadUpdates(ImmutableArray<ManagedHotReloadUpdate>.Empty, ImmutableArray<ManagedHotReloadDiagnostic>.Empty);
            }
 
            var workspace = WorkspaceProvider.Value.Workspace;
            var designTimeSolution = workspace.CurrentSolution;
            var solution = GetCurrentCompileTimeSolution(designTimeSolution);
            var activeStatementSpanProvider = GetActiveStatementSpanProvider(solution);
            var (moduleUpdates, diagnosticData, rudeEdits, syntaxError) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, _diagnosticService, _diagnosticUpdateSource, cancellationToken).ConfigureAwait(false);
 
            // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called.
            if (moduleUpdates.Status == ModuleUpdateStatus.Ready)
            {
                _pendingUpdatedDesignTimeSolution = designTimeSolution;
            }
 
            var diagnostics = await EmitSolutionUpdateResults.GetHotReloadDiagnosticsAsync(solution, diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status, cancellationToken).ConfigureAwait(false);
            return new ManagedHotReloadUpdates(moduleUpdates.Updates.FromContract(), diagnostics.FromContract());
        }
 
        public async ValueTask<SourceSpan?> GetCurrentActiveStatementPositionAsync(ManagedInstructionId instruction, CancellationToken cancellationToken)
        {
            try
            {
                var solution = GetCurrentCompileTimeSolution();
                var activeStatementTrackingService = GetActiveStatementTrackingService();
 
                var activeStatementSpanProvider = new ActiveStatementSpanProvider((documentId, filePath, cancellationToken) =>
                    activeStatementTrackingService.GetSpansAsync(solution, documentId, filePath, cancellationToken));
 
                var span = await GetDebuggingSession().GetCurrentActiveStatementPositionAsync(solution, activeStatementSpanProvider, instruction.ToContract(), cancellationToken).ConfigureAwait(false);
                return span?.ToSourceSpan().FromContract();
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
                return null;
            }
        }
 
        public async ValueTask<bool?> IsActiveStatementInExceptionRegionAsync(ManagedInstructionId instruction, CancellationToken cancellationToken)
        {
            try
            {
                var solution = GetCurrentCompileTimeSolution();
                return await GetDebuggingSession().IsActiveStatementInExceptionRegionAsync(solution, instruction.ToContract(), cancellationToken).ConfigureAwait(false);
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
                return null;
            }
        }
    }
}