EditorConfigDocumentOptionsProvider.cs 5.7 KB
Newer Older
1 2 3 4
// 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;
using System.Collections.Generic;
5
using System.IO;
6 7
using System.Threading;
using System.Threading.Tasks;
8 9
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.ErrorLogger;
10
using Microsoft.CodeAnalysis.Options;
11
using Microsoft.CodeAnalysis.Shared.Utilities;
12 13 14 15
using Microsoft.VisualStudio.CodingConventions;

namespace Microsoft.CodeAnalysis.Editor.Options
{
16 17
    // NOTE: this type depends Microsoft.VisualStudio.CodingConventions, so for now it's living in EditorFeatures.Wpf as that assembly
    // isn't yet available outside of Visual Studio.
18
    internal sealed partial class EditorConfigDocumentOptionsProvider : IDocumentOptionsProvider
19 20 21 22
    {
        private readonly object _gate = new object();

        /// <summary>
23 24
        /// The map of cached contexts for currently open documents. Should only be accessed if holding a monitor lock
        /// on <see cref="_gate"/>.
25 26 27 28
        /// </summary>
        private readonly Dictionary<DocumentId, Task<ICodingConventionContext>> _openDocumentContexts = new Dictionary<DocumentId, Task<ICodingConventionContext>>();

        private readonly ICodingConventionsManager _codingConventionsManager;
29
        private readonly IErrorLoggerService _errorLogger;
30 31 32 33

        internal EditorConfigDocumentOptionsProvider(Workspace workspace)
        {
            _codingConventionsManager = CodingConventionsManagerFactory.CreateCodingConventionsManager();
34
            _errorLogger = workspace.Services.GetService<IErrorLoggerService>();
35 36 37 38 39 40 41 42 43

            workspace.DocumentOpened += Workspace_DocumentOpened;
            workspace.DocumentClosed += Workspace_DocumentClosed;
        }

        private void Workspace_DocumentClosed(object sender, DocumentEventArgs e)
        {
            lock (_gate)
            {
44
                if (_openDocumentContexts.TryGetValue(e.Document.Id, out var contextTask))
45 46 47 48
                {
                    _openDocumentContexts.Remove(e.Document.Id);

                    // Ensure we dispose the context, which we'll do asynchronously
49 50 51 52 53
                    contextTask.ContinueWith(
                        t => t.Result.Dispose(),
                        CancellationToken.None,
                        TaskContinuationOptions.OnlyOnRanToCompletion,
                        TaskScheduler.Default);
54 55 56 57 58 59 60 61
                }
            }
        }

        private void Workspace_DocumentOpened(object sender, DocumentEventArgs e)
        {
            lock (_gate)
            {
62
                _openDocumentContexts.Add(e.Document.Id, Task.Run(() => GetConventionContextAsync(e.Document.FilePath, CancellationToken.None)));
63 64 65
            }
        }

66
        public async Task<IDocumentOptions> GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken)
67 68 69 70 71
        {
            Task<ICodingConventionContext> contextTask;

            lock (_gate)
            {
72
                _openDocumentContexts.TryGetValue(document.Id, out contextTask);
73 74
            }

75 76
            if (contextTask != null)
            {
77 78 79 80 81 82 83 84 85 86
                // 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);
87
                return new DocumentOptions(context.CurrentConventions, _errorLogger);
88 89 90
            }
            else
            {
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
                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
108 109
                // 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.
110
                var conventionsAsync = Task.Run(() => GetConventionContextAsync(path, cancellationToken));
111

112
                using (var context = await conventionsAsync.ConfigureAwait(false))
113
                {
114
                    return new DocumentOptions(context.CurrentConventions, _errorLogger);
115
                }
116
            }
117 118
        }

119
        private Task<ICodingConventionContext> GetConventionContextAsync(string path, CancellationToken cancellationToken)
120
        {
121 122 123
            return IOUtilities.PerformIOAsync(
                () => _codingConventionsManager.GetConventionContextAsync(path, cancellationToken),
                defaultValue: EmptyCodingConventionContext.Instance);
124 125 126
        }
    }
}