diff options
author | Kirill Osenkov <github@osenkov.com> | 2018-08-16 06:12:44 +0300 |
---|---|---|
committer | Kirill Osenkov <github@osenkov.com> | 2018-08-16 06:12:44 +0300 |
commit | ff21ab7431a8ea823c4f78653c088cf77ec1b1e3 (patch) | |
tree | f50372923c2b1adac5a5423a7619eff0c0166054 | |
parent | bc41f9e81d80e04feb18214558e4d271a5636078 (diff) |
Add Features.
-rw-r--r-- | src/Core/Impl/Features/FeatureCookie.cs | 52 | ||||
-rw-r--r-- | src/Core/Impl/Features/FeatureDisableToken.cs | 22 | ||||
-rw-r--r-- | src/Core/Impl/Features/FeatureService.cs | 179 | ||||
-rw-r--r-- | src/Core/Impl/Features/FeatureServiceFactory.cs | 96 | ||||
-rw-r--r-- | src/Core/Impl/Features/IFeatureDefinitionMetadata.cs | 23 | ||||
-rw-r--r-- | src/Core/Impl/Features/StandardEditorFeatureDefinitions.cs | 29 |
6 files changed, 401 insertions, 0 deletions
diff --git a/src/Core/Impl/Features/FeatureCookie.cs b/src/Core/Impl/Features/FeatureCookie.cs new file mode 100644 index 0000000..7d884f8 --- /dev/null +++ b/src/Core/Impl/Features/FeatureCookie.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.VisualStudio.Utilities.Features.Implementation +{ + internal class FeatureCookie : IFeatureCookie + { + public string FeatureName { get; private set; } + + public event EventHandler<FeatureChangedEventArgs> StateChanged; + + public bool IsEnabled + { + get => this.featureIsEnabled; + private set + { + if (this.featureIsEnabled == value) + return; + + this.featureIsEnabled = value; + StateChanged?.Invoke(this, new FeatureChangedEventArgs(FeatureName, value)); + } + } + + + private bool featureIsEnabled; + private IEnumerable<string> aliases; + private FeatureService service; + + internal FeatureCookie(string featureName, IEnumerable<string> aliases, FeatureService service) + { + FeatureName = featureName; + this.aliases = aliases; + this.service = service; + + IsEnabled = service.IsEnabled(FeatureName); + this.service.StateUpdated += OnStateUpdated; + } + + /// <summary> + /// Recalculates <see cref="IsEnabled" /> after pertinent feature or its base feature has updated. + /// </summary> + private void OnStateUpdated(object sender, FeatureUpdatedEventArgs args) + { + if (aliases.Contains(args.FeatureName)) + { + IsEnabled = this.service.IsEnabled(this.FeatureName); + } + } + } +} diff --git a/src/Core/Impl/Features/FeatureDisableToken.cs b/src/Core/Impl/Features/FeatureDisableToken.cs new file mode 100644 index 0000000..17b8e36 --- /dev/null +++ b/src/Core/Impl/Features/FeatureDisableToken.cs @@ -0,0 +1,22 @@ +namespace Microsoft.VisualStudio.Utilities.Features.Implementation +{ + internal class FeatureDisableToken : IFeatureDisableToken + { + private readonly FeatureService service; + private readonly string featureName; + private readonly IFeatureController controller; + + internal FeatureDisableToken(FeatureService service, string featureName, IFeatureController controller) + { + this.service = service; + this.featureName = featureName; + this.controller = controller; + } + +#pragma warning disable CA1063 // Implement IDisposable Correctly (no need, as there are no managed resources) + + public void Dispose() => this.service.Restore(this.featureName, this.controller); + +#pragma warning restore CA1063 // Implement IDisposable Correctly + } +} diff --git a/src/Core/Impl/Features/FeatureService.cs b/src/Core/Impl/Features/FeatureService.cs new file mode 100644 index 0000000..cc2e1ea --- /dev/null +++ b/src/Core/Impl/Features/FeatureService.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Utilities.Features.Implementation +{ + internal class FeatureService : IFeatureService + { + /// <summary> + /// Maintains list of currently active requests to disable a feature. + /// </summary> + IDictionary<string, FrugalList<IFeatureController>> Annulations { get; set; } + + /// <summary> + /// Reference to the feature's scope. + /// Currently used only to distinguish service pertinent to (local) scope from global service, where this value is null + /// </summary> + internal IFeatureService Parent { get; } + + /// <summary> + /// Reference to the <see cref="FeatureServiceFactory"/>. + /// </summary> + internal FeatureServiceFactory Factory { get; } + + public event EventHandler<FeatureUpdatedEventArgs> StateUpdated; + + // Telemetry: + const string TelemetryDisableEventName = "VS/Editor/FeatureService/Disable"; + const string TelemetryRestoreEventName = "VS/Editor/FeatureService/Restore"; + const string TelemetryFeatureNameKey = "Property.FeatureName"; + const string TelemetryControllerKey = "Property.Controller"; + const string TelemetryGlobalScopeKey = "Property.IsGlobalScope"; + + /// <summary> + /// Creates an instance of FeatureService + /// </summary> + /// <param name="parent"></param> + /// <param name="factory"></param> + internal FeatureService(IFeatureService parent, FeatureServiceFactory factory) + { + this.Parent = parent; + this.Factory = factory; + + Annulations = new Dictionary<string, FrugalList<IFeatureController>>(factory.AllDefinitions.Count()); + foreach (var featureDefinition in factory.AllDefinitions) + { + // TODO: I don't think we need to initialize all lists. + Annulations[featureDefinition.Metadata.Name] = new FrugalList<IFeatureController>(); + } + + if (Parent != null) + { + // subscribe to events of the parent service + Parent.StateUpdated += OnParentServiceStateUpdated; + } + } + + public bool IsEnabled(string featureName) + { + if (string.IsNullOrEmpty(featureName)) + throw new ArgumentNullException(nameof(featureName)); + if (!Factory.RelatedDefinitions.ContainsKey(featureName)) + throw new ArgumentOutOfRangeException(nameof(featureName), $"Feature {featureName} is not registered"); + + foreach (var definition in Factory.RelatedDefinitions[featureName]) + { + if (Annulations[definition].Count > 0) + return false; + } + + if (Parent != null) + { + // Also check the parent service + return Parent.IsEnabled(featureName); + } + else + { + // This is the global service. Looks like the feature is enabled. + return true; + } + } + + public IFeatureDisableToken Disable(string featureName, IFeatureController controller) + { + if (string.IsNullOrEmpty(featureName)) + throw new ArgumentNullException(nameof(featureName)); + if (controller == null) + throw new ArgumentNullException(nameof(controller)); + if (!Factory.RelatedDefinitions.ContainsKey(featureName)) + throw new ArgumentOutOfRangeException(nameof(featureName), $"Feature {featureName} is not registered"); + + var token = new FeatureDisableToken(this, featureName, controller); + + var annulations = Annulations[featureName]; + if (annulations.Contains(controller)) + return token; // This controller already disables this feature + annulations.Add(controller); + + if (annulations.Count == 1) // Notify of update + Factory.GuardedOperations.RaiseEvent(this, StateUpdated, new FeatureUpdatedEventArgs(featureName)); + + Factory.Telemetry?.PostEvent( + TelemetryEventType.Operation, + TelemetryDisableEventName, + TelemetryResult.Success, + (TelemetryFeatureNameKey, featureName), + (TelemetryControllerKey, controller.GetType().ToString()), + (TelemetryGlobalScopeKey, Parent == null) + ); + + return token; + } + + /// <summary> + /// Cancels the request to disable a feature. + /// If another <see cref="IFeatureController"/> disabled this feature or its group, the feature remains disabled. + /// This method is internal, and called from <see cref="FeatureDisableToken"/> + /// </summary> + /// <remarks> + /// While this service does have a thread affinity, its implementation does not guarantee thread safety. + /// It is advised to change feature state from UI thread, otherwise simultaneous changes may result in race conditions. + /// </remarks> + /// <param name="featureName">Name of previously disabled feature</param> + /// <param name="controller">Object that uniquely identifies the entity that disables and restores the feature.</param> + internal void Restore(string featureName, IFeatureController controller) + { + if (string.IsNullOrEmpty(featureName)) + throw new ArgumentNullException(nameof(featureName)); + if (controller == null) + throw new ArgumentNullException(nameof(controller)); + if (!Factory.RelatedDefinitions.ContainsKey(featureName)) + throw new ArgumentOutOfRangeException(nameof(featureName), $"Feature {featureName} is not registered"); + + var annulations = Annulations[featureName]; + if (!annulations.Contains(controller)) + return; // This controller is not disabling this feature + annulations.Remove(controller); + + if (annulations.Count == 0) // Notify of update + Factory.GuardedOperations.RaiseEvent(this, StateUpdated, new FeatureUpdatedEventArgs(featureName)); + + Factory.Telemetry?.PostEvent( + TelemetryEventType.Operation, + TelemetryRestoreEventName, + TelemetryResult.Success, + (TelemetryFeatureNameKey, featureName), + (TelemetryControllerKey, controller.GetType().ToString()), + (TelemetryGlobalScopeKey, Parent == null) + ); + } + + Dictionary<string, IFeatureCookie> CookieCache = new Dictionary<string, IFeatureCookie>(); + + public IFeatureCookie GetCookie(string featureName) + { + if (string.IsNullOrEmpty(featureName)) + throw new ArgumentNullException(nameof(featureName)); + if (!Factory.RelatedDefinitions.ContainsKey(featureName)) + throw new ArgumentOutOfRangeException(nameof(featureName), $"Feature {featureName} is not registered"); + + if (!CookieCache.ContainsKey(featureName)) + CookieCache[featureName] = new FeatureCookie(featureName, Factory.RelatedDefinitions[featureName], this); + return CookieCache[featureName]; + } + + /// <summary> + /// Event handler that listens to updates in <see cref="IFeatureService" /> of parent scope, + /// and propagates it further. The intent of this event is to update <see cref="IFeatureCookie"/> + /// </summary> + /// <param name="sender"><see cref="IFeatureService" /> that updated a feature</param> + /// <param name="e">Instace of <see cref="FeatureUpdatedEventArgs"/></param> + private void OnParentServiceStateUpdated(object sender, FeatureUpdatedEventArgs e) + { + Factory.GuardedOperations.RaiseEvent(sender, StateUpdated, e); + } + } +} diff --git a/src/Core/Impl/Features/FeatureServiceFactory.cs b/src/Core/Impl/Features/FeatureServiceFactory.cs new file mode 100644 index 0000000..86bbe5f --- /dev/null +++ b/src/Core/Impl/Features/FeatureServiceFactory.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Utilities.Features.Implementation +{ + /// <inheritdoc /> + [Export(typeof(IFeatureServiceFactory))] + internal class FeatureServiceFactory : IFeatureServiceFactory, IPartImportsSatisfiedNotification + { + /// <summary> + /// All <see cref="FeatureDefinition"/>s imported by MEF + /// </summary> + [ImportMany] + internal IEnumerable<Lazy<FeatureDefinition, IFeatureDefinitionMetadata>> AllDefinitions { get; set; } + + [Import] + internal IGuardedOperations GuardedOperations { get; set; } + + [Import(AllowDefault = true)] + internal ILoggingServiceInternal Telemetry { get; set; } + + /// <summary> + /// Maps feature name to all names that may disable the feature, + /// i.e. the name itself and names of base <see cref="FeatureDefinition"/>s + /// </summary> + internal IDictionary<string, SortedSet<string>> RelatedDefinitions { get; set; } + + private bool initializing = false; + private IFeatureService _globalFeatureService; + + /// <inheritdoc /> + public IFeatureService GlobalFeatureService + { + get + { + if (this.initializing) // Protection from stack oveflow + throw new InvalidOperationException($"Do not access {nameof(GlobalFeatureService)} when it is being initialized"); + + if (_globalFeatureService == null) + { + this.initializing = true; + _globalFeatureService = new FeatureService(parent: null, factory: this); + this.initializing = false; + } + return _globalFeatureService; + } + } + + /// <inheritdoc /> + public IFeatureService GetOrCreate(IPropertyOwner scope) + { + if (scope == null) + throw new ArgumentNullException(nameof(scope)); + + return scope.Properties.GetOrCreateSingletonProperty( + () => new FeatureService(GlobalFeatureService, this)); + } + + /// <summary> + /// Does the initial setup: iterates over imported definitions and builds a mapping + /// from base feature definitions to leaf feature definitions + /// </summary> + void IPartImportsSatisfiedNotification.OnImportsSatisfied() + { + RelatedDefinitions = new Dictionary<string, SortedSet<string>>(); + foreach (var featureDefinition in AllDefinitions) + { + var alsoKnownAs = new SortedSet<string>(); + AddBaseDefinitionNamesToSet(featureDefinition.Metadata.Name, alsoKnownAs); + RelatedDefinitions[featureDefinition.Metadata.Name] = alsoKnownAs; + } + } + + /// <summary> + /// Recursively collects names of base <see cref="FeatureDefinition"/>s + /// </summary> + /// <param name="name">Feature name</param> + /// <param name="set">Collection that stores names that may be used to disable the feature with given <paramref name="name"/></param> + private void AddBaseDefinitionNamesToSet(string name, ISet<string> set) + { + foreach (var feature in AllDefinitions.Where(n => n.Metadata.Name.Equals(name, StringComparison.Ordinal))) + { + set.Add(feature.Metadata.Name); + if (feature.Metadata.BaseDefinition == null) + continue; + foreach (var baseDefinition in feature.Metadata.BaseDefinition) + { + AddBaseDefinitionNamesToSet(baseDefinition, set); + } + } + } + } +} diff --git a/src/Core/Impl/Features/IFeatureDefinitionMetadata.cs b/src/Core/Impl/Features/IFeatureDefinitionMetadata.cs new file mode 100644 index 0000000..a351191 --- /dev/null +++ b/src/Core/Impl/Features/IFeatureDefinitionMetadata.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.VisualStudio.Utilities.Features.Implementation +{ + /// <summary> + /// Describes metadata required of <see cref="FeatureDefinition"/> imports. + /// </summary> + public interface IFeatureDefinitionMetadata + { + /// <summary> + /// Name of the feature + /// </summary> + string Name { get; } + + /// <summary> + /// Optionally, a collection of names of parent features + /// </summary> + [System.ComponentModel.DefaultValue(null)] + IEnumerable<string> BaseDefinition { get; } + } +} diff --git a/src/Core/Impl/Features/StandardEditorFeatureDefinitions.cs b/src/Core/Impl/Features/StandardEditorFeatureDefinitions.cs new file mode 100644 index 0000000..c8c04fb --- /dev/null +++ b/src/Core/Impl/Features/StandardEditorFeatureDefinitions.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.Composition; + +namespace Microsoft.VisualStudio.Utilities.Features.Implementation +{ + /// <summary> + /// Contains exports for <see cref="FeatureDefinition"/>s shared in <see cref="PredefinedEditorFeatureNames"/> + /// </summary> + internal class StandardEditorFeatureDefinitions + { + [Export] + [Name(PredefinedEditorFeatureNames.Editor)] + public FeatureDefinition EditorDefinition; + + [Export] + [Name(PredefinedEditorFeatureNames.Popup)] + public FeatureDefinition PopupDefinition; + + [Export] + [Name(PredefinedEditorFeatureNames.InteractivePopup)] + [BaseDefinition(PredefinedEditorFeatureNames.Popup)] + public FeatureDefinition InteractivePopupDefinition; + + [Export] + [Name(PredefinedEditorFeatureNames.Completion)] + [BaseDefinition(PredefinedEditorFeatureNames.InteractivePopup)] + [BaseDefinition(PredefinedEditorFeatureNames.Editor)] + public FeatureDefinition CompletionDefinition; + } +} |