// 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.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorLogger; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.VisualStudio.CodingConventions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Options { // This class is currently linked into both EditorFeatures.Wpf (VS in-process) and RemoteWorkspaces (Roslyn out-of-process). internal sealed partial class LegacyEditorConfigDocumentOptionsProvider : IDocumentOptionsProvider { private readonly object _gate = new object(); /// /// The map of cached contexts for currently open documents. Should only be accessed if holding a monitor lock /// on . /// private readonly Dictionary> _openDocumentContexts = new Dictionary>(); private readonly Workspace _workspace; private readonly IAsynchronousOperationListener _listener; private readonly ICodingConventionsManager _codingConventionsManager; private readonly IErrorLoggerService _errorLogger; internal LegacyEditorConfigDocumentOptionsProvider(Workspace workspace, ICodingConventionsManager codingConventionsManager, IAsynchronousOperationListenerProvider listenerProvider) { _workspace = workspace; _listener = listenerProvider.GetListener(FeatureAttribute.Workspace); _codingConventionsManager = codingConventionsManager; _errorLogger = workspace.Services.GetService(); workspace.DocumentOpened += Workspace_DocumentOpened; workspace.DocumentClosed += Workspace_DocumentClosed; // workaround until this is fixed. // https://github.com/dotnet/roslyn/issues/26377 // otherwise, we will leak files in _openDocumentContexts workspace.WorkspaceChanged += Workspace_WorkspaceChanged; } /// /// This partial method allows implementations of (which are /// linked into both the in-process and out-of-process implementations as source files) to handle the creation /// of in different ways. /// partial void OnCodingConventionContextCreated(DocumentId documentId, ICodingConventionContext context); private void Workspace_DocumentClosed(object sender, DocumentEventArgs e) { lock (_gate) { if (_openDocumentContexts.TryGetValue(e.Document.Id, out var contextTask)) { _openDocumentContexts.Remove(e.Document.Id); ReleaseContext_NoLock(contextTask); } } } private void Workspace_DocumentOpened(object sender, DocumentEventArgs e) { lock (_gate) { var documentId = e.Document.Id; var filePath = e.Document.FilePath; _openDocumentContexts.Add(documentId, Task.Run(async () => { var context = await GetConventionContextAsync(filePath, CancellationToken.None).ConfigureAwait(false); OnCodingConventionContextCreated(documentId, context); return context; })); } } private void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { switch (e.Kind) { case WorkspaceChangeKind.SolutionRemoved: case WorkspaceChangeKind.SolutionCleared: ClearOpenFileCache(); break; case WorkspaceChangeKind.ProjectRemoved: ClearOpenFileCache(e.ProjectId); break; default: break; } } private void ClearOpenFileCache(ProjectId projectId = null) { lock (_gate) { var itemsToRemove = new List(); foreach (var (documentId, contextTask) in _openDocumentContexts) { if (projectId is null || documentId.ProjectId == projectId) { itemsToRemove.Add(documentId); ReleaseContext_NoLock(contextTask); } } // If any items were released due to the Clear operation, we need to remove them from the map. foreach (var documentId in itemsToRemove) { _openDocumentContexts.Remove(documentId); } } } private void ReleaseContext_NoLock(Task contextTask) { Debug.Assert(Monitor.IsEntered(_gate)); // Ensure we dispose the context, which we'll do asynchronously contextTask.ContinueWith( t => t.Result.Dispose(), CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); } public async Task GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken) { Task contextTask; lock (_gate) { _openDocumentContexts.TryGetValue(document.Id, out contextTask); } if (contextTask != null) { // The file is open, let's reuse our cached data for that file. That task might be running, but we don't want to await // it as awaiting it wouldn't respect the cancellation of our caller. By creating a trivial continuation like this // that uses eager cancellation, if the cancellationToken is cancelled our await will end early. var cancellableContextTask = contextTask.ContinueWith( t => t.Result, cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); var context = await cancellableContextTask.ConfigureAwait(false); return new DocumentOptions(context.CurrentConventions, _errorLogger); } else { var path = document.FilePath; // The file might not actually have a path yet, if it's a file being proposed by a code action. We'll guess a file path to use if (path == null) { if (document.Name != null && document.Project.FilePath != null) { path = Path.Combine(Path.GetDirectoryName(document.Project.FilePath), document.Name); } else { // Really no idea where this is going, so bail return null; } } // We don't have anything cached, so we'll just get it now lazily and not hold onto it. The workspace layer will ensure // that we maintain snapshot rules for the document options. We'll also run it on the thread pool // as in some builds the ICodingConventionsManager captures the thread pool. var conventionsAsync = Task.Run(() => GetConventionContextAsync(path, cancellationToken)); using var context = await conventionsAsync.ConfigureAwait(false); return new DocumentOptions(context.CurrentConventions, _errorLogger); } } private Task GetConventionContextAsync(string path, CancellationToken cancellationToken) { return IOUtilities.PerformIOAsync( () => _codingConventionsManager.GetConventionContextAsync(path, cancellationToken), defaultValue: EmptyCodingConventionContext.Instance); } } }