diff --git a/src/Workspaces/Core/Portable/Options/DocumentOptionSet.cs b/src/Workspaces/Core/Portable/Options/DocumentOptionSet.cs index 1dba460027b2a..40bdd999145d7 100644 --- a/src/Workspaces/Core/Portable/Options/DocumentOptionSet.cs +++ b/src/Workspaces/Core/Portable/Options/DocumentOptionSet.cs @@ -2,9 +2,14 @@ // 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.Generic; +using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ErrorReporting; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Options { @@ -15,28 +20,79 @@ namespace Microsoft.CodeAnalysis.Options /// public sealed class DocumentOptionSet : OptionSet { - private readonly OptionSet _backingOptionSet; + private readonly OptionSet _underlyingOptions; + private readonly StructuredAnalyzerConfigOptions? _configOptions; + private ImmutableDictionary _values; private readonly string _language; - internal DocumentOptionSet(OptionSet backingOptionSet, string language) + internal DocumentOptionSet(StructuredAnalyzerConfigOptions? configOptions, OptionSet underlyingOptions, string language) + : this(configOptions, underlyingOptions, language, ImmutableDictionary.Empty) + { + } + + internal DocumentOptionSet(StructuredAnalyzerConfigOptions? configOptions, OptionSet underlyingOptions, string language, ImmutableDictionary values) { - _backingOptionSet = backingOptionSet; _language = language; + _configOptions = configOptions; + _underlyingOptions = underlyingOptions; + _values = values; } internal string Language => _language; + [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/30819", AllowLocks = false)] private protected override object? GetOptionCore(OptionKey optionKey) - => _backingOptionSet.GetOption(optionKey); + { + // If we already know the document specific value, we're done + if (_values.TryGetValue(optionKey, out var value)) + { + return value; + } + + if (TryGetAnalyzerConfigOption(optionKey, out value)) + { + // Cache and return + return ImmutableInterlocked.GetOrAdd(ref _values, optionKey, value); + } + + // We don't have a document specific value, so forward + return _underlyingOptions.GetOption(optionKey); + } + + private bool TryGetAnalyzerConfigOption(OptionKey option, out object? value) + { + if (_configOptions == null) + { + value = null; + return false; + } + + var editorConfigPersistence = (IEditorConfigStorageLocation?)option.Option.StorageLocations.SingleOrDefault(static location => location is IEditorConfigStorageLocation); + if (editorConfigPersistence == null) + { + value = null; + return false; + } + + try + { + return editorConfigPersistence.TryGetOption(_configOptions, option.Option.Type, out value); + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + value = null; + return false; + } + } public T GetOption(PerLanguageOption option) - => _backingOptionSet.GetOption(option, _language); + => GetOption(option, _language); internal T GetOption(PerLanguageOption2 option) - => _backingOptionSet.GetOption(option, _language); + => GetOption(option, _language); public override OptionSet WithChangedOption(OptionKey optionAndLanguage, object? value) - => new DocumentOptionSet(_backingOptionSet.WithChangedOption(optionAndLanguage, value), _language); + => new DocumentOptionSet(_configOptions, _underlyingOptions, _language, _values.SetItem(optionAndLanguage, value)); /// /// Creates a new that contains the changed value. @@ -53,10 +109,10 @@ internal DocumentOptionSet WithChangedOption(PerLanguageOption2 option, T private protected override AnalyzerConfigOptions CreateAnalyzerConfigOptions(IOptionService optionService, string? language) { Debug.Assert((language ?? _language) == _language, $"Use of a {nameof(DocumentOptionSet)} is not expected to differ from the language it was constructed with."); - return _backingOptionSet.AsAnalyzerConfigOptions(optionService, language ?? _language); + return base.CreateAnalyzerConfigOptions(optionService, language ?? _language); } internal override IEnumerable GetChangedOptions(OptionSet optionSet) - => _backingOptionSet.GetChangedOptions(optionSet); + => GetChangedOptions(optionSet); } } diff --git a/src/Workspaces/Core/Portable/Options/IOptionService.cs b/src/Workspaces/Core/Portable/Options/IOptionService.cs index be6537578d4c6..c3ac1c6ba1445 100644 --- a/src/Workspaces/Core/Portable/Options/IOptionService.cs +++ b/src/Workspaces/Core/Portable/Options/IOptionService.cs @@ -77,11 +77,6 @@ internal interface IOptionService : IWorkspaceService event EventHandler OptionChanged; - /// - /// Returns the that applies to a specific document, given that document and the global options. - /// - Task GetUpdatedOptionSetForDocumentAsync(Document document, OptionSet optionSet, CancellationToken cancellationToken); - /// /// Registers a workspace with the option service. /// diff --git a/src/Workspaces/Core/Portable/Options/OptionServiceFactory.cs b/src/Workspaces/Core/Portable/Options/OptionServiceFactory.cs index fe3b69e202cb7..8b93b77a5122c 100644 --- a/src/Workspaces/Core/Portable/Options/OptionServiceFactory.cs +++ b/src/Workspaces/Core/Portable/Options/OptionServiceFactory.cs @@ -126,87 +126,6 @@ public event EventHandler OptionChanged public void SetOptions(OptionSet optionSet) => _globalOptionService.SetOptions(optionSet); public void RegisterWorkspace(Workspace workspace) => _globalOptionService.RegisterWorkspace(workspace); public void UnregisterWorkspace(Workspace workspace) => _globalOptionService.UnregisterWorkspace(workspace); - - public async Task GetUpdatedOptionSetForDocumentAsync(Document document, OptionSet optionSet, CancellationToken cancellationToken) - { - var provider = (ProjectState.ProjectAnalyzerConfigOptionsProvider)document.Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider; - var options = await provider.GetOptionsAsync(document.DocumentState, cancellationToken).ConfigureAwait(false); - return new DocumentSpecificOptionSet(options, optionSet); - } - - private sealed class DocumentSpecificOptionSet : OptionSet - { - private readonly OptionSet _underlyingOptions; - private readonly StructuredAnalyzerConfigOptions? _configOptions; - private ImmutableDictionary _values; - - public DocumentSpecificOptionSet(StructuredAnalyzerConfigOptions? configOptions, OptionSet underlyingOptions) - : this(configOptions, underlyingOptions, ImmutableDictionary.Empty) - { - } - - public DocumentSpecificOptionSet(StructuredAnalyzerConfigOptions? configOptions, OptionSet underlyingOptions, ImmutableDictionary values) - { - _configOptions = configOptions; - _underlyingOptions = underlyingOptions; - _values = values; - } - - [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/30819", AllowLocks = false)] - private protected override object? GetOptionCore(OptionKey optionKey) - { - // If we already know the document specific value, we're done - if (_values.TryGetValue(optionKey, out var value)) - { - return value; - } - - if (TryGetAnalyzerConfigOption(optionKey, out value)) - { - // Cache and return - return ImmutableInterlocked.GetOrAdd(ref _values, optionKey, value); - } - - // We don't have a document specific value, so forward - return _underlyingOptions.GetOption(optionKey); - } - - private bool TryGetAnalyzerConfigOption(OptionKey option, out object? value) - { - if (_configOptions == null) - { - value = null; - return false; - } - - var editorConfigPersistence = (IEditorConfigStorageLocation?)option.Option.StorageLocations.SingleOrDefault(static location => location is IEditorConfigStorageLocation); - if (editorConfigPersistence == null) - { - value = null; - return false; - } - - try - { - return editorConfigPersistence.TryGetOption(_configOptions, option.Option.Type, out value); - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - value = null; - return false; - } - } - - public override OptionSet WithChangedOption(OptionKey optionAndLanguage, object? value) - => new DocumentSpecificOptionSet(_configOptions, _underlyingOptions, _values.SetItem(optionAndLanguage, value)); - - internal override IEnumerable GetChangedOptions(OptionSet optionSet) - { - // GetChangedOptions only needs to be supported for OptionSets that need to be compared during application, - // but that's already enforced it must be a full SerializableOptionSet. - throw new NotSupportedException(); - } - } } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs index 27f7c17b2f5fc..a74f2087c6dd1 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -505,9 +505,10 @@ private void InitializeCachedOptions(OptionSet solutionOptions) { var newAsyncLazy = new AsyncLazy(async c => { - var optionsService = Project.Solution.Workspace.Services.GetRequiredService(); - var documentOptionSet = await optionsService.GetUpdatedOptionSetForDocumentAsync(this, solutionOptions, c).ConfigureAwait(false); - return new DocumentOptionSet(documentOptionSet, Project.Language); + var provider = (ProjectState.ProjectAnalyzerConfigOptionsProvider)Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider; + var options = await provider.GetOptionsAsync(DocumentState, c).ConfigureAwait(false); + + return new DocumentOptionSet(options, solutionOptions, Project.Language); }, cacheResult: true); Interlocked.CompareExchange(ref _cachedOptions, newAsyncLazy, comparand: null);