Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKirill Osenkov <github@osenkov.com>2018-08-16 06:12:44 +0300
committerKirill Osenkov <github@osenkov.com>2018-08-16 06:12:44 +0300
commitff21ab7431a8ea823c4f78653c088cf77ec1b1e3 (patch)
treef50372923c2b1adac5a5423a7619eff0c0166054
parentbc41f9e81d80e04feb18214558e4d271a5636078 (diff)
Add Features.
-rw-r--r--src/Core/Impl/Features/FeatureCookie.cs52
-rw-r--r--src/Core/Impl/Features/FeatureDisableToken.cs22
-rw-r--r--src/Core/Impl/Features/FeatureService.cs179
-rw-r--r--src/Core/Impl/Features/FeatureServiceFactory.cs96
-rw-r--r--src/Core/Impl/Features/IFeatureDefinitionMetadata.cs23
-rw-r--r--src/Core/Impl/Features/StandardEditorFeatureDefinitions.cs29
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;
+ }
+}