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

namespace Microsoft.CodeAnalysis.Editor.Options
{
15 16
    // 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.
17
    internal sealed partial class EditorConfigDocumentOptionsProvider : IDocumentOptionsProvider
18
    {
19 20
        private const int EventDelayInMillisecond = 50;

21 22 23
        private readonly object _gate = new object();

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

29
        private readonly Workspace _workspace;
30
        private readonly ICodingConventionsManager _codingConventionsManager;
31
        private readonly IErrorLoggerService _errorLogger;
32

33 34 35
        private ResettableDelay _resettableDelay;

        internal EditorConfigDocumentOptionsProvider(Workspace workspace, ICodingConventionsManager codingConventionsManager)
36
        {
37 38 39
            _workspace = workspace;

            _codingConventionsManager = codingConventionsManager;
40
            _errorLogger = workspace.Services.GetService<IErrorLoggerService>();
41

42 43
            _resettableDelay = ResettableDelay.CompletedDelay;

44 45 46 47 48 49 50 51
            workspace.DocumentOpened += Workspace_DocumentOpened;
            workspace.DocumentClosed += Workspace_DocumentClosed;
        }

        private void Workspace_DocumentClosed(object sender, DocumentEventArgs e)
        {
            lock (_gate)
            {
52
                if (_openDocumentContexts.TryGetValue(e.Document.Id, out var contextTask))
53 54 55 56
                {
                    _openDocumentContexts.Remove(e.Document.Id);

                    // Ensure we dispose the context, which we'll do asynchronously
57
                    contextTask.ContinueWith(
58 59 60 61 62 63 64
                        t =>
                        {
                            var context = t.Result;

                            context.CodingConventionsChangedAsync -= OnCodingConventionsChangedAsync;
                            context.Dispose();
                        },
65 66 67
                        CancellationToken.None,
                        TaskContinuationOptions.OnlyOnRanToCompletion,
                        TaskScheduler.Default);
68 69 70 71 72 73 74 75
                }
            }
        }

        private void Workspace_DocumentOpened(object sender, DocumentEventArgs e)
        {
            lock (_gate)
            {
76 77 78 79 80 81 82 83
                var contextTask = Task.Run(async () =>
                {
                    var context = await GetConventionContextAsync(e.Document.FilePath, CancellationToken.None).ConfigureAwait(false);
                    context.CodingConventionsChangedAsync += OnCodingConventionsChangedAsync;
                    return context;
                });

                _openDocumentContexts.Add(e.Document.Id, contextTask);
84 85 86
            }
        }

87
        public async Task<IDocumentOptions> GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken)
88 89 90 91 92
        {
            Task<ICodingConventionContext> contextTask;

            lock (_gate)
            {
93
                _openDocumentContexts.TryGetValue(document.Id, out contextTask);
94 95
            }

96 97
            if (contextTask != null)
            {
98 99 100 101 102 103 104 105 106 107
                // 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);
108
                return new DocumentOptions(context.CurrentConventions, _errorLogger);
109 110 111
            }
            else
            {
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
                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
129 130
                // 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.
131
                var conventionsAsync = Task.Run(() => GetConventionContextAsync(path, cancellationToken));
132

133
                using (var context = await conventionsAsync.ConfigureAwait(false))
134
                {
135
                    return new DocumentOptions(context.CurrentConventions, _errorLogger);
136
                }
137
            }
138 139
        }

140
        private Task<ICodingConventionContext> GetConventionContextAsync(string path, CancellationToken cancellationToken)
141
        {
142 143 144
            return IOUtilities.PerformIOAsync(
                () => _codingConventionsManager.GetConventionContextAsync(path, cancellationToken),
                defaultValue: EmptyCodingConventionContext.Instance);
145
        }
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179

        private Task OnCodingConventionsChangedAsync(object sender, CodingConventionsChangedEventArgs arg)
        {
            // this is a temporary workaround. once we finish the work to put editorconfig file as a part of roslyn solution snapshot,
            // that system will automatically pick up option changes and update snapshot. and it will work regardless
            // whether a file is opened in editor or not.
            // 
            // but until then, we need to explicitly touch workspace to update snapshot. and 
            // only works for open files. it is not easy to track option changes for closed files with current model.
            // related tracking issue - https://github.com/dotnet/roslyn/issues/26250

            lock (_gate)
            {
                if (!_resettableDelay.Task.IsCompleted)
                {
                    _resettableDelay.Reset();
                }
                else
                {
                    // since this event gets raised for all documents that are affected by 1 editconfig file,
                    // and since for now we make that event as whole solution changed event, we don't need to update
                    // snapshot for each events. aggregate all events to 1.
                    var delay = new ResettableDelay(EventDelayInMillisecond);
                    delay.Task.ContinueWith(_ => _workspace.OnOptionChanged(),
                        CancellationToken.None,
                        TaskContinuationOptions.ExecuteSynchronously,
                        TaskScheduler.Default);

                    _resettableDelay = delay;
                }
            }

            return Task.CompletedTask;
        }
180 181
    }
}