diff --git a/src/EditorFeatures/Test/TestOptionsServiceFactory.cs b/src/EditorFeatures/Test/TestOptionsServiceFactory.cs index f90691fdadc4a1476efc28f3b5a3bf8496ec1f4d..e75b7e9544506aa9bf1e0bc66ba0e298a57cabad 100644 --- a/src/EditorFeatures/Test/TestOptionsServiceFactory.cs +++ b/src/EditorFeatures/Test/TestOptionsServiceFactory.cs @@ -29,7 +29,8 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) { // give out new option service per workspace return new OptionServiceFactory.OptionService( - workspaceServices, _providers, SpecializedCollections.EmptyEnumerable>()); + new GlobalOptionService(_providers, SpecializedCollections.EmptyEnumerable>()), + workspaceServices); } } } \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Options/GlobalOptionService.cs b/src/Workspaces/Core/Portable/Options/GlobalOptionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..a8e39da1e9775d29c62b7ac0151fb8cd7df62c7a --- /dev/null +++ b/src/Workspaces/Core/Portable/Options/GlobalOptionService.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis.Options.Providers; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Options +{ + [Export(typeof(IGlobalOptionService)), Shared] + internal class GlobalOptionService : IGlobalOptionService + { + private readonly Lazy> _options; + private readonly ImmutableDictionary>> _featureNameToOptionSerializers = + ImmutableDictionary.Create>>(); + + private readonly object _gate = new object(); + + private ImmutableDictionary _currentValues; + + [ImportingConstructor] + public GlobalOptionService( + IEnumerable> optionProviders, + IEnumerable> optionSerializers) + { + _options = new Lazy>(() => + { + var options = new HashSet(); + + foreach (var provider in optionProviders) + { + options.AddRange(provider.Value.GetOptions()); + } + + return options; + }); + + foreach (var optionSerializerAndMetadata in optionSerializers) + { + foreach (var featureName in optionSerializerAndMetadata.Metadata.Features) + { + ImmutableArray> existingSerializers; + if (!_featureNameToOptionSerializers.TryGetValue(featureName, out existingSerializers)) + { + existingSerializers = ImmutableArray.Create>(); + } + + _featureNameToOptionSerializers = _featureNameToOptionSerializers.SetItem(featureName, existingSerializers.Add(optionSerializerAndMetadata)); + } + } + + _currentValues = ImmutableDictionary.Create(); + } + + private object LoadOptionFromSerializerOrGetDefault(OptionKey optionKey) + { + lock (_gate) + { + ImmutableArray> optionSerializers; + if (_featureNameToOptionSerializers.TryGetValue(optionKey.Option.Feature, out optionSerializers)) + { + foreach (var serializer in optionSerializers) + { + // There can be options (ex, formatting) that only exist in only one specific language. In those cases, + // feature's serializer should exist in only that language. + if (!SupportedSerializer(optionKey, serializer.Metadata)) + { + continue; + } + + // We have a deserializer, so deserialize and use that value. + object deserializedValue; + if (serializer.Value.TryFetch(optionKey, out deserializedValue)) + { + return deserializedValue; + } + } + } + + // Just use the default. We will still cache this so we aren't trying to deserialize + // over and over. + return optionKey.Option.DefaultValue; + } + } + + public IEnumerable GetRegisteredOptions() + { + return _options.Value; + } + + public T GetOption(Option option) + { + return (T)GetOption(new OptionKey(option, language: null)); + } + + public T GetOption(PerLanguageOption option, string language) + { + return (T)GetOption(new OptionKey(option, language)); + } + + public object GetOption(OptionKey optionKey) + { + lock (_gate) + { + object value; + + if (_currentValues.TryGetValue(optionKey, out value)) + { + return value; + } + + value = LoadOptionFromSerializerOrGetDefault(optionKey); + + _currentValues = _currentValues.Add(optionKey, value); + + return value; + } + } + + public void SetOptions(OptionSet optionSet) + { + if (optionSet == null) + { + throw new ArgumentNullException(nameof(optionSet)); + } + + var workspaceOptionSet = optionSet as WorkspaceOptionSet; + + if (workspaceOptionSet == null) + { + throw new ArgumentException(WorkspacesResources.OptionsDidNotComeFromWorkspace, paramName: nameof(optionSet)); + } + + var changedOptions = new List(); + + lock (_gate) + { + foreach (var optionKey in workspaceOptionSet.GetAccessedOptions()) + { + var setValue = optionSet.GetOption(optionKey); + object currentValue = this.GetOption(optionKey); + + if (object.Equals(currentValue, setValue)) + { + // Identical, so nothing is changing + continue; + } + + // The value is actually changing, so update + changedOptions.Add(new OptionChangedEventArgs(optionKey, setValue)); + + _currentValues = _currentValues.SetItem(optionKey, setValue); + + ImmutableArray> optionSerializers; + if (_featureNameToOptionSerializers.TryGetValue(optionKey.Option.Feature, out optionSerializers)) + { + foreach (var serializer in optionSerializers) + { + // There can be options (ex, formatting) that only exist in only one specific language. In those cases, + // feature's serializer should exist in only that language. + if (!SupportedSerializer(optionKey, serializer.Metadata)) + { + continue; + } + + if (serializer.Value.TryPersist(optionKey, setValue)) + { + break; + } + } + } + } + } + + // Outside of the lock, raise the events on our task queue. + RaiseEvents(changedOptions); + } + + private void RaiseEvents(List changedOptions) + { + var optionChanged = OptionChanged; + if (optionChanged != null) + { + foreach (var changedOption in changedOptions) + { + optionChanged(this, changedOption); + } + } + } + + private static bool SupportedSerializer(OptionKey optionKey, OptionSerializerMetadata metadata) + { + return optionKey.Language == null || optionKey.Language == metadata.Language; + } + + public event EventHandler OptionChanged; + } +} \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Options/IGlobalOptionService.cs b/src/Workspaces/Core/Portable/Options/IGlobalOptionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..66e4ff5aaef7e07f249d8c8723daa44d21abb8c5 --- /dev/null +++ b/src/Workspaces/Core/Portable/Options/IGlobalOptionService.cs @@ -0,0 +1,42 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Options +{ + /// + /// Provides services for reading and writing options. + /// This will provide support for options at the global level (i.e. shared among + /// all workspaces/services). + /// + internal interface IGlobalOptionService + { + /// + /// Gets the current value of the specific option. + /// + T GetOption(Option option); + + /// + /// Gets the current value of the specific option. + /// + T GetOption(PerLanguageOption option, string languageName); + + /// + /// Gets the current value of the specific option. + /// + object GetOption(OptionKey optionKey); + + /// + /// Applies a set of options. + /// + void SetOptions(OptionSet optionSet); + + /// + /// Returns the set of all registered options. + /// + IEnumerable GetRegisteredOptions(); + + event EventHandler OptionChanged; + } +} \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Options/IOptionService.cs b/src/Workspaces/Core/Portable/Options/IOptionService.cs index 1e3e11441991d19ac0a90f1470fed9e6bd9a032a..30ce0e98f4737463add4b32f253b683e5bbe4961 100644 --- a/src/Workspaces/Core/Portable/Options/IOptionService.cs +++ b/src/Workspaces/Core/Portable/Options/IOptionService.cs @@ -7,7 +7,10 @@ namespace Microsoft.CodeAnalysis.Options { /// - /// Provides services for reading and writing options. + /// Provides services for reading and writing options. This will provide support for + /// customizations workspaces need to perform around options. Note that global options + /// will normally still be offered through implementations of this. However, implementations + /// may customize things differently depending on their needs. /// internal interface IOptionService : IWorkspaceService { @@ -43,4 +46,4 @@ internal interface IOptionService : IWorkspaceService event EventHandler OptionChanged; } -} +} \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Options/IWorkspaceOptionService.cs b/src/Workspaces/Core/Portable/Options/IWorkspaceOptionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..13fd1404871f5c7a8146b6e0ca0607131fcf2e28 --- /dev/null +++ b/src/Workspaces/Core/Portable/Options/IWorkspaceOptionService.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Options +{ + /// + /// Interface used for exposing functionality from the option service that we don't want to + /// ever be public. + /// + internal interface IWorkspaceOptionService : IOptionService + { + void OnWorkspaceDisposed(Workspace workspace); + } +} \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Options/OptionService.cs b/src/Workspaces/Core/Portable/Options/OptionService.cs index 6dd1f3f0b2d3067616c4b13834ce00c5d9e8a531..f195f5bc52e9656563dfbf63080e718d88298cf3 100644 --- a/src/Workspaces/Core/Portable/Options/OptionService.cs +++ b/src/Workspaces/Core/Portable/Options/OptionService.cs @@ -6,234 +6,116 @@ using System.Composition; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Options.Providers; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Options { [ExportWorkspaceServiceFactory(typeof(IOptionService)), Shared] internal class OptionServiceFactory : IWorkspaceServiceFactory { - private readonly IEnumerable> _optionProviders; - private readonly IEnumerable> _optionSerializers; + private readonly IGlobalOptionService _globalOptionService; [ImportingConstructor] - public OptionServiceFactory( - [ImportMany] IEnumerable> optionProviders, - [ImportMany] IEnumerable> optionSerializers) + public OptionServiceFactory(IGlobalOptionService globalOptionService) { - _optionProviders = optionProviders; - _optionSerializers = optionSerializers; + _globalOptionService = globalOptionService; } public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) { - return new OptionService(workspaceServices, _optionProviders, _optionSerializers); + return new OptionService(_globalOptionService, workspaceServices); } - // Internal for testing purposes only. - internal class OptionService : IOptionService + /// + /// Wraps an underlying and exposes its data to workspace + /// clients. Also takes the notifications + /// and forwards them along using the same used by the + /// this is connected to. i.e. instead of synchronously just passing + /// along the underlying events, these will be enqueued onto the workspaces eventing queue. + /// + // Internal for testing purposes. + internal class OptionService : IWorkspaceOptionService { - private readonly Lazy> _options; - private readonly ImmutableDictionary>> _featureNameToOptionSerializers = - ImmutableDictionary.Create>>(); + private readonly IGlobalOptionService _globalOptionService; - private readonly object _gate = new object(); - - /// - /// Can be null during tests. - /// + // Can be null during testing. private readonly IWorkspaceTaskScheduler _taskQueue; - private ImmutableDictionary _currentValues; + private readonly object _gate = new object(); + private ImmutableArray> _eventHandlers = + ImmutableArray>.Empty; public OptionService( - HostWorkspaceServices workspaceServices, - IEnumerable> optionProviders, - IEnumerable> optionSerializers) + IGlobalOptionService globalOptionService, + HostWorkspaceServices workspaceServices) { + _globalOptionService = globalOptionService; + var workspaceTaskSchedulerFactory = workspaceServices?.GetRequiredService(); _taskQueue = workspaceTaskSchedulerFactory?.CreateTaskQueue(); - _options = new Lazy>(() => - { - var options = new HashSet(); - - foreach (var provider in optionProviders) - { - options.AddRange(provider.Value.GetOptions()); - } - - return options; - }); - - foreach (var optionSerializerAndMetadata in optionSerializers) - { - foreach (var featureName in optionSerializerAndMetadata.Metadata.Features) - { - ImmutableArray> existingSerializers; - if (!_featureNameToOptionSerializers.TryGetValue(featureName, out existingSerializers)) - { - existingSerializers = ImmutableArray.Create>(); - } - - _featureNameToOptionSerializers = _featureNameToOptionSerializers.SetItem(featureName, existingSerializers.Add(optionSerializerAndMetadata)); - } - } + _globalOptionService.OptionChanged += OnGlobalOptionServiceOptionChanged; + } - _currentValues = ImmutableDictionary.Create(); + public void OnWorkspaceDisposed(Workspace workspace) + { + // Disconnect us from the underlying global service. That way it doesn't + // keep us around (and all the event handlers we're holding onto) forever. + _globalOptionService.OptionChanged -= OnGlobalOptionServiceOptionChanged; } - private object LoadOptionFromSerializerOrGetDefault(OptionKey optionKey) + private void OnGlobalOptionServiceOptionChanged(object sender, OptionChangedEventArgs e) { - lock (_gate) + var eventHandlers = GetEventHandlers(); + if (eventHandlers.Length > 0) { - ImmutableArray> optionSerializers; - if (_featureNameToOptionSerializers.TryGetValue(optionKey.Option.Feature, out optionSerializers)) + _taskQueue?.ScheduleTask(() => { - foreach (var serializer in optionSerializers) + foreach (var handler in eventHandlers) { - // There can be options (ex, formatting) that only exist in only one specific language. In those cases, - // feature's serializer should exist in only that language. - if (!SupportedSerializer(optionKey, serializer.Metadata)) - { - continue; - } - - // We have a deserializer, so deserialize and use that value. - object deserializedValue; - if (serializer.Value.TryFetch(optionKey, out deserializedValue)) - { - return deserializedValue; - } + handler(this, e); } - } - - // Just use the default. We will still cache this so we aren't trying to deserialize - // over and over. - return optionKey.Option.DefaultValue; + }, "OptionsService.SetOptions"); } } - public IEnumerable GetRegisteredOptions() - { - return _options.Value; - } - - public OptionSet GetOptions() - { - return new WorkspaceOptionSet(this); - } - - public T GetOption(Option option) - { - return (T)GetOption(new OptionKey(option, language: null)); - } - - public T GetOption(PerLanguageOption option, string language) - { - return (T)GetOption(new OptionKey(option, language)); - } - - public object GetOption(OptionKey optionKey) + private ImmutableArray> GetEventHandlers() { lock (_gate) { - object value; - - if (_currentValues.TryGetValue(optionKey, out value)) - { - return value; - } - - value = LoadOptionFromSerializerOrGetDefault(optionKey); - - _currentValues = _currentValues.Add(optionKey, value); - - return value; + return _eventHandlers; } } - public void SetOptions(OptionSet optionSet) + public event EventHandler OptionChanged { - if (optionSet == null) - { - throw new ArgumentNullException(nameof(optionSet)); - } - - var workspaceOptionSet = optionSet as WorkspaceOptionSet; - - if (workspaceOptionSet == null) - { - throw new ArgumentException(WorkspacesResources.OptionsDidNotComeFromWorkspace, paramName: nameof(optionSet)); - } - - var changedOptions = new List(); - - lock (_gate) + add { - foreach (var optionKey in workspaceOptionSet.GetAccessedOptions()) + lock (_gate) { - var setValue = optionSet.GetOption(optionKey); - object currentValue = this.GetOption(optionKey); - - if (object.Equals(currentValue, setValue)) - { - // Identical, so nothing is changing - continue; - } - - // The value is actually changing, so update - changedOptions.Add(new OptionChangedEventArgs(optionKey, setValue)); - - _currentValues = _currentValues.SetItem(optionKey, setValue); - - ImmutableArray> optionSerializers; - if (_featureNameToOptionSerializers.TryGetValue(optionKey.Option.Feature, out optionSerializers)) - { - foreach (var serializer in optionSerializers) - { - // There can be options (ex, formatting) that only exist in only one specific language. In those cases, - // feature's serializer should exist in only that language. - if (!SupportedSerializer(optionKey, serializer.Metadata)) - { - continue; - } - - if (serializer.Value.TryPersist(optionKey, setValue)) - { - break; - } - } - } + _eventHandlers = _eventHandlers.Add(value); } } - // Outside of the lock, raise the events on our task queue. - _taskQueue?.ScheduleTask(() => - { - RaiseEvents(changedOptions); - }, "OptionsService.SetOptions"); - } - - private void RaiseEvents(List changedOptions) - { - var optionChanged = OptionChanged; - if (optionChanged != null) + remove { - foreach (var changedOption in changedOptions) + lock(_gate) { - optionChanged(this, changedOption); + _eventHandlers = _eventHandlers.Remove(value); } } } - private static bool SupportedSerializer(OptionKey optionKey, OptionSerializerMetadata metadata) + public OptionSet GetOptions() { - return optionKey.Language == null || optionKey.Language == metadata.Language; + return new WorkspaceOptionSet(this); } - public event EventHandler OptionChanged; + // Simple forwarding functions. + public object GetOption(OptionKey optionKey) => _globalOptionService.GetOption(optionKey); + public T GetOption(Option option) => _globalOptionService.GetOption(option); + public T GetOption(PerLanguageOption option, string languageName) => _globalOptionService.GetOption(option, languageName); + public IEnumerable GetRegisteredOptions() => _globalOptionService.GetRegisteredOptions(); + public void SetOptions(OptionSet optionSet) => _globalOptionService.SetOptions(optionSet); } } } \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Workspace/Workspace.cs b/src/Workspaces/Core/Portable/Workspace/Workspace.cs index 387adb45b46f2956046920730528c6d6975f5de5..4e5f9818e59469a650cd32aabd40a702738543e8 100644 --- a/src/Workspaces/Core/Portable/Workspace/Workspace.cs +++ b/src/Workspaces/Core/Portable/Workspace/Workspace.cs @@ -290,6 +290,8 @@ protected virtual void Dispose(bool finalize) { this.ClearSolutionData(); } + + ((IWorkspaceOptionService)this.Services.GetService()).OnWorkspaceDisposed(this); } #region Host API diff --git a/src/Workspaces/Core/Portable/Workspaces.csproj b/src/Workspaces/Core/Portable/Workspaces.csproj index d708691d4499d347ac2c68541fc1b9e064bac37b..a5852f06f8bb798fd81097319ce41b004b3e10bd 100644 --- a/src/Workspaces/Core/Portable/Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Workspaces.csproj @@ -361,6 +361,9 @@ + + + diff --git a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestOptionService.cs b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestOptionService.cs index 055d6b00bea2a90e07f3b4bab32b7ce6c67b7ba2..0964cd83827131b49e5a5753368bfa1009febf0c 100644 --- a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestOptionService.cs +++ b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestOptionService.cs @@ -13,7 +13,7 @@ public static OptionServiceFactory.OptionService GetService() { var features = new Dictionary(); features.Add("Features", new List(new[] { "Test Features" })); - return new OptionServiceFactory.OptionService(null, new[] + return new OptionServiceFactory.OptionService(new GlobalOptionService(new[] { new Lazy(() => new TestOptionsProvider()) }, @@ -25,7 +25,7 @@ public static OptionServiceFactory.OptionService GetService() return new TestOptionSerializer(); }, new OptionSerializerMetadata(features)) - }); + }), null); } internal class TestOptionsProvider : IOptionProvider