diff --git a/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProvider.cs b/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProvider.cs index 00b10c770e5de1a3cd13c0e9b75232186e61b40a..ee13dcf005fa7606523a70640d9dd432a9c90582 100644 --- a/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProvider.cs +++ b/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProvider.cs @@ -1,11 +1,10 @@ // 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; using System.IO; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.ErrorLogger; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Utilities; @@ -17,6 +16,8 @@ namespace Microsoft.CodeAnalysis.Editor.Options // isn't yet available outside of Visual Studio. internal sealed partial class EditorConfigDocumentOptionsProvider : IDocumentOptionsProvider { + private const int EventDelayInMillisecond = 50; + private readonly object _gate = new object(); /// @@ -25,14 +26,21 @@ internal sealed partial class EditorConfigDocumentOptionsProvider : IDocumentOpt /// private readonly Dictionary> _openDocumentContexts = new Dictionary>(); + private readonly Workspace _workspace; private readonly ICodingConventionsManager _codingConventionsManager; private readonly IErrorLoggerService _errorLogger; - internal EditorConfigDocumentOptionsProvider(Workspace workspace) + private ResettableDelay _resettableDelay; + + internal EditorConfigDocumentOptionsProvider(Workspace workspace, ICodingConventionsManager codingConventionsManager) { - _codingConventionsManager = CodingConventionsManagerFactory.CreateCodingConventionsManager(); + _workspace = workspace; + + _codingConventionsManager = codingConventionsManager; _errorLogger = workspace.Services.GetService(); + _resettableDelay = ResettableDelay.CompletedDelay; + workspace.DocumentOpened += Workspace_DocumentOpened; workspace.DocumentClosed += Workspace_DocumentClosed; } @@ -47,7 +55,13 @@ private void Workspace_DocumentClosed(object sender, DocumentEventArgs e) // Ensure we dispose the context, which we'll do asynchronously contextTask.ContinueWith( - t => t.Result.Dispose(), + t => + { + var context = t.Result; + + context.CodingConventionsChangedAsync -= OnCodingConventionsChangedAsync; + context.Dispose(); + }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); @@ -59,7 +73,14 @@ private void Workspace_DocumentOpened(object sender, DocumentEventArgs e) { lock (_gate) { - _openDocumentContexts.Add(e.Document.Id, Task.Run(() => GetConventionContextAsync(e.Document.FilePath, CancellationToken.None))); + 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); } } @@ -122,5 +143,39 @@ private Task GetConventionContextAsync(string path, Ca () => _codingConventionsManager.GetConventionContextAsync(path, cancellationToken), defaultValue: EmptyCodingConventionContext.Instance); } + + 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; + } } } diff --git a/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProviderFactory.cs b/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProviderFactory.cs index 32b650532d41469fa28fe94e97883e076c2bc012..7ae91f27def9371508495ead6c658a520e98ea79 100644 --- a/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProviderFactory.cs +++ b/src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProviderFactory.cs @@ -3,15 +3,25 @@ using System; using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Options; +using Microsoft.VisualStudio.CodingConventions; namespace Microsoft.CodeAnalysis.Editor.Options { [Export(typeof(IDocumentOptionsProviderFactory))] class EditorConfigDocumentOptionsProviderFactory : IDocumentOptionsProviderFactory { + private readonly ICodingConventionsManager _codingConventionsManager; + + [ImportingConstructor] + [Obsolete("Never call this directly")] + public EditorConfigDocumentOptionsProviderFactory(ICodingConventionsManager codingConventionsManager) + { + _codingConventionsManager = codingConventionsManager; + } + public IDocumentOptionsProvider Create(Workspace workspace) { - return new EditorConfigDocumentOptionsProvider(workspace); + return new EditorConfigDocumentOptionsProvider(workspace, _codingConventionsManager); } } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs b/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs index 05133be012a37b4247c2564f5c2d5c5df7067101..54acf0e2b994435f5102aade51a2bd5e597e4847 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs @@ -9,6 +9,8 @@ namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities { internal class ResettableDelay { + public static readonly ResettableDelay CompletedDelay = new ResettableDelay(); + private readonly int _delayInMilliseconds; private readonly TaskCompletionSource _taskCompletionSource; @@ -39,6 +41,16 @@ public ResettableDelay(int delayInMilliseconds, TaskScheduler foregroundTaskSche } } + private ResettableDelay() + { + // create resettableDelay with completed state + _delayInMilliseconds = 0; + _taskCompletionSource = new TaskCompletionSource(); + _taskCompletionSource.SetResult(null); + + Reset(); + } + public Task Task => _taskCompletionSource.Task; public void Reset() diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index fe5782103d93b5f69c95acc48f88c864cc61e8d9..ec7feead4ee221ad1d9c8481ad9abdb33a62b3ad 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -1051,5 +1051,18 @@ public OptionSet Options return this.Workspace.Options; } } + + /// + /// Update current solution as a result of option changes. + /// + /// this is a temporary workaround until editorconfig becomes real part of roslyn solution snapshot. + /// until then, this will explicitly fork current solution snapshot + /// + internal Solution WithOptionChanged() + { + // options are associated with solution snapshot. creating new snapshot + // will cause us to retrieve new options + return new Solution(_state); + } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Workspace_Editor.cs b/src/Workspaces/Core/Portable/Workspace/Workspace_Editor.cs index 89e8fce54617b225d36f16f266b590b0609b76f5..c3efb3e282c551b6c7264e821741125a07263a1e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Workspace_Editor.cs +++ b/src/Workspaces/Core/Portable/Workspace/Workspace_Editor.cs @@ -725,5 +725,27 @@ protected virtual Project AdjustReloadedProject(Project oldProject, Project relo return newSolution.GetProject(oldProject.Id); } + + /// + /// Update current solution as a result of option changes. + /// + /// this is a temporary workaround until editorconfig becomes real part of roslyn solution snapshot. + /// until then, this will explicitly move current solution forward when such event happened + /// + internal void OnOptionChanged() + { + using (_serializationLock.DisposableWait()) + { + var oldSolution = this.CurrentSolution; + var newSolution = this.SetCurrentSolution(oldSolution.WithOptionChanged()); + + // for now, this says whole solution is changed. + // in future, we probably going to just raise additional file changed event (for editconfig file) and then + // let IOptionService.OptionChanged event to raise what option has changed. + // currently, since editorconfig file is not part of solution, we can't say which file is changed. + // 1 editorconfig file can affect multiple files in multiple projects so solution changed is easiest options for now. + this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionChanged, oldSolution, newSolution); + } + } } }