diff options
Diffstat (limited to 'src')
352 files changed, 11042 insertions, 6204 deletions
diff --git a/src/Core/Def/BaseUtility/BaseDefinitionAttribute.cs b/src/Core/Def/BaseUtility/BaseDefinitionAttribute.cs index ba807c3..538f6bb 100644 --- a/src/Core/Def/BaseUtility/BaseDefinitionAttribute.cs +++ b/src/Core/Def/BaseUtility/BaseDefinitionAttribute.cs @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.Utilities { if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException("name"); + throw new ArgumentNullException(nameof(name)); } baseDefinition = name; } diff --git a/src/Core/Def/BaseUtility/DefaultOrderings.cs b/src/Core/Def/BaseUtility/DefaultOrderings.cs new file mode 100644 index 0000000..ebbbc29 --- /dev/null +++ b/src/Core/Def/BaseUtility/DefaultOrderings.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Static class defining some default placeholders for the ordering attributes. + /// </summary> + /// <remarks> + /// <para> + /// Orderable items that do not explicitly indicate they are before <see cref="DefaultOrderings.Lowest"/> have an implicit constraint + /// that they are after <see cref="DefaultOrderings.Lowest"/>. + /// </para> + /// <para> + /// Orderable items that do not explicitly indicate they are after <see cref="DefaultOrderings.Highest"/> have an implicit constraint + /// that they are before <see cref="DefaultOrderings.Highest"/>. + /// </para> + /// </remarks> + public static class DefaultOrderings + { + public const string Lowest = "Lowest Priority"; + public const string Low = "Low Priority"; + public const string Default = "Default Priority"; + public const string High = "High Priority"; + public const string Highest = "Highest Priority"; + } +} diff --git a/src/Core/Def/BaseUtility/DisplayNameAttribute.cs b/src/Core/Def/BaseUtility/DisplayNameAttribute.cs index 5099fa8..f5a0644 100644 --- a/src/Core/Def/BaseUtility/DisplayNameAttribute.cs +++ b/src/Core/Def/BaseUtility/DisplayNameAttribute.cs @@ -12,9 +12,9 @@ namespace Microsoft.VisualStudio.Utilities /// <remarks> /// This attribute should be localized wherever it is used. /// </remarks> + [Obsolete("Use " + nameof(LocalizedNameAttribute) + " instead.")] public sealed class DisplayNameAttribute : SingletonBaseMetadataAttribute { - private string displayName; /// <summary> /// Initializes a new instance of <see cref="DisplayNameAttribute"/>. @@ -22,22 +22,12 @@ namespace Microsoft.VisualStudio.Utilities /// <param name="displayName">The display name of an editor component part.</param> public DisplayNameAttribute(string displayName) { - if (displayName == null) - { - throw new ArgumentNullException("displayName"); - } - this.displayName = displayName; + this.DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); } /// <summary> /// Gets the display name of an editor component part. /// </summary> - public string DisplayName - { - get - { - return this.displayName; - } - } + public string DisplayName { get; } } -}
\ No newline at end of file +} diff --git a/src/Core/Def/BaseUtility/IGuardedOperations.cs b/src/Core/Def/BaseUtility/IGuardedOperations.cs index 839018f..1a94a38 100644 --- a/src/Core/Def/BaseUtility/IGuardedOperations.cs +++ b/src/Core/Def/BaseUtility/IGuardedOperations.cs @@ -10,7 +10,7 @@ using Microsoft.VisualStudio.Threading; namespace Microsoft.VisualStudio.Utilities { /// <summary> - /// Operations that guard calls to extensions code and log errors. + /// Operations that guard calls to extensions code, track performance and log errors. /// </summary> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> @@ -19,6 +19,7 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Makes a guarded call to an extension point. /// </summary> + /// <param name="call">Delegate that calls the extension point.</param> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> void CallExtensionPoint(Action call); @@ -26,6 +27,9 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Makes a guarded call to an extension point. /// </summary> + /// <param name="errorSource">Reference to the extension object or event handler that may throw an exception. + /// Used for tracking performance and errors.</param> + /// <param name="call">Delegate that calls the extension point.</param> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> void CallExtensionPoint(object errorSource, Action call); @@ -33,6 +37,21 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Makes a guarded call to an extension point. /// </summary> + /// <param name="errorSource">Reference to the extension object or event handler that may throw an exception. + /// Used for tracking performance and errors.</param> + /// <param name="call">Delegate that calls the extension point.</param> + /// <param name="exceptionGuardFilter">Determines which exceptions should be guarded against. + /// An exception gets handled only if <paramref name="exceptionGuardFilter"/> returns <c>true</c>.</param> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + void CallExtensionPoint(object errorSource, Action call, Predicate<Exception> exceptionGuardFilter); + + /// <summary> + /// Makes a guarded call to an extension point. + /// </summary> + /// <param name="call">Delegate that calls the extension point.</param> + /// <param name="valueOnThrow">The value returned if the delegate call failed.</param> + /// <returns>The result of the <paramref name="call"/> or <paramref name="valueOnThrow"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> T CallExtensionPoint<T>(Func<T> call, T valueOnThrow); @@ -40,6 +59,11 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Makes a guarded call to an extension point. /// </summary> + /// <param name="errorSource">Reference to the extension object or event handler that may throw an exception. + /// Used for tracking performance and errors.</param> + /// <param name="call">Delegate that calls the extension point.</param> + /// <param name="valueOnThrow">The value returned if the delegate call failed.</param> + /// <returns>The result of the <paramref name="call"/> or <paramref name="valueOnThrow"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow); @@ -47,7 +71,7 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Makes a guarded call to an async extension point. /// </summary> - /// <param name="asyncAction">The extension point to be called.</param> + /// <param name="asyncCall">Delegate that calls the extension point.</param> /// <returns>A <see cref="Task"/> that asynchronously executes the <paramref name="asyncAction"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> @@ -56,7 +80,9 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Makes a guarded call to an async extension point. /// </summary> - /// <param name="asyncAction">The extension point to be called.</param> + /// <param name="errorSource">Reference to the extension object or event handler that may throw an exception. + /// Used for tracking performance and errors.</param> + /// <param name="asyncCall">Delegate that calls the extension point.</param> /// <returns>A <see cref="Task"/> that asynchronously executes the <paramref name="asyncAction"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> @@ -66,9 +92,9 @@ namespace Microsoft.VisualStudio.Utilities /// Makes a guarded call to an async extension point. /// </summary> /// <typeparam name="T">The type of the value returned from the <paramref name="asyncCall"/>.</typeparam> - /// <param name="asyncCall">The extension point to be called.</param> - /// <param name="valueOnThrow">The value returned if call failed.</param> - /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/>.</returns> + /// <param name="asyncCall">Delegate that calls the extension point.</param> + /// <param name="valueOnThrow">The value returned if the delegate call failed.</param> + /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/> or provides <paramref name="valueOnThrow"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> Task<T> CallExtensionPointAsync<T>(Func<Task<T>> asyncCall, T valueOnThrow); @@ -77,16 +103,23 @@ namespace Microsoft.VisualStudio.Utilities /// Makes a guarded call to an async extension point. /// </summary> /// <typeparam name="T">The type of the value returned from the <paramref name="asyncCall"/>.</typeparam> - /// <param name="asyncCall">The extension point to be called.</param> - /// <param name="valueOnThrow">The value returned if call failed.</param> - /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/>.</returns> + /// <param name="errorSource">Reference to the extension object or event handler that may throw an exception. + /// Used for tracking performance and errors.</param> + /// <param name="asyncCall">Delegate that calls the extension point.</param> + /// <param name="valueOnThrow">The value returned if the delegate call failed.</param> + /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/> or provides <paramref name="valueOnThrow"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> Task<T> CallExtensionPointAsync<T>(object errorSource, Func<Task<T>> asyncCall, T valueOnThrow); /// <summary> - /// Selects eligible extension factories. + /// Selects extension factories whose declared content type metadata + /// matches the provided target content type, taking into account that extension factory + /// may be disabled by a Replace attribute on another factory. /// </summary> + /// <param name="lazyFactories">Lazy references that will be evaluated.</param> + /// <param name="dataContentType">Target content type.</param> + /// <param name="contentTypeRegistryService">Instance of <see cref="IContentTypeRegistryService"/> which orders content types.</param> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> IEnumerable<Lazy<TExtensionFactory, TMetadataView>> FindEligibleFactories<TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService) @@ -96,6 +129,8 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Handles an exception occured in a call to an extension point. /// </summary> + /// <param name="errorSource">Reference to the extension object or event handler that threw the exception</param> + /// <param name="e">Exception to handle</param> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> void HandleException(object errorSource, Exception e); @@ -103,6 +138,9 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Safely instantiates an extension point. /// </summary> + /// <param name="errorSource">Reference to the object that will be blamed for potential exceptions.</param> + /// <param name="provider">Lazy reference that will be initialized.</param> + /// <returns>Initialized instance stored in <paramref name="provider"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> TExtension InstantiateExtension<TExtension>(object errorSource, Lazy<TExtension> provider); @@ -110,27 +148,47 @@ namespace Microsoft.VisualStudio.Utilities /// <summary> /// Safely instantiates an extension point. /// </summary> + /// <param name="errorSource">Reference to the object that will be blamed for potential exceptions.</param> + /// <param name="provider">Lazy reference that will be initialized.</param> + /// <returns>Initialized instance stored in <paramref name="provider"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> TExtension InstantiateExtension<TExtension, TMetadata>(object errorSource, Lazy<TExtension, TMetadata> provider); /// <summary> - /// Safely instantiates an extension point. + /// Safely invokes a delegate on the extension point. /// </summary> + /// <param name="errorSource">Reference to the object that will be blamed for potential exceptions.</param> + /// <param name="provider">Lazy reference that will be initialized.</param> + /// <param name="getter">Delegate which constructs an instance of the extension from its <paramref name="provider"/>.</param> + /// <returns>The result of <paramref name="getter"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> TExtensionInstance InstantiateExtension<TExtension, TMetadata, TExtensionInstance>(object errorSource, Lazy<TExtension, TMetadata> provider, Func<TExtension, TExtensionInstance> getter); /// <summary> - /// Safely invokes best matching extension factory. + /// Safely instantiates an extension point whose declared content type metadata + /// is the closest match to the provided target content type. /// </summary> + /// <param name="providerHandles">Lazy references that will be evaluated.</param> + /// <param name="dataContentType">Target content type.</param> + /// <param name="contentTypeRegistryService">Instance of <see cref="IContentTypeRegistryService"/> which orders content types.</param> + /// <param name="errorSource">Reference to the object that will be blamed for potential exceptions.</param> + /// <returns>The selected element of <paramref name="providerHandles"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> TExtension InvokeBestMatchingFactory<TExtension, TMetadataView>(IList<Lazy<TExtension, TMetadataView>> providerHandles, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService, object errorSource) where TMetadataView : IContentTypeMetadata; /// <summary> - /// Safely invokes best matching extension factory. + /// Safely invokes a delegate on the extension factory whose declared content type metadata + /// is the best match to the provided target content type. /// </summary> + /// <param name="providerHandles">Lazy references that will be evaluated.</param> + /// <param name="dataContentType">Target content type.</param> + /// <param name="getter">Delegate which constructs an instance of the extension from the best matching element of <paramref name="providerHandles"/>.</param> + /// <param name="contentTypeRegistryService">Instance of <see cref="IContentTypeRegistryService"/> which orders content types.</param> + /// <param name="errorSource">Reference to the object that will be blamed for potential exceptions.</param> + /// <returns>The result of <paramref name="getter"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> TExtensionInstance InvokeBestMatchingFactory<TExtensionFactory, TExtensionInstance, TMetadataView>(IList<Lazy<TExtensionFactory, TMetadataView>> providerHandles, IContentType dataContentType, Func<TExtensionFactory, TExtensionInstance> getter, IContentTypeRegistryService contentTypeRegistryService, object errorSource) @@ -138,8 +196,16 @@ namespace Microsoft.VisualStudio.Utilities where TMetadataView : IContentTypeMetadata; /// <summary> - /// Safely invokes all eligible extension factories. + /// Safely invokes a delegate on all extension factories whose declared content type metadata + /// matches the provided target content type, taking into account that extension factory + /// may be disabled by a Replace attribute on another factory. /// </summary> + /// <param name="lazyFactories">Lazy references that will be evaluated.</param> + /// <param name="getter">Delegate which constructs an instance of the extension from each element of <paramref name="lazyFactories"/>.</param> + /// <param name="dataContentType">Target content type.</param> + /// <param name="contentTypeRegistryService">Instance of <see cref="IContentTypeRegistryService"/> which orders content types.</param> + /// <param name="errorSource">Reference to the object that will be blamed for potential exceptions.</param> + /// <returns>The list of results of <paramref name="getter"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> List<TExtensionInstance> InvokeEligibleFactories<TExtensionInstance, TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, Func<TExtensionFactory, TExtensionInstance> getter, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService, object errorSource) @@ -148,8 +214,14 @@ namespace Microsoft.VisualStudio.Utilities where TMetadataView : INamedContentTypeMetadata; /// <summary> - /// Safely invokes all matching extension factories. + /// Safely invokes a delegate on all extension factories whose declared content type metadata + /// matches the provided target content type. /// </summary> + /// <param name="lazyFactories">Lazy references that will be evaluated.</param> + /// <param name="getter">Delegate which constructs an instance of the extension from each element of <paramref name="lazyFactories"/>.</param> + /// <param name="dataContentType">Target content type.</param> + /// <param name="errorSource">Reference to the object that will be blamed for potential exceptions.</param> + /// <returns>The list of results of <paramref name="getter"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> List<TExtensionInstance> InvokeMatchingFactories<TExtensionInstance, TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, Func<TExtensionFactory, TExtensionInstance> getter, IContentType dataContentType, object errorSource) @@ -157,25 +229,39 @@ namespace Microsoft.VisualStudio.Utilities where TExtensionFactory : class where TMetadataView : IContentTypeMetadata; +#pragma warning disable CA1030 // Use events where appropriate /// <summary> - /// Safely raises an event. + /// Safely raises an event with empty <see cref="EventArgs"/>. + /// Errors are tracked per sender, performance is tracked per handler. /// </summary> + /// <param name="sender">Reference to the sender of the event. Tracks errors.</param> + /// <param name="eventHandlers">Event to raise. Each handler tracks performance.</param> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> void RaiseEvent(object sender, EventHandler eventHandlers); /// <summary> - /// Safely raises an event. + /// Safely raises an event with specified <paramref name="args"/>. + /// Errors are tracked per sender, performance is tracked per handler. /// </summary> + /// <param name="sender">Reference to the sender of the event. Tracks errors.</param> + /// <param name="eventHandlers">Event to raise. Each handler tracks performance.</param> + /// <param name="args">Event data.</param> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> void RaiseEvent<TArgs>(object sender, EventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs; /// <summary> - /// Safely raises an event on a background thread. + /// Safely raises an event on a background thread with specified <paramref name="args"/>. + /// Errors are tracked per sender, performance is tracked per handler. /// </summary> + /// <param name="sender">Reference to the sender of the event. Tracks errors.</param> + /// <param name="eventHandlers">Event to raise. Each handler tracks performance.</param> + /// <param name="args">Event data.</param> + /// <returns>A <see cref="Task"/> that asynchronously executes the <paramref name="eventHandlers"/>.</returns> /// <remarks>This class supports the Visual Studio /// infrastructure and in general is not intended to be used directly from your code.</remarks> Task RaiseEventOnBackgroundAsync<TArgs>(object sender, AsyncEventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs; +#pragma warning restore CA1030 // Use events where appropriate } } diff --git a/src/Core/Def/BaseUtility/ITelemetryIdProvider.cs b/src/Core/Def/BaseUtility/ITelemetryIdProvider.cs index 73fe1ba..014f3f1 100644 --- a/src/Core/Def/BaseUtility/ITelemetryIdProvider.cs +++ b/src/Core/Def/BaseUtility/ITelemetryIdProvider.cs @@ -6,14 +6,14 @@ namespace Microsoft.VisualStudio.Utilities { /// <summary> /// Represents an object that can provide a unique ID for telemetry purposes. - /// <typeparam name="Tid">Type of the telemetry ID.</typeparam> + /// <typeparam name="TId">Type of the telemetry ID.</typeparam> /// </summary> - public interface ITelemetryIdProvider<Tid> + public interface ITelemetryIdProvider<TId> { /// <summary> /// Tries to get a unique ID for telemetry purposes. /// </summary> /// <returns><c>true</c> if a unique telemetry ID was returned, <c>false</c> if this object refuses to participate in telemetry logging.</returns> - bool TryGetTelemetryId(out Tid telemetryId); + bool TryGetTelemetryId(out TId telemetryId); } } diff --git a/src/Core/Def/BaseUtility/LocalizedNameAttribute.cs b/src/Core/Def/BaseUtility/LocalizedNameAttribute.cs new file mode 100644 index 0000000..f199594 --- /dev/null +++ b/src/Core/Def/BaseUtility/LocalizedNameAttribute.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System; +using System.Globalization; +using System.Resources; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Represents an attribute which can provide a localized name as metadata for a MEF extension. + /// </summary> + public sealed class LocalizedNameAttribute : SingletonBaseMetadataAttribute + { + /// <summary> + /// Note: the localized name is cached rather than the type to prevent + /// MEF from referencing the type in its cache. Types exposed as metadata + /// cause MEF to load the assembly containing the type during composition. + /// </summary> + private readonly string localizedName; + + /// <summary> + /// Creates an instance of this attribute, which caches the localized name represented + /// by the given type and resource name. + /// </summary> + /// <param name="type">The type from which to load the localized resource. This should + /// be a type created by the resource designer.</param> + /// <param name="resourceId">The name of the localized resource string contained the + /// resource type.</param> + public LocalizedNameAttribute(Type type, string resourceId) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + if (resourceId == null) + { + throw new ArgumentNullException(nameof(resourceId)); + } + + ResourceManager resourceManager = new ResourceManager(type); + this.localizedName = resourceManager.GetString(resourceId, CultureInfo.CurrentUICulture); + } + + /// <summary> + /// Creates an instance of this attribute, which caches the localized name represented + /// by the given type and resource name. + /// </summary> + /// <param name="type">The type from which to load the localized resource.</param> + /// <param name="resourceStreamName">The base name of the resource stream containing the resource.</param> + /// <param name="resourceId">The name of the localized resource string contained the + /// resource type.</param> + public LocalizedNameAttribute(Type type, string resourceStreamName, string resourceId) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + if (resourceStreamName == null) + { + throw new ArgumentNullException(nameof(resourceStreamName)); + } + if (resourceId == null) + { + throw new ArgumentNullException(nameof(resourceId)); + } + + ResourceManager resourceManager = new ResourceManager(resourceStreamName, type.Assembly); + this.localizedName = resourceManager.GetString(resourceId, CultureInfo.CurrentUICulture); + } + + /// <summary> + /// Gets the localized name specified by the constructor. + /// </summary> + public string LocalizedName => this.localizedName; + } +} diff --git a/src/Core/Def/BaseUtility/NameAttribute.cs b/src/Core/Def/BaseUtility/NameAttribute.cs index af34702..92698ac 100644 --- a/src/Core/Def/BaseUtility/NameAttribute.cs +++ b/src/Core/Def/BaseUtility/NameAttribute.cs @@ -11,7 +11,6 @@ namespace Microsoft.VisualStudio.Utilities /// </summary> public sealed class NameAttribute : SingletonBaseMetadataAttribute { - private string name; /// <summary> /// Constructs a new instance of the attribute. @@ -23,24 +22,18 @@ namespace Microsoft.VisualStudio.Utilities { if (name == null) { - throw new ArgumentNullException("name"); + throw new ArgumentNullException(nameof(name)); } if (name.Length == 0) { - throw new ArgumentException("name must not be empty", "name"); + throw new ArgumentException("name must not be empty", nameof(name)); } - this.name = name; + this.Name = name; } /// <summary> /// The name of the editor extension part. /// </summary> - public string Name - { - get - { - return name; - } - } + public string Name { get; } } } diff --git a/src/Core/Def/BaseUtility/OptionUserModifiableAttribute.cs b/src/Core/Def/BaseUtility/OptionUserModifiableAttribute.cs new file mode 100644 index 0000000..381d447 --- /dev/null +++ b/src/Core/Def/BaseUtility/OptionUserModifiableAttribute.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// A MEF attribute determining if an option is user modifiable. + /// </summary> + public sealed class OptionUserModifiableAttribute : SingletonBaseMetadataAttribute + { + /// <summary> + /// Initializes a new instance of the <see cref="OptionUserModifiableAttribute"/>. + /// </summary> + /// <param name="userModifiable"><c>true</c> if the option is user modifiable; otherwise <c>false</c>.</param> + public OptionUserModifiableAttribute(bool userModifiable) + { + this.OptionUserModifiable = userModifiable; + } + + /// <summary> + /// Determines whether the option is modifiable to the user. + /// </summary> + public bool OptionUserModifiable { get; } + } +} diff --git a/src/Core/Def/BaseUtility/OptionUserVisibleAttribute.cs b/src/Core/Def/BaseUtility/OptionUserVisibleAttribute.cs new file mode 100644 index 0000000..4b3dbf5 --- /dev/null +++ b/src/Core/Def/BaseUtility/OptionUserVisibleAttribute.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// A MEF attribute determining if an option is visible to the user. + /// </summary> + public sealed class OptionUserVisibleAttribute : SingletonBaseMetadataAttribute + { + /// <summary> + /// Initializes a new instance of the <see cref="OptionUserVisibleAttribute"/>. + /// </summary> + /// <param name="userVisible"><c>true</c> if the option is visible to the user; otherwise <c>false</c>.</param> + public OptionUserVisibleAttribute(bool userVisible) + { + this.OptionUserVisible = userVisible; + } + + /// <summary> + /// Determines whether the option is visible to the user. + /// </summary> + public bool OptionUserVisible { get; } + } +} diff --git a/src/Core/Def/BaseUtility/OrderAttribute.cs b/src/Core/Def/BaseUtility/OrderAttribute.cs index 3f3d86d..9e2293b 100644 --- a/src/Core/Def/BaseUtility/OrderAttribute.cs +++ b/src/Core/Def/BaseUtility/OrderAttribute.cs @@ -24,19 +24,19 @@ namespace Microsoft.VisualStudio.Utilities { get { - return before; + return this.before; } set { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } if (value.Length == 0) { - throw new ArgumentException("Before value must not be empty", "value"); + throw new ArgumentException("Before value must not be empty", nameof(value)); } - before = value; + this.before = value; } } @@ -50,19 +50,19 @@ namespace Microsoft.VisualStudio.Utilities { get { - return after; + return this.after; } set { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } if (value.Length == 0) { - throw new ArgumentException("After value must not be empty", "value"); + throw new ArgumentException("After value must not be empty", nameof(value)); } - after = value; + this.after = value; } } } diff --git a/src/Core/Def/BaseUtility/Orderer.cs b/src/Core/Def/BaseUtility/Orderer.cs index b285e2c..7ab90e4 100644 --- a/src/Core/Def/BaseUtility/Orderer.cs +++ b/src/Core/Def/BaseUtility/Orderer.cs @@ -13,6 +13,15 @@ namespace Microsoft.VisualStudio.Utilities /// </summary> public static class Orderer { + // This is really sad but, because we actually convert all before/after attributes to upper case, we need upper case versions + // of the highest and lowest priorities in order to find them. A better approach would be to use a case insensitive comparison + // for the map but that could cause a behavior change. + private readonly static string HighestUC = DefaultOrderings.Highest.ToUpperInvariant(); + private readonly static string HighUC = DefaultOrderings.High.ToUpperInvariant(); + private readonly static string DefaultUC = DefaultOrderings.Default.ToUpperInvariant(); + private readonly static string LowUC = DefaultOrderings.Low.ToUpperInvariant(); + private readonly static string LowestUC = DefaultOrderings.Lowest.ToUpperInvariant(); + /// <summary> /// Orders a list of items that are all orderable, that is, items that implement the IOrderable interface. /// </summary> @@ -26,13 +35,14 @@ namespace Microsoft.VisualStudio.Utilities { if (itemsToOrder == null) { - throw new ArgumentNullException("itemsToOrder"); + throw new ArgumentNullException(nameof(itemsToOrder)); } #if false && DEBUG Debug.WriteLine("Before ordering"); DumpGraph(itemsToOrder); #endif + var roots = new Queue<Node<TValue, TMetadata>>(); var unsortedItems = new List<Node<TValue, TMetadata>>(); @@ -58,7 +68,7 @@ namespace Microsoft.VisualStudio.Utilities { var node = new Node<TValue, TMetadata>(item); - if (node.Name != string.Empty) + if (node.Name.Length != 0) { if (map.ContainsKey(node.Name)) { @@ -81,14 +91,62 @@ namespace Microsoft.VisualStudio.Utilities } } + // Only resolve the exported nodes by counting down. Placeholders don't need to be resolved since they have no explicit + // after or before (and, when created as a side-effect of resolving an exported node, are added to the end of the list). for (int i = unsortedItems.Count - 1; (i >= 0); --i) { unsortedItems[i].Resolve(map, unsortedItems); //Placeholders are added to the end of unsorted items (and do not need to be resolved). } + // Do the special handling for the highest and lowest placeholders. Specifically, unless something says that it is after Highest + // then it gets the implicit constraint that it is before Highest. + // + // Note that if you have a situation like: + // if A declares it is after Highest & B declares that is is after A then, you need to put B in the "after highest" group (otherwise it + // will get the before highest constraint, which will cause a cycle). + if (map.TryGetValue(HighestUC, out Node<TValue, TMetadata> highest) && (highest.Before.Count != 0)) + { + // There are one or more nodes that explicitly state that they come after highest + // collect all of those nodes (and the nodes that come after them) into a single list. + var afterHighest = new HashSet<Node<TValue, TMetadata>>(); + AddToAfterHighest(highest.Before, afterHighest); + + // Go through all of the nodes and, unless they are explicitly in the afterHighest group, add a constraint + // that they are explicitly before highest. + for (int i = unsortedItems.Count - 1; (i >= 0); --i) + { + var n = unsortedItems[i]; + if ((n != highest) && !afterHighest.Contains(n)) + { + n.Before.Add(highest); + highest.After.Add(n); + } + } + } + + // Give lowest the same handling as highest, inverting the logic as appropriate. + if (map.TryGetValue(LowestUC, out Node<TValue, TMetadata> lowest) && (lowest.After.Count != 0)) + { + var beforeLowest = new HashSet<Node<TValue, TMetadata>>(); + AddToBeforeLowest(lowest.After, beforeLowest); + + for (int i = unsortedItems.Count - 1; (i >= 0); --i) + { + var n = unsortedItems[i]; + if ((n != lowest) && !beforeLowest.Contains(n)) + { + n.After.Add(lowest); + lowest.Before.Add(n); + } + } + } + + AddPlaceHolders(map, LowestUC, LowUC, DefaultUC, HighUC, HighestUC); + List<Node<TValue, TMetadata>> initialRoots = new List<Node<TValue, TMetadata>>(); - foreach (Node<TValue, TMetadata> node in unsortedItems) + for (int i = unsortedItems.Count - 1; (i >= 0); --i) { + var node = unsortedItems[i]; if (node.After.Count == 0) { initialRoots.Add(node); @@ -98,6 +156,60 @@ namespace Microsoft.VisualStudio.Utilities AddToRoots(roots, initialRoots); } + private static void AddPlaceHolders<TValue, TMetadata>(Dictionary<string, Node<TValue, TMetadata>> map, + params string[] names) + where TValue : class + where TMetadata : IOrderable + { + // Make sure there's an explicit ordering where the node for name[0] come before the node for name[1], etc. + // + // If the node for a name doesn't exist, just skip it (no one else was using it so we don't need to order it + // with respect to anything). + Node<TValue, TMetadata> previousNode = null; + for (int i = 0; (i < names.Length); ++i) + { + Node<TValue, TMetadata> node; + if (map.TryGetValue(names[i], out node)) + { + if (previousNode != null) + { + previousNode.Before.Add(node); + node.After.Add(previousNode); + } + + previousNode = node; + } + } + } + + // We need to find all the nodes that are after Highest (or after a node that is after Highest, ad infinitum). + private static void AddToAfterHighest<TValue, TMetadata>(IEnumerable<Node<TValue, TMetadata>> nodes, HashSet<Node<TValue, TMetadata>> afterHighest) + where TValue : class + where TMetadata : IOrderable + { + foreach (var n in nodes) + { + if (afterHighest.Add(n) && (n.Before.Count != 0)) + { + AddToAfterHighest(n.Before, afterHighest); + } + } + } + + // The Before/Lowest analog of AddToAfterHighest. + private static void AddToBeforeLowest<TValue, TMetadata>(IEnumerable<Node<TValue, TMetadata>> nodes, HashSet<Node<TValue, TMetadata>> beforeLowest) + where TValue : class + where TMetadata : IOrderable + { + foreach (var n in nodes) + { + if (beforeLowest.Add(n) && (n.After.Count != 0)) + { + AddToBeforeLowest(n.After, beforeLowest); + } + } + } + private static IList<Lazy<TValue, TMetadata>> TopologicalSort<TValue, TMetadata>(Queue<Node<TValue, TMetadata>> roots, List<Node<TValue, TMetadata>> unsortedItems) where TValue : class where TMetadata : IOrderable @@ -115,7 +227,7 @@ namespace Microsoft.VisualStudio.Utilities sortedItems.Add(node.Item); } - unsortedItems.Remove(node); + unsortedItems.Remove(node); node.ClearBefore(roots); } @@ -126,10 +238,10 @@ namespace Microsoft.VisualStudio.Utilities where TValue : class where TMetadata : IOrderable { - newRoots.Sort((l, r) => l.Name.CompareTo(r.Name)); - foreach (Node<TValue, TMetadata> n in newRoots) + newRoots.Sort((l, r) => string.CompareOrdinal(l.Name, r.Name)); + for (int i = 0; (i < newRoots.Count); ++i) { - roots.Enqueue(n); + roots.Enqueue(newRoots[i]); } } @@ -159,11 +271,13 @@ namespace Microsoft.VisualStudio.Utilities //Find the cycle with the fewest inbound links from other cycles. int bestInwardLinkCount = int.MaxValue; List<Node<TValue, TMetadata>> bestCycle = null; - foreach (List<Node<TValue, TMetadata>> cycle in cycles) + for (int i = 0; (i < cycles.Count); ++i) { + var cycle = cycles[i]; int inwardLinkCount = 0; - foreach (Node<TValue, TMetadata> node in cycle) + for (int j = 0; (j < cycle.Count); ++j) { + var node = cycle[j]; foreach (Node<TValue, TMetadata> child in node.After) { if (child.LowIndex != node.LowIndex) @@ -216,8 +330,9 @@ namespace Microsoft.VisualStudio.Utilities where TValue : class where TMetadata : IOrderable { - foreach (Node<TValue, TMetadata> n in unsortedItems) + for (int i = 0; (i < unsortedItems.Count); ++i) { + var n = unsortedItems[i]; n.Index = -1; n.LowIndex = -1; n.ContainedInKnownCycle = false; @@ -227,8 +342,9 @@ namespace Microsoft.VisualStudio.Utilities Stack<Node<TValue, TMetadata>> stack = new Stack<Node<TValue, TMetadata>>(unsortedItems.Count); int index = 0; - foreach (Node<TValue, TMetadata> node in unsortedItems) + for (int i = 0; (i < unsortedItems.Count); ++i) { + var node = unsortedItems[i]; if (node.Index == -1) { Orderer.FindCycles(node, stack, ref index, cycles); @@ -400,7 +516,7 @@ namespace Microsoft.VisualStudio.Utilities Node<TValue, TMetadata> node; if (!map.TryGetValue(name, out node)) { - //We need place-holder to handle the case where A comes before B and C comes after B but B is never defined. + //We need place-holder to handle the case where A comes before B and C comes after B but B is never defined. //We still want C to come after A though so we need to create a "B". // //B doesn't show up in the output. diff --git a/src/Core/Def/BaseUtility/PriorityAttribute.cs b/src/Core/Def/BaseUtility/PriorityAttribute.cs new file mode 100644 index 0000000..49c8ceb --- /dev/null +++ b/src/Core/Def/BaseUtility/PriorityAttribute.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Represents an attribute which assigns an integer priority to a MEF component part. + /// </summary> + public sealed class PriorityAttribute : SingletonBaseMetadataAttribute + { + /// <summary> + /// Creates a new instance of this attribute, assigning it a priority value. + /// </summary> + /// <param name="priority">The priority for the MEF component part. Lower integer + /// values represent higher precedence.</param> + public PriorityAttribute(int priority) + { + this.Priority = priority; + } + + /// <summary> + /// Gets the priority for the attributed MEF extension. + /// </summary> + public int Priority { get; } + } +} diff --git a/src/Core/Def/BaseUtility/PropertyCollection.cs b/src/Core/Def/BaseUtility/PropertyCollection.cs index 87b7ab1..b3ceb58 100644 --- a/src/Core/Def/BaseUtility/PropertyCollection.cs +++ b/src/Core/Def/BaseUtility/PropertyCollection.cs @@ -70,7 +70,7 @@ namespace Microsoft.VisualStudio.Utilities public T GetOrCreateSingletonProperty<T>(object key, Func<T> creator) where T : class { if (creator == null) - throw new ArgumentNullException("creator"); + throw new ArgumentNullException(nameof(creator)); lock (this.syncLock) { diff --git a/src/Core/Def/ContentType/ContentTypeAttribute.cs b/src/Core/Def/ContentType/ContentTypeAttribute.cs index e5939a1..45f5af8 100644 --- a/src/Core/Def/ContentType/ContentTypeAttribute.cs +++ b/src/Core/Def/ContentType/ContentTypeAttribute.cs @@ -14,7 +14,6 @@ namespace Microsoft.VisualStudio.Utilities /// <seealso cref="ContentTypeDefinition"></seealso> public sealed class ContentTypeAttribute : MultipleBaseMetadataAttribute { - private string contentTypes; /// <summary> /// Initializes a new instance of <see cref="ContentTypeAttribute"/>. @@ -26,20 +25,14 @@ namespace Microsoft.VisualStudio.Utilities { if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException("name"); + throw new ArgumentNullException(nameof(name)); } - contentTypes = name; + ContentTypes = name; } /// <summary> /// The content type name. /// </summary> - public string ContentTypes - { - get - { - return contentTypes; - } - } + public string ContentTypes { get; } } } diff --git a/src/Core/Def/ContentType/FileExtensionAttribute.cs b/src/Core/Def/ContentType/FileExtensionAttribute.cs index 6407a34..6df6ad3 100644 --- a/src/Core/Def/ContentType/FileExtensionAttribute.cs +++ b/src/Core/Def/ContentType/FileExtensionAttribute.cs @@ -11,7 +11,6 @@ namespace Microsoft.VisualStudio.Utilities /// </summary> public sealed class FileExtensionAttribute : SingletonBaseMetadataAttribute { - private string fileExtension; /// <summary> /// Constructs a new instance of the attribute. @@ -22,20 +21,14 @@ namespace Microsoft.VisualStudio.Utilities { if (string.IsNullOrWhiteSpace(fileExtension)) { - throw new ArgumentNullException("fileExtension"); + throw new ArgumentNullException(nameof(fileExtension)); } - this.fileExtension = fileExtension; + this.FileExtension = fileExtension; } /// <summary> /// Gets the file extension. /// </summary> - public string FileExtension - { - get - { - return this.fileExtension; - } - } + public string FileExtension { get; } } } diff --git a/src/Core/Def/ContentType/FileExtensionToContentTypeDefinition.cs b/src/Core/Def/ContentType/FileExtensionToContentTypeDefinition.cs index e88896f..80fd079 100644 --- a/src/Core/Def/ContentType/FileExtensionToContentTypeDefinition.cs +++ b/src/Core/Def/ContentType/FileExtensionToContentTypeDefinition.cs @@ -14,10 +14,14 @@ namespace Microsoft.VisualStudio.Utilities /// internal sealed class Components /// { /// [Export] - /// [FileExtension(".abc")] + /// [FileExtension(".abc")] // Any file with the extention "abc" will get the "alphabet" content type. /// [ContentType("alphabet")] /// internal FileExtensionToContentTypeDefinition abcFileExtensionDefinition; /// + /// [Export] + /// [FileName("readme")] // Any file named "readme" will get the "alphabet" content type. + /// [ContentType("alphabet")] + /// internal FileExtensionToContentTypeDefinition readmeFileNameDefinition; /// { other components } /// } /// </example> diff --git a/src/Core/Def/ContentType/IFileExtensionRegistryService2.cs b/src/Core/Def/ContentType/IFileExtensionRegistryService2.cs index 2d63021..e0dfd99 100644 --- a/src/Core/Def/ContentType/IFileExtensionRegistryService2.cs +++ b/src/Core/Def/ContentType/IFileExtensionRegistryService2.cs @@ -49,6 +49,6 @@ namespace Microsoft.VisualStudio.Utilities /// </summary> /// <remarks>If the specified name does not exist, then the method does nothing.</remarks> /// <param name="name">The file name (the period is optional).</param> - void RemoveFileName(string name); + void RemoveFileName(string name); } } diff --git a/src/Core/Def/ContentType/IFilePathToContentTypeProvider.cs b/src/Core/Def/ContentType/IFilePathToContentTypeProvider.cs new file mode 100644 index 0000000..605e64a --- /dev/null +++ b/src/Core/Def/ContentType/IFilePathToContentTypeProvider.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// MEF export to map full file names to a content type. + /// </summary> + /// <remarks> + /// <para>Instances of this class should define the following MEF attributes. + /// <code> + /// [Export(typeof(IFilePathToContentTypeProvider)] -- Required + /// [Name("BamBam")] -- Required + /// [Order(After = "Fred", Before="Barney")] -- Optional, can have more than one. + /// [FileExtension(".abc")] -- Optional, but must have either a FileExtension or a FileName attribute + /// [FileName("George")] -- Optional, but must have either a FileExtension or a FileName attribute + /// </code> + /// You can use "*" as the FileExtension attribute to match any file extension.</para> + /// + /// <para> + /// The <see cref="IFilePathToContentTypeProvider"/> will be called in order (based on the <see cref="OrderAttribute"/>) if their + /// <see cref="FileExtensionAttribute"/> matches the extension of the file in question (or is a "*") or the <see cref="FileNameAttribute"/> + /// matches the name of the file in question. + /// </para> + /// </remarks> + public interface IFilePathToContentTypeProvider + { + bool TryGetContentTypeForFilePath(string filePath, out IContentType contentType); + } +} diff --git a/src/Core/Def/ContentType/IFileToContentTypeService.cs b/src/Core/Def/ContentType/IFileToContentTypeService.cs new file mode 100644 index 0000000..7c8bde0 --- /dev/null +++ b/src/Core/Def/ContentType/IFileToContentTypeService.cs @@ -0,0 +1,107 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// + +using System; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Service for mapping files to the appropriate <see cref="IContentType"/> for that file. + /// </summary> + /// <remarks> + /// <para> + /// Note that this interface duplicates the methods from <see cref="IFileExtensionRegistryService"/> and + /// <see cref="IFileExtensionRegistryService2"/>. The eventual goal is to deprecate the other interfaces + /// and only use <see cref="IFileToContentTypeService"/>. + /// </para></remarks> + public interface IFileToContentTypeService + { + /// <summary> + /// Get the default <see cref="IContentType"/> for a file located at <paramref name="filePath"/>. + /// </summary> + /// <param name="filePath">Name of the file in question.</param> + /// <returns>Excpected content type or + /// <see cref="IContentTypeRegistryService.UnknownContentType"/> if no content type is found.</returns> + /// <remarks>If no <see cref="IContentType"/> is found using declared <see cref="IFilePathToContentTypeProvider"/> + /// assets, then the <see cref="GetContentTypeForFileNameOrExtension(string)"/> is used.</remarks> + IContentType GetContentTypeForFilePath(string filePath); + + /// <summary> + /// Get the default <see cref="IContentType"/> for a file located at <paramref name="filePath"/>. + /// </summary> + /// <param name="filePath">Name of the file in question.</param> + /// <returns>Excpected content type or + /// <see cref="IContentTypeRegistryService.UnknownContentType"/> if no content type is found.</returns> + /// <remarks>If no <see cref="IContentType"/> is found using declared <see cref="IFilePathToContentTypeProvider"/> + /// assets, then <see cref="IContentTypeRegistryService.UnknownContentType"/> is returned.</remarks> + IContentType GetContentTypeForFilePathOnly(string filePath); + + /// <summary> + /// Gets the content type associated with the given file name. + /// </summary> + /// <param name="name">The file name. It cannot be null.</param> + /// <returns>The <see cref="IContentType"></see> associated with this name. If no association exists, it returns the "unknown" content type. It never returns null.</returns> + IContentType GetContentTypeForFileName(string name); + + /// <summary> + /// Gets the content type associated with the given file name or its extension. + /// </summary> + /// <param name="name">The file name. It cannot be null.</param> + /// <returns>The <see cref="IContentType"></see> associated with this name. If no association exists, it returns the "unknown" content type. It never returns null.</returns> + IContentType GetContentTypeForFileNameOrExtension(string name); + + /// <summary> + /// Gets the list of file names associated with the specified content type. + /// </summary> + /// <param name="contentType">The content type. It cannot be null.</param> + /// <returns>The list of file names associated with the content type.</returns> + IEnumerable<string> GetFileNamesForContentType(IContentType contentType); + + /// <summary> + /// Adds a new file name to the registry. + /// </summary> + /// <param name="name">The file name (the period is optional).</param> + /// <param name="contentType">The content type for the file name.</param> + /// <exception cref="InvalidOperationException"><see paramref="name"/> is already present in the registry.</exception> + void AddFileName(string name, IContentType contentType); + + /// <summary> + /// Removes the specified file name from the registry. + /// </summary> + /// <remarks>If the specified name does not exist, then the method does nothing.</remarks> + /// <param name="name">The file name (the period is optional).</param> + void RemoveFileName(string name); + + /// <summary> + /// Gets the content type associated with the given file extension. + /// </summary> + /// <param name="extension">The file extension. It cannot be null, and it should not contain a period.</param> + /// <returns>The <see cref="IContentType"></see> associated with this extension. If no association exists, it returns the "unknown" content type. It never returns null.</returns> + IContentType GetContentTypeForExtension(string extension); + + /// <summary> + /// Gets the list of file extensions associated with the specified content type. + /// </summary> + /// <param name="contentType">The content type. It cannot be null.</param> + /// <returns>The list of file extensions associated with the content type.</returns> + IEnumerable<string> GetExtensionsForContentType(IContentType contentType); + + /// <summary> + /// Adds a new file extension to the registry. + /// </summary> + /// <param name="extension">The file extension (the period is optional).</param> + /// <param name="contentType">The content type for the file extension.</param> + /// <exception cref="InvalidOperationException"><see paramref="extension"/> is already present in the registry.</exception> + void AddFileExtension(string extension, IContentType contentType); + + /// <summary> + /// Removes the specified file extension from the registry. + /// </summary> + /// <remarks>If the specified extension does not exist, then the method does nothing.</remarks> + /// <param name="extension">The file extension (the period is optional).</param> + void RemoveFileExtension(string extension); + } +} diff --git a/src/Core/Def/Features/FeatureDefinition.cs b/src/Core/Def/Features/FeatureDefinition.cs new file mode 100644 index 0000000..4960167 --- /dev/null +++ b/src/Core/Def/Features/FeatureDefinition.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Defines a feature which may be disabled using <see cref="IFeatureService"/> and grouped using <see cref="BaseDefinitionAttribute"/> + /// </summary> + /// <remarks> + /// Because you cannot subclass this type, you can use the [Export] attribute with no type. + /// </remarks> + /// <example> + /// [Export] + /// [Name(nameof(MyFeature))] // required + /// [BaseDefinition(PredefinedEditorFeatureNames.Popup)] // zero or more BaseDefinitions are allowed + /// public FeatureDefinition MyFeature; + /// </example> + public sealed class FeatureDefinition + { + } +} diff --git a/src/Core/Def/Features/FeatureEventArgs.cs b/src/Core/Def/Features/FeatureEventArgs.cs new file mode 100644 index 0000000..fa791b1 --- /dev/null +++ b/src/Core/Def/Features/FeatureEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Notifies that a specific feature was updated and might have changed its state, + /// without computing the state value. + /// </summary> + public class FeatureUpdatedEventArgs : EventArgs + { + /// <summary> + /// Name of feature that was updated. + /// </summary> + public string FeatureName { get; } + + /// <summary> + /// Creates an instance of <see cref="FeatureUpdatedEventArgs"/>. + /// </summary> + /// <param name="featureName">Name of feature that was updated</param> + public FeatureUpdatedEventArgs(string featureName) + { + FeatureName = featureName; + } + } + + /// <summary> + /// Notifies that a specific feature changed state, and provides the new state value. + /// </summary> + public class FeatureChangedEventArgs : EventArgs + { + /// <summary> + /// Name of feature that was changed. + /// </summary> + public string FeatureName { get; } + + /// <summary> + /// New value of the feature state. + /// </summary> + public bool IsEnabled { get; } + + /// <summary> + /// Creates an instance of <see cref="FeatureChangedEventArgs"/>. + /// </summary> + /// <param name="featureName">Name of feature that was changed</param> + /// <param name="isEnabled">New value of the feature state</param> + public FeatureChangedEventArgs(string featureName, bool isEnabled) + { + FeatureName = featureName; + IsEnabled = isEnabled; + } + } +} diff --git a/src/Core/Def/Features/IFeatureController.cs b/src/Core/Def/Features/IFeatureController.cs new file mode 100644 index 0000000..45f91e4 --- /dev/null +++ b/src/Core/Def/Features/IFeatureController.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Keeps track of requests to disable a feature using <see cref="IFeatureService"/>. + /// Each <see cref="IFeatureController"/> may re-enable a feature it disabled, + /// but may not re-enable a feature disabled by another <see cref="IFeatureController"/>. + /// </summary> + public interface IFeatureController + { + } +} diff --git a/src/Core/Def/Features/IFeatureCookie.cs b/src/Core/Def/Features/IFeatureCookie.cs new file mode 100644 index 0000000..9b890bf --- /dev/null +++ b/src/Core/Def/Features/IFeatureCookie.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Provides O(1) read only view on the state of the feature + /// in the <see cref="IFeatureService" /> that created this <see cref="IFeatureCookie" />. + /// Also exposes an event that provides notification when the state of the feature changes. + /// </summary> + public interface IFeatureCookie + { + /// <summary> + /// Provides notification when <see cref="IsEnabled"/> value changes. + /// </summary> + event EventHandler<FeatureChangedEventArgs> StateChanged; + + /// <summary> + /// Up to date state of the feature. + /// </summary> + bool IsEnabled { get; } + + /// <summary> + /// Name of the tracked feature. + /// </summary> + string FeatureName { get; } + } +} diff --git a/src/Core/Def/Features/IFeatureDisableToken.cs b/src/Core/Def/Features/IFeatureDisableToken.cs new file mode 100644 index 0000000..69a9169 --- /dev/null +++ b/src/Core/Def/Features/IFeatureDisableToken.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Keeps track of the request to disable the feature. + /// To restore the feature, + /// </summary> + public interface IFeatureDisableToken : IDisposable + { + } +} diff --git a/src/Core/Def/Features/IFeatureService.cs b/src/Core/Def/Features/IFeatureService.cs new file mode 100644 index 0000000..88a8c9f --- /dev/null +++ b/src/Core/Def/Features/IFeatureService.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Service that keeps track of <see cref="IFeatureController"/>'s requests to disable a feature in given scope. + /// When multiple <see cref="IFeatureController"/>s disable a feature and one <see cref="IFeatureController"/> + /// enables it back, it will not interfere with other disable requests, and feature will ultimately remain disabled. + /// + /// 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. + /// </summary> + /// <example> + /// // In an exported MEF part: + /// [Import] + /// IFeatureServiceFactory FeatureServiceFactory; + /// + /// IFeatureService globalService = FeatureServiceFactory.GlobalFeatureService; + /// IFeatureService localService = FeatureServiceFactory.GetOrCreate(scope); // scope is an IPropertyOwner + /// + /// // Also have a reference to <see cref="IFeatureController"/>: + /// IFeatureController MyFeatureController; + /// // Interact with the <see cref="IFeatureService"/>: + /// globalService.Disable(PredefinedEditorFeatureNames.Popup, MyFeatureController); + /// localService.IsEnabled(PredefinedEditorFeatureNames.Completion); // returns false, because Popup is a base definition of Completion and because global scope is a superset of local scope. + /// </example> + public interface IFeatureService + { + /// <summary> + /// Checks if feature is enabled. By default, every feature is enabled. + /// </summary> + /// <param name="featureName">Name of the feature</param> + /// <returns>False if there are any disable requests. True otherwise.</returns> + bool IsEnabled(string featureName); + + /// <summary> + /// Disables a feature. + /// </summary> + /// <param name="featureName">Name of the feature to disable</param> + /// <param name="controller">Object that uniquely identifies the caller.</param> + IFeatureDisableToken Disable(string featureName, IFeatureController controller); + + /// <summary> + /// Provides a notification when this feature or its base feature was updated. + /// We use FeatureUpdatedEventArgs and not FeatureChangedEventArgs + /// because there are base features and disable requests from parent scopes that affect the factual state of given feature. + /// We use this event to let the interested parties (<see cref="IFeatureCookie"/>) recalculate the actual state of the feature. + /// </summary> + event EventHandler<FeatureUpdatedEventArgs> StateUpdated; + + /// <summary> + /// Creates a new <see cref="IFeatureCookie"/> that provides O(1) access to the feature's value, in this service's scope. + /// The <see cref="IFeatureCookie" /> is updated when the feature or its base is updated in this scope or in the global scope. + /// </summary> + /// <param name="featureName">Name of the feature</param> + /// <returns>New instance of <see cref="IFeatureCookie"/></returns> + IFeatureCookie GetCookie(string featureName); + } +} diff --git a/src/Core/Def/Features/IFeatureServiceFactory.cs b/src/Core/Def/Features/IFeatureServiceFactory.cs new file mode 100644 index 0000000..245fb5c --- /dev/null +++ b/src/Core/Def/Features/IFeatureServiceFactory.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Service that provides <see cref="IFeatureService" />s used to track feature availability and to request feature to be disabled. + /// Feature may be tracked by scope, using <see cref="IFeatureServiceFactory.GetOrCreate" /> and passing <see cref="IPropertyOwner" /> e.g. a text view. + /// or throughout the application using <see cref="IFeatureServiceFactory.GlobalFeatureService" />. + /// + /// Features are implemented by exporting <see cref="FeatureDefinition"/> and grouped using <see cref="BaseDefinitionAttribute"/>. + /// Grouping allows alike features to be disabling at once. + /// Grouping also relieves <see cref="IFeatureController"/> from updating its code when new feature of appropriate category is introduced. + /// Standard editor feature names are available in <see cref="PredefinedEditorFeatureNames"/>. + /// </summary> + /// <example> + /// // In an exported MEF part: + /// [Import] + /// IFeatureServiceFactory FeatureServiceFactory; + /// + /// IFeatureService globalService = FeatureServiceFactory.GlobalFeatureService; + /// IFeatureService localService = FeatureServiceFactory.GetOrCreate(scope); // scope is an IPropertyOwner + /// + /// // Also have a reference to <see cref="IFeatureController"/>: + /// IFeatureController MyFeatureController; + /// // Interact with the <see cref="IFeatureService"/>: + /// globalService.Disable(PredefinedEditorFeatureNames.Popup, MyFeatureController); + /// localService.IsEnabled(PredefinedEditorFeatureNames.Completion); // returns false, because Popup is a base definition of Completion and because global scope is a superset of local scope. + /// </example> + public interface IFeatureServiceFactory + { + /// <summary> + /// Gets the global <see cref="IFeatureService"/> + /// </summary> + IFeatureService GlobalFeatureService { get; } + + /// <summary> + /// Gets the <see cref="IFeatureService"/> for the specified scope. + /// </summary> + /// <param name="scope"></param> + /// <returns></returns> + IFeatureService GetOrCreate(IPropertyOwner scope); + } +} diff --git a/src/Core/Def/Features/PredefinedEditorFeatureNames.cs b/src/Core/Def/Features/PredefinedEditorFeatureNames.cs new file mode 100644 index 0000000..08d7568 --- /dev/null +++ b/src/Core/Def/Features/PredefinedEditorFeatureNames.cs @@ -0,0 +1,30 @@ +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Contains definitions for known <see cref="FeatureDefinition"/>s and their groupings. + /// </summary> + public static class PredefinedEditorFeatureNames + { + /// <summary> + /// Definition of group of features that make up the core editor. + /// </summary> + public const string Editor = nameof(Editor); + + /// <summary> + /// Definition of group of features that appear in a popup. + /// </summary> + public const string Popup = nameof(Popup); + + /// <summary> + /// Definition of group of features that appear in an interactive popup. + /// Descends from <see cref="Popup"/> + /// </summary> + public const string InteractivePopup = nameof(InteractivePopup); + + /// <summary> + /// Definition of IntelliSense Completion. + /// Descends from <see cref="InteractivePopup"/> and <see cref="Editor"/> + /// </summary> + public const string Completion = nameof(Completion); + } +} diff --git a/src/Core/Def/ImageId.cs b/src/Core/Def/ImageId.cs index d540c16..0f7486f 100644 --- a/src/Core/Def/ImageId.cs +++ b/src/Core/Def/ImageId.cs @@ -2,6 +2,8 @@ { using System; +#pragma warning disable CA1720 // Identifier contains type name +#pragma warning disable CA1051 // Do not declare visible instance fields /// <summary> /// Unique identifier for Visual Studio image asset. /// </summary> @@ -9,7 +11,7 @@ /// On Windows systems, <see cref="ImageId"/> can be converted to and from /// various other image representations via the ImageIdExtensions extension methods. /// </remarks> - public struct ImageId + public struct ImageId : IEquatable<ImageId> { /// <summary> /// The <see cref="Guid"/> identifying the group to which this image belongs. @@ -31,5 +33,26 @@ this.Guid = guid; this.Id = id; } + public override string ToString() + { + return this.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + public string ToString(IFormatProvider provider) + { + return string.Format(provider, @"{0} : {1}", this.Guid.ToString("D", provider), Id.ToString(provider)); + } + + bool IEquatable<ImageId>.Equals(ImageId other) => Id.Equals(other.Id) && Guid.Equals(other.Guid); + + public override bool Equals(object other) => other is ImageId otherImage && ((IEquatable<ImageId>)this).Equals(otherImage); + + public static bool operator ==(ImageId left, ImageId right) => left.Equals(right); + + public static bool operator !=(ImageId left, ImageId right) => !(left == right); + + public override int GetHashCode() => Guid.GetHashCode() ^ Id.GetHashCode(); } +#pragma warning restore CA1720 // Identifier contains type name +#pragma warning restore CA1051 // Do not declare visible instance fields } diff --git a/src/Core/Impl/ContentType/ContentTypeImpl.cs b/src/Core/Impl/ContentType/ContentTypeImpl.cs index 1a6e3b9..29f8bea 100644 --- a/src/Core/Impl/ContentType/ContentTypeImpl.cs +++ b/src/Core/Impl/ContentType/ContentTypeImpl.cs @@ -13,32 +13,28 @@ namespace Microsoft.VisualStudio.Utilities.Implementation { internal partial class ContentTypeImpl : IContentType { - private readonly string name; private readonly static ContentTypeImpl[] emptyBaseTypes = Array.Empty<ContentTypeImpl>(); private ContentTypeImpl[] baseTypeList = emptyBaseTypes; internal ContentTypeImpl(string name, string mimeType = null, IEnumerable<string> baseTypes = null) { - this.name = name; + this.TypeName = name; this.MimeType = mimeType; this.UnprocessedBaseTypes = baseTypes; } - public string TypeName - { - get { return this.name; } - } + public string TypeName { get; private set; } public string DisplayName { - get { return this.name; } + get { return this.TypeName; } } public string MimeType { get; } public bool IsOfType(string type) { - if (string.Compare(type, this.name, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Equals(type, this.TypeName, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -62,7 +58,7 @@ namespace Microsoft.VisualStudio.Utilities.Implementation public override string ToString() { - return this.name; + return this.TypeName; } internal void ProcessBaseTypes(IDictionary<string, ContentTypeImpl> nameToContentTypeBuilder, @@ -70,7 +66,7 @@ namespace Microsoft.VisualStudio.Utilities.Implementation { if (this.UnprocessedBaseTypes != null) { - List<ContentTypeImpl> newBaseTypes = new List<ContentTypeImpl>(); + var newBaseTypes = new List<ContentTypeImpl>(); foreach (var baseTypeName in this.UnprocessedBaseTypes) { // The expectation is that the base type will already exists but (if it doesn't) add a stub for it (& the ctor for a stub base type leaves it in a state @@ -78,8 +74,8 @@ namespace Microsoft.VisualStudio.Utilities.Implementation var baseType = ContentTypeRegistryImpl.AddContentTypeFromMetadata(baseTypeName, /* mime type */null, /* base types */null, nameToContentTypeBuilder, mimeTypeToContentTypeBuilder); if (baseType == ContentTypeRegistryImpl.UnknownContentTypeImpl) { - throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, - Strings.ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown, this.TypeName)); + throw new InvalidOperationException(string.Format(provider: System.Globalization.CultureInfo.CurrentCulture, + format: Strings.ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown, arg0: this.TypeName)); } if (!newBaseTypes.Contains(baseType)) diff --git a/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs b/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs index 943c6c3..2f32dba 100644 --- a/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs +++ b/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs @@ -30,10 +30,10 @@ namespace Microsoft.VisualStudio.Utilities.Implementation [Export(typeof(IFileExtensionRegistryService))] [Export(typeof(IFileExtensionRegistryService2))] - [Export(typeof(IFilePathRegistryService))] + [Export(typeof(IFileToContentTypeService))] [Export(typeof(IContentTypeRegistryService))] [Export(typeof(IContentTypeRegistryService2))] - internal sealed partial class ContentTypeRegistryImpl : IContentTypeRegistryService2, IFileExtensionRegistryService, IFileExtensionRegistryService2, IFilePathRegistryService + internal sealed partial class ContentTypeRegistryImpl : IContentTypeRegistryService2, IFileExtensionRegistryService, IFileExtensionRegistryService2, IFileToContentTypeService { [ImportMany] internal List<Lazy<ContentTypeDefinition, IContentTypeDefinitionMetadata>> ContentTypeDefinitions { get; set; } @@ -60,9 +60,10 @@ namespace Microsoft.VisualStudio.Utilities.Implementation } else { - _orderedFilePathToContentTypeProductions = new List<Lazy<IFilePathToContentTypeProvider, IFilePathToContentTypeMetadata>>(); + _orderedFilePathToContentTypeProductions = new List<Lazy<IFilePathToContentTypeProvider, IFilePathToContentTypeMetadata>>(0); } } + return _orderedFilePathToContentTypeProductions; } } @@ -256,10 +257,7 @@ namespace Microsoft.VisualStudio.Utilities.Implementation #region IContentTypeRegistryService Members public IContentType GetContentType(string typeName) { - if (string.IsNullOrWhiteSpace(typeName)) - { - throw new ArgumentException(nameof(typeName)); - } + Requires.NotNullOrWhiteSpace(typeName, nameof(typeName)); this.BuildContentTypes(); @@ -287,16 +285,16 @@ namespace Microsoft.VisualStudio.Utilities.Implementation public IContentType AddContentType(string typeName, IEnumerable<string> baseTypeNames) { - if (string.IsNullOrWhiteSpace(typeName)) - { - throw new ArgumentException(nameof(typeName)); - } + Requires.NotNullOrWhiteSpace(typeName, nameof(typeName)); // This has the side effect of building the content types. if (this.GetContentType(typeName) != null) { // Cannot dynamically add a new content type if a content type with the same name already exists - throw new ArgumentException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotAddExistentType, typeName)); + throw new ArgumentException( + string.Format( + System.Globalization.CultureInfo.CurrentCulture, + Strings.ContentTypeRegistry_CannotAddExistentType, typeName)); } var oldMaps = Volatile.Read(ref this.maps); @@ -312,7 +310,10 @@ namespace Microsoft.VisualStudio.Utilities.Implementation if (type.CheckForCycle(breakCycle: false)) { - throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CausesCycles, type.TypeName)); + throw new InvalidOperationException( + string.Format( + System.Globalization.CultureInfo.CurrentCulture, + Strings.ContentTypeRegistry_CausesCycles, type.TypeName)); } var newMaps = new MapCollection(nameToContentTypeMap.Source, mimeTypeToContentTypeMap.Source, oldMaps.FileExtensionToContentTypeMap, oldMaps.FileNameToContentTypeMap); @@ -329,10 +330,7 @@ namespace Microsoft.VisualStudio.Utilities.Implementation public void RemoveContentType(string typeName) { - if (string.IsNullOrWhiteSpace(typeName)) - { - throw new ArgumentException(nameof(typeName)); - } + Requires.NotNullOrWhiteSpace(typeName, nameof(typeName)); this.BuildContentTypes(); @@ -356,21 +354,34 @@ namespace Microsoft.VisualStudio.Utilities.Implementation if (IsBaseType(type, out derivedType)) { // Check if the type is base type for another registered type - throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveBaseType, type.TypeName, derivedType.TypeName)); + throw new InvalidOperationException( + string.Format( + System.Globalization.CultureInfo.CurrentCulture, + Strings.ContentTypeRegistry_CannotRemoveBaseType, + type.TypeName, + derivedType.TypeName)); } // If there are file extensions using this content type we won't allow removing it if (this.maps.FileExtensionToContentTypeMap.Values.Any(c => c == type)) { // If there are file extensions using this content type we won't allow removing it - throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, type.TypeName)); + throw new InvalidOperationException( + string.Format( + System.Globalization.CultureInfo.CurrentCulture, + Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, + type.TypeName)); } // If there are file extensions using this content type we won't allow removing it if (this.maps.FileNameToContentTypeMap.Values.Any(c => c == type)) { // If there are file extensions using this content type we won't allow removing it - throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, type.TypeName)); + throw new InvalidOperationException( + string.Format( + System.Globalization.CultureInfo.CurrentCulture, + Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, + type.TypeName)); } var newMaps = new MapCollection(oldMaps.NameToContentTypeMap.Remove(typeName), @@ -394,7 +405,7 @@ namespace Microsoft.VisualStudio.Utilities.Implementation var typeImpl = type as ContentTypeImpl; if (typeImpl == null) { - throw new ArgumentException(nameof(type)); + throw new ArgumentNullException(nameof(type)); } else if (typeImpl == UnknownContentTypeImpl) { @@ -406,19 +417,16 @@ namespace Microsoft.VisualStudio.Utilities.Implementation public IContentType GetContentTypeForMimeType(string mimeType) { - if (string.IsNullOrWhiteSpace(mimeType)) - { - throw new ArgumentException(nameof(mimeType)); - } + Requires.NotNullOrWhiteSpace(mimeType, nameof(mimeType)); this.BuildContentTypes(); ContentTypeImpl contentType = null; if (!this.maps.MimeTypeToContentTypeMap.TryGetValue(mimeType, out contentType)) { - if (mimeType.StartsWith(BaseMimePrefix)) + if (mimeType.StartsWith(BaseMimePrefix, StringComparison.Ordinal)) { - if (!(mimeType.StartsWith(MimePrefix) && this.maps.NameToContentTypeMap.TryGetValue(mimeType.Substring(MimePrefix.Length), out contentType))) + if (!(mimeType.StartsWith(MimePrefix, StringComparison.Ordinal) && this.maps.NameToContentTypeMap.TryGetValue(mimeType.Substring(MimePrefix.Length), out contentType))) { this.maps.NameToContentTypeMap.TryGetValue(mimeType.Substring(BaseMimePrefix.Length), out contentType); } @@ -432,17 +440,21 @@ namespace Microsoft.VisualStudio.Utilities.Implementation #region IFileExtensionRegistryService Members public IContentType GetContentTypeForExtension(string extension) { - if (extension == null) + ContentTypeImpl contentType = null; + if (string.IsNullOrEmpty(extension)) { - throw new ArgumentNullException(nameof(extension)); + if (extension == null) + { + throw new ArgumentNullException(nameof(extension)); + } } + else + { - this.BuildContentTypes(); - - ContentTypeImpl contentType = null; - this.maps.FileExtensionToContentTypeMap.TryGetValue(RemoveExtensionDot(extension), out contentType); + this.BuildContentTypes(); + this.maps.FileExtensionToContentTypeMap.TryGetValue(RemoveExtensionDot(extension), out contentType); + } - // TODO: should we return null if contentType is null? return contentType ?? ContentTypeRegistryImpl.UnknownContentTypeImpl; } @@ -467,15 +479,12 @@ namespace Microsoft.VisualStudio.Utilities.Implementation public void AddFileExtension(string extension, IContentType contentType) { - if (string.IsNullOrWhiteSpace(extension)) - { - throw new ArgumentException(nameof(extension)); - } + Requires.NotNullOrWhiteSpace(extension, nameof(extension)); var contentTypeImpl = contentType as ContentTypeImpl; if ((contentTypeImpl == null) || (contentTypeImpl == UnknownContentTypeImpl)) { - throw new ArgumentException(nameof(contentType)); + throw new ArgumentException(nameof(contentType) + " cannot be null or unknown"); } this.BuildContentTypes(); @@ -489,9 +498,9 @@ namespace Microsoft.VisualStudio.Utilities.Implementation { if (type != contentTypeImpl) { - throw new InvalidOperationException - (String.Format(System.Globalization.CultureInfo.CurrentUICulture, - Strings.FileExtensionRegistry_NoMultipleContentTypes, extension)); + throw new InvalidOperationException( + string.Format(System.Globalization.CultureInfo.CurrentCulture, + Strings.FileExtensionRegistry_NoMultipleContentTypes, extension)); } return; @@ -550,17 +559,21 @@ namespace Microsoft.VisualStudio.Utilities.Implementation #region IFileExtensionRegistryService2 Members public IContentType GetContentTypeForFileName(string fileName) { - if (fileName == null) + ContentTypeImpl contentType = null; + + if (string.IsNullOrWhiteSpace(fileName)) { - throw new ArgumentNullException(nameof(fileName)); + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + } + else + { + this.BuildContentTypes(); + this.maps.FileNameToContentTypeMap.TryGetValue(fileName, out contentType); } - this.BuildContentTypes(); - - ContentTypeImpl contentType = null; - this.maps.FileNameToContentTypeMap.TryGetValue(fileName, out contentType); - - // TODO: should we return null if contentType is null? return contentType ?? ContentTypeRegistryImpl.UnknownContentTypeImpl; } @@ -572,21 +585,16 @@ namespace Microsoft.VisualStudio.Utilities.Implementation } // No need to lock, we are calling locking public method. - var fileNameContentType = this.GetContentTypeForFileName(name); + var contentType = this.GetContentTypeForFileName(name); // Attempt to use extension as fallback ContentType if file name isn't recognized. - if (fileNameContentType == ContentTypeRegistryImpl.UnknownContentTypeImpl) + if (contentType == ContentTypeRegistryImpl.UnknownContentTypeImpl) { var extension = Path.GetExtension(name); - - if (!string.IsNullOrEmpty(extension)) - { - // No need to lock, we are calling locking public method. - return this.GetContentTypeForExtension(extension); - } + contentType = this.GetContentTypeForExtension(extension); } - return fileNameContentType; + return contentType; } public IEnumerable<string> GetFileNamesForContentType(IContentType contentType) @@ -612,13 +620,13 @@ namespace Microsoft.VisualStudio.Utilities.Implementation { if (string.IsNullOrWhiteSpace(fileName)) { - throw new ArgumentException(nameof(fileName)); + throw new ArgumentException(nameof(fileName) + " cannot be null or whitespace"); } var contentTypeImpl = contentType as ContentTypeImpl; if ((contentTypeImpl == null) || (contentTypeImpl == UnknownContentTypeImpl)) { - throw new ArgumentException(nameof(contentType)); + throw new ArgumentException(nameof(contentType) + " cannot be null or unknown"); } this.BuildContentTypes(); @@ -631,9 +639,9 @@ namespace Microsoft.VisualStudio.Utilities.Implementation { if (type != contentTypeImpl) { - throw new InvalidOperationException - (String.Format(System.Globalization.CultureInfo.CurrentUICulture, - Strings.FileExtensionRegistry_NoMultipleContentTypes, fileName)); + throw new InvalidOperationException( + string.Format(System.Globalization.CultureInfo.CurrentCulture, + Strings.FileExtensionRegistry_NoMultipleContentTypes, fileName)); } return; @@ -688,7 +696,18 @@ namespace Microsoft.VisualStudio.Utilities.Implementation #endregion #region IFilePathRegistryService Members - public IContentType GetContentTypeForPath(string filePath) + public IContentType GetContentTypeForFilePath(string filePath) + { + return this.InternalGetContentTypeForPath(filePath) ?? this.GetContentTypeForFileNameOrExtension(Path.GetFileName(filePath)); + } + + public IContentType GetContentTypeForFilePathOnly(string filePath) + { + return this.InternalGetContentTypeForPath(filePath) ?? ContentTypeRegistryImpl.UnknownContentTypeImpl; + } + #endregion + + private IContentType InternalGetContentTypeForPath(string filePath) { if (filePath == null) { @@ -696,28 +715,32 @@ namespace Microsoft.VisualStudio.Utilities.Implementation } string fileName = Path.GetFileName(filePath); - string extension = Path.GetExtension(filePath); - var providers = OrderedFilePathToContentTypeProductions.Where(md => - (md.Metadata.FileExtension == null || md.Metadata.FileExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)) && - (md.Metadata.FileName == null || md.Metadata.FileName.Equals(fileName, StringComparison.OrdinalIgnoreCase))); + string extension = Path.GetExtension(fileName); - IContentType contentType = null; - foreach(var curProvider in providers) + for (int i = 0; (i < this.OrderedFilePathToContentTypeProductions.Count); ++i) { - if (curProvider.Value.TryGetContentTypeForFilePath(filePath, out IContentType curContentType)) + var provider = this.OrderedFilePathToContentTypeProductions[i]; + if (WildcardMatch(provider.Metadata.FileExtension, extension) || + ((provider.Metadata.FileName != null) && provider.Metadata.FileName.Equals(fileName, StringComparison.OrdinalIgnoreCase))) { - contentType = curContentType; - break; + if (provider.Value.TryGetContentTypeForFilePath(filePath, out IContentType contentType)) + { + return contentType; + } } } - return contentType ?? ContentTypeRegistryImpl.UnknownContentTypeImpl; + return null; + } + + private static bool WildcardMatch(string s1, string s2) + { + return (s1 != null) && ((s1.Equals("*", StringComparison.Ordinal)) || s1.Equals(s2, StringComparison.OrdinalIgnoreCase)); } - #endregion private static string RemoveExtensionDot(string extension) { - if (extension.StartsWith(".")) + if (extension.StartsWith(".", StringComparison.Ordinal)) { return extension.TrimStart('.'); } @@ -838,4 +861,13 @@ namespace Microsoft.VisualStudio.Utilities.Implementation #endregion } } + + public interface IFilePathToContentTypeMetadata : IOrderable + { + [System.ComponentModel.DefaultValue(null)] + string FileExtension { get; } + + [System.ComponentModel.DefaultValue(null)] + string FileName { get; } + } } diff --git a/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs deleted file mode 100644 index 4e3bbd9..0000000 --- a/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -// This file contain implementations details that are subject to change without notice. -// Use at your own risk. -// -namespace Microsoft.VisualStudio.Utilities.Implementation -{ - using System; - using System.Collections.Generic; - - public interface IFileExtensionToContentTypeMetadata - { - string FileExtension { get; } - IEnumerable<string> ContentTypes { get; } - } -} - diff --git a/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs deleted file mode 100644 index 48ec87a..0000000 --- a/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -// This file contain implementations details that are subject to change without notice. -// Use at your own risk. -// -namespace Microsoft.VisualStudio.Utilities.Implementation -{ - using System; - using System.Collections.Generic; - - public interface IFileNameToContentTypeMetadata - { - string FileName { get; } - IEnumerable<string> ContentTypes { get; } - } -} - diff --git a/src/Core/Impl/ContentType/IFilePathRegistryService.cs b/src/Core/Impl/ContentType/IFilePathRegistryService.cs deleted file mode 100644 index 451d95c..0000000 --- a/src/Core/Impl/ContentType/IFilePathRegistryService.cs +++ /dev/null @@ -1,12 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// - -namespace Microsoft.VisualStudio.Utilities -{ - public interface IFilePathRegistryService - { - IContentType GetContentTypeForPath(string filePath); - } -} diff --git a/src/Core/Impl/ContentType/IFilePathToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFilePathToContentTypeMetadata.cs deleted file mode 100644 index eb15212..0000000 --- a/src/Core/Impl/ContentType/IFilePathToContentTypeMetadata.cs +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -// This file contain implementations details that are subject to change without notice. -// Use at your own risk. -// - -namespace Microsoft.VisualStudio.Utilities.Implementation -{ - public interface IFilePathToContentTypeMetadata : IOrderable - { - [System.ComponentModel.DefaultValue(null)] - string FileExtension { get; } - - [System.ComponentModel.DefaultValue(null)] - string FileName { get; } - } -} diff --git a/src/Core/Impl/ContentType/IFilePathToContentTypeProvider.cs b/src/Core/Impl/ContentType/IFilePathToContentTypeProvider.cs deleted file mode 100644 index a4c624b..0000000 --- a/src/Core/Impl/ContentType/IFilePathToContentTypeProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// - -namespace Microsoft.VisualStudio.Utilities -{ - public interface IFilePathToContentTypeProvider - { - bool TryGetContentTypeForFilePath(string filePath, out IContentType contentType); - } -} diff --git a/src/Core/Impl/ContentType/StringToContentTypesMap.cs b/src/Core/Impl/ContentType/StringToContentTypesMap.cs deleted file mode 100644 index 52fae34..0000000 --- a/src/Core/Impl/ContentType/StringToContentTypesMap.cs +++ /dev/null @@ -1,129 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -// This file contain implementations details that are subject to change without notice. -// Use at your own risk. -// -namespace Microsoft.VisualStudio.Utilities.Implementation -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - - internal sealed class StringToContentTypesMap - { - // Map of extensions to their corresponding content types - private Dictionary<string, IContentType> stringMap; - - public StringToContentTypesMap(IEnumerable<Tuple<string, IContentType>> mappings) - { - - if (mappings == null) - { - throw new ArgumentNullException(nameof(mappings)); - } - - this.stringMap = new Dictionary<string, IContentType>(StringComparer.OrdinalIgnoreCase); - - foreach (var mapping in mappings) - { - // Any failures should ideally be logged somehow. - TryAddString(mapping.Item1, mapping.Item2); - } - } - - public IContentType GetContentTypeForString(string str) - { - if (str == null) - { - throw new ArgumentNullException(nameof(str)); - } - - IContentType contentType; - - if (!this.stringMap.TryGetValue(str, out contentType)) - { - return null; - } - - return contentType; - } - - public IEnumerable<string> GetStringsForContentType(IContentType contentType) - { - if (contentType == null) - { - throw new ArgumentNullException(nameof(contentType)); - } - - // Asymptotically slow, however, after searching the VS code base, we found that - // looking up extensions for ContentType is only used by tests, and is probably - // rarely used. This method used be backed by a second map for quick lookup, - // but for simplicity, we're going to move to a single map, barring any regressions. - return this.stringMap - .Where(pair => pair.Value == contentType) - .Select(pair => pair.Key); - } - - public void AddMapping(string str, IContentType contentType) - { - if (str == null) - { - throw new ArgumentNullException(nameof(str)); - } - - if (contentType == null) - { - throw new ArgumentNullException(nameof(contentType)); - } - - if (!TryAddString(str, contentType)) - { - throw new InvalidOperationException - (String.Format(System.Globalization.CultureInfo.CurrentUICulture, - Strings.FileExtensionRegistry_NoMultipleContentTypes, str)); - } - } - - public void RemoveMapping(string str) - { - if (str == null) - { - throw new ArgumentNullException(nameof(str)); - } - - this.stringMap.Remove(str); - } - - private bool TryAddString(string str, IContentType contentType) - { - if (str == null) - { - throw new ArgumentNullException(nameof(str)); - } - - if (contentType == null) - { - throw new ArgumentNullException(nameof(contentType)); - } - - // Check if the string is already registered - IContentType existingContentType; - if (this.stringMap.TryGetValue(str, out existingContentType)) - { - // Return false if there is a conflict. - return contentType == existingContentType; - } - else - { - // A new string - lets add it to the map - this.stringMap.Add(str, contentType); - } - - return true; - } - } -} - diff --git a/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs new file mode 100644 index 0000000..d947d61 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs @@ -0,0 +1,76 @@ +using System.Collections.Immutable; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Contains data of <see cref="IAsyncCompletionSession"/> valid at a specific, instantenous moment pertinent to current computation. + /// This data is passed to <see cref="IAsyncCompletionItemManager"/> to filter the list and select appropriate item. + /// </summary> + public class AsyncCompletionSessionDataSnapshot + { + /// <summary> + /// Set of <see cref="CompletionItem"/>s to filter and sort, originally returned from <see cref="IAsyncCompletionItemManager.SortCompletionListAsync"/>. + /// </summary> + public ImmutableArray<CompletionItem> InitialSortedList { get; } + + /// <summary> + /// The <see cref="ITextSnapshot"/> applicable for this computation. The snapshot comes from the view's data buffer. + /// </summary> + public ITextSnapshot Snapshot { get; } + + /// <summary> + /// The <see cref="InitialTrigger"/> that started this completion session. + /// </summary> + public InitialTrigger InitialTrigger { get; } + + /// <summary> + /// The <see cref="UpdateTrigger"/> for this update. + /// </summary> + public UpdateTrigger UpdateTrigger { get; } + + /// <summary> + /// Filters, their availability and selection state. + /// </summary> + public ImmutableArray<CompletionFilterWithState> SelectedFilters { get; } + + /// <summary> + /// Inidicates whether the session is using soft selection + /// </summary> + public bool IsSoftSelected { get; } + + /// <summary> + /// Inidicates whether the session displays a suggestion item. + /// </summary> + public bool DisplaySuggestionItem { get; } + + /// <summary> + /// Constructs <see cref="AsyncCompletionSessionDataSnapshot"/> + /// </summary> + /// <param name="initialSortedList">Set of <see cref="CompletionItem"/>s to filter and sort, originally returned from <see cref="IAsyncCompletionItemManager.SortCompletionListAsync"/></param> + /// <param name="snapshot">The <see cref="ITextSnapshot"/> applicable for this computation. The snapshot comes from the view's data buffer</param> + /// <param name="initialTrigger">The <see cref="InitialTrigger"/> that started this completion session</param> + /// <param name="updateTrigger">The <see cref="UpdateTrigger"/> for this update</param> + /// <param name="selectedFilters">Filters, their availability and selection state</param> + /// <param name="isSoftSelected">Inidicates whether the session is using soft selection</param> + /// <param name="displaySuggestionItem">Inidicates whether the session has a suggestion item</param> + public AsyncCompletionSessionDataSnapshot( + ImmutableArray<CompletionItem> initialSortedList, + ITextSnapshot snapshot, + InitialTrigger initialTrigger, + UpdateTrigger updateTrigger, + ImmutableArray<CompletionFilterWithState> selectedFilters, + bool isSoftSelected, + bool displaySuggestionItem + ) + { + InitialSortedList = initialSortedList; + Snapshot = snapshot; + InitialTrigger = initialTrigger; + UpdateTrigger = updateTrigger; + SelectedFilters = selectedFilters; + IsSoftSelected = isSoftSelected; + DisplaySuggestionItem = displaySuggestionItem; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs new file mode 100644 index 0000000..8b3f3dd --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs @@ -0,0 +1,44 @@ +using System.Collections.Immutable; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Contains data of <see cref="IAsyncCompletionSession"/> valid at a specific, instantenous moment pertinent to current computation. + /// This data is passed to <see cref="IAsyncCompletionItemManager"/> to initially sort the list prior to filtering and selecting. + /// </summary> + public class AsyncCompletionSessionInitialDataSnapshot + { + /// <summary> + /// Set of <see cref="CompletionItem"/>s to sort. + /// </summary> + public ImmutableArray<CompletionItem> InitialList { get; } + + /// <summary> + /// The <see cref="ITextSnapshot"/> applicable for this computation. The snapshot comes from the view's data buffer. + /// </summary> + public ITextSnapshot Snapshot { get; } + + /// <summary> + /// The <see cref="InitialTrigger"/> that started this completion session. + /// </summary> + public InitialTrigger InitialTrigger { get; } + + /// <summary> + /// Constructs <see cref="AsyncCompletionSessionInitialDataSnapshot"/> + /// </summary> + /// <param name="initialList">Set of <see cref="CompletionItem"/>s to sort</param> + /// <param name="snapshot">The <see cref="ITextSnapshot"/> applicable for this computation. The snapshot comes from the view's data buffer</param> + /// <param name="initialTrigger">The <see cref="InitialTrigger"/> that started this completion session</param> + public AsyncCompletionSessionInitialDataSnapshot( + ImmutableArray<CompletionItem> initialList, + ITextSnapshot snapshot, + InitialTrigger initialTrigger + ) + { + InitialList = initialList; + Snapshot = snapshot; + InitialTrigger = initialTrigger; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs b/src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs new file mode 100644 index 0000000..659fa2a --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs @@ -0,0 +1,38 @@ +using System; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Instructs the editor how to behave after committing a <see cref="CompletionItem"/>. + /// </summary> + [Flags] +#pragma warning disable CA1714 // Flags enums should have plural names + public enum CommitBehavior +#pragma warning restore CA1714 // Flags enums should have plural names + { + /// <summary> + /// Use the default behavior, + /// that is, to propagate TypeChar command, but surpress ReturnKey and TabKey commands. + /// </summary> + None = 0b0000, + + /// <summary> + /// Surpresses further invocation of the TypeChar command handlers. + /// By default, editor invokes these command handlers to enable features such as brace completion. + /// </summary> + SuppressFurtherTypeCharCommandHandlers = 0b0001, + + /// <summary> + /// Raises further invocation of the ReturnKey and Tab command handlers. + /// By default, editor doesn't invoke ReturnKey and Tab command handlers after committing completion session. + /// </summary> + RaiseFurtherReturnKeyAndTabKeyCommandHandlers = 0b0010, + + /// <summary> + /// Cancels the commit operation, does not call any other <see cref="IAsyncCompletionCommitManager.TryCommit(Text.Editor.ITextView, Text.ITextBuffer, CompletionItem, Text.ITrackingSpan, char, System.Threading.CancellationToken)"/>. + /// Functionally, acts as if the typed character was not a commit character, + /// allowing the user to continue working with the <see cref="IAsyncCompletionSession"/> + /// </summary> + CancelCommit = 0b0100, + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CommitResult.cs b/src/Language/Def/Language/AsyncCompletion/Data/CommitResult.cs new file mode 100644 index 0000000..5b7af68 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CommitResult.cs @@ -0,0 +1,53 @@ +using System; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Tracks whether the commit occured, and provides instructions for behavior after committing. + /// </summary> + public struct CommitResult : IEquatable<CommitResult> + { + /// <summary> + /// Marks this commit as handled. No other <see cref="IAsyncCompletionCommitManager"/> will be asked to commit. + /// </summary> + public readonly static CommitResult Handled = new CommitResult(isHandled: true, behavior: CommitBehavior.None); + + /// <summary> + /// Marks this commit as unhandled. Another <see cref="IAsyncCompletionCommitManager"/> will be asked to commit. + /// </summary> + public readonly static CommitResult Unhandled = new CommitResult(isHandled: false, behavior: CommitBehavior.None); + + /// <summary> + /// Whether the commit occured. + /// If true, no other <see cref="IAsyncCompletionCommitManager"/> will be asked to commit. + /// If false, another <see cref="IAsyncCompletionCommitManager"/> will be asked to commit. + /// </summary> + public bool IsHandled { get; } + + /// <summary> + /// Desired behavior after committing, respected even when <see cref="IsHandled"/> is unset. + /// </summary> + public CommitBehavior Behavior { get; } + + /// <summary> + /// Creates a <see cref="CommitResult"/> with specified properties. + /// </summary> + /// <param name="isHandled">Whether the commit occured</param> + /// <param name="behavior">Desired behavior after committing</param> + public CommitResult(bool isHandled, CommitBehavior behavior) + { + IsHandled = isHandled; + Behavior = behavior; + } + + bool IEquatable<CommitResult>.Equals(CommitResult other) => IsHandled.Equals(other.IsHandled) && Behavior.Equals(other.Behavior); + + public override bool Equals(object other) => (other is CommitResult otherCR) ? ((IEquatable<CommitResult>)this).Equals(otherCR) : false; + + public static bool operator ==(CommitResult left, CommitResult right) => left.Equals(right); + + public static bool operator !=(CommitResult left, CommitResult right) => !(left == right); + + public override int GetHashCode() => (Behavior.GetHashCode() << 1) | (IsHandled ? 1 : 0); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionClosedEventArgs.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionClosedEventArgs.cs new file mode 100644 index 0000000..17cccd6 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionClosedEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class is used to notify completion's logic when the UI closes + /// </summary> + public sealed class CompletionClosedEventArgs : EventArgs + { + /// <summary> + /// <see cref="ITextView"/> that hosted completion UI + /// </summary> + public ITextView TextView { get; } + + /// <summary> + /// Constructs instance of <see cref="CompletionClosedEventArgs"/>. + /// </summary> + /// <param name="textView"><see cref="ITextView"/> that hosted this completion UI</param> + public CompletionClosedEventArgs(ITextView textView) + { + TextView = textView; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionContext.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionContext.cs new file mode 100644 index 0000000..d098d3a --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionContext.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This type is used to transfer data from <see cref="IAsyncCompletionSource"/> + /// to <see cref="IAsyncCompletionBroker"/> and further to <see cref="IAsyncCompletionItemManager"/> + /// </summary> + [DebuggerDisplay("{Items.Length} items")] + public sealed class CompletionContext + { + /// <summary> + /// Empty completion context, when <see cref="IAsyncCompletionSource"/> offers no items pertinent to given location. + /// </summary> + public static CompletionContext Empty { get; } = new CompletionContext(ImmutableArray<CompletionItem>.Empty); + + /// <summary> + /// Set of completion items available at a location + /// </summary> + public ImmutableArray<CompletionItem> Items { get; } + + /// <summary> + /// Recommends the initial selection method for the completion list. + /// When <see cref="SuggestionItemOptions"/> is defined, "soft selection" will be used without a need to set this property. + /// </summary> + public InitialSelectionHint SelectionHint { get; } + + /// <summary> + /// When defined, uses suggestion mode with options specified in this object. + /// When null, this context does not activate the suggestion mode. + /// Suggestion mode puts selection in "soft selection" mode without need to set <see cref="SelectionHint"/> + /// </summary> + public SuggestionItemOptions SuggestionItemOptions { get; } + + /// <summary> + /// Constructs <see cref="CompletionContext"/> with specified <see cref="CompletionItem"/>s, + /// with recommendation to not use suggestion mode and to use use regular selection. + /// </summary> + /// <param name="items">Available completion items. If none are available, use <code>CompletionContext.Default</code></param> + public CompletionContext(ImmutableArray<CompletionItem> items) + : this(items, suggestionItemOptions: null, selectionHint: InitialSelectionHint.RegularSelection) + { + } + + /// <summary> + /// Constructs <see cref="CompletionContext"/> with specified <see cref="CompletionItem"/>s, + /// with recommendation to use suggestion mode and to use regular selection. + /// </summary> + /// <param name="items">Available completion items</param> + /// <param name="suggestionItemOptions">Suggestion item options, or null to not use suggestion mode. Default is <code>null</code></param> + public CompletionContext( + ImmutableArray<CompletionItem> items, + SuggestionItemOptions suggestionItemOptions) + : this(items, suggestionItemOptions, InitialSelectionHint.RegularSelection) + { + } + + /// <summary> + /// Constructs <see cref="CompletionContext"/> with specified <see cref="CompletionItem"/>s, + /// with recommendation to use suggestion mode item and to use a specific selection mode. + /// </summary> + /// <param name="items">Available completion items</param> + /// <param name="suggestionItemOptions">Suggestion mode options, or null to not use suggestion mode. Default is <code>null</code></param> + /// <param name="selectionHint">Recommended selection mode. Suggestion mode automatically sets soft selection Default is <code>InitialSelectionHint.RegularSelection</code></param> + public CompletionContext( + ImmutableArray<CompletionItem> items, + SuggestionItemOptions suggestionItemOptions, + InitialSelectionHint selectionHint) + { + if (items.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(items)); + Items = items; + SelectionHint = selectionHint; + SuggestionItemOptions = suggestionItemOptions; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilter.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilter.cs new file mode 100644 index 0000000..b9e2e1c --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilter.cs @@ -0,0 +1,57 @@ +using System; +using System.Diagnostics; +using Microsoft.VisualStudio.Text.Adornments; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Identifies a filter that toggles exclusive display of <see cref="CompletionItem"/>s that reference it. + /// </summary> + /// <remarks> + /// These instances should be singletons. All <see cref="CompletionItem"/>s that should be filtered + /// using the same filter button must use the same reference to the instance of <see cref="CompletionFilter"/>. + /// </remarks> + /// <example> + /// static CompletionFilter MyFilter = new CompletionFilter("My items", "m", MyItemsImageElement); + /// </example> + [DebuggerDisplay("{DisplayText}")] + public sealed class CompletionFilter + { + /// <summary> + /// Localized name of this filter. + /// </summary> + public string DisplayText { get; } + + /// <summary> + /// Key used in a keyboard shortcut that toggles this filter. + /// </summary> + public string AccessKey { get; } + + /// <summary> + /// <see cref="ImageElement"/> that represents this filter. + /// </summary> + public ImageElement Image { get; } + + /// <summary> + /// Constructs an instance of CompletionFilter. + /// </summary> + /// <param name="displayText">Name of this filter</param> + /// <param name="accessKey">Key used in a keyboard shortcut that toggles this filter.</param> + /// <param name="image">Image that represents this filter</param> + public CompletionFilter(string displayText, string accessKey, ImageElement image) + { + if (string.IsNullOrWhiteSpace(displayText)) + { + throw new ArgumentException("Display text must be non-empty", nameof(displayText)); + } + if (string.IsNullOrWhiteSpace(accessKey)) + { + throw new ArgumentException("Access key must be non-empty", nameof(accessKey)); + } + + DisplayText = displayText; + AccessKey = accessKey; + Image = image; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterEventArgs.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterEventArgs.cs new file mode 100644 index 0000000..e8bea33 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterEventArgs.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class is used to notify completion's logic of selection change in the filter UI + /// </summary> + public sealed class CompletionFilterChangedEventArgs : EventArgs + { + /// <summary> + /// Current state of the filters + /// </summary> + public ImmutableArray<CompletionFilterWithState> Filters { get; } + + /// <summary> + /// Constructs instance of <see cref="CompletionFilterChangedEventArgs"/>. + /// </summary> + /// <param name="filters">Current state of the filters</param> + public CompletionFilterChangedEventArgs( + ImmutableArray<CompletionFilterWithState> filters) + { + if (filters.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(filters)); + this.Filters = filters; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs new file mode 100644 index 0000000..7712e93 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs @@ -0,0 +1,82 @@ +using System; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Immutable data transfer object used to communicate between the completion session and completion UI + /// </summary> + public sealed class CompletionFilterWithState + { + /// <summary> + /// Reference to the completion filter + /// </summary> + public CompletionFilter Filter { get; } + + /// <summary> + /// Whether the filter is available. + /// Filter should be available when there are visible <see cref="CompletionItem"/>s that define this <see cref="Filter"/> in their <see cref="CompletionItem.Filters"/> + /// </summary> + public bool IsAvailable { get; } + + /// <summary> + /// Whether the filter is selected by the user. + /// </summary> + public bool IsSelected { get; } + + /// <summary> + /// Constructs a new instance of <see cref="CompletionFilterWithState"/>. + /// </summary> + /// <param name="filter">Reference to <see cref="CompletionFilter"/></param> + /// <param name="isAvailable">Whether this <see cref="CompletionFilter"/> is available</param> + public CompletionFilterWithState(CompletionFilter filter, bool isAvailable) + : this(filter, isAvailable, isSelected: false) + { } + + /// <summary> + /// Constructs a new instance of <see cref="CompletionFilterWithState"/> when selected state is known. + /// </summary> + /// <param name="filter">Reference to <see cref="CompletionFilter"/></param> + /// <param name="isAvailable">Whether this <see cref="CompletionFilter"/> is available</param> + /// <param name="isSelected">Whether this <see cref="CompletionFilter"/> is selected</param> + public CompletionFilterWithState(CompletionFilter filter, bool isAvailable, bool isSelected) + { + Filter = filter ?? throw new ArgumentNullException(nameof(filter)); + IsAvailable = isAvailable; + IsSelected = isSelected; + } + + /// <summary> + /// Returns instance of <see cref="CompletionFilterWithState"/> with specified <see cref="IsAvailable"/> + /// </summary> + /// <param name="isAvailable">Value to use for <see cref="IsAvailable"/></param> + /// <returns>Updated instance of <see cref="CompletionFilterWithState"/></returns> + public CompletionFilterWithState WithAvailability(bool isAvailable) + { + return this.IsAvailable == isAvailable + ? this + : new CompletionFilterWithState(Filter, isAvailable, IsSelected); + } + + /// <summary> + /// Returns instance of <see cref="CompletionFilterWithState"/> with specified <see cref="IsSelected"/> + /// </summary> + /// <param name="availability">Value to use for <see cref="IsSelected"/></param> + /// <returns>Updated instance of <see cref="CompletionFilterWithState"/></returns> + public CompletionFilterWithState WithSelected(bool isSelected) + { + return this.IsSelected == isSelected + ? this + : new CompletionFilterWithState(Filter, IsAvailable, isSelected); + } + + /// <summary> + /// Override for nice debugger display + /// </summary> + public override string ToString() + { + var availableStatus = IsAvailable ? "available" : "unavailable"; + var selectedStatus = IsSelected ? "selected" : "not selected"; + return $"{Filter.DisplayText} - {availableStatus}, {selectedStatus}"; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionItem.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItem.cs new file mode 100644 index 0000000..6168457 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItem.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.VisualStudio.Core.Imaging; +using Microsoft.VisualStudio.Text.Adornments; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class, returned from <see cref="IAsyncCompletionSource"/>, represents a single entry + /// to be displayed in the completion UI. This class implements <see cref="IPropertyOwner"/> + /// </summary> + [DebuggerDisplay("{DisplayText}")] + public sealed class CompletionItem : IPropertyOwner + { + /// <summary> + /// Text used in the UI + /// </summary> + public string DisplayText { get; } + + /// <summary> + /// Text inserted when completing this item + /// </summary> + public string InsertText { get; } + + /// <summary> + /// Text used by <see cref="IAsyncCompletionItemManager"/> when sorting against other items + /// </summary> + public string SortText { get; } + + /// <summary> + /// Text used by <see cref="IAsyncCompletionItemManager"/> when matching against the applicable span + /// </summary> + public string FilterText { get; } + + /// <summary> + /// Reference to the instance that will provide tooltip and custom commit method. + /// This should be the same instance as the one that created this <see cref="CompletionItem"/> + /// </summary> + public IAsyncCompletionSource Source { get; } + + /// <summary> + /// <see cref="ImmutableArray"/> of references to <see cref="CompletionFilter"/>s applicable to this item + /// </summary> + public ImmutableArray<CompletionFilter> Filters { get; } + + /// <summary> + /// Image displayed in the UI + /// </summary> + public ImageElement Icon { get; } + + /// <summary> + /// Additional text to display in the UI, after <see cref="DisplayText"/>. + /// This text has less emphasis than <see cref="DisplayText"/> and is usually right-aligned. + /// </summary> + public string Suffix { get; } + + /// <summary> + /// Additional images to display in the UI. + /// Usually, these images are displayed on the far right side of the UI. + /// </summary> + public ImmutableArray<ImageElement> AttributeIcons { get; } + + /// <summary> + /// The collection of properties controlled by the property owner. See <see cref="IPropertyOwner.Properties"/> + /// </summary> + public PropertyCollection Properties { get; } + + /// <summary> + /// Creates a completion item whose <see cref="DisplayText"/>, <see cref="InsertText"/>, <see cref="SortText"/> and <see cref="FilterText"/> are all the same, + /// and there are no icon, filter, suffix nor attribute icons associated with this item. + /// </summary> + /// <param name="displayText">Text to use in the UI, when sorting, filtering and completing</param> + /// <param name="source">Reference to <see cref="IAsyncCompletionSource"/> that created this item</param> + public CompletionItem(string displayText, IAsyncCompletionSource source) + : this(displayText, insertText: displayText, sortText: displayText, filterText: displayText, + source: source, filters: ImmutableArray<CompletionFilter>.Empty, icon: default(ImageElement), + suffix: string.Empty, attributeIcons: ImmutableArray<ImageElement>.Empty) + { + } + + /// <summary> + /// Creates a completion item whose <see cref="DisplayText"/>, <see cref="InsertText"/>, <see cref="SortText"/> and <see cref="FilterText"/> are all the same, + /// there is an image, and there are no filter, suffix nor attribute images associated with this item. + /// </summary> + /// <param name="displayText">Text to use in the UI, when sorting, filtering and completing</param> + /// <param name="source">Reference to <see cref="IAsyncCompletionSource"/> that created this item</param> + /// <param name="icon">Image displayed in the UI. Default is <code>default(ImageElement)</code></param> + public CompletionItem(string displayText, IAsyncCompletionSource source, ImageElement icon) + : this(displayText, insertText: displayText, sortText: displayText, filterText: displayText, + source: source, filters: ImmutableArray<CompletionFilter>.Empty, icon: icon, + suffix: string.Empty, attributeIcons: ImmutableArray<ImageElement>.Empty) + { + } + + /// <summary> + /// Creates a completion item whose <see cref="DisplayText"/>, <see cref="InsertText"/>, <see cref="SortText"/> and <see cref="FilterText"/> are all the same, + /// there is an image, filters, and there are no suffix nor attribute images associated with this item. + /// </summary> + /// <param name="displayText">Text to use in the UI, when sorting, filtering and completing</param> + /// <param name="source">Reference to <see cref="IAsyncCompletionSource"/> that created this item</param> + /// <param name="icon">Image displayed in the UI</param> + /// <param name="filters"><see cref="ImmutableArray"/> of references to <see cref="CompletionFilter"/>s applicable to this item. Default is <code>ImmutableArray<CompletionFilter>.Empty</code></param> + public CompletionItem(string displayText, IAsyncCompletionSource source, ImageElement icon, ImmutableArray<CompletionFilter> filters) + : this(displayText, insertText: displayText, sortText: displayText, filterText: displayText, + source: source, filters: filters, icon: icon, + suffix: string.Empty, attributeIcons: ImmutableArray<ImageElement>.Empty) + { + } + + /// <summary> + /// Creates a completion item whose <see cref="DisplayText"/>, <see cref="InsertText"/>, <see cref="SortText"/> and <see cref="FilterText"/> are all the same, + /// there is an image, filters, suffix, and there are no attribute images associated with this item. + /// </summary> + /// <param name="displayText">Text to use in the UI, when sorting, filtering and completing</param> + /// <param name="source">Reference to <see cref="IAsyncCompletionSource"/> that created this item</param> + /// <param name="icon">Image displayed in the UI</param> + /// <param name="filters"><see cref="ImmutableArray"/> of references to <see cref="CompletionFilter"/>s applicable to this item</param> + /// <param name="suffix">Additional text to display in the UI. Default is <code>string.Empty</code></param> + public CompletionItem(string displayText, IAsyncCompletionSource source, ImageElement icon, ImmutableArray<CompletionFilter> filters, string suffix) + : this(displayText, insertText: displayText, sortText: displayText, filterText: displayText, + source: source, filters: filters, icon: icon, + suffix: suffix, attributeIcons: ImmutableArray<ImageElement>.Empty) + { + } + + /// <summary> + /// Creates a completion item, allowing customization of all of its properties. + /// </summary> + /// <param name="displayText">Text used in the UI</param> + /// <param name="source">Reference to <see cref="IAsyncCompletionSource"/> that created this item</param> + /// <param name="icon">Image displayed in the UI. Default is <code>default(ImageElement)</code></param> + /// <param name="filters"><see cref="ImmutableArray"/> of references to <see cref="CompletionFilter"/>s applicable to this item. Default is <code>ImmutableArray<CompletionFilter>.Empty</code></param> + /// <param name="suffix">Additional text to display in the UI. Default is <code>string.Empty</code></param> + /// <param name="insertText">Text inserted when completing this item. Default is <see cref="displayText"/></param> + /// <param name="sortText">Text used by <see cref="IAsyncCompletionItemManager"/> when sorting against other items. Default is <see cref="displayText"/></param> + /// <param name="filterText">Text used by <see cref="IAsyncCompletionItemManager"/> when matching against the applicable span. Default is <see cref="displayText"/></param> + /// <param name="attributeIcons">Additional images to display in the UI. Default is <code>ImmutableArray<ImageElement>.Empty</code></param> + public CompletionItem(string displayText, IAsyncCompletionSource source, ImageElement icon, ImmutableArray<CompletionFilter> filters, + string suffix, string insertText, string sortText, string filterText, ImmutableArray<ImageElement> attributeIcons) + { + if (displayText == null) + displayText = string.Empty; + if (insertText == null) + insertText = string.Empty; + if (sortText == null) + sortText = string.Empty; + if (filterText == null) + filterText = string.Empty; + if (filters.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(filters)); + + DisplayText = displayText; + InsertText = insertText; + SortText = sortText; + FilterText = filterText; + Source = source ?? throw new ArgumentNullException(nameof(source)); + Icon = icon; + Filters = filters; + Suffix = suffix; + AttributeIcons = attributeIcons; + Properties = new PropertyCollection(); + } + + public override string ToString() => DisplayText; + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemEventArgs.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemEventArgs.cs new file mode 100644 index 0000000..1fda80f --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class is used to notify of an operation that affects a single <see cref="CompletionItem"/>. + /// </summary> + [DebuggerDisplay("EventArgs: {Item}")] + public sealed class CompletionItemEventArgs : EventArgs + { + /// <summary> + /// Relevant item + /// </summary> + public CompletionItem Item { get; } + + /// <summary> + /// Constructs instance of <see cref="CompletionItemEventArgs"/>. + /// </summary> + public CompletionItemEventArgs(CompletionItem item) + { + this.Item = item ?? throw new ArgumentNullException(nameof(item)); + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemSelectedEventArgs.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemSelectedEventArgs.cs new file mode 100644 index 0000000..3844800 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemSelectedEventArgs.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class is used to notify completion's logic of selecting through the UI + /// </summary> + [DebuggerDisplay("EventArgs: {SelectedItem}, is suggestion: {SuggestionItemSelected}")] + public sealed class CompletionItemSelectedEventArgs : EventArgs + { + /// <summary> + /// Selected item. Might be null if there is no selection + /// </summary> + public CompletionItem SelectedItem { get; } + + /// <summary> + /// Whether selected item is a suggestion mode item + /// </summary> + public bool SuggestionItemSelected { get; } + + /// <summary> + /// Constructs instance of <see cref="CompletionItemSelectedEventArgs"/>. + /// </summary> + /// <param name="selectedItem">User-selected item</param> + /// <param name="suggestionItemSelected">Whether the selected item is a suggestion item</param> + public CompletionItemSelectedEventArgs(CompletionItem selectedItem, bool suggestionItemSelected) + { + this.SelectedItem = selectedItem; + this.SuggestionItemSelected = suggestionItemSelected; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs new file mode 100644 index 0000000..0591dd3 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Wraps <see cref="CompletionItem"/> with information about highlighted parts of its <see cref="CompletionItem.DisplayText"/>. + /// </summary> + [DebuggerDisplay("{CompletionItem}")] + public struct CompletionItemWithHighlight : IEquatable<CompletionItemWithHighlight> + { + /// <summary> + /// The completion item + /// </summary> + public CompletionItem CompletionItem { get; } + + /// <summary> + /// Which parts of <see cref="CompletionItem.DisplayText"/> to highlight + /// </summary> + public ImmutableArray<Span> HighlightedSpans { get; } + + /// <summary> + /// Constructs <see cref="CompletionItemWithHighlight"/> without any highlighting. + /// Used when the <see cref="CompletionItem"/> appears in the completion list without being a text match. + /// </summary> + /// <param name="completionItem">Instance of the <see cref="CompletionItem"/></param> + public CompletionItemWithHighlight(CompletionItem completionItem) + : this (completionItem, ImmutableArray<Span>.Empty) + { + } + + /// <summary> + /// Constructs <see cref="CompletionItemWithHighlight"/> with given highlighting. + /// Used when text used to filter the completion list can be found in the <see cref="CompletionItem.DisplayText"/>. + /// </summary> + /// <param name="completionItem">Instance of the <see cref="CompletionItem"/></param> + /// <param name="highlightedSpans"><see cref="Span"/>s of <see cref="CompletionItem.DisplayText"/> to highlight</param> + public CompletionItemWithHighlight(CompletionItem completionItem, ImmutableArray<Span> highlightedSpans) + { + CompletionItem = completionItem ?? throw new ArgumentNullException(nameof(completionItem)); + if (highlightedSpans.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(highlightedSpans)); + HighlightedSpans = highlightedSpans; + } + + bool IEquatable<CompletionItemWithHighlight>.Equals(CompletionItemWithHighlight other) + => CompletionItem != null && CompletionItem.Equals(other.CompletionItem) && HighlightedSpans.Equals(other.HighlightedSpans); + + public override bool Equals(object other) => (other is CompletionItemWithHighlight otherItem) ? ((IEquatable<CompletionItemWithHighlight>)this).Equals(otherItem) : false; + + public static bool operator ==(CompletionItemWithHighlight left, CompletionItemWithHighlight right) => left.Equals(right); + + public static bool operator !=(CompletionItemWithHighlight left, CompletionItemWithHighlight right) => !(left == right); + + public override int GetHashCode() => CompletionItem.GetHashCode() ^ HighlightedSpans.GetHashCode(); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemsWithHighlightEventArgs.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemsWithHighlightEventArgs.cs new file mode 100644 index 0000000..6bc01cc --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemsWithHighlightEventArgs.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class is used to notify of an operation that affects multiple <see cref="CompletionItemWithHighlight"/>s. + /// </summary> + [DebuggerDisplay("EventArgs: {Items.Length} items")] + public sealed class ComputedCompletionItemsEventArgs : EventArgs + { + /// <summary> + /// Relevant items + /// </summary> + public ComputedCompletionItems Items { get; } + + /// <summary> + /// Constructs instance of <see cref="CompletionItemEventArgs"/>. + /// </summary> + public ComputedCompletionItemsEventArgs(ComputedCompletionItems items) + { + Items = items ?? throw new ArgumentNullException(nameof(items)); + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionPresentationViewModel.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionPresentationViewModel.cs new file mode 100644 index 0000000..96baa4f --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionPresentationViewModel.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class contains completion items, filters and other pieces of information + /// used by <see cref="ICompletionPresenter"/> to render the completion UI. + /// </summary> + public sealed class CompletionPresentationViewModel + { + /// <summary> + /// Completion items to display with their highlighted spans. + /// </summary> + public ImmutableArray<CompletionItemWithHighlight> Items { get; } + + /// <summary> + /// Completion filters with their available and selected state. + /// </summary> + public ImmutableArray<CompletionFilterWithState> Filters { get; } + + /// <summary> + /// Span pertinent to the completion session. + /// </summary> + public ITrackingSpan ApplicableToSpan { get; } + + /// <summary> + /// Controls whether selected item should be soft selected. + /// </summary> + public bool UseSoftSelection { get; } + + /// <summary> + /// Controls whether suggestion item is visible. + /// </summary> + public bool DisplaySuggestionItem { get; } + + /// <summary> + /// Controls whether suggestion item is selected. + /// </summary> + public bool SelectSuggestionItem { get; } + + /// <summary> + /// Controls which item is selected. Use -1 in suggestion mode. + /// </summary> + public int SelectedItemIndex { get; } + + /// <summary> + /// Suggestion item to display when <see cref="DisplaySuggestionItem"/> is set. + /// </summary> + public CompletionItem SuggestionItem { get; } + + /// <summary> + /// How to display the <see cref="SuggestionItem"/>. + /// </summary> + public SuggestionItemOptions SuggestionItemOptions { get; } + + /// <summary> + /// Constructs <see cref="CompletionPresentationViewModel"/> + /// </summary> + /// <param name="items">Completion items to display with their highlighted spans</param> + /// <param name="filters">Completion filters with their available and selected state</param> + /// <param name="selectedItemIndex">Controls which item is selected. Use -1 in suggestion mode</param> + /// <param name="applicableToSpan">Span pertinent to the completion session</param> + /// <param name="useSoftSelection">Controls whether selected item should be soft selected. Default is <code>false</code></param> + /// <param name="displaySuggestionItem">Controls whether suggestion mode item is visible. Default is <code>false</code></param> + /// <param name="selectSuggestionItem">Controls whether suggestion mode item is selected. Default is <code>false</code></param> + /// <param name="suggestionItem">Suggestion mode item to display. Default is <code>null</code></param> + /// <param name="suggestionItemOptions">How to present the suggestion mode item. This is required because completion may be in suggestion mode even if there is no explicit suggestion mode item</param> + public CompletionPresentationViewModel( + ImmutableArray<CompletionItemWithHighlight> items, + ImmutableArray<CompletionFilterWithState> filters, + int selectedItemIndex, + ITrackingSpan applicableToSpan, + bool useSoftSelection, + bool displaySuggestionItem, + bool selectSuggestionItem, + CompletionItem suggestionItem, + SuggestionItemOptions suggestionItemOptions) + { + if (selectedItemIndex < -1) + throw new ArgumentOutOfRangeException(nameof(selectedItemIndex), "Selected index value must be greater than or equal to 0, or -1 to indicate no selection"); + if (items.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(items)); + if (filters.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(filters)); + + Items = items; + Filters = filters; + ApplicableToSpan = applicableToSpan ?? throw new NullReferenceException(nameof(applicableToSpan)); + UseSoftSelection = useSoftSelection; + DisplaySuggestionItem = displaySuggestionItem; + SelectSuggestionItem = selectSuggestionItem; + SelectedItemIndex = selectedItemIndex; + SuggestionItem = suggestionItem; + SuggestionItemOptions = suggestionItemOptions ?? throw new NullReferenceException(nameof(suggestionItemOptions)); + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionPresenterOptions.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionPresenterOptions.cs new file mode 100644 index 0000000..0ca2c44 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionPresenterOptions.cs @@ -0,0 +1,25 @@ +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Contains additional properties of thie <see cref="ICompletionPresenter"/> that may be accessed + /// prior to initializing an instance of <see cref="ICompletionPresenter"/> + /// </summary> + public sealed class CompletionPresenterOptions + { + /// <summary> + /// Declares the length of the jump when user presses PageUp and PageDown keys. + /// </summary> + /// <remarks>This value needs to be known before the UI is created, hence it is defined in this class instead of <see cref="ICompletionPresenter"/>. + /// Note that <see cref="IAsyncCompletionSession"/> handles keyboard scrolling, including using PageUp and PageDown keys.</remarks> + public int ResultsPerPage { get; } + + /// <summary> + /// Constructs instance of <see cref="CompletionPresenterOptions"/> + /// </summary> + /// <param name="resultsPerPage">Declares the length of the jump when user presses PageUp and PageDown keys</param> + public CompletionPresenterOptions(int resultsPerPage) + { + ResultsPerPage = resultsPerPage; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionTriggeredEventArgs.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionTriggeredEventArgs.cs new file mode 100644 index 0000000..9f67b01 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionTriggeredEventArgs.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class is used to notify about new <see cref="IAsyncCompletionSession"/> being triggered + /// </summary> + public sealed class CompletionTriggeredEventArgs : EventArgs + { + /// <summary> + /// Newly created <see cref="IAsyncCompletionSession"/>. + /// </summary> + public IAsyncCompletionSession CompletionSession { get; } + + /// <summary> + /// <see cref="ITextView"/> where completion was triggered. + /// </summary> + public ITextView TextView { get; } + + /// <summary> + /// Constructs instance of <see cref="CompletionItemSelectedEventArgs"/>. + /// </summary> + /// <param name="completionSession">Newly created <see cref="IAsyncCompletionSession"/></param> + /// <param name="textView"><see cref="ITextView"/> where completion was triggered</param> + public CompletionTriggeredEventArgs(IAsyncCompletionSession completionSession, ITextView textView) + { + this.CompletionSession = completionSession; + this.TextView = textView; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/ComputedCompletionItems.cs b/src/Language/Def/Language/AsyncCompletion/Data/ComputedCompletionItems.cs new file mode 100644 index 0000000..463b5f7 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/ComputedCompletionItems.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Stores information on computed <see cref="CompletionItem"/>s and their selection information. + /// </summary> + public sealed class ComputedCompletionItems + { + /// <summary> + /// Constructs instance of <see cref="ComputedCompletionItems"/> with recently computed + /// <see cref="CompletionItem"/>s and their selection infomration. + /// </summary> + /// <param name="items"><see cref="CompletionItem"/>s displayed in the completion UI</param> + /// <param name="suggestionItem">Suggestion <see cref="CompletionItem"/> displayed in the UI, or null if no suggestion is displayed</param> + /// <param name="selectedItem">Currently selected <see cref="CompletionItem"/></param> + /// <param name="suggestionItemSelected">Whether <see cref="SelectedItem"/> is a suggestion item</param> + /// <param name="usesSoftSelection">Whether <see cref="SelectedItem"/> is soft selected.</param> + public ComputedCompletionItems( + ImmutableArray<CompletionItem> items, + CompletionItem suggestionItem, + CompletionItem selectedItem, + bool suggestionItemSelected, + bool usesSoftSelection) + { + _items = items; + SuggestionItem = suggestionItem; + SelectedItem = selectedItem; + SuggestionItemSelected = suggestionItemSelected; + UsesSoftSelection = usesSoftSelection; + } + + /// <summary> + /// Constructs instance of <see cref="ComputedCompletionItems"/> with recently computed + /// <see cref="CompletionItemWithHighlight"/>s and their selection infomration. + /// </summary> + /// <param name="itemsWithHighlight"><see cref="CompletionItemWithHighlight"/>s displayed in the completion UI</param> + /// <param name="suggestionItem">Suggestion <see cref="CompletionItem"/> displayed in the UI, or null if no suggestion is displayed</param> + /// <param name="selectedItem">Currently selected <see cref="CompletionItem"/></param> + /// <param name="suggestionItemSelected">Whether <see cref="SelectedItem"/> is a suggestion item</param> + /// <param name="usesSoftSelection">Whether <see cref="SelectedItem"/> is soft selected.</param> + public ComputedCompletionItems( + ImmutableArray<CompletionItemWithHighlight> itemsWithHighlight, + CompletionItem suggestionItem, + CompletionItem selectedItem, + bool suggestionItemSelected, + bool usesSoftSelection) + { + _itemsWithHighlight = itemsWithHighlight; + SuggestionItem = suggestionItem; + SelectedItem = selectedItem; + SuggestionItemSelected = suggestionItemSelected; + UsesSoftSelection = usesSoftSelection; + } + + /// <summary> + /// Empty data structure, used when no computation was performed + /// </summary> + public static ComputedCompletionItems Empty { get; } = new ComputedCompletionItems(ImmutableArray<CompletionItem>.Empty, null, null, false, false); + + private IEnumerable<CompletionItem> _items = null; + private IEnumerable<CompletionItemWithHighlight> _itemsWithHighlight = null; + + /// <summary> + /// <see cref="CompletionItem"/>s displayed in the completion UI + /// </summary> + public IEnumerable<CompletionItem> Items => _items ?? _itemsWithHighlight.Select(n => n.CompletionItem); + + /// <summary> + /// Suggestion <see cref="CompletionItem"/> displayed in the UI, or null if no suggestion is displayed + /// </summary> + public CompletionItem SuggestionItem { get; } + + /// <summary> + /// Currently selected <see cref="CompletionItem"/> + /// </summary> + public CompletionItem SelectedItem { get; } + + /// <summary> + /// Whether <see cref="SelectedItem"/> is a suggestion item + /// </summary> + public bool SuggestionItemSelected { get; } + + /// <summary> + /// Whether <see cref="SelectedItem"/> is soft selected. + /// </summary> + public bool UsesSoftSelection { get; } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/FilteredCompletionModel.cs b/src/Language/Def/Language/AsyncCompletion/Data/FilteredCompletionModel.cs new file mode 100644 index 0000000..07f76d1 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/FilteredCompletionModel.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// This class, returned from <see cref="IAsyncCompletionItemManager"/>, + /// contains completion items to display in the UI, recommended item to display, selection mode and available filters. + /// </summary> + public sealed class FilteredCompletionModel + { + /// <summary> + /// Items to display in the completion UI. + /// </summary> + public ImmutableArray<CompletionItemWithHighlight> Items { get; } + + /// <summary> + /// Recommended item index to select. -1 selects suggestion item. + /// </summary> + public int SelectedItemIndex { get; } + + /// <summary> + /// Completion filters with their availability and selection state. + /// </summary> + public ImmutableArray<CompletionFilterWithState> Filters { get; } + + /// <summary> + /// Controls the selection mode of the selected item. + /// </summary> + public UpdateSelectionHint SelectionHint { get; } + + /// <summary> + /// Whether selected item should be displayed in the center of the list. Usually, this is true + /// </summary> + public bool CenterSelection { get; } + + /// <summary> + /// Optionally, provides an item that should be committed using the "commit if unique" command. + /// </summary> + public CompletionItem UniqueItem { get; } + + /// <summary> + /// Constructs <see cref="FilteredCompletionModel"/> without completion filters. + /// </summary> + /// <param name="items">Items to display in the completion UI.</param> + /// <param name="selectedItemIndex">Recommended item index to select. -1 selects suggestion item.</param> + public FilteredCompletionModel(ImmutableArray<CompletionItemWithHighlight> items, int selectedItemIndex) + : this(items, selectedItemIndex, ImmutableArray<CompletionFilterWithState>.Empty, selectionHint: UpdateSelectionHint.NoChange, centerSelection: true, uniqueItem: null) + { + } + + /// <summary> + /// Constructs <see cref="FilteredCompletionModel"/> with completion filters. + /// </summary> + /// <param name="items">Items to display in the completion UI.</param> + /// <param name="selectedItemIndex">Recommended item index to select. -1 selects suggestion item.</param> + /// <param name="filters">Completion filters with their availability and selection state. Default is empty array.</param> + public FilteredCompletionModel(ImmutableArray<CompletionItemWithHighlight> items, int selectedItemIndex, ImmutableArray<CompletionFilterWithState> filters) + : this(items, selectedItemIndex, filters, selectionHint: UpdateSelectionHint.NoChange, centerSelection: true, uniqueItem: null) + { + } + + /// <summary> + /// Constructs <see cref="FilteredCompletionModel"/> with completion filters, indication regarding selection mode and the unique item + /// </summary> + /// <param name="items">Items to display in the completion UI.</param> + /// <param name="selectedItemIndex">Recommended item index to select. -1 selects suggestion item.</param> + /// <param name="filters">Completion filters with their availability and selection state. Default is empty array.</param> + /// <param name="selectionHint">Allows <see cref="IAsyncCompletionItemManager"/> to influence the selection mode. Default is <see cref="UpdateSelectionHint.NoChange" /></param> + /// <param name="uniqueItem">Provides <see cref="CompletionItem"/> to commit using "commit if unique" command despite displaying more than one item. Default is <code>null</code></param> + public FilteredCompletionModel( + ImmutableArray<CompletionItemWithHighlight> items, + int selectedItemIndex, + ImmutableArray<CompletionFilterWithState> filters, + UpdateSelectionHint selectionHint, + bool centerSelection, + CompletionItem uniqueItem) + { + if (selectedItemIndex < -1) + throw new ArgumentOutOfRangeException(nameof(selectedItemIndex), "Selected index value must be greater than or equal to 0, or -1 to indicate selection of the suggestion item"); + if (items.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(items)); + if (filters.IsDefault) + throw new ArgumentException("Array must be initialized", nameof(filters)); + + Items = items; + SelectedItemIndex = selectedItemIndex; + Filters = filters; + SelectionHint = selectionHint; + CenterSelection = centerSelection; + UniqueItem = uniqueItem; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/InitialSelectionHint.cs b/src/Language/Def/Language/AsyncCompletion/Data/InitialSelectionHint.cs new file mode 100644 index 0000000..ae225c7 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/InitialSelectionHint.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Used by <see cref="IAsyncCompletionSource"/> to recommend the selection mode. + /// </summary> + public enum InitialSelectionHint + { + /// <summary> + /// Item is selected. + /// It will be committed by pressing a commit character, e.g. a token delimeter, + /// Tab, Enter and mouse click. + /// When multiple <see cref="IAsyncCompletionSource"/> give different results, this value has the lowest priority. + /// </summary> + RegularSelection, + + /// <summary> + /// Item is soft selected. + /// It will be committed only by pressing Tab or clicking the item. + /// Typing a commit character will dismiss the <see cref="IAsyncCompletionSession"/>. + /// Selecting another item automatically disables soft selection and enables regular selection. + /// When multiple <see cref="IAsyncCompletionSource"/> give different results, this value has higher priority than <see cref="RegularSelection"/>. + /// </summary> + SoftSelection, + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/InitialTrigger.cs b/src/Language/Def/Language/AsyncCompletion/Data/InitialTrigger.cs new file mode 100644 index 0000000..9319b3e --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/InitialTrigger.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// What triggered the completion, but not where it happened. + /// The reason we don't expose location is that for each extension, + /// we map the point to a buffer with matching content type. + /// </summary> + [DebuggerDisplay("{Reason} {Character}")] + public struct InitialTrigger : IEquatable<InitialTrigger> + { + /// <summary> + /// The reason that completion was started. + /// </summary> + public InitialTriggerReason Reason { get; } + + /// <summary> + /// The text edit associated with the triggering action. + /// </summary> + public char Character { get; } + + /// <summary> + /// Creates a <see cref="InitialTrigger"/> associated with a text edit + /// </summary> + /// <param name="reason">The kind of action that triggered completion to start</param> + /// <param name="character">Character that triggered completion</param> + public InitialTrigger(InitialTriggerReason reason, char character) + { + this.Reason = reason; + this.Character = character; + } + + /// <summary> + /// Creates a <see cref="InitialTrigger"/> not associated with a text edit + /// </summary> + /// <param name="reason">The kind of action that triggered completion to start</param> + public InitialTrigger(InitialTriggerReason reason) : this(reason, default) + { } + + bool IEquatable<InitialTrigger>.Equals(InitialTrigger other) => Reason.Equals(other.Reason) && Character.Equals(other.Character); + + public override bool Equals(object other) => (other is InitialTrigger otherImage) ? ((IEquatable<InitialTrigger>)this).Equals(otherImage) : false; + + public static bool operator ==(InitialTrigger left, InitialTrigger right) => left.Equals(right); + + public static bool operator !=(InitialTrigger left, InitialTrigger right) => !(left == right); + + public override int GetHashCode() => Reason.GetHashCode() ^ Character.GetHashCode(); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/InitialTriggerReason.cs b/src/Language/Def/Language/AsyncCompletion/Data/InitialTriggerReason.cs new file mode 100644 index 0000000..ec1942e --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/InitialTriggerReason.cs @@ -0,0 +1,35 @@ +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Describes the kind of action that initially triggered completion to open. + /// </summary> + public enum InitialTriggerReason + { + /// <summary> + /// Completion was triggered by a direct invocation of the completion feature + /// using the Edit.ListMember command. + /// </summary> + Invoke, + + /// <summary> + /// Completion was triggered with a request to commit if a single item would be selected + /// using the Edit.CompleteWord command. + /// </summary> + InvokeAndCommitIfUnique, + + /// <summary> + /// Completion was triggered via an action inserting a character into the document. + /// </summary> + Insertion, + + /// <summary> + /// Completion was triggered via an action deleting a character from the document. + /// </summary> + Deletion, + + /// <summary> + /// Completion was triggered for snippets only. + /// </summary> + Snippets, + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs b/src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs new file mode 100644 index 0000000..a9550e5 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Instructs the editor if and how to display the suggestion item. + /// When in suggestion mode, the UI displays a single <see cref="CompletionItem"/> whose <see cref="CompletionItem.DisplayText"/> + /// and <see cref="CompletionItem.InsertText"/> is equal to text typed by the user so far. + /// This class specifies the tooltip to use for this item, and <see cref="CompletionItem.DisplayText"/> when user has not typed anything. + /// </summary> + public sealed class SuggestionItemOptions + { + /// <summary> + /// Text to use as suggestion item's <see cref="CompletionItem.DisplayText"/> when user has not typed anything. + /// Usually prompts user to begin typing and describes what does the suggestion item represent. + /// </summary> + public string DisplayTextWhenEmpty { get; } + + /// <summary> + /// Localized tooltip text for the suggestion item. + /// Usually describes why suggestion mode is active, and what does the suggestion item represent. + /// </summary> + public string ToolTipText { get; } + + /// <summary> + /// Creates instance of SuggestionItemOptions with specified tooltip text and text to display in absence of user input. + /// Provide this instance to <see cref="CompletionContext"/> to activate suggestion mode. + /// </summary> + /// <param name="displayTextWhenEmpty"><see cref="CompletionItem.DisplayText"/> to use when user has not typed anything</param> + /// <param name="toolTipText">Localized tooltip text for the suggestion item</param> + public SuggestionItemOptions(string displayTextWhenEmpty, string toolTipText) + { + if (string.IsNullOrWhiteSpace(toolTipText)) + throw new ArgumentNullException(nameof(toolTipText)); + + DisplayTextWhenEmpty = displayTextWhenEmpty ?? throw new ArgumentNullException(nameof(displayTextWhenEmpty)); + ToolTipText = toolTipText; + } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/UpdateSelectionHint.cs b/src/Language/Def/Language/AsyncCompletion/Data/UpdateSelectionHint.cs new file mode 100644 index 0000000..bb9d45d --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/UpdateSelectionHint.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Used by <see cref="IAsyncCompletionItemManager" /> to recommend the selection mode. + /// </summary> + public enum UpdateSelectionHint + { + /// <summary> + /// Don't change the current selection mode. This is the recommended value. + /// </summary> + NoChange, + + /// <summary> + /// Set selection mode to soft selection: item is committed only using Tab or mouse. + /// </summary> + SoftSelected, + + /// <summary> + /// Set selection mode to regular selection: item is committed using Tab, mouse, enter and commit characters. + /// </summary> + Selected + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/UpdateTrigger.cs b/src/Language/Def/Language/AsyncCompletion/Data/UpdateTrigger.cs new file mode 100644 index 0000000..6151d88 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/UpdateTrigger.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// What triggered updating of completion. + /// </summary> + [DebuggerDisplay("{Reason} {Character}")] + public struct UpdateTrigger : IEquatable<UpdateTrigger> + { + /// <summary> + /// The reason that completion was updated. + /// </summary> + public UpdateTriggerReason Reason { get; } + + /// <summary> + /// The text edit associated with the triggering action. + /// </summary> + public char Character { get; } + + /// <summary> + /// Creates a <see cref="UpdateTrigger"/> associated with a text edit + /// </summary> + /// <param name="reason">The kind of action that triggered completion to update</param> + /// <param name="character">Character that triggered the update</param> + public UpdateTrigger(UpdateTriggerReason reason, char character) + { + this.Reason = reason; + this.Character = character; + } + + /// <summary> + /// Creates a <see cref="InitialTrigger"/> not associated with a text edit + /// </summary> + /// <param name="reason">The kind of action that triggered completion to update</param> + public UpdateTrigger(UpdateTriggerReason reason) : this(reason, default(char)) + { } + + bool IEquatable<UpdateTrigger>.Equals(UpdateTrigger other) => Reason.Equals(other.Reason) && Character.Equals(other.Character); + + public override bool Equals(object other) => (other is InitialTrigger otherImage) ? ((IEquatable<UpdateTrigger>)this).Equals(otherImage) : false; + + public static bool operator ==(UpdateTrigger left, UpdateTrigger right) => left.Equals(right); + + public static bool operator !=(UpdateTrigger left, UpdateTrigger right) => !(left == right); + + public override int GetHashCode() => Reason.GetHashCode() ^ Character.GetHashCode(); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/Data/UpdateTriggerReason.cs b/src/Language/Def/Language/AsyncCompletion/Data/UpdateTriggerReason.cs new file mode 100644 index 0000000..83ce4d1 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/Data/UpdateTriggerReason.cs @@ -0,0 +1,31 @@ +using System; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Describes the kind of action that triggered completion to filter. + /// </summary> + public enum UpdateTriggerReason + { + /// <summary> + /// Completion was triggered by a direct invocation of the completion feature + /// using the Edit.ListMember command. + /// </summary> + Initial, + + /// <summary> + /// Completion was triggered via an action inserting a character into the document. + /// </summary> + Insertion, + + /// <summary> + /// Completion was triggered via an action deleting a character from the document. + /// </summary> + Deletion, + + /// <summary> + /// Update was triggered by changing filters + /// </summary> + FilterChange, + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs new file mode 100644 index 0000000..f882166 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Represents a class that manages the completion feature. + /// The editor uses this class to trigger completion and obtain instance of <see cref="IAsyncCompletionSession"/> + /// which contains methods and events relevant to the active completion session. + /// </summary> + /// <remarks> + /// This is a MEF component and may be imported by another MEF component: + /// </remarks> + /// <example> + /// [Import] + /// IAsyncCompletionBroker CompletionBroker; + /// </example> + public interface IAsyncCompletionBroker + { + /// <summary> + /// Returns whether <see cref="IAsyncCompletionSession"/> is active in given <see cref="ITextView"/>. + /// </summary> + /// <remarks> + /// The data may be stale if <see cref="IAsyncCompletionSession"/> was simultaneously dismissed on another thread. + /// </remarks> + /// <param name="textView">View that hosts completion and relevant buffers</param> + bool IsCompletionActive(ITextView textView); + + /// <summary> + /// Returns whether there are any completion item sources available for given <see cref="IContentType"/>. + /// This method should be called prior to calling <see cref="TriggerCompletion(ITextView, SnapshotPoint, char, CancellationToken)"/> to avoid traversal of the buffer graph. + /// </summary> + /// <param name="textView"><see cref="ITextView"/> to check for available completion source exports</param> + bool IsCompletionSupported(IContentType contentType); + + /// <summary> + /// Returns <see cref="IAsyncCompletionSession"/> if there is one active in a given <see cref="ITextView"/>, or null if not. + /// </summary> + /// <remarks> + /// The data may be stale if <see cref="IAsyncCompletionSession"/> was simultaneously dismissed on another thread. + /// Use <see cref="IAsyncCompletionSession.IsDismissed"/> to check state of returned session. + /// </remarks> + /// <param name="textView">View that hosts completion and relevant buffers</param> + IAsyncCompletionSession GetSession(ITextView textView); + + /// <summary> + /// Activates completion and returns <see cref="IAsyncCompletionSession"/>. + /// If completion was already active, returns the existing session without changing it. + /// Must be invoked on UI thread. + /// This does not cause the completion popup to appear. + /// To compute available icons and display the UI, call <see cref="IAsyncCompletionSession.OpenOrUpdate(InitialTrigger, SnapshotPoint, CancellationToken)"/>. + /// Invoke <see cref="IsCompletionSupported(IContentType)"/> prior to invoking this method to more efficiently verify whether feature is disabled or if there are no completion providers. + /// </summary> + /// <param name="textView">View that hosts completion and relevant buffers</param> + /// <param name="triggerLocation">Location of completion on the view's data buffer: <see cref="ITextView.TextBuffer"/>. Used to pick relevant <see cref="IAsyncCompletionSource"/>s and <see cref="IAsyncCompletionItemManager"/></param> + /// <param name="typeChar">Character that triggered completion, '\t', '\n' or default ('\0') </param> + /// <param name="token">Cancellation token that may interrupt this operation, despire running on the UI thread</param> + /// <returns> + /// Returns existing <see cref="IAsyncCompletionSession"/> if one already exists + /// Returns null if the completion feature is disabled or if there are no applicable completion providers. Invoke <see cref="IsCompletionSupported(IContentType)"/> prior to invoking this method to perform this check more efficiently. + /// Returns null if applicable <see cref="IAsyncCompletionSource"/>s determine that completion is not applicable at the given <paramref name="triggerLocation"/>. + /// Returns a new <see cref="IAsyncCompletionSession"/>. Invoke <see cref="IAsyncCompletionSession.OpenOrUpdate(InitialTrigger, SnapshotPoint, CancellationToken)"/> to compute and display the available completions. + /// </returns> + IAsyncCompletionSession TriggerCompletion(ITextView textView, SnapshotPoint triggerLocation, char typedChar, CancellationToken token); + + /// <summary> + /// Raised on UI thread when new <see cref="IAsyncCompletionSession"/> is triggered. + /// </summary> + event EventHandler<CompletionTriggeredEventArgs> CompletionTriggered; + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs new file mode 100644 index 0000000..c365393 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Represents a class that provides means to adjust the commit behavior, + /// including which typed characters commit the <see cref="IAsyncCompletionSession"/> + /// and how to commit <see cref="CompletionItem"/>s. + /// </summary> + /// <remarks> + /// Instances of this class should be created by <see cref="IAsyncCompletionCommitManagerProvider"/>, which is a MEF part. + /// </remarks> + public interface IAsyncCompletionCommitManager + { + /// <summary> + /// Returns characters that may commit completion. + /// When completion is active and a text edit matches one of these characters, + /// <see cref="ShouldCommitCompletion(char, SnapshotPoint, CancellationToken)"/> is called to verify that the character + /// is indeed a commit character at a given location. + /// Called on UI thread. + /// </summary> + IEnumerable<char> PotentialCommitCharacters { get; } + + /// <summary> + /// Returns whether this character is a commit character in a given location. + /// If every character returned by <see cref="PotentialCommitCharacters"/> should always commit the active completion session, return true. + /// Called on UI thread. + /// </summary> + /// <param name="typedChar">Character typed by the user</param> + /// <param name="location">Location in the snapshot of the view's topmost buffer. The character is not inserted into this snapshot.</param> + /// <param name="token">Token used to cancel this operation</param> + /// <returns>True if this character should commit the active session.</returns> + bool ShouldCommitCompletion(char typedChar, SnapshotPoint location, CancellationToken token); + + /// <summary> + /// Allows the instance of <see cref="IAsyncCompletionCommitManager"/> to commit of specified <see cref="CompletionItem"/>. + /// Implementer does not need to commit the item. Return <see cref="CommitResult.Unhandled"/> to allow another + /// <see cref="IAsyncCompletionCommitManager"/> to attempt the commit, or to invoke default commit behavior. + /// Called on UI thread. + /// </summary> + /// <param name="view">View that hosts completion and relevant buffers</param> + /// <param name="buffer">Reference to the buffer with matching content type to perform text edits etc.</param> + /// <param name="item">Which completion item is to be applied</param> + /// <param name="applicableToSpan">Span augmented by completion, on the view's data buffer: <see cref="ITextView.TextBuffer"/></param> + /// <param name="typedChar">Text change associated with this commit</param> + /// <param name="token">Token used to cancel this operation</param> + /// <returns>Instruction for the editor how to proceed after invoking this method. Default is <see cref="CommitResult.Unhandled"/></returns> + CommitResult TryCommit(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableToSpan, char typedChar, CancellationToken token); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManagerProvider.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManagerProvider.cs new file mode 100644 index 0000000..b235e38 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManagerProvider.cs @@ -0,0 +1,36 @@ +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Provides instances of <see cref="IAsyncCompletionCommitManager"/> which provides means to adjust the commit behavior, + /// including which typed characters commit the <see cref="IAsyncCompletionSession"/> + /// and how to commit <see cref="CompletionItem"/>s. + /// </summary> + /// <remarks> + /// This is a MEF component and should be exported with [ContentType] and [Name] attributes + /// and optional [Order] and [TextViewRoles] attributes. + /// An instance of <see cref="IAsyncCompletionItemManager"/> is selected + /// first by matching ContentType with content type of the <see cref="ITextView.TextBuffer"/>, and then by Order. + /// Only one <see cref="IAsyncCompletionItemManager"/> is used in a given view. + /// </remarks> + /// <example> + /// [Export(typeof(IAsyncCompletionCommitManagerProvider))] + /// [Name(nameof(MyCompletionCommitManagerProvider))] + /// [ContentType("text")] + /// [TextViewRoles(PredefinedTextViewRoles.Editable)] + /// [Order(Before = "OtherCompletionCommitManager")] + /// public class MyCompletionCommitManagerProvider : IAsyncCompletionCommitManagerProvider + /// </example> + public interface IAsyncCompletionCommitManagerProvider + { + /// <summary> + /// Creates an instance of <see cref="IAsyncCompletionCommitManager"/> for the specified <see cref="ITextView"/>. + /// Called on the UI thread. + /// </summary> + /// <param name="textView">Text view that will host the completion. Completion acts on buffers of this view.</param> + /// <returns>Instance of <see cref="IAsyncCompletionItemManager"/></returns> + IAsyncCompletionCommitManager GetOrCreate(ITextView textView); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs new file mode 100644 index 0000000..7967e15 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs @@ -0,0 +1,52 @@ +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Represents a class that filters and sorts available <see cref="CompletionItem"/>s given the current state of the editor. + /// It also declares which completion filters are available for the returned subset of <see cref="CompletionItem"/>s. + /// All methods are called on background thread. + /// </summary> + /// <remarks> + /// Instances of this class should be created by <see cref="IAsyncCompletionItemManagerProvider"/>, which is a MEF part. + /// </remarks> + public interface IAsyncCompletionItemManager + { + /// <summary> + /// This method is first called before completion is about to appear, + /// and then on subsequent typing events and when user toggles completion filters. + /// <paramref name="session"/> tracks user user's input tracked with <see cref="IAsyncCompletionSession.ApplicableToSpan"/>. + /// <paramref name="data"/> provides applicable <see cref="AsyncCompletionSessionDataSnapshot.Snapshot"/> and + /// and <see cref="AsyncCompletionSessionDataSnapshot.SelectedFilters"/>s that indicate user's filter selection. + /// </summary> + /// <param name="session">The active <see cref="IAsyncCompletionSession"/>. See <see cref="IAsyncCompletionSession.ApplicableToSpan"/> and <see cref="IAsyncCompletionSession.TextView"/></param> + /// <param name="data">Contains properties applicable at the time this method is invoked.</param> + /// <param name="token">Cancellation token that may interrupt this operation</param> + /// <returns>Instance of <see cref="FilteredCompletionModel"/> that contains completion items to render, filters to display and recommended item to select</returns> + Task<FilteredCompletionModel> UpdateCompletionListAsync( + IAsyncCompletionSession session, + AsyncCompletionSessionDataSnapshot data, + CancellationToken token); + + /// <summary> + /// This method is first called before completion is about to appear, + /// and then on subsequent typing events and when user toggles completion filters. + /// The result of this method will be used in subsequent invocations of <see cref="UpdateCompletionListAsync"/> + /// <paramref name="session"/> tracks user user's input tracked with <see cref="IAsyncCompletionSession.ApplicableToSpan"/>. + /// <paramref name="data"/> provides applicable <see cref="AsyncCompletionSessionDataSnapshot.Snapshot"/> and + /// </summary> + /// <param name="session">The active <see cref="IAsyncCompletionSession"/>. See <see cref="IAsyncCompletionSession.ApplicableToSpan"/> and <see cref="IAsyncCompletionSession.TextView"/></param> + /// <param name="data">Contains properties applicable at the time this method is invoked.</param> + /// <param name="token">Cancellation token that may interrupt this operation</param> + /// <returns>Instance of <see cref="FilteredCompletionModel"/> that contains completion items to render, filters to display and recommended item to select</returns> + Task<ImmutableArray<CompletionItem>> SortCompletionListAsync( + IAsyncCompletionSession session, + AsyncCompletionSessionInitialDataSnapshot data, + CancellationToken token); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManagerProvider.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManagerProvider.cs new file mode 100644 index 0000000..8becc26 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManagerProvider.cs @@ -0,0 +1,34 @@ +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Provides instances of <see cref="IAsyncCompletionItemManager"/> which filters and sorts available <see cref="CompletionItem"/>s given the current state of the editor. + /// </summary> + /// <remarks> + /// This is a MEF component and should be exported with [ContentType] and [Name] attributes + /// and optional [Order] and [TextViewRoles] attributes. + /// An instance of <see cref="IAsyncCompletionItemManager"/> is selected + /// first by matching ContentType with content type of the <see cref="ITextView.TextBuffer"/>, and then by Order. + /// Only one <see cref="IAsyncCompletionItemManager"/> is used in a given view. + /// </remarks> + /// <example> + /// [Export(typeof(IAsyncCompletionItemManagerProvider))] + /// [Name(nameof(MyCompletionItemManagerProvider))] + /// [ContentType("text")] + /// [TextViewRoles(PredefinedTextViewRoles.Editable)] + /// [Order(Before = "OtherCompletionItemManager")] + /// public class MyCompletionItemManagerProvider : IAsyncCompletionItemManagerProvider + /// </example> + public interface IAsyncCompletionItemManagerProvider + { + /// <summary> + /// Creates an instance of <see cref="IAsyncCompletionItemManager"/> for the specified <see cref="ITextView"/>. + /// Called on the UI thread. + /// </summary> + /// <param name="textView">Text view that will host the completion</param> + /// <returns>Instance of <see cref="IAsyncCompletionItemManager"/> that will sort and filter <see cref="CompletionItem"/>s</returns> + IAsyncCompletionItemManager GetOrCreate(ITextView textView); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs new file mode 100644 index 0000000..f6549f1 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Represents a class that tracks completion within a single <see cref="ITextView"/>. + /// Constructed and managed by an instance of <see cref="IAsyncCompletionBroker"/> + /// </summary> + public interface IAsyncCompletionSession : IPropertyOwner + { + /// <summary> + /// Request completion to be opened or updated in a given location, + /// the completion items to be filtered and sorted, and the UI updated. + /// Must be called on UI thread. Enqueues work on a worker thread. + /// </summary> + /// <param name="trigger">What caused completion</param> + /// <param name="triggerLocation">Location of the trigger on the subject buffer</param> + /// <param name="token">Token used to cancel this and other queued operation.</param> + void OpenOrUpdate(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token); + + /// <summary> + /// Stops the session and hides associated UI. + /// May be called from any thread. + /// </summary> + void Dismiss(); + + /// <summary> + /// Returns whether given text edit should result in committing this session. + /// Since this method is on a typing hot path, it returns quickly if the <paramref name="typedChar"/> + /// is not found among characters collected from <see cref="IAsyncCompletionCommitManager.PotentialCommitCharacters"/> + /// Else, we map the top-buffer <paramref name="triggerLocation"/> to subject buffers and query + /// <see cref="IAsyncCompletionCommitManager.ShouldCommitCompletion(char, SnapshotPoint, CancellationToken)"/> + /// to see whether any <see cref="IAsyncCompletionCommitManager"/> would like to commit completion. + /// Must be called on UI thread. + /// </summary> + /// <remarks>This method must run on UI thread because of mapping the point across buffers.</remarks> + /// <param name="typedChar">The text edit which caused this action. May be null.</param> + /// <param name="triggerLocation">Location on the view's data buffer: <see cref="ITextView.TextBuffer"/></param> + /// <param name="token">Token used to cancel this operation</param> + /// <returns>Whether any <see cref="IAsyncCompletionCommitManager.ShouldCommitCompletion(char, SnapshotPoint, CancellationToken)"/> returned true</returns> + bool ShouldCommit(char typedChar, SnapshotPoint triggerLocation, CancellationToken token); + + /// <summary> + /// Commits the currently selected <see cref="CompletionItem"/>. + /// Must be called on UI thread. + /// </summary> + /// <param name="typedChar">The text edit which caused this action. + /// May be default(char) when commit was requested by an explcit command (e.g. hitting Tab, Enter or clicking)</param> + /// <param name="token">Token used to cancel this operation</param> + /// <returns>Instruction for the editor how to proceed after invoking this method</returns> + CommitBehavior Commit(char typedChar, CancellationToken token); + + /// <summary> + /// Commits the single <see cref="CompletionItem"/> or opens the completion UI. + /// Must be called on UI thread. + /// </summary> + /// <param name="token">Token used to cancel this operation</param> + /// <returns>Whether the unique item was committed.</returns> + bool CommitIfUnique(CancellationToken token); + + /// <summary> + /// Returns the <see cref="ITextView"/> this session is active on. + /// </summary> + ITextView TextView { get; } + + /// <summary> + /// Gets span applicable to this completion session. + /// The span is defined on the session's <see cref="ITextView.TextBuffer"/>. + /// </summary> + ITrackingSpan ApplicableToSpan { get; } + + /// <summary> + /// Returns whether session is dismissed. + /// When session is dismissed, all work is canceled. + /// </summary> + bool IsDismissed { get; } + + /// <summary> + /// Raised on UI thread when completion item is committed + /// </summary> + event EventHandler<CompletionItemEventArgs> ItemCommitted; + + /// <summary> + /// Raised on UI thread when completion session is dismissed. + /// </summary> + event EventHandler Dismissed; + + /// <summary> + /// Provides elements that are visible in the UI + /// Raised on worker thread when filtering and sorting of items has finished. + /// There may be more updates happening immediately after this update. + /// </summary> + event EventHandler<ComputedCompletionItemsEventArgs> ItemsUpdated; + + /// <summary> + /// Gets items visible in the UI and information about selection. + /// This is a blocking call. As a side effect, prevents the UI from displaying. + /// </summary> + ComputedCompletionItems GetComputedItems(CancellationToken token); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs new file mode 100644 index 0000000..fc17d0b --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs @@ -0,0 +1,60 @@ +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Adornments; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Represents a class that provides <see cref="CompletionItem"/>s and other information + /// relevant to the completion feature at a specific <see cref="SnapshotPoint"/>. + /// </summary> + /// <remarks> + /// Instances of this class should be created by <see cref="IAsyncCompletionSourceProvider"/>, which is a MEF part. + /// </remarks> + public interface IAsyncCompletionSource + { + /// <summary> + /// Called once per completion session to fetch the set of all completion items available at a given location. + /// Called on a background thread. + /// </summary> + /// <param name="trigger">What caused the completion</param> + /// <param name="triggerLocation">Location where completion was triggered, on the subject buffer that matches this <see cref="IAsyncCompletionSource"/>'s content type</param> + /// <param name="applicableToSpan">Location where completion will take place, on the view's data buffer: <see cref="ITextView.TextBuffer"/></param> + /// <param name="token">Cancellation token that may interrupt this operation</param> + /// <returns>A struct that holds completion items and applicable span</returns> + Task<CompletionContext> GetCompletionContextAsync(InitialTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token); + + /// <summary> + /// Returns tooltip associated with provided <see cref="CompletionItem"/>. + /// The returned object will be rendered by <see cref="IViewElementFactoryService"/>. See its documentation for default supported types. + /// You may export a <see cref="IViewElementFactory"/> to provide a renderer for a custom type. + /// Since this method is called on a background thread and on multiple platforms, an instance of UIElement may not be returned. + /// </summary> + /// <param name="item"><see cref="CompletionItem"/> which is a subject of the tooltip</param> + /// <param name="token">Cancellation token that may interrupt this operation</param> + /// <returns>An object that will be passed to <see cref="IViewElementFactoryService"/>. See its documentation for supported types.</returns> + Task<object> GetDescriptionAsync(CompletionItem item, CancellationToken token); + + /// <summary> + /// Provides the span applicable to the prospective session. + /// Called on UI thread and expected to return very quickly, based on textual information. + /// This method is called sequentially on available <see cref="IAsyncCompletionSource"/>s until one of them returns true. + /// Returning <code>false</code> does not exclude this source from participating in completion session. + /// If no <see cref="IAsyncCompletionSource"/>s return <code>true</code>, there will be no completion session. + /// </summary> + /// <remarks> + /// A language service should provide the span and return <code>true</code> even if it does not wish to provide completion. + /// This will enable extensions to provide completion in syntactically appropriate location. + /// </remarks> + /// <param name="typedChar">Character typed by the user</param> + /// <param name="triggerLocation">Location on the subject buffer that matches this <see cref="IAsyncCompletionSource"/>'s content type</param> + /// <param name="applicableToSpan">Applicable span for the prospective completion session. You may set it to <code>default</code> if returning false</param> + /// <param name="token">Cancellation token that may interrupt this operation</param> + /// <returns>Whether completion should use the supplied applicable span.</returns> + bool TryGetApplicableToSpan(char typedChar, SnapshotPoint triggerLocation, out SnapshotSpan applicableToSpan, CancellationToken token); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSourceProvider.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSourceProvider.cs new file mode 100644 index 0000000..0e850e1 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSourceProvider.cs @@ -0,0 +1,37 @@ +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Provides instances of <see cref="IAsyncCompletionSource"/> which filters and sorts available <see cref="CompletionItem"/>s given the current state of the editor. + /// </summary> + /// <summary> + /// Provides instances of <see cref="IAsyncCompletionSource"/> which provides <see cref="CompletionItem"/>s + /// and other information relevant to the completion feature at a specific <see cref="SnapshotPoint"/> + /// </summary> + /// <remarks> + /// This is a MEF component and should be exported with [ContentType] and [Name] attributes + /// and optional [TextViewRoles] attribute. + /// Completion feature will request data from all exported <see cref="IAsyncCompletionSource"/>s whose ContentType + /// matches content type of any buffer in the completion's trigger location. + /// </remarks> + /// <example> + /// [Export(typeof(IAsyncCompletionSourceProvider))] + /// [Name(nameof(MyCompletionSource))] + /// [ContentType("text")] + /// [TextViewRoles(PredefinedTextViewRoles.Editable)] + /// public class MyCompletionSource : IAsyncCompletionSource + /// </example> + public interface IAsyncCompletionSourceProvider + { + /// <summary> + /// Creates an instance of <see cref="IAsyncCompletionSource"/> for the specified <see cref="ITextView"/>. + /// Called on the UI thread. + /// </summary> + /// <param name="textView">Text view that will host the completion. Completion acts on buffers of this view.</param> + /// <returns>Instance of <see cref="IAsyncCompletionSource"/></returns> + IAsyncCompletionSource GetOrCreate(ITextView textView); + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs b/src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs new file mode 100644 index 0000000..1187224 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Represents a class that manages user interface for the completion feature. + /// All methods are called on UI thread. + /// </summary> + /// <remarks> + /// Instances of this class should be created by <see cref="ICompletionPresenterProvider"/>, which is a MEF part. + /// </remarks> + public interface ICompletionPresenter : IDisposable + { + /// <summary> + /// Opens the UI and displays provided data + /// </summary> + /// <param name="presentation">Data to display in the UI</param> + void Open(CompletionPresentationViewModel presentation); + + /// <summary> + /// Updates the UI with provided data + /// </summary> + /// <param name="presentation">Data to display in the UI</param> + void Update(CompletionPresentationViewModel presentation); + + /// <summary> + /// Hides the completion UI + /// </summary> + void Close(); + + /// <summary> + /// Notifies of user changing the selection state of filters + /// </summary> + event EventHandler<CompletionFilterChangedEventArgs> FiltersChanged; + + /// <summary> + /// Notifies of user selecting an item + /// </summary> + event EventHandler<CompletionItemSelectedEventArgs> CompletionItemSelected; + + /// <summary> + /// Notifies of user committing an item for completion + /// </summary> + event EventHandler<CompletionItemEventArgs> CommitRequested; + + /// <summary> + /// Notifies of UI closing + /// </summary> + event EventHandler<CompletionClosedEventArgs> CompletionClosed; + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/ICompletionPresenterProvider.cs b/src/Language/Def/Language/AsyncCompletion/ICompletionPresenterProvider.cs new file mode 100644 index 0000000..719251a --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/ICompletionPresenterProvider.cs @@ -0,0 +1,41 @@ +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Represents a class that produces instances of <see cref="ICompletionPresenter"/> + /// </summary> + /// <remarks> + /// This is a MEF component and should be exported with [ContentType] and [Name] attributes + /// and optional [Order] attribute. + /// An instance of <see cref="ICompletionPresenterProvider"/> is selected + /// first by matching ContentType with content type of the <see cref="ITextView.TextBuffer"/>, and then by Order. + /// Only one <see cref="ICompletionPresenterProvider"/> is used in a given view. + /// </remarks> + /// <example> + /// [Export(typeof(ICompletionPresenterProvider))] + /// [Name(nameof(MyCompletionPresenterProvider))] + /// [ContentType("any")] + /// [TextViewRoles(PredefinedTextViewRoles.Editable)] + /// [Order(Before = KnownCompletionNames.DefaultCompletionPresenter)] + /// public class MyCompletionPresenterProvider : ICompletionPresenterProvider + /// </example> + public interface ICompletionPresenterProvider + { + /// <summary> + /// Returns instance of <see cref="ICompletionPresenter"/> that will host completion for given <see cref="ITextView"/>. + /// Called on the UI thread. + /// </summary> + /// <remarks>It is encouraged to reuse the UI over creating new UI each time this method is called.</remarks> + /// <param name="textView">Text view that will host the completion. Completion acts on buffers of this view.</param> + /// <returns>Instance of <see cref="ICompletionPresenter"/></returns> + ICompletionPresenter GetOrCreate(ITextView textView); + + /// <summary> + /// Contains additional properties of thie <see cref="ICompletionPresenter"/> that may be accessed + /// prior to initializing an instance of <see cref="ICompletionPresenter"/> + /// </summary> + CompletionPresenterOptions Options { get; } + } +} diff --git a/src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs b/src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs new file mode 100644 index 0000000..80debb8 --- /dev/null +++ b/src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs @@ -0,0 +1,35 @@ +using Microsoft.VisualStudio.Commanding; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion +{ + /// <summary> + /// Provides names used by the Async Completion feature. + /// </summary> + public static class PredefinedCompletionNames + { + /// <summary> + /// Name of the default <see cref="IAsyncCompletionItemManagerProvider"/>. Use to order your MEF part. + /// </summary> + public const string DefaultCompletionItemManager = "DefaultCompletionItemManager"; + + /// <summary> + /// Name of the default <see cref="ICompletionPresenterProvider"/>. Use to order your MEF part. + /// </summary> + public const string DefaultCompletionPresenter = "DefaultCompletionPresenter"; + + /// <summary> + /// Name of the completion's <see cref="ICommandHandler"/>. Use to order your MEF part. + /// </summary> + public const string CompletionCommandHandler = "CompletionCommandHandler"; + + /// <summary> + /// Name of the editor option that stores user's preference for the completion mode. + /// </summary> + public const string SuggestionModeInCompletionOptionName = "SuggestionModeInCompletion"; + + /// <summary> + /// Name of the editor option that stores user's preference for the completion mode during debugging. + /// </summary> + public const string SuggestionModeInDebuggerCompletionOptionName = "SuggestionModeInCompletionDuringDebugging"; + } +} diff --git a/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoBroker.cs b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoBroker.cs new file mode 100644 index 0000000..9375a22 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoBroker.cs @@ -0,0 +1,90 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// Controls invocation and dismissal of Quick Info tooltips for <see cref="ITextView"/> instances. + /// </summary> + /// <remarks> + /// This type can be called from any thread and will marshal its work to the UI thread as required. + /// </remarks> + public interface IAsyncQuickInfoBroker + { + /// <summary> + /// Determines whether there is at least one active Quick Info session in the specified <see cref="ITextView" />. + /// </summary> + /// <remarks> + /// Quick info is considered to be active if there is a visible, calculating, or recalculating quick info session. + /// </remarks> + /// <param name="textView">The <see cref="ITextView" /> for which Quick Info session status is to be determined.</</param> + /// <returns> + /// <c>true</c> if there is at least one active or calculating Quick Info session over the specified <see cref="ITextView" />, <c>false</c> + /// otherwise. + /// </returns> + bool IsQuickInfoActive(ITextView textView); + + /// <summary> + /// Triggers Quick Info tooltip in the specified <see cref="ITextView"/> at the caret or optional <paramref name="triggerPoint"/>. + /// </summary> + /// <exception cref="OperationCanceledException"> + /// <paramref name="cancellationToken"/> was canceled by the caller or the operation was interrupted by another call to + /// <see cref="TriggerQuickInfoAsync(ITextView, ITrackingPoint, QuickInfoSessionOptions, CancellationToken)"/> + /// </exception> + /// <param name="cancellationToken">If canceled before the method returns, cancels any computations in progress.</param> + /// <param name="textView"> + /// The <see cref="ITextView" /> for which Quick Info is to be triggered. + /// </param> + /// <param name="triggerPoint"> + /// The <see cref="ITrackingPoint" /> in the view's text buffer at which Quick Info should be triggered. + /// </param> + /// <param name="options">Options for customizing Quick Info behavior.</param> + /// <returns> + /// An <see cref="IAsyncQuickInfoSession"/> tracking the state of the session or null if there are no items. + /// </returns> + Task<IAsyncQuickInfoSession> TriggerQuickInfoAsync( + ITextView textView, + ITrackingPoint triggerPoint = null, + QuickInfoSessionOptions options = QuickInfoSessionOptions.None, + CancellationToken cancellationToken = default); + + /// <summary> + /// Gets Quick Info items for the <see cref="ITextView"/> at the <paramref name="triggerPoint"/>. + /// </summary> + /// <exception cref="OperationCanceledException"> + /// <paramref name="cancellationToken"/> was canceled by the caller. + /// </exception> + /// <exception cref="AggregateException"> + /// One or more errors occured during query of quick info items sources. + /// </exception> + /// <param name="cancellationToken">If canceled before the method returns, cancels any computations in progress.</param> + /// <param name="textView"> + /// The <see cref="ITextView" /> for which Quick Info is to be triggered. + /// </param> + /// <param name="triggerPoint"> + /// The <see cref="ITrackingPoint" /> in the view's text buffer at which Quick Info should be triggered. + /// </param> + /// <param name="options">Options for customizing Quick Info behavior.</param> + /// <returns> + /// A series of Quick Info items and a span for which they are applicable. + /// </returns> + Task<QuickInfoItemsCollection> GetQuickInfoItemsAsync( + ITextView textView, + ITrackingPoint triggerPoint, + CancellationToken cancellationToken); + + /// <summary> + /// Gets the current <see cref="IAsyncQuickInfoSession"/> for the <see cref="ITextView"/>. + /// </summary> + /// <remarks> + /// Quick info is considered to be active if there is a visible, calculating, or recalculating quick info session. + /// </remarks> + /// <param name="textView">The <see cref="ITextView" /> for which to lookup the session.</param> + /// <returns>The session, or <c>null</c> if there is no active session.</returns> + IAsyncQuickInfoSession GetSession(ITextView textView); + } +} diff --git a/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSession.cs b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSession.cs new file mode 100644 index 0000000..46e1f17 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSession.cs @@ -0,0 +1,81 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Tracks state of a visible or calculating Quick Info session. + /// </summary> + public interface IAsyncQuickInfoSession : IPropertyOwner + { + /// <summary> + /// Dispatched on the UI thread whenever the Quick Info Session changes state. + /// </summary> + event EventHandler<QuickInfoSessionStateChangedEventArgs> StateChanged; + + /// <summary> + /// The span of text to which this Quick Info session applies. + /// </summary> + ITrackingSpan ApplicableToSpan { get; } + + /// <summary> + /// The ordered, merged collection of content to be displayed in the Quick Info. + /// </summary> + /// <remarks> + /// This field is originally null and is updated with the content once the session has + /// finished querying the providers. + /// </remarks> + IEnumerable<object> Content { get; } + + /// <summary> + /// Indicates that this Quick Info has interactive content that can request to stay open. + /// </summary> + bool HasInteractiveContent { get; } + + /// <summary> + /// Specifies attributes of the Quick Info session and Quick Info session presentation. + /// </summary> + QuickInfoSessionOptions Options { get; } + + /// <summary> + /// The current state of the Quick Info session. + /// </summary> + QuickInfoSessionState State { get; } + + /// <summary> + /// The <see cref="ITextView"/> for which this Quick Info session was created. + /// </summary> + ITextView TextView { get; } + + /// <summary> + /// Gets the point at which the Quick Info tip was triggered in the <see cref="ITextView"/>. + /// </summary> + /// <remarks> + /// Returned <see cref="ITrackingPoint"/> is on the buffer requested by the caller. + /// </remarks> + /// <param name="textBuffer">The <see cref="ITextBuffer"/> relative to which to obtain the point.</param> + /// <returns>A <see cref="ITrackingPoint"/> indicating the point over which Quick Info was invoked.</returns> + ITrackingPoint GetTriggerPoint(ITextBuffer textBuffer); + + /// <summary> + /// Gets the point at which the Quick Info tip was triggered in the <see cref="ITextView"/>. + /// </summary> + /// <remarks> + /// Returned point is on the buffer requested by the caller. + /// </remarks> + /// <param name="snapshot">The <see cref="ITextSnapshot"/> relative to which to obtain the point.</param> + /// <returns>The point over which Quick Info was invoked or <c>null</c> if it does not exist in <paramref name="snapshot"/>.</returns> + SnapshotPoint? GetTriggerPoint(ITextSnapshot snapshot); + + /// <summary> + /// Dismisses the Quick Info session, if applicable. If the session is already dismissed, + /// this method no-ops. + /// </summary> + /// <returns>A task tracking the completion of the operation.</returns> + Task DismissAsync(); + } +} diff --git a/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSource.cs b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSource.cs new file mode 100644 index 0000000..9641be1 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSource.cs @@ -0,0 +1,34 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.VisualStudio.Text.Adornments; + using Microsoft.VisualStudio.Threading; + + /// <summary> + /// Source of Quick Info tooltip content item, proffered to the IDE by a <see cref="IAsyncQuickInfoSourceProvider"/>. + /// </summary> + /// <remarks> + /// This class is always constructed and disposed on the UI thread and called on + /// a non-UI thread. Callers that require the UI thread must explicitly marshal there with + /// <see cref="JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken)"/>. + /// Content objects are resolved into UI constructs via the <see cref="IViewElementFactoryService"/>. + /// </remarks> + public interface IAsyncQuickInfoSource : IDisposable + { + /// <summary> + /// Gets Quick Info item and tracking span via a <see cref="QuickInfoItem"/>. + /// </summary> + /// <remarks> + /// This method is always called on a background thread. Multiple elements can be + /// be returned by a single source by wrapping them in a <see cref="ContainerElement"/>. + /// </remarks> + /// <param name="session">An object tracking the current state of the Quick Info.</param> + /// <param name="cancellationToken">Cancels an in-progress computation.</param> + /// <returns>item and a tracking span for which these item are applicable.</returns> + Task<QuickInfoItem> GetQuickInfoItemAsync( + IAsyncQuickInfoSession session, + CancellationToken cancellationToken); + } +} diff --git a/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSourceProvider.cs b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSourceProvider.cs new file mode 100644 index 0000000..ffba691 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/IAsyncQuickInfoSourceProvider.cs @@ -0,0 +1,29 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using Microsoft.VisualStudio.Text; + + /// <summary> + /// A MEF component part that is proffered to the IDE to construct an <see cref="IAsyncQuickInfoSource"/>. + /// </summary> + /// <remarks> + /// This class is always constructed and called on the UI thread. + /// </remarks> + /// <example> + /// [Export(typeof(IAsyncQuickInfoSourceProvider))] + /// [Name("Foo QuickInfo Provider")] + /// [Order(After = "default")] + /// [ContentType("text")] + /// </example> + public interface IAsyncQuickInfoSourceProvider + { + /// <summary> + /// Creates an <see cref="IAsyncQuickInfoSource"/> for the specified <see cref="ITextBuffer"/>. + /// </summary> + /// <param name="textBuffer">The <see cref="ITextBuffer"/> for which this source produces items.</param> + /// <returns> + /// An instance of <see cref="IAsyncQuickInfoSource"/> for <paramref name="textBuffer"/> + /// or null if no source could be created. + /// </returns> + IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer); + } +} diff --git a/src/Language/Def/Language/QuickInfo/IInteractiveQuickInfoContent.cs b/src/Language/Def/Language/QuickInfo/IInteractiveQuickInfoContent.cs new file mode 100644 index 0000000..4f39082 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/IInteractiveQuickInfoContent.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved + +namespace Microsoft.VisualStudio.Language.Intellisense +{ + /// <summary> + /// Represents an interactive Quick Info content. This interface can be used to add an interactive content such as hyperlinks to + /// the Quick Info popup. + /// If any object implementing this interface is provided to + /// <see cref="IAsyncQuickInfoSource"/> via <see cref="IAsyncQuickInfoSource.GetQuickInfoItemAsync(IAsyncQuickInfoSession, System.Threading.CancellationToken,)"/>, + /// the Quick Info presenter will allow to interact with this content, particulartly it will keep Quick Info popup open when mouse + /// is over it and will allow this content to recieve mouse events. + /// </summary> + public interface IInteractiveQuickInfoContent + { + /// <summary> + /// Gets whether the interactive Quick Info content wants to keep current Quick Info session open. Until this property is true, + /// the <see cref="IAsyncQuickInfoSession"/> containing this content won't be dismissed even if mouse is moved somewhere else. + /// This is useful in very rare scenarios when an interactive Quick Info content handles all input interaction, while needs to + /// keep this <see cref="IAsyncQuickInfoSession"/> open (the only known example so far is LightBulb in its expanded state hosted in + /// Quick Info). + /// </summary> + bool KeepQuickInfoOpen { get; } + + /// <summary> + /// Gets a value indicating whether the mouse pointer is located over this interactive Quick Info content, + /// including any parts that are out of the Quick Info visual tree (such as popups). + /// </summary> + bool IsMouseOverAggregated { get; } + } +} diff --git a/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoBrokerSupport.cs b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoBrokerSupport.cs new file mode 100644 index 0000000..9def5c8 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoBrokerSupport.cs @@ -0,0 +1,30 @@ +namespace Microsoft.Internal.VisualStudio.Language.Intellisense +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Utilities; + + // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs. +#pragma warning disable 618 + /// <summary> + /// This interface supports the product infrastructure and should not be used. + /// </summary> + [Obsolete("This interface supports legacy product infrastructure, is subject to breakage without notice, and should not be used")] + internal interface ILegacyQuickInfoBrokerSupport : IAsyncQuickInfoBroker + { + /// <summary> + /// This method supports the product infrastructure and should not be used. + /// </summary> + Task<IAsyncQuickInfoSession> TriggerQuickInfoAsync( + ITextView textView, + ITrackingPoint triggerPoint, + QuickInfoSessionOptions options, + PropertyCollection propertyCollection, + CancellationToken cancellationToken); + } +#pragma warning restore 618 +} diff --git a/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoMetadata.cs b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoMetadata.cs new file mode 100644 index 0000000..3db2d0a --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoMetadata.cs @@ -0,0 +1,56 @@ +namespace Microsoft.Internal.VisualStudio.Language.Intellisense +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.VisualStudio.Utilities; + +#pragma warning disable 618 + /// <summary> + /// This interface supports the product infrastructure and should not be used. + /// </summary> + /// <remarks> + /// This is a MEF metadata view, similar to IContentTypeMetadata, however it uses + /// an explicit metadata class to allow it to be internal. Internal MEF metadata + /// view interfaces are supported but are currently suffering from intermittent + /// type load exceptions resulting from a bug in either the CLR or VS MEF. + /// </remarks> + [Obsolete("This interface supports legacy product infrastructure, is subject to breakage without notice, and should not be used")] + internal class LegacyQuickInfoMetadata : IContentTypeMetadata, IOrderable + { + public LegacyQuickInfoMetadata(IDictionary<string, object> data) + { + // Values are all optional. + data.TryGetValue(nameof(Name), out var name); + data.TryGetValue(nameof(ContentTypes), out var contentTypes); + data.TryGetValue(nameof(Before), out var before); + data.TryGetValue(nameof(After), out var after); + + this.ContentTypes = (IEnumerable<string>)contentTypes; + this.Name = (string)name; + this.Before = (IEnumerable<string>)before; + this.After = (IEnumerable<string>)after; + } + + internal LegacyQuickInfoMetadata( + string name, + IEnumerable<string> contentTypes, + IEnumerable<string> before, + IEnumerable<string> after) + { + this.Name = name; + this.ContentTypes = contentTypes; + this.Before = before ?? Enumerable.Empty<string>(); + this.After = after ?? Enumerable.Empty<string>(); + } + + public IEnumerable<string> ContentTypes { get; } + + public string Name { get; } + + public IEnumerable<string> Before { get; } + + public IEnumerable<string> After { get; } + } +#pragma warning restore 618 +} diff --git a/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoRecalculateSupport.cs b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoRecalculateSupport.cs new file mode 100644 index 0000000..290ed4f --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoRecalculateSupport.cs @@ -0,0 +1,15 @@ +namespace Microsoft.Internal.VisualStudio.Language.Intellisense +{ + using System; + +#pragma warning disable 618 + /// <summary> + /// This interface supports the product infrastructure and should not be used. + /// </summary> + [Obsolete("This interface supports legacy product infrastructure, is subject to breakage without notice, and should not be used")] + internal interface ILegacyQuickInfoRecalculateSupport + { + void Recalculate(); + } +#pragma warning restore 618 +} diff --git a/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoSource.cs b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoSource.cs new file mode 100644 index 0000000..94860a4 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoSource.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Internal.VisualStudio.Language.Intellisense +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.Text; + + // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs. +#pragma warning disable 618 + /// <summary> + /// This interface supports the product infrastructure and should not be used. + /// </summary> + [Obsolete("This interface supports legacy product infrastructure, is subject to breakage without notice, and should not be used")] + internal interface ILegacyQuickInfoSource : IAsyncQuickInfoSource + { + /// <summary> + /// This interface supports the product infrastructure and should not be used. + /// </summary> + void AugmentQuickInfoSession(IAsyncQuickInfoSession session, IList<object> content, out ITrackingSpan applicableToSpan); + } +#pragma warning restore 618 +} diff --git a/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoSourcesSupport.cs b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoSourcesSupport.cs new file mode 100644 index 0000000..e47a03f --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/Legacy/ILegacyQuickInfoSourcesSupport.cs @@ -0,0 +1,21 @@ +namespace Microsoft.Internal.VisualStudio.Language.Intellisense +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Language.Intellisense; + + // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs. +#pragma warning disable 618 + /// <summary> + /// This interface supports the product infrastructure and should not be used. + /// </summary> + [Obsolete("This interface supports legacy product infrastructure, is subject to breakage without notice, and should not be used")] + internal interface ILegacyQuickInfoSourcesSupport + { + /// <summary> + /// This interface supports the product infrastructure and should not be used. + /// </summary> + IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, LegacyQuickInfoMetadata>> LegacySources { get; } + } +#pragma warning restore 618 +} diff --git a/src/Language/Def/Language/QuickInfo/QuickInfoItem.cs b/src/Language/Def/Language/QuickInfo/QuickInfoItem.cs new file mode 100644 index 0000000..b890b7a --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/QuickInfoItem.cs @@ -0,0 +1,37 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using System; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Adornments; + + /// <summary> + /// The result generated by an <see cref="IAsyncQuickInfoSource"/>. + /// </summary> + public sealed class QuickInfoItem + { + /// <summary> + /// Constructs a new instance of <see cref="QuickInfoItem"/>. + /// </summary> + /// <exception cref="ArgumentNullException">Thrown if item is null.</exception> + /// <param name="applicableToSpan">The span to which <paramref name="item"/> is applicable.</param> + /// <param name="item">The Quick Info item.</param> + public QuickInfoItem(ITrackingSpan applicableToSpan, object item) + { + this.ApplicableToSpan = applicableToSpan; + this.Item = item ?? throw new ArgumentNullException(nameof(item)); + } + + /// <summary> + /// The <see cref="ITrackingSpan"/> to which <see cref="Item"/> is applicable. + /// </summary> + /// <remarks> + /// This parameter can be null. + /// </remarks> + public ITrackingSpan ApplicableToSpan { get; } + + /// <summary> + /// The item to be displayed in the Quick Info <see cref="IToolTipPresenter"/>. + /// </summary> + public object Item { get; } + } +} diff --git a/src/Language/Def/Language/QuickInfo/QuickInfoItemsCollection.cs b/src/Language/Def/Language/QuickInfo/QuickInfoItemsCollection.cs new file mode 100644 index 0000000..1d89f8c --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/QuickInfoItemsCollection.cs @@ -0,0 +1,35 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using Microsoft.VisualStudio.Text; + + /// <summary> + /// An immutable collection of Quick Info items and the span to which they are applicable. + /// </summary> + public sealed class QuickInfoItemsCollection + { + /// <summary> + /// The collection of Quick Info items. + /// </summary> + public IEnumerable<object> Items { get; } + + /// <summary> + /// The span to which the Quick Info items apply. + /// </summary> + public ITrackingSpan ApplicableToSpan { get; } + + /// <summary> + /// Creates a new <see cref="QuickInfoItemsCollection"/>. + /// </summary> + /// <param name="items">The Quick Info items.</param> + /// <param name="applicableToSpan">The span to which the items are applicable.</param> + public QuickInfoItemsCollection(IEnumerable<object> items, ITrackingSpan applicableToSpan) + { + this.Items = items.ToImmutableList() ?? throw new ArgumentNullException(nameof(items)); + this.ApplicableToSpan = applicableToSpan ?? throw new ArgumentNullException(nameof(applicableToSpan)); + } + } +} + diff --git a/src/Language/Def/Language/QuickInfo/QuickInfoSessionOptions.cs b/src/Language/Def/Language/QuickInfo/QuickInfoSessionOptions.cs new file mode 100644 index 0000000..38b4e05 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/QuickInfoSessionOptions.cs @@ -0,0 +1,21 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using System; + + /// <summary> + /// Options for customization of Quick Info behavior. + /// </summary> + [Flags] + public enum QuickInfoSessionOptions + { + /// <summary> + /// No options. + /// </summary> + None = 0b00000000, + + /// <summary> + /// Dismisses Quick Info when the mouse moves away. + /// </summary> + TrackMouse = 0b00000001 + } +} diff --git a/src/Language/Def/Language/QuickInfo/QuickInfoSessionState.cs b/src/Language/Def/Language/QuickInfo/QuickInfoSessionState.cs new file mode 100644 index 0000000..6767c1f --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/QuickInfoSessionState.cs @@ -0,0 +1,28 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + /// <summary> + /// Defines the possible <see cref="IAsyncQuickInfoSession"/> states. + /// </summary> + public enum QuickInfoSessionState + { + /// <summary> + /// Session has been created but is not yet active. + /// </summary> + Created, + + /// <summary> + /// Session is currently computing Quick Info content. + /// </summary> + Calculating, + + /// <summary> + /// Session has been dismissed and is no longer active. + /// </summary> + Dismissed, + + /// <summary> + /// Computation is complete and session is visible. + /// </summary> + Visible + } +} diff --git a/src/Language/Def/Language/QuickInfo/QuickInfoStateChangedEventArgs.cs b/src/Language/Def/Language/QuickInfo/QuickInfoStateChangedEventArgs.cs new file mode 100644 index 0000000..539dbf0 --- /dev/null +++ b/src/Language/Def/Language/QuickInfo/QuickInfoStateChangedEventArgs.cs @@ -0,0 +1,31 @@ +namespace Microsoft.VisualStudio.Language.Intellisense +{ + using System; + + /// <summary> + /// Arguments for the <see cref="IAsyncQuickInfoSession.StateChanged"/> event. + /// </summary> + public sealed class QuickInfoSessionStateChangedEventArgs : EventArgs + { + /// <summary> + /// Creates a new instance of <see cref="QuickInfoSessionStateChangedEventArgs"/>. + /// </summary> + /// <param name="oldState">The state before the transition.</param> + /// <param name="newState">The state after the transition.</param> + public QuickInfoSessionStateChangedEventArgs(QuickInfoSessionState oldState, QuickInfoSessionState newState) + { + this.OldState = oldState; + this.NewState = newState; + } + + /// <summary> + /// The state before the transition. + /// </summary> + public QuickInfoSessionState OldState { get; } + + /// <summary> + /// The state after the transition. + /// </summary> + public QuickInfoSessionState NewState { get; } + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs new file mode 100644 index 0000000..66c3976 --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Projection; +using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + [Export(typeof(IAsyncCompletionBroker))] + [Export(typeof(AsyncCompletionBroker))] + internal sealed class AsyncCompletionBroker : IAsyncCompletionBroker + { + [Import] + private IGuardedOperations GuardedOperations; + + [Import] + private JoinableTaskContext JoinableTaskContext; + + [Import] + private IContentTypeRegistryService ContentTypeRegistryService; + + [Import] + private CompletionAvailabilityUtility CompletionAvailability; + + [ImportMany] + private IEnumerable<Lazy<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> UnorderedCompletionSourceProviders; + + [ImportMany] + private IEnumerable<Lazy<IAsyncCompletionItemManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> UnorderedCompletionItemManagerProviders; + + [ImportMany] + private IEnumerable<Lazy<IAsyncCompletionCommitManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> UnorderedCompletionCommitManagerProviders; + + [ImportMany] + private IEnumerable<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> UnorderedPresenterProviders; + + // Used for telemetry + [Import(AllowDefault = true)] + private ILoggingServiceInternal Logger; + + // Used for legacy telemetry + [Import(AllowDefault = true)] + private ITextDocumentFactoryService TextDocumentFactoryService; + + private IList<Lazy<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> _orderedCompletionSourceProviders; + private IList<Lazy<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> OrderedCompletionSourceProviders + => _orderedCompletionSourceProviders ?? (_orderedCompletionSourceProviders = Orderer.Order(UnorderedCompletionSourceProviders)); + + private IList<Lazy<IAsyncCompletionItemManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> _orderedCompletionItemManagerProviders; + private IList<Lazy<IAsyncCompletionItemManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> OrderedCompletionItemManagerProviders + => _orderedCompletionItemManagerProviders ?? (_orderedCompletionItemManagerProviders = Orderer.Order(UnorderedCompletionItemManagerProviders)); + + private IList<Lazy<IAsyncCompletionCommitManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> _orderedCompletionCommitManagerProviders; + private IList<Lazy<IAsyncCompletionCommitManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> OrderedCompletionCommitManagerProviders + => _orderedCompletionCommitManagerProviders ?? (_orderedCompletionCommitManagerProviders = Orderer.Order(UnorderedCompletionCommitManagerProviders)); + + private IList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> _orderedPresenterProviders; + private IList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> OrderedPresenterProviders + => _orderedPresenterProviders ?? (_orderedPresenterProviders = Orderer.Order(UnorderedPresenterProviders)); + + private bool firstRun = true; // used only for diagnostics + private bool _firstInvocationReported; // used for "time to code" + private StableContentTypeComparer _contentTypeComparer; + private Dictionary<IContentType, bool> _providerAvailabilityByContentType = new Dictionary<IContentType, bool>(); + + public event EventHandler<CompletionTriggeredEventArgs> CompletionTriggered; + + #region IAsyncCompletionBroker implementation + + public bool IsCompletionActive(ITextView textView) + { + return textView.Properties.ContainsProperty(typeof(IAsyncCompletionSession)); + } + + public bool IsCompletionSupported(IContentType contentType) + { + // This will call HasCompletionProviders among doing other checks + return CompletionAvailability.IsAvailable(contentType); + } + + /// <summary> + /// Returns whether there exist any <see cref="IAsyncCompletionSourceProvider"/> + /// for the provided <see cref="IContentType"/> or any of its base content types. + /// Since MEF parts don't change on runtime, the answer is cached per <see cref="IContentType"/> for faster retrieval. + /// </summary> + internal bool HasCompletionProviders(IContentType contentType) + { + // Use cache if available + if (_providerAvailabilityByContentType.TryGetValue(contentType, out bool featureIsAvailable)) + return featureIsAvailable; + + featureIsAvailable = UnorderedCompletionSourceProviders.Any(n => n.Metadata.ContentTypes.Any(ct => contentType.IsOfType(ct))); + + _providerAvailabilityByContentType[contentType] = featureIsAvailable; + return featureIsAvailable; + } + + public IAsyncCompletionSession GetSession(ITextView textView) + { + if (textView.Properties.TryGetProperty(typeof(IAsyncCompletionSession), out IAsyncCompletionSession session)) + { + return session; + } + return null; + } + + public IAsyncCompletionSession TriggerCompletion(ITextView textView, SnapshotPoint triggerLocation, char typedChar, CancellationToken token) + { + var session = GetSession(textView); + if (session != null) + { + return session; + } + + // This is a simple check that only queries the feature service. + // If it succeeds, we will map triggerLocation to available buffers to discover MEF parts. + // This is expensive but projected languages require it to discover parts in all available buffers. + // To avoid doing this work, call IsCompletionSupported with appropriate IContentType prior to calling TriggerCompletion + if (!CompletionAvailability.IsAvailable(textView, contentTypeToCheckBlacklist: triggerLocation.Snapshot.ContentType)) + return null; + + if (!JoinableTaskContext.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + var telemetryHost = GetOrCreateTelemetry(textView); + var telemetry = new CompletionSessionTelemetry(telemetryHost); + + GetCommitManagersAndChars(textView.BufferGraph, textView.Roles, textView, triggerLocation, GetCommitManagerProviders, telemetry, + out var managersWithBuffers, out var potentialCommitChars); + + GetCompletionSources(textView.TextBuffer, textView.Roles, textView, textView.BufferGraph, triggerLocation, GetItemSourceProviders, telemetry, typedChar, token, + out var sourcesWithLocations, out var applicableToSpan); + + // No source declared an appropriate ApplicableToSpan + if (applicableToSpan == default) + return null; + + if (_contentTypeComparer == null) + _contentTypeComparer = new StableContentTypeComparer(ContentTypeRegistryService); + + var itemManager = GetItemManager(textView.BufferGraph, textView.Roles, textView, triggerLocation, GetItemManagerProviders, _contentTypeComparer); + var presenterProvider = GetPresenterProvider(textView.BufferGraph, textView.Roles, triggerLocation, GetPresenters, _contentTypeComparer); + + session = new AsyncCompletionSession(applicableToSpan, potentialCommitChars, JoinableTaskContext, presenterProvider, sourcesWithLocations, managersWithBuffers, itemManager, this, textView, telemetry, GuardedOperations); + textView.Properties.AddProperty(typeof(IAsyncCompletionSession), session); + + textView.Closed += TextView_Closed; + EmulateLegacyCompletionTelemetry(textView); + GuardedOperations.RaiseEvent(this, CompletionTriggered, new CompletionTriggeredEventArgs(session, textView)); + + return session; + } + + #endregion + + #region Internal communication with AsyncCompletionSession + + /// <summary> + /// This method is used by <see cref="IAsyncCompletionSession"/> to inform the broker that it should forget about the session. + /// Invoked as a result of dismissing. This method does not dismiss the session! + /// </summary> + /// <param name="session">Session being dismissed</param> +#pragma warning disable CA1822 // Member does not access instance data and can be marked as static + internal void ForgetSession(IAsyncCompletionSession session) + { + session.TextView.Properties.RemoveProperty(typeof(IAsyncCompletionSession)); + } +#pragma warning restore CA1822 + + #endregion + + #region MEF part helper methods + + private void GetCommitManagersAndChars( + IBufferGraph bufferGraph, + ITextViewRoleSet roles, + ITextView textViewForGetOrCreate, /* This name conveys that we're using ITextView only to init the MEF part. this is subject to change. */ + SnapshotPoint triggerLocation, + Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<IAsyncCompletionCommitManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>>> getImports, + CompletionSessionTelemetry telemetry, + out IList<(IAsyncCompletionCommitManager, ITextBuffer)> managersWithBuffers, + out ImmutableArray<char> potentialCommitChars) + { + var commitManagersWithData = MetadataUtilities<IAsyncCompletionCommitManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata> + .GetBuffersAndImports(bufferGraph, roles, triggerLocation, getImports); + + var potentialCommitCharsBuilder = ImmutableArray.CreateBuilder<char>(); + managersWithBuffers = new List<(IAsyncCompletionCommitManager, ITextBuffer)>(1); + foreach (var (buffer, point, import) in commitManagersWithData) + { + telemetry.UiStopwatch.Restart(); + var managerProvider = GuardedOperations.InstantiateExtension(this, import); + var manager = GuardedOperations.CallExtensionPoint( + errorSource: managerProvider, + call: () => managerProvider.GetOrCreate(textViewForGetOrCreate), + valueOnThrow: null); + + if (manager == null) + continue; + + GuardedOperations.CallExtensionPoint( + errorSource: manager, + call: () => + { + var characters = manager.PotentialCommitCharacters; + potentialCommitCharsBuilder.AddRange(characters); + }); + managersWithBuffers.Add((manager, buffer)); + telemetry.UiStopwatch.Stop(); + telemetry.RecordObtainingCommitManagerData(manager, telemetry.UiStopwatch.ElapsedMilliseconds); + } + potentialCommitChars = potentialCommitCharsBuilder.ToImmutable(); + } + + private void GetCompletionSources( + ITextBuffer editBuffer, + ITextViewRoleSet roles, + ITextView textViewForGetOrCreate, /* This name conveys that we're using ITextView only to init the MEF part. this is subject to change. */ + IBufferGraph bufferGraph, + SnapshotPoint triggerLocation, + Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>>> getImports, + CompletionSessionTelemetry telemetry, + char typedChar, + CancellationToken token, + out IList<(IAsyncCompletionSource Source, SnapshotPoint Point)> sourcesWithLocations, + out SnapshotSpan applicableToSpan) + { + var sourcesWithData = MetadataUtilities<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata> + .GetBuffersAndImports(bufferGraph, roles, triggerLocation, getImports); + + var applicableToSpanBuilder = default(SnapshotSpan); + bool applicableToSpanExists = false; + var sourcesWithLocationsBuider = new List<(IAsyncCompletionSource, SnapshotPoint)>(sourcesWithData.Count()); + + foreach (var (buffer, point, import) in sourcesWithData) + { + telemetry.UiStopwatch.Restart(); + + var sourceProvider = GuardedOperations.InstantiateExtension(this, import); + var source = GuardedOperations.CallExtensionPoint( + errorSource: sourceProvider, + call: () => sourceProvider.GetOrCreate(textViewForGetOrCreate), + valueOnThrow: null); + + if (source == null) + continue; + + applicableToSpanExists |= GuardedOperations.CallExtensionPoint( + errorSource: source, + call: () => + { + // We want to iterate through all sources and add them to collection + sourcesWithLocationsBuider.Add((source, point)); + // Get the span only if we haven't received one yet + if (!applicableToSpanExists) + return source.TryGetApplicableToSpan(typedChar, point, out applicableToSpanBuilder, token); + else + return false; // applicableToSpanExists is already true, so it doesn't matter what we return here + }, + valueOnThrow: false); + + telemetry.UiStopwatch.Stop(); + telemetry.RecordObtainingSourceSpan(source, telemetry.UiStopwatch.ElapsedMilliseconds); + } + + // Assume that sources are ordered. If this source is the first one to provide span, map it to the view's edit buffer and use it for completion, + if (applicableToSpanExists) + { + var mappingSpan = bufferGraph.CreateMappingSpan(applicableToSpanBuilder, SpanTrackingMode.EdgeInclusive); + applicableToSpanBuilder = mappingSpan.GetSpans(editBuffer)[0]; + } + + // Copying temporary values because we can't access out&ref params in lambdas + sourcesWithLocations = sourcesWithLocationsBuider; + applicableToSpan = applicableToSpanBuilder; + } + + private IAsyncCompletionItemManager GetItemManager( + IBufferGraph bufferGraph, + ITextViewRoleSet textViewRoles, + ITextView textViewForGetOrCreate, /* This name conveys that we're using ITextView only to init the MEF part. this is subject to change. */ + SnapshotPoint triggerLocation, + Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<IAsyncCompletionItemManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>>> getImports, + StableContentTypeComparer contentTypeComparer + ) + { + var itemManagerProvidersWithData = MetadataUtilities<IAsyncCompletionItemManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata> + .GetOrderedBuffersAndImports(bufferGraph, textViewRoles, triggerLocation, getImports, contentTypeComparer); + if (!itemManagerProvidersWithData.Any()) + { + // This should never happen because we provide a default and IsCompletionFeatureAvailable would have returned false + throw new InvalidOperationException("No completion services not found. Completion will be unavailable."); + } + + var bestItemManagerProvider = GuardedOperations.InstantiateExtension(this, itemManagerProvidersWithData.First().import); + return GuardedOperations.CallExtensionPoint(bestItemManagerProvider, () => bestItemManagerProvider.GetOrCreate(textViewForGetOrCreate), null); + } + + private ICompletionPresenterProvider GetPresenterProvider( + IBufferGraph bufferGraph, + ITextViewRoleSet textViewRoles, + SnapshotPoint triggerLocation, + Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>>> getImports, + StableContentTypeComparer contentTypeComparer) + { + var presenterProvidersWithData = MetadataUtilities<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata> + .GetOrderedBuffersAndImports(bufferGraph, textViewRoles, triggerLocation, getImports, contentTypeComparer); + ICompletionPresenterProvider presenterProvider = null; + if (presenterProvidersWithData.Any()) + presenterProvider = GuardedOperations.InstantiateExtension(this, presenterProvidersWithData.First().import); + + if (firstRun) + { + System.Diagnostics.Debug.Assert(presenterProvider != null, $"No instance of {nameof(ICompletionPresenterProvider)} is loaded. Completion will work without the UI."); + firstRun = false; + } + + return presenterProvider; + } + + private IReadOnlyList<Lazy<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> GetItemSourceProviders(IContentType contentType, ITextViewRoleSet textViewRoles) + { + return OrderedCompletionSourceProviders.Where(n => n.Metadata.ContentTypes.Any(c => contentType.IsOfType(c)) && (n.Metadata.TextViewRoles == null || textViewRoles.ContainsAny(n.Metadata.TextViewRoles))).ToList(); + } + + private IReadOnlyList<Lazy<IAsyncCompletionItemManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> GetItemManagerProviders(IContentType contentType, ITextViewRoleSet textViewRoles) + { + return OrderedCompletionItemManagerProviders.Where(n => n.Metadata.ContentTypes.Any(c => contentType.IsOfType(c)) && (n.Metadata.TextViewRoles == null || textViewRoles.ContainsAny(n.Metadata.TextViewRoles))).OrderBy(n => n.Metadata.ContentTypes, _contentTypeComparer).ToList(); + } + + private IReadOnlyList<Lazy<IAsyncCompletionCommitManagerProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> GetCommitManagerProviders(IContentType contentType, ITextViewRoleSet textViewRoles) + { + return OrderedCompletionCommitManagerProviders.Where(n => n.Metadata.ContentTypes.Any(c => contentType.IsOfType(c)) && (n.Metadata.TextViewRoles == null || textViewRoles.ContainsAny(n.Metadata.TextViewRoles))).ToList(); + } + + private IReadOnlyList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> GetPresenters(IContentType contentType, ITextViewRoleSet textViewRoles) + { + return OrderedPresenterProviders.Where(n => n.Metadata.ContentTypes.Any(c => contentType.IsOfType(c)) && (n.Metadata.TextViewRoles == null || textViewRoles.ContainsAny(n.Metadata.TextViewRoles))).OrderBy(n => n.Metadata.ContentTypes, _contentTypeComparer).ToList(); + } + + #endregion + + #region Telemetry + + private CompletionTelemetryHost GetOrCreateTelemetry(ITextView textView) + { + if (textView.Properties.TryGetProperty(typeof(CompletionTelemetryHost), out CompletionTelemetryHost telemetry)) + { + return telemetry; + } + else + { + var newTelemetry = new CompletionTelemetryHost(Logger, this); + textView.Properties.AddProperty(typeof(CompletionTelemetryHost), newTelemetry); + return newTelemetry; + } + } + +#pragma warning disable CA1822 // Member does not access instance data and can be marked as static + private static void SendTelemetry(ITextView textView) + { + if (textView.Properties.TryGetProperty(typeof(CompletionTelemetryHost), out CompletionTelemetryHost telemetry)) + { + telemetry.Send(); + textView.Properties.RemoveProperty(typeof(CompletionTelemetryHost)); + } + } +#pragma warning restore CA1822 + + // Parity with legacy telemetry + private void EmulateLegacyCompletionTelemetry(ITextView textView) + { + if (Logger == null || _firstInvocationReported) + return; + + string GetFileExtension(ITextBuffer buffer) + { + var documentFactoryService = TextDocumentFactoryService; + if (buffer != null && documentFactoryService != null) + { + documentFactoryService.TryGetTextDocument(buffer, out ITextDocument currentDocument); + if (currentDocument != null && currentDocument.FilePath != null) + { + return System.IO.Path.GetExtension(currentDocument.FilePath); + } + } + return null; + } + var fileExtension = GetFileExtension(textView.TextBuffer) ?? "Unknown"; + var reportedContentType = textView.TextBuffer.ContentType?.ToString() ?? "Unknown"; + + _firstInvocationReported = true; + Logger.PostEvent(TelemetryEventType.Operation, "VS/Editor/IntellisenseFirstRun/Opened", TelemetryResult.Success, + ("VS.Editor.IntellisenseFirstRun.Opened.ContentType", reportedContentType), + ("VS.Editor.IntellisenseFirstRun.Opened.FileExtension", fileExtension)); + } + + #endregion + + private void TextView_Closed(object sender, EventArgs e) + { + var view = (ITextView)sender; + view.Closed -= TextView_Closed; + GetSession(view)?.Dismiss(); + try + { + SendTelemetry(view); + } + catch (Exception ex) + { + GuardedOperations.HandleException(this, ex); + } + } + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs new file mode 100644 index 0000000..aad950d --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs @@ -0,0 +1,1030 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Utilities; +using Strings = Microsoft.VisualStudio.Language.Intellisense.Implementation.Strings; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Holds a state of the session + /// and a reference to the UI element + /// </summary> + internal class AsyncCompletionSession : IAsyncCompletionSession, IAsyncCompletionSessionOperations, IModelComputationCallbackHandler<CompletionModel> + { + // Available data and services + private readonly IList<(IAsyncCompletionSource Source, SnapshotPoint Point)> _completionSources; + private readonly IList<(IAsyncCompletionCommitManager, ITextBuffer)> _commitManagers; + private readonly IAsyncCompletionItemManager _completionItemManager; + private readonly JoinableTaskContext JoinableTaskContext; + private readonly ICompletionPresenterProvider _presenterProvider; + private readonly AsyncCompletionBroker _broker; + private readonly ITextView _textView; + private readonly IGuardedOperations _guardedOperations; + private readonly ImmutableArray<char> _potentialCommitChars; + + // Presentation: + private ICompletionPresenter _gui; // Must be accessed from GUI thread + private readonly int PageStepSize; + private const int FirstIndex = 0; + + // Computation state machine + private ModelComputation<CompletionModel> _computation; + private readonly CancellationTokenSource _computationCancellation = new CancellationTokenSource(); + private int _lastFilteringTaskId; + + // IAsyncCompletionSessionOperations properties for shims + public bool IsStarted => _computation != null; + + // ------------------------------------------------------------------------ + // Fixed completion model data that is guaranteed not to change when another thread accesses it. + // Rare exceptions: + // * model is Unavailable - we change ApplicableToSpan on the worker thread, but we know that UI thread won't access it + // * session was triggered in virtual whitespace, but not updated yet. We update ApplicableToSpan, and we know that worker thread won't access it. + + /// <summary> + /// Span pertinent to this completion. + /// </summary> + public ITrackingSpan ApplicableToSpan { get; set; } + + /// <summary> + /// Stores the initial reason this session was triggererd. + /// </summary> + private InitialTrigger InitialTrigger { get; set; } + + /// <summary> + /// Text to display in place of suggestion mode when filtered text is empty. + /// </summary> + private SuggestionItemOptions SuggestionItemOptions { get; set; } + + /// <summary> + /// Source that will provide tooltip for the suggestion item. + /// </summary> + private IAsyncCompletionSource SuggestionModeCompletionItemSource { get; set; } + + // ------------------------------------------------------------------------ + + /// <summary> + /// Telemetry aggregator for this session + /// </summary> + private readonly CompletionSessionTelemetry _telemetry; + + /// <summary> + /// Self imposed maximum delay for commits due to user double-clicking completion item in the UI + /// </summary> + private static readonly TimeSpan MaxCommitDelayWhenClicked = TimeSpan.FromSeconds(1); + + private static SuggestionItemOptions DefaultSuggestionModeOptions = new SuggestionItemOptions(string.Empty, Strings.SuggestionModeDefaultTooltip); + + // Facilitate experience when there are no items to display + private bool _selectionModeBeforeNoResultFallback; + private bool _inNoResultFallback; + private bool _ignoreCaretMovement; + + public event EventHandler<CompletionItemEventArgs> ItemCommitted; + public event EventHandler Dismissed; + public event EventHandler<ComputedCompletionItemsEventArgs> ItemsUpdated; + + public ITextView TextView => _textView; + + // When set, UI will no longer be updated + public bool IsDismissed { get; private set; } + + public PropertyCollection Properties { get; } + + public AsyncCompletionSession(SnapshotSpan initialApplicableToSpan, ImmutableArray<char> potentialCommitChars, + JoinableTaskContext joinableTaskContext, ICompletionPresenterProvider presenterProvider, + IList<(IAsyncCompletionSource, SnapshotPoint)> completionSources, IList<(IAsyncCompletionCommitManager, ITextBuffer)> commitManagers, + IAsyncCompletionItemManager completionService, AsyncCompletionBroker broker, ITextView textView, CompletionSessionTelemetry telemetry, + IGuardedOperations guardedOperations) + { + _potentialCommitChars = potentialCommitChars; + JoinableTaskContext = joinableTaskContext; + _presenterProvider = presenterProvider; + _broker = broker; + _completionSources = completionSources; // still prorotype at the momemnt. + _commitManagers = commitManagers; + _completionItemManager = completionService; + _textView = textView; + _guardedOperations = guardedOperations; + ApplicableToSpan = initialApplicableToSpan.Snapshot.CreateTrackingSpan(initialApplicableToSpan, SpanTrackingMode.EdgeInclusive); + _telemetry = telemetry; + PageStepSize = presenterProvider?.Options.ResultsPerPage ?? 1; + _textView.Caret.PositionChanged += OnCaretPositionChanged; + Properties = new PropertyCollection(); + } + + bool IAsyncCompletionSession.ShouldCommit(char typedChar, SnapshotPoint triggerLocation, CancellationToken token) + { + if (!JoinableTaskContext.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + if (!_potentialCommitChars.Contains(typedChar)) + return false; + + var mappingPoint = _textView.BufferGraph.CreateMappingPoint(triggerLocation, PointTrackingMode.Negative); + return _commitManagers + .Select(n => (n.Item1, mappingPoint.GetPoint(n.Item2, PositionAffinity.Predecessor))) + .Where(n => n.Item2.HasValue) + .Any(n => _guardedOperations.CallExtensionPoint( + errorSource: n.Item1, + call: () => n.Item1.ShouldCommitCompletion(typedChar, n.Item2.Value, token), + valueOnThrow: false)); + } + + bool IAsyncCompletionSession.CommitIfUnique(CancellationToken token) + { + if (IsDismissed) + return false; + + if (!JoinableTaskContext.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + _telemetry.UiStopwatch.Restart(); + var lastModel = _computation.WaitAndGetResult(cancelUi: true, token); + _telemetry.UiStopwatch.Stop(); + _telemetry.RecordBlockingWaitForComputation(_telemetry.UiStopwatch.ElapsedMilliseconds); + + if (lastModel == null) + { + return false; + } + else if (lastModel.Uninitialized) + { + return false; + } + else if (lastModel.UniqueItem != null) + { + var behavior = CommitItem(default, lastModel.UniqueItem, ApplicableToSpan, token); + if (behavior == CommitBehavior.CancelCommit) + { + // Show the UI, because waitAndGetResult canceled showing the UI. + UpdateUiInner(lastModel); // We are on the UI thread, so we may call UpdateUiInner + return false; + } + else + { + return true; + } + } + else if (!lastModel.PresentedItems.IsDefaultOrEmpty && lastModel.PresentedItems.Length == 1) + { + var behavior = CommitItem(default, lastModel.PresentedItems[0].CompletionItem, ApplicableToSpan, token); + if (behavior == CommitBehavior.CancelCommit) + { + // Show the UI, because waitAndGetResult canceled showing the UI. + UpdateUiInner(lastModel); // We are on the UI thread, so we may call UpdateUiInner + return false; + } + else + { + return true; + } + } + else + { + // Show the UI, because waitAndGetResult canceled showing the UI. + UpdateUiInner(lastModel); // We are on the UI thread, so we may call UpdateUiInner + return false; + } + } + + CommitBehavior IAsyncCompletionSession.Commit(char typedChar, CancellationToken token) + { + if (IsDismissed) + return CommitBehavior.None; + + if (!JoinableTaskContext.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + _telemetry.UiStopwatch.Restart(); + var lastModel = _computation.WaitAndGetResult(cancelUi: true, token); + _telemetry.UiStopwatch.Stop(); + _telemetry.RecordBlockingWaitForComputation(_telemetry.UiStopwatch.ElapsedMilliseconds); + + if (lastModel == null) + { + ((IAsyncCompletionSession)this).Dismiss(); + return CommitBehavior.None; + } + else if (lastModel.Uninitialized) + { + ((IAsyncCompletionSession)this).Dismiss(); + return CommitBehavior.None; + } + else if (lastModel.UseSoftSelection && !(typedChar.Equals(default) || typedChar.Equals('\t')) ) + { + // In soft selection mode, user commits explicitly (click, tab, e.g. not tied to a text change). Otherwise, we dismiss the session + ((IAsyncCompletionSession)this).Dismiss(); + return CommitBehavior.None; + } + else if (lastModel.SelectSuggestionItem && string.IsNullOrWhiteSpace(lastModel.SuggestionItem?.InsertText)) + { + // When suggestion mode is selected, don't commit empty suggestion + return CommitBehavior.None; + } + else if (lastModel.SelectSuggestionItem) + { + // Commit the suggestion mode item + return CommitItem(typedChar, lastModel.SuggestionItem, ApplicableToSpan, token); + } + else if (lastModel.PresentedItems.IsDefaultOrEmpty) + { + // There is nothing to commit + Dismiss(); + return CommitBehavior.None; + } + else + { + // Regular commit + return CommitItem(typedChar, lastModel.PresentedItems[lastModel.SelectedIndex].CompletionItem, ApplicableToSpan, token); + } + } + + private CommitBehavior CommitItem(char typedChar, CompletionItem itemToCommit, ITrackingSpan applicableToSpan, CancellationToken token) + { + CommitBehavior behavior = CommitBehavior.None; + if (IsDismissed) + return behavior; + + _telemetry.UiStopwatch.Restart(); + IAsyncCompletionCommitManager managerWhoCommitted = null; + + bool commitHandled = false; + foreach (var commitManager in _commitManagers) + { + var commitResult = _guardedOperations.CallExtensionPoint( + errorSource: commitManager, + call: () => commitManager.Item1.TryCommit(_textView, commitManager.Item2 /* buffer */, itemToCommit, applicableToSpan, typedChar, token), + valueOnThrow: CommitResult.Unhandled); + + if (commitResult.Behavior == CommitBehavior.CancelCommit) + { + // Return quickly without dismissing. + // Return this behavior so that CommitIfUnique displays the UI + _telemetry.UiStopwatch.Stop(); + return commitResult.Behavior; + } + + if (behavior == CommitBehavior.None) // Don't override behavior returned by higher priority commit manager + behavior = commitResult.Behavior; + + commitHandled |= commitResult.IsHandled; + if (commitResult.IsHandled) + { + managerWhoCommitted = commitManager.Item1; + break; + } + } + if (!commitHandled) + { + // Fallback if item is still not committed. + InsertIntoBuffer(_textView, applicableToSpan, itemToCommit.InsertText); + } + + _telemetry.UiStopwatch.Stop(); + _guardedOperations.RaiseEvent(this, ItemCommitted, new CompletionItemEventArgs(itemToCommit)); + _telemetry.RecordCommitted(_telemetry.UiStopwatch.ElapsedMilliseconds, managerWhoCommitted); + + Dismiss(); + + return behavior; + } + + private static void InsertIntoBuffer(ITextView view, ITrackingSpan applicableToSpan, string insertText) + { + var buffer = view.TextBuffer; + var bufferEdit = buffer.CreateEdit(); + + // ApplicableToSpan already contains the typedChar and brace completion. Replacing this span will cause us to lose this data. + // The command handler who invoked this code needs to re-play the type char command, such that we get these changes back. + bufferEdit.Replace(applicableToSpan.GetSpan(buffer.CurrentSnapshot), insertText); + bufferEdit.Apply(); + } + + public void Dismiss() + { + if (IsDismissed) + return; + + IsDismissed = true; + _broker.ForgetSession(this); + _guardedOperations.RaiseEvent(this, Dismissed); + _textView.Caret.PositionChanged -= OnCaretPositionChanged; + _computationCancellation.Cancel(); + + if (_gui != null) + { + var copyOfGui = _gui; + _guardedOperations.CallExtensionPointAsync( + errorSource: _gui, + asyncAction: async () => + { + await JoinableTaskContext.Factory.SwitchToMainThreadAsync(); + _telemetry.UiStopwatch.Restart(); + copyOfGui.FiltersChanged -= OnFiltersChanged; + copyOfGui.CommitRequested -= OnCommitRequested; + copyOfGui.CompletionItemSelected -= OnItemSelected; + copyOfGui.CompletionClosed -= OnGuiClosed; + copyOfGui.Close(); + _telemetry.UiStopwatch.Stop(); + _telemetry.RecordClosing(_telemetry.UiStopwatch.ElapsedMilliseconds); + await Task.Yield(); + _telemetry.Save(_completionItemManager, _presenterProvider); + }); + _gui = null; + } + } + + void IAsyncCompletionSession.OpenOrUpdate(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken commandToken) + { + if (IsDismissed) + return; + + if (!JoinableTaskContext.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + commandToken.Register(_computationCancellation.Cancel); + + if (_computation == null) + { + _computation = new ModelComputation<CompletionModel>( + PrioritizedTaskScheduler.AboveNormalInstance, + JoinableTaskContext, + (model, token) => GetInitialModel(trigger, triggerLocation, token), + _computationCancellation.Token, + _guardedOperations, + this + ); + } + + var taskId = Interlocked.Increment(ref _lastFilteringTaskId); + _computation.Enqueue((model, token) => UpdateSnapshot(model, trigger, new UpdateTrigger(FromCompletionTriggerReason(trigger.Reason), trigger.Character), triggerLocation, taskId, token), updateUi: true); + } + + ComputedCompletionItems IAsyncCompletionSession.GetComputedItems(CancellationToken token) + { + if (_computation == null) + return ComputedCompletionItems.Empty; // Call OpenOrUpdate first to kick off computation + + var model = _computation.WaitAndGetResult(cancelUi: true, token); // We don't want user initiated action to hide UI + return ComputeCompletionItems(model); + } + + private static UpdateTriggerReason FromCompletionTriggerReason(InitialTriggerReason reason) + { + switch (reason) + { + case InitialTriggerReason.Invoke: + case InitialTriggerReason.InvokeAndCommitIfUnique: + return UpdateTriggerReason.Initial; + case InitialTriggerReason.Insertion: + return UpdateTriggerReason.Insertion; + case InitialTriggerReason.Deletion: + return UpdateTriggerReason.Deletion; + default: + throw new ArgumentOutOfRangeException(nameof(reason)); + } + } + + #region IAsyncCompletionSessionOperations implementation + + public void InvokeAndCommitIfUnique(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + { + if (IsDismissed) + return; + + if (_computation == null) + { + // Compute the unique item. + // Don't recompute If we already have a model, so that we don't change user's selection. + ((IAsyncCompletionSession)this).OpenOrUpdate(trigger, triggerLocation, token); + } + + if (((IAsyncCompletionSession)this).CommitIfUnique(token)) + { + ((IAsyncCompletionSession)this).Dismiss(); + } + } + + public void SetSuggestionMode(bool useSuggestionMode) + { + _computation.Enqueue((model, token) => ToggleCompletionModeInner(model, useSuggestionMode, token), updateUi: true); + } + + public void SelectDown() + { + _computation.Enqueue((model, token) => UpdateSelectedItem(model, +1, token), updateUi: true); + } + + public void SelectPageDown() + { + _computation.Enqueue((model, token) => UpdateSelectedItem(model, +PageStepSize, token), updateUi: true); + } + + public void SelectUp() + { + _computation.Enqueue((model, token) => UpdateSelectedItem(model, -1, token), updateUi: true); + } + + public void SelectPageUp() + { + _computation.Enqueue((model, token) => UpdateSelectedItem(model, -PageStepSize, token), updateUi: true); + } + + public void SelectCompletionItem(CompletionItem item) + { + // To prevent inifinite loops, UI interacts with computation using the OnItemSelected event handler + _computation.Enqueue((model, token) => UpdateSelectedItem(model, item, false, token), updateUi: true); + } + + #endregion + + #region Internal methods that are implementation specific + + internal void IgnoreCaretMovement(bool ignore) + { + if (IsDismissed) + return; // This method will be called after committing. Don't act on it. + + _ignoreCaretMovement = ignore; + if (!ignore) + { + // Don't let the session exist in invalid state: ensure that the location of the session is still valid + HandleCaretPositionChanged(_textView.Caret.Position); + } + } + + #endregion + + private void OnFiltersChanged(object sender, CompletionFilterChangedEventArgs args) + { + var taskId = Interlocked.Increment(ref _lastFilteringTaskId); + _computation.Enqueue((model, token) => UpdateFilters(model, args.Filters, taskId, token), updateUi: true); + } + + /// <summary> + /// Handler for GUI requesting commit, usually through double-clicking. + /// There is no UI for cancellation, so use self-imposed expiration. + /// </summary> + private void OnCommitRequested(object sender, CompletionItemEventArgs args) + { + try + { + if (_computation == null) + return; + var expiringTokenSource = new CancellationTokenSource(MaxCommitDelayWhenClicked); + CommitItem(default, args.Item, ApplicableToSpan, expiringTokenSource.Token); + } + catch (Exception ex) + { + _guardedOperations.HandleException(this, ex); + } + } + + private void OnItemSelected(object sender, CompletionItemSelectedEventArgs args) + { + // Note 1: Use this only to react to selection changes initiated by user's mouse\touch operation in the UI, since they cancel the soft selection + // Note 2: we are not enqueuing a call to update the UI, since this would put us in infinite loop, and the UI is already updated + _computation.Enqueue((model, token) => UpdateSelectedItem(model, args.SelectedItem, args.SuggestionItemSelected, token), updateUi: false); + } + + private void OnGuiClosed(object sender, CompletionClosedEventArgs args) + { + Dismiss(); + } + + /// <summary> + /// Monitors when user scrolled outside of the applicable span. Note that: + /// * This event is not raised during regular typing. + /// * This event is raised by brace completion. + /// * Typing stretches the applicable span + /// </summary> + private void OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e) + { + // http://source.roslyn.io/#Microsoft.CodeAnalysis.EditorFeatures/Implementation/IntelliSense/Completion/Controller_CaretPositionChanged.cs,40 + if (_ignoreCaretMovement) + return; + + HandleCaretPositionChanged(e.NewPosition); + } + + async Task IModelComputationCallbackHandler<CompletionModel>.UpdateUI(CompletionModel model, CancellationToken token) + { + if (_presenterProvider == null) return; + await JoinableTaskContext.Factory.SwitchToMainThreadAsync(token); + if (token.IsCancellationRequested) return; + UpdateUiInner(model); + } + + /// <summary> + /// Opens or updates the UI. Must be called on UI thread. + /// </summary> + /// <param name="model"></param> + private void UpdateUiInner(CompletionModel model) + { + if (IsDismissed) + return; + if (model == null) + throw new ArgumentNullException(nameof(model)); + if (model.Uninitialized) + return; // Language service wishes to not show completion yet. + if (!JoinableTaskContext.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + // TODO: Consider building CompletionPresentationViewModel in BG and passing it here + _telemetry.UiStopwatch.Restart(); + if (_gui == null) + { + _gui = _guardedOperations.CallExtensionPoint(errorSource: _presenterProvider, call: () => _presenterProvider.GetOrCreate(_textView), valueOnThrow: null); + if (_gui != null) + { + _guardedOperations.CallExtensionPoint( + errorSource: _gui, + call: () => + { + _gui = _presenterProvider.GetOrCreate(_textView); + _gui.Open(new CompletionPresentationViewModel(model.PresentedItems, model.Filters, + model.SelectedIndex, ApplicableToSpan, model.UseSoftSelection, model.DisplaySuggestionItem, + model.SelectSuggestionItem, model.SuggestionItem, SuggestionItemOptions)); + _gui.FiltersChanged += OnFiltersChanged; + _gui.CommitRequested += OnCommitRequested; + _gui.CompletionItemSelected += OnItemSelected; + _gui.CompletionClosed += OnGuiClosed; + }); + } + } + else + { + _guardedOperations.CallExtensionPoint( + errorSource: _gui, + call: () => _gui.Update(new CompletionPresentationViewModel(model.PresentedItems, model.Filters, + model.SelectedIndex, ApplicableToSpan, model.UseSoftSelection, model.DisplaySuggestionItem, + model.SelectSuggestionItem, model.SuggestionItem, SuggestionItemOptions))); + } + _telemetry.UiStopwatch.Stop(); + _telemetry.RecordRendering(_telemetry.UiStopwatch.ElapsedMilliseconds); + } + + /// <summary> + /// Creates a new model and populates it with initial data + /// </summary> + private async Task<CompletionModel> GetInitialModel(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + { + bool sourceUsesSuggestionMode = false; + SuggestionItemOptions requestedSuggestionItemOptions = null; + InitialSelectionHint initialSelectionHint = InitialSelectionHint.RegularSelection; + var initialItemsBuilder = ImmutableArray.CreateBuilder<CompletionItem>(); + + for (int i = 0; i < _completionSources.Count; i++) + { + var index = i; // Capture i, since it will change during the async call + + _telemetry.ComputationStopwatch.Restart(); + var context = await _guardedOperations.CallExtensionPointAsync( + errorSource: _completionSources[index].Source, + asyncCall: () => _completionSources[index].Source.GetCompletionContextAsync(trigger, _completionSources[index].Point, ApplicableToSpan.GetSpan(ApplicableToSpan.TextBuffer.CurrentSnapshot), token), + valueOnThrow: null + ).ConfigureAwait(true); + _telemetry.ComputationStopwatch.Stop(); + _telemetry.RecordObtainingSourceContext(_completionSources[index].Source, _telemetry.ComputationStopwatch.ElapsedMilliseconds); + + if (context == null) + continue; + + sourceUsesSuggestionMode |= context.SuggestionItemOptions != null; + + // Set initial selection option, in order of precedence: soft selection, regular selection + if (context.SelectionHint == InitialSelectionHint.SoftSelection) + initialSelectionHint = InitialSelectionHint.SoftSelection; + + if (!context.Items.IsDefaultOrEmpty) + initialItemsBuilder.AddRange(context.Items); + // We use SuggestionModeOptions of the first source that provides it + if (requestedSuggestionItemOptions == null && context.SuggestionItemOptions != null) + requestedSuggestionItemOptions = context.SuggestionItemOptions; + } + + // Do not continue without items + if (initialItemsBuilder.Count == 0) + { + return CompletionModel.GetUninitializedModel(triggerLocation.Snapshot); + } + + // If no source provided suggestion item options, provide default options for suggestion mode + SuggestionItemOptions = requestedSuggestionItemOptions ?? DefaultSuggestionModeOptions; + + // Store the data that won't change throughout the session + InitialTrigger = trigger; + SuggestionModeCompletionItemSource = new SuggestionModeCompletionItemSource(SuggestionItemOptions); + + var initialCompletionItems = initialItemsBuilder.ToImmutable(); + + var availableFilters = initialCompletionItems + .SelectMany(n => n.Filters) + .Distinct() + .Select(n => new CompletionFilterWithState(n, true)) + .ToImmutableArray(); + + var customerUsesSuggestionMode = CompletionUtilities.GetSuggestionModeOption(_textView); + var viewUsesSuggestionMode = CompletionUtilities.IsDebuggerTextView(_textView); + + var useSuggestionMode = customerUsesSuggestionMode || sourceUsesSuggestionMode || viewUsesSuggestionMode; + // Select suggestion item only if source explicity provided it. This means that debugger view or ctrl+alt+space won't select the suggestion item. + var selectSuggestionItem = sourceUsesSuggestionMode; + // Use soft selection if suggestion item is present, unless source selects that item. Also, use soft selection if source wants to. + var useSoftSelection = useSuggestionMode && !selectSuggestionItem || initialSelectionHint == InitialSelectionHint.SoftSelection; + + _telemetry.ComputationStopwatch.Restart(); + var sortedList = await _guardedOperations.CallExtensionPointAsync( + errorSource: _completionItemManager, + asyncCall: () => _completionItemManager.SortCompletionListAsync( + session: this, + data: new AsyncCompletionSessionInitialDataSnapshot(initialCompletionItems, triggerLocation.Snapshot, InitialTrigger), + token: token), + valueOnThrow: initialCompletionItems).ConfigureAwait(true); + _telemetry.ComputationStopwatch.Stop(); + _telemetry.RecordProcessing(_telemetry.ComputationStopwatch.ElapsedMilliseconds, initialCompletionItems.Length); + _telemetry.RecordKeystroke(); + + return new CompletionModel(initialCompletionItems, sortedList, triggerLocation.Snapshot, + availableFilters, useSoftSelection, useSuggestionMode, selectSuggestionItem, suggestionItem: null); + } + + /// <summary> + /// User has moved the caret. Ensure that the caret is still within the applicable span. If not, dismiss the session. + /// </summary> + private void HandleCaretPositionChanged(CaretPosition caretPosition) + { + // TODO: when caret goes to the beginning of the span, we should enter soft selection + // when caret moves back into another location in the span, we should resume previous selection mode. + if (!ApplicableToSpan.GetSpan(caretPosition.VirtualBufferPosition.Position.Snapshot).IntersectsWith(new SnapshotSpan(caretPosition.VirtualBufferPosition.Position, 0))) + { + ((IAsyncCompletionSession)this).Dismiss(); + } + } + + /// <summary> + /// Sets or unsets suggestion mode. + /// </summary> +#pragma warning disable CA1822 // Member does not access instance data and can be marked as static +#pragma warning disable CA1801 // Parameter token is never used + private Task<CompletionModel> ToggleCompletionModeInner(CompletionModel model, bool useSuggestionMode, CancellationToken token) + { + return Task.FromResult(model.WithSuggestionItemVisibility(useSuggestionMode)); + } +#pragma warning restore CA1822 +#pragma warning restore CA1801 + + /// <summary> + /// User has typed. Update the known snapshot, filter the items and update the model. + /// </summary> + private async Task<CompletionModel> UpdateSnapshot(CompletionModel model, InitialTrigger initialTrigger, UpdateTrigger updateTrigger, SnapshotPoint updateLocation, int thisId, CancellationToken token) + { + // Always record keystrokes, even if filtering is preempted + _telemetry.RecordKeystroke(); + + // Completion got cancelled + if (token.IsCancellationRequested || model == null) + return default; + + var instantenousSnapshot = updateLocation.Snapshot; + + // Dismiss if we are outside of the applicable span + var currentlyApplicableToSpan = ApplicableToSpan.GetSpan(instantenousSnapshot); + if (updateLocation < currentlyApplicableToSpan.Start + || updateLocation > currentlyApplicableToSpan.End) + { + ((IAsyncCompletionSession)this).Dismiss(); + return model; + } + // Record the first time the span is empty. If it is empty the second time we're here, and user is deleting, then dismiss + if (currentlyApplicableToSpan.IsEmpty && model.ApplicableToSpanWasEmpty && initialTrigger.Reason == InitialTriggerReason.Deletion) + { + ((IAsyncCompletionSession)this).Dismiss(); + return model; + } + // If we were soft selected at the beginning of the span + model = model.WithApplicableToSpanStatus(currentlyApplicableToSpan.IsEmpty); + + // The model has no items. There is a chance that there will be items available + // after user types something. Due to timing issues, we can't just dismiss and start another session, + // so we need to attempt to get items again within this session. + if (model.Uninitialized && thisId > 1) // Don't attempt to get items on the very first UpdateSnapshot + { + // previous ApplicableToSpan returned no items. + // When we try getting items again, use a span that doesn't have characters present in the previous span + // Update the applicable span to the new snapshot, without the span that previously did not return any items + var previousSpan = ApplicableToSpan.GetSpan(model.Snapshot); + var pointThatDoesntTrackAdditions = model.Snapshot.CreateTrackingPoint(previousSpan.End, PointTrackingMode.Negative); + var newSpan = ApplicableToSpan.GetSpan(updateLocation.Snapshot); + + var newApplicableToSpanStart = pointThatDoesntTrackAdditions.GetPosition(updateLocation.Snapshot); + var newApplicableToSpanEnd = newSpan.End; + + var newApplicableToSpan = updateLocation.Snapshot.CreateTrackingSpan(newApplicableToSpanStart, newApplicableToSpanEnd - newApplicableToSpanStart, SpanTrackingMode.EdgeInclusive); + + this.ApplicableToSpan = newApplicableToSpan; // Everyone expects this to not change, but we are confident that the UI thread is waiting for this method to complete. + // Attempt to get new completion items + model = await GetInitialModel(initialTrigger, updateLocation, token).ConfigureAwait(true); + } + + if (model.Uninitialized) // Check if we just received some items + { + // If not, dismiss, unless there is another task queued. + var dismissed = await TryDismissSafely(thisId).ConfigureAwait(true); + return model; + } + + // Filtering got preempted, so store the most recent snapshot for the next time we filter. UpdateSnapshot will be called again. + if (thisId != _lastFilteringTaskId) + return model.WithSnapshot(instantenousSnapshot); + + _telemetry.ComputationStopwatch.Restart(); + + var filteredCompletion = await _guardedOperations.CallExtensionPointAsync( + errorSource: _completionItemManager, + asyncCall: () => _completionItemManager.UpdateCompletionListAsync( + session: this, + data: new AsyncCompletionSessionDataSnapshot( + model.InitialItems, + instantenousSnapshot, + initialTrigger, + updateTrigger, + model.Filters, + model.UseSoftSelection, + model.DisplaySuggestionItem), + token: token), + valueOnThrow: null).ConfigureAwait(true); + + // Error cases are handled by logging them above and dismissing the session. + if (filteredCompletion == null) + { + ((IAsyncCompletionSession)this).Dismiss(); + return model; + } + + // Special experience when there are no more selected items: + ImmutableArray<CompletionItemWithHighlight> returnedItems; + int selectedIndex = filteredCompletion.SelectedItemIndex; + if (filteredCompletion.Items.IsDefault) + { + // Prevent null references when service returns default(ImmutableArray) + returnedItems = ImmutableArray<CompletionItemWithHighlight>.Empty; + } + else if (filteredCompletion.Items.IsEmpty) + { + if (model.PresentedItems.IsDefaultOrEmpty) + { + // There were no previously visible results. Return a valid empty array + returnedItems = ImmutableArray<CompletionItemWithHighlight>.Empty; + } + else + { + // Show previously visible results, without highlighting + returnedItems = model.PresentedItems.Select(n => new CompletionItemWithHighlight(n.CompletionItem)).ToImmutableArray(); + selectedIndex = model.SelectedIndex; + if (!_inNoResultFallback) + { + // Enter the no results mode to preserve the selection state + _selectionModeBeforeNoResultFallback = model.UseSoftSelection; + _inNoResultFallback = true; + model = model.WithSoftSelection(true); + } + } + } + else + { + if (_inNoResultFallback) + { + // we were in the no result mode and just received no items. Restore the selection mode. + model = model.WithSoftSelection(_selectionModeBeforeNoResultFallback); + _inNoResultFallback = false; + } + returnedItems = filteredCompletion.Items; + } + + _telemetry.ComputationStopwatch.Stop(); + _telemetry.RecordProcessing(_telemetry.ComputationStopwatch.ElapsedMilliseconds, returnedItems.Length); + + // Allow the item manager to control the selection of the suggestion item + if (model.DisplaySuggestionItem) + { + if (filteredCompletion.SelectedItemIndex == -1) + model = model.WithSuggestionItemSelected(); + else + model = model.WithSelectedIndex(selectedIndex, preserveSoftSelection: true); + // If suggestion item is present, we default to soft selection. + model = model.WithSoftSelection(true); + } + else + { + model = model.WithSelectedIndex(selectedIndex, preserveSoftSelection: true); + } + + // Allow the item manager to override the selection style. + // Our recommendation for extenders is to use UpdateSelectionHint.NoChange whenever possible + if (filteredCompletion.SelectionHint == UpdateSelectionHint.SoftSelected) + model = model.WithSoftSelection(true); + else if (filteredCompletion.SelectionHint == UpdateSelectionHint.Selected + && (!model.DisplaySuggestionItem || model.SelectSuggestionItem)) + // Allow the language service wishes to fully select the item if we are not in suggestion mode, + // or if the item to select is the suggestion item. + model = model.WithSoftSelection(false); + + // Prepare the suggestionItem if user ever activates suggestion mode + var enteredText = currentlyApplicableToSpan.GetText(); + var suggestionItem = new CompletionItem(enteredText, SuggestionModeCompletionItemSource); + + var updatedModel = model.WithSnapshotItemsAndFilters(updateLocation.Snapshot, returnedItems, filteredCompletion.UniqueItem, suggestionItem, filteredCompletion.Filters); + RaiseCompletionItemsComputedEvent(updatedModel); + return updatedModel; + } + + /// <summary> + /// Dismisses this <see cref="AsyncCompletionSession"/> only if called from the last task. + /// If there are any extra tasks, this method will return <code>false</code> + /// </summary> + /// <param name="currentTaskId"></param> + /// <returns></returns> + private async Task<bool> TryDismissSafely(int currentTaskId) + { + await JoinableTaskContext.Factory.SwitchToMainThreadAsync(); + + // Tasks are enqueued on the UI thread, so we know that _lastFilteringTaskId won't change + if (currentTaskId < _lastFilteringTaskId) + { + // This is not the last task, so we should not dismiss. + return false; + } + else + { + Dismiss(); + return true; + } + } + + /// <summary> + /// Reacts to user toggling a filter + /// </summary> + /// <param name="newFilters">Filters with updated Selected state, as indicated by the user.</param> + private async Task<CompletionModel> UpdateFilters(CompletionModel model, ImmutableArray<CompletionFilterWithState> newFilters, int thisId, CancellationToken token) + { + _telemetry.RecordChangingFilters(); + _telemetry.RecordKeystroke(); + + // Filtering got preempted, so store the most updated filters for the next time we filter + if (token.IsCancellationRequested || thisId != _lastFilteringTaskId) + return model.WithFilters(newFilters); + + var filteredCompletion = await _guardedOperations.CallExtensionPointAsync( + errorSource: _completionItemManager, + asyncCall: () => _completionItemManager.UpdateCompletionListAsync( + session: this, + data: new AsyncCompletionSessionDataSnapshot( + model.InitialItems, + model.Snapshot, + InitialTrigger, + new UpdateTrigger(UpdateTriggerReason.FilterChange), + newFilters, + model.UseSoftSelection, + model.DisplaySuggestionItem), + token: token), + valueOnThrow: null).ConfigureAwait(true); + + // Handle error cases by logging the issue and discarding the request to filter + if (filteredCompletion == null) + return model; + if (filteredCompletion.Filters.Length != newFilters.Length) + { + _guardedOperations.HandleException( + errorSource: _completionItemManager, + e: new InvalidOperationException("Completion service returned incorrect set of filters.")); + return model; + } + + var updatedModel = model.WithFilters(filteredCompletion.Filters).WithPresentedItems(filteredCompletion.Items, filteredCompletion.SelectedItemIndex); + RaiseCompletionItemsComputedEvent(updatedModel); + return updatedModel; + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +#pragma warning disable CA1801 // Parameter token is never used + /// <summary> + /// Reacts to user scrolling the list using keyboard + /// </summary> + private async Task<CompletionModel> UpdateSelectedItem(CompletionModel model, int offset, CancellationToken token) +#pragma warning restore CS1998 +#pragma warning restore CA1801 + { + _telemetry.RecordScrolling(); + _telemetry.RecordKeystroke(); + + if (!model.PresentedItems.Any()) + { + // No-op if there are no items, unless there is a suggestion item. + if (model.DisplaySuggestionItem) + { + return model.WithSuggestionItemSelected(); // Select the sole item which is a suggestion item. + } + return model; + } + + var lastIndex = model.PresentedItems.Count() - 1; + var currentIndex = model.SelectSuggestionItem ? -1 : model.SelectedIndex; + + if (offset > 0) // Scrolling down. Stop at last index and don't wrap around. + { + if (currentIndex == lastIndex) + return model; + + var newIndex = currentIndex + offset; + return model.WithSelectedIndex(Math.Min(newIndex, lastIndex)); + } + else // Scrolling up. Stop at first index and don't wrap around. + { + if (currentIndex < FirstIndex) // Suggestion mode item is selected. + { + return model; // Don't wrap around. + } + else if (currentIndex == FirstIndex) // The first item is selected. + { + if (model.DisplaySuggestionItem) // If there is a suggestion, select it. + return model.WithSuggestionItemSelected(); + else + return model; // Don't wrap around. + } + var newIndex = currentIndex + offset; + return model.WithSelectedIndex(Math.Max(newIndex, FirstIndex)); + } + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +#pragma warning disable CA1801 // Parameter token is never used + /// <summary> + /// Reacts to user selecting a specific item in the list + /// </summary> + private async Task<CompletionModel> UpdateSelectedItem(CompletionModel model, CompletionItem selectedItem, bool suggestionItemSelected, CancellationToken token) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously +#pragma warning restore CA1801 + { + _telemetry.RecordScrolling(); + if (suggestionItemSelected) + { + var updatedModel = model.WithSuggestionItemSelected(); + RaiseCompletionItemsComputedEvent(updatedModel); + return updatedModel; + } + else + { + for (int i = 0; i < model.PresentedItems.Length; i++) + { + if (model.PresentedItems[i].CompletionItem == selectedItem) + { + var updatedModel = model.WithSelectedIndex(i); + RaiseCompletionItemsComputedEvent(updatedModel); + return updatedModel; + } + } + // This item is not in the model + return model; + } + } + + private void RaiseCompletionItemsComputedEvent(CompletionModel model) + { + if (ItemsUpdated == null) + return; + + var computedItems = ComputeCompletionItems(model); + + // Warning: if the event handler throws and anyone blocks UI thread now, there will be a deadlock. + // This won't happen for now, because all callers of this method are private and nobody waits on them. + _guardedOperations.RaiseEvent(this, ItemsUpdated, new ComputedCompletionItemsEventArgs(computedItems)); + } + + private static ComputedCompletionItems ComputeCompletionItems(CompletionModel model) + { + if (model == null || model.Uninitialized) + return ComputedCompletionItems.Empty; + + return new ComputedCompletionItems( + itemsWithHighlight: model.PresentedItems, + suggestionItem: model.DisplaySuggestionItem ? model.SuggestionItem : null, + selectedItem: model.SelectSuggestionItem + ? model.SuggestionItem + : model.PresentedItems.IsDefaultOrEmpty || model.SelectedIndex < 0 + ? null + : model.PresentedItems[model.SelectedIndex].CompletionItem, + suggestionItemSelected: model.SelectSuggestionItem, + usesSoftSelection: model.UseSoftSelection); + } + } +} diff --git a/src/Language/Impl/Language/Completion/CaretPreservingEditTransaction.cs b/src/Language/Impl/Language/AsyncCompletion/CaretPreservingEditTransaction.cs index 45c0aed..9c2337b 100644 --- a/src/Language/Impl/Language/Completion/CaretPreservingEditTransaction.cs +++ b/src/Language/Impl/Language/AsyncCompletion/CaretPreservingEditTransaction.cs @@ -4,7 +4,7 @@ using System; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation { internal class CaretPreservingEditTransaction : IDisposable { @@ -74,6 +74,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation EndTransaction(); } +#pragma warning disable CA1063 // Dispose pattern public void Dispose() { if (_transaction != null) @@ -82,6 +83,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation Cancel(); } } +#pragma warning restore CA1063 public IMergeTextUndoTransactionPolicy MergePolicy { diff --git a/src/Language/Impl/Language/AsyncCompletion/CompletionAvailabilityUtility.cs b/src/Language/Impl/Language/AsyncCompletion/CompletionAvailabilityUtility.cs new file mode 100644 index 0000000..a46c0b6 --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/CompletionAvailabilityUtility.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Provides information whether modern completion should be enabled, + /// based on the state of <see cref="IExperimentationServiceInternal"/> and <see cref="IFeatureServiceFactory" /> + /// for the given <see cref="IContentType"/> and <see cref="ITextView"/>. + /// </summary> + [Export] + internal class CompletionAvailabilityUtility + { + [Import] + private IExperimentationServiceInternal ExperimentationService; + + [Import] + private IFeatureServiceFactory FeatureServiceFactory; + + [Import] + private AsyncCompletionBroker Broker; // We're using internal method to check if relevant MEF parts exist. + + // Black list by content type + private const string CompletionFlightName = "CompletionAPI"; + private const string RoslynLanguagesContentType = "Roslyn Languages"; + private const string RazorContentType = "Razor"; + private bool _treatmentFlightDataInitialized; + + // Quick access data: + private bool _treatmentFlightEnabled; + private IFeatureCookie _globalCompletionCookie; + private IFeatureCookie GlobalCompletionCookie => + _globalCompletionCookie + ?? (_globalCompletionCookie = FeatureServiceFactory.GlobalFeatureService.GetCookie(PredefinedEditorFeatureNames.Completion)); + + /// <summary> + /// Returns whether completion is available for the given <see cref="IContentType" />. + /// </summary> + /// <returns>true if experiment is enabled, feature is enabled in the global scope, and broker has providers that match the supplied <see cref="IContentType" /></returns> + internal bool IsAvailable(IContentType contentType) + { + if (!GlobalCompletionCookie.IsEnabled) + return false; + + if (!Broker.HasCompletionProviders(contentType)) + return false; + + // Roslyn and Razor providers exist in the MEF cache, but Roslyn is not ready for public rollout yet. + // However, We do want other languages (e.g. AXML, EditorConfig) to work with Async Completion API + // We will remove this check once Roslyn fully embraces Async Completion API. + if (!IsExperimentEnabled() && (contentType.IsOfType(RoslynLanguagesContentType) || contentType.IsOfType(RazorContentType))) + return false; + + return true; + } + + /// <summary> + /// Returns whether completion is available for the given <see cref="IContentType"/> in the given <see cref="ITextView" />. + /// </summary> + /// <returns>true if experiment is enabled, feature is enabled in the <see cref="ITextView" />'s scope, and broker has providers that match the supplied <see cref="IContentType" /></returns> + internal bool IsAvailable(IContentType contentType, ITextView textView) + { + if (!Broker.HasCompletionProviders(contentType)) + return false; + + var featureService = FeatureServiceFactory.GetOrCreate(textView); + if (!featureService.IsEnabled(PredefinedEditorFeatureNames.Completion)) + return false; + + // Roslyn and Razor providers exist in the MEF cache, but Roslyn is not ready for public rollout yet. + // However, We do want other languages (e.g. AXML, EditorConfig) to work with Async Completion API + // We will remove this check once Roslyn fully embraces Async Completion API. + if (!IsExperimentEnabled() && (contentType.IsOfType(RoslynLanguagesContentType) || contentType.IsOfType(RazorContentType))) + return false; + + return true; + } + + /// <summary> + /// Returns whether completion is available in the given <see cref="ITextView" />. + /// Note: the second parameter <see cref="IContentType"/> is to be removed in dev16 when the experiment ends. + /// </summary> + /// <returns>true if experiment is enabled and feature is enabled in <see cref="ITextView"/>'s scope</returns> + internal bool IsAvailable(ITextView textView, IContentType contentTypeToCheckBlacklist) + { + var featureService = FeatureServiceFactory.GetOrCreate(textView); + if (!featureService.IsEnabled(PredefinedEditorFeatureNames.Completion)) + return false; + + // Roslyn and Razor providers exist in the MEF cache, but Roslyn is not ready for public rollout yet. + // However, We do want other languages (e.g. AXML, EditorConfig) to work with Async Completion API + // We will remove this check once Roslyn fully embraces Async Completion API. + if (!IsExperimentEnabled() && (contentTypeToCheckBlacklist.IsOfType(RoslynLanguagesContentType) || contentTypeToCheckBlacklist.IsOfType(RazorContentType))) + return false; + + return true; + } + + private bool IsExperimentEnabled() + { + if (_treatmentFlightDataInitialized) + return _treatmentFlightEnabled; + + _treatmentFlightEnabled = ExperimentationService.IsCachedFlightEnabled(CompletionFlightName); + _treatmentFlightDataInitialized = true; + return _treatmentFlightEnabled; + } + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs b/src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs new file mode 100644 index 0000000..e0599f1 --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs @@ -0,0 +1,564 @@ +using System; +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Commanding; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; +using Microsoft.VisualStudio.Text.Operations; +using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; +using CommonImplementation = Microsoft.VisualStudio.Language.Intellisense.Implementation; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Reacts to the down arrow command and attempts to scroll the completion list. + /// </summary> + [Name(PredefinedCompletionNames.CompletionCommandHandler)] + [ContentType("text")] + [TextViewRole(PredefinedTextViewRoles.Interactive)] + [Export(typeof(ICommandHandler))] + internal sealed class CompletionCommandHandler : + ICommandHandler<DownKeyCommandArgs>, + ICommandHandler<PageDownKeyCommandArgs>, + ICommandHandler<PageUpKeyCommandArgs>, + ICommandHandler<UpKeyCommandArgs>, + IChainedCommandHandler<BackspaceKeyCommandArgs>, + IDynamicCommandHandler<BackspaceKeyCommandArgs>, + ICommandHandler<EscapeKeyCommandArgs>, + IDynamicCommandHandler<EscapeKeyCommandArgs>, + ICommandHandler<InvokeCompletionListCommandArgs>, + ICommandHandler<CommitUniqueCompletionListItemCommandArgs>, + ICommandHandler<InsertSnippetCommandArgs>, + ICommandHandler<SurroundWithCommandArgs>, + ICommandHandler<ToggleCompletionModeCommandArgs>, + IChainedCommandHandler<DeleteKeyCommandArgs>, + IDynamicCommandHandler<DeleteKeyCommandArgs>, + ICommandHandler<WordDeleteToEndCommandArgs>, + ICommandHandler<WordDeleteToStartCommandArgs>, + ICommandHandler<SaveCommandArgs>, + ICommandHandler<SelectAllCommandArgs>, + ICommandHandler<RenameCommandArgs>, + ICommandHandler<UndoCommandArgs>, + ICommandHandler<RedoCommandArgs>, + IChainedCommandHandler<ReturnKeyCommandArgs>, + IDynamicCommandHandler<ReturnKeyCommandArgs>, + IChainedCommandHandler<TabKeyCommandArgs>, + IDynamicCommandHandler<TabKeyCommandArgs>, + IChainedCommandHandler<TypeCharCommandArgs>, + IDynamicCommandHandler<TypeCharCommandArgs> + { + [Import] + private IAsyncCompletionBroker Broker; + + [Import] + private ITextUndoHistoryRegistry UndoHistoryRegistry; + + [Import] + private IEditorOperationsFactoryService EditorOperationsFactoryService; + + [Import] + private CompletionAvailabilityUtility CompletionAvailability; + + string INamed.DisplayName => CommonImplementation.Strings.CompletionCommandHandlerName; + + /// <summary> + /// Helper method that returns command state for commands + /// that are always available - unless the completion feature is available. + /// </summary> + private CommandState GetCommandStateIfCompletionIsAvailable(IContentType contentType, ITextView textView) + { + return CompletionAvailability.IsAvailable(contentType, textView) + ? CommandState.Available + : CommandState.Unspecified; + } + + /// <summary> + /// Helper method that returns command state + /// for commands that are available IF AND ONLY IF completion is active, + /// even if the commands would be otherwise unavailable. + /// </summary> + /// <remarks> + /// For commands whose availability is not influenced by completion, use <see cref="CommandState.Unspecified"/> + /// </remarks> + private CommandState GetCommandStateIfCompletionIsActive(ITextView textView) + { + return Broker.IsCompletionActive(textView) + ? CommandState.Available + : CommandState.Unspecified; + } + + /// <summary> + /// Helper method that returns command state for commands that are available when completion is + /// either currently active, or available. + /// This is used by commands that may trigger completion session on a specified buffer, or interact with an active completion session on another buffer + /// </summary> + private CommandState GetCommandStateIfCompletionIsActiveOrAvailable(IContentType contentType, ITextView textView) + { + return Broker.IsCompletionActive(textView) || CompletionAvailability.IsAvailable(contentType, textView) + ? CommandState.Available + : CommandState.Unspecified; + } + + /// <summary> + /// Helper method that returns command state for the suggestion mode toggle button. + /// This command state controls not only whether the toggle button is enabled, but also if it's toggled. + /// </summary> + private CommandState GetCommandStateForSuggestionModeToggle(IContentType contentType, ITextView textView) + { + var isAvailable = CompletionAvailability.IsAvailable(contentType, textView); + var isChecked = CompletionUtilities.IsDebuggerTextView(textView) + ? CompletionUtilities.GetSuggestionModeOption(textView) + : CompletionUtilities.GetSuggestionModeInDebuggerCompletionOption(textView); + return new CommandState(isAvailable, isChecked); + } + + /// <summary> + /// Realizes the virtual space and updates session's applicable to span + /// </summary> + private void RealizeVirtualSpaceUpdateApplicableToSpan(IAsyncCompletionSessionOperations session, ITextView textView) + { + if (session == null // We may only act if we have internal reference to the session + || !textView.Caret.InVirtualSpace // We only act if caret is in virtual space + || !session.ApplicableToSpan.GetSpan(textView.TextSnapshot).IsEmpty) // We only act if the applicable to span is of zero length (at the beginning of the line) + { + return; + } + + // Realize the virtual space before triggering the session by inserting nothing through the editor opertaions. + IEditorOperations editorOperations = EditorOperationsFactoryService.GetEditorOperations(textView); + editorOperations?.InsertText(""); + + // ApplicableToSpan just grew to include the realized white space. + // We know that ApplicableToSpan was zero length, so let's recreate a zero length span at the caret location + session.ApplicableToSpan = textView.TextSnapshot.CreateTrackingSpan( + start: textView.Caret.Position.BufferPosition.Position, + length: 0, + trackingMode: SpanTrackingMode.EdgePositive); + } + + // ----- Command handlers: + + CommandState IChainedCommandHandler<BackspaceKeyCommandArgs>.GetCommandState(BackspaceKeyCommandArgs args, Func<CommandState> nextCommandHandler) + => CommandState.Unspecified; + + bool IDynamicCommandHandler<BackspaceKeyCommandArgs>.CanExecuteCommand(BackspaceKeyCommandArgs args) + => Broker.IsCompletionActive(args.TextView); + + void IChainedCommandHandler<BackspaceKeyCommandArgs>.ExecuteCommand(BackspaceKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + // Execute other commands in the chain to see the change in the buffer. + nextCommandHandler(); + + var session = Broker.GetSession(args.TextView); + if (session != null) + { + var trigger = new InitialTrigger(InitialTriggerReason.Deletion); + var location = args.TextView.Caret.Position.BufferPosition; + session.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + } + } + + CommandState ICommandHandler<EscapeKeyCommandArgs>.GetCommandState(EscapeKeyCommandArgs args) + => GetCommandStateIfCompletionIsActive(args.TextView); + + bool IDynamicCommandHandler<EscapeKeyCommandArgs>.CanExecuteCommand(EscapeKeyCommandArgs args) + => Broker.IsCompletionActive(args.TextView); + + bool ICommandHandler<EscapeKeyCommandArgs>.ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.Dismiss(); + return true; + } + return false; + } + + CommandState ICommandHandler<InvokeCompletionListCommandArgs>.GetCommandState(InvokeCompletionListCommandArgs args) + => GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView); + + bool ICommandHandler<InvokeCompletionListCommandArgs>.ExecuteCommand(InvokeCompletionListCommandArgs args, CommandExecutionContext executionContext) + { + if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable) + return false; + + var trigger = new InitialTrigger(InitialTriggerReason.Invoke); + var location = args.TextView.Caret.Position.BufferPosition; + var session = Broker.TriggerCompletion(args.TextView, location, default, executionContext.OperationContext.UserCancellationToken); + if (session is IAsyncCompletionSessionOperations sessionInternal) + { + RealizeVirtualSpaceUpdateApplicableToSpan(sessionInternal, args.TextView); + location = args.TextView.Caret.Position.BufferPosition; // Buffer may have changed. Update the location. + sessionInternal.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + return true; + } + return false; + } + + CommandState ICommandHandler<CommitUniqueCompletionListItemCommandArgs>.GetCommandState(CommitUniqueCompletionListItemCommandArgs args) + => GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView); + + bool ICommandHandler<CommitUniqueCompletionListItemCommandArgs>.ExecuteCommand(CommitUniqueCompletionListItemCommandArgs args, CommandExecutionContext executionContext) + { + if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable) + return false; + + var trigger = new InitialTrigger(InitialTriggerReason.InvokeAndCommitIfUnique); + var location = args.TextView.Caret.Position.BufferPosition; + var session = Broker.TriggerCompletion(args.TextView, location, default, executionContext.OperationContext.UserCancellationToken); + if (session is IAsyncCompletionSessionOperations sessionInternal) + { + RealizeVirtualSpaceUpdateApplicableToSpan(sessionInternal, args.TextView); + location = args.TextView.Caret.Position.BufferPosition; // Buffer may have changed. Update the location. + sessionInternal.InvokeAndCommitIfUnique(trigger, location, executionContext.OperationContext.UserCancellationToken); + return true; + } + return false; + } + + CommandState ICommandHandler<InsertSnippetCommandArgs>.GetCommandState(InsertSnippetCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<InsertSnippetCommandArgs>.ExecuteCommand(InsertSnippetCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<SurroundWithCommandArgs>.GetCommandState(SurroundWithCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<SurroundWithCommandArgs>.ExecuteCommand(SurroundWithCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<ToggleCompletionModeCommandArgs>.GetCommandState(ToggleCompletionModeCommandArgs args) + => GetCommandStateForSuggestionModeToggle(args.SubjectBuffer.ContentType, args.TextView); + + bool ICommandHandler<ToggleCompletionModeCommandArgs>.ExecuteCommand(ToggleCompletionModeCommandArgs args, CommandExecutionContext executionContext) + { + var toggledValue = !CompletionUtilities.GetSuggestionModeOption(args.TextView); + CompletionUtilities.SetSuggestionModeOption(args.TextView, toggledValue); + + if (Broker.GetSession(args.TextView) is IAsyncCompletionSessionOperations sessionInternal) // we are accessing an internal method + { + sessionInternal.SetSuggestionMode(toggledValue); + return true; + } + return false; + } + + CommandState IChainedCommandHandler<DeleteKeyCommandArgs>.GetCommandState(DeleteKeyCommandArgs args, Func<CommandState> nextCommandHandler) + => CommandState.Unspecified; + + bool IDynamicCommandHandler<DeleteKeyCommandArgs>.CanExecuteCommand(DeleteKeyCommandArgs args) + => Broker.IsCompletionActive(args.TextView); + + void IChainedCommandHandler<DeleteKeyCommandArgs>.ExecuteCommand(DeleteKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + // Execute other commands in the chain to see the change in the buffer. + nextCommandHandler(); + + var session = Broker.GetSession(args.TextView); + if (session != null) + { + var trigger = new InitialTrigger(InitialTriggerReason.Deletion); + var location = args.TextView.Caret.Position.BufferPosition; + session.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + } + } + + CommandState ICommandHandler<WordDeleteToEndCommandArgs>.GetCommandState(WordDeleteToEndCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<WordDeleteToEndCommandArgs>.ExecuteCommand(WordDeleteToEndCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<WordDeleteToStartCommandArgs>.GetCommandState(WordDeleteToStartCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<WordDeleteToStartCommandArgs>.ExecuteCommand(WordDeleteToStartCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<SaveCommandArgs>.GetCommandState(SaveCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<SaveCommandArgs>.ExecuteCommand(SaveCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<SelectAllCommandArgs>.GetCommandState(SelectAllCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<SelectAllCommandArgs>.ExecuteCommand(SelectAllCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<RenameCommandArgs>.GetCommandState(RenameCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<RenameCommandArgs>.ExecuteCommand(RenameCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<UndoCommandArgs>.GetCommandState(UndoCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<UndoCommandArgs>.ExecuteCommand(UndoCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState ICommandHandler<RedoCommandArgs>.GetCommandState(RedoCommandArgs args) + => CommandState.Unspecified; + + bool ICommandHandler<RedoCommandArgs>.ExecuteCommand(RedoCommandArgs args, CommandExecutionContext executionContext) + { + Broker.GetSession(args.TextView)?.Dismiss(); + return false; + } + + CommandState IChainedCommandHandler<ReturnKeyCommandArgs>.GetCommandState(ReturnKeyCommandArgs args, Func<CommandState> nextCommandHandler) + => GetCommandStateIfCompletionIsActiveOrAvailable(args.SubjectBuffer.ContentType, args.TextView); + + bool IDynamicCommandHandler<ReturnKeyCommandArgs>.CanExecuteCommand(ReturnKeyCommandArgs args) + => Broker.IsCompletionActive(args.TextView) || Broker.IsCompletionSupported(args.SubjectBuffer.ContentType); + + void IChainedCommandHandler<ReturnKeyCommandArgs>.ExecuteCommand(ReturnKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable) + { + // In IChainedCommandHandler, we have to explicitly call the next command handler + nextCommandHandler(); + return; + } + char typedChar = '\n'; + + var session = Broker.GetSession(args.TextView); + if (session != null) + { + var commitBehavior = session.Commit(typedChar, executionContext.OperationContext.UserCancellationToken); + session.Dismiss(); + + // Mark this command as handled (return true), + // unless extender set the RaiseFurtherCommandHandlers flag - with exception of the debugger text view + if ((commitBehavior & CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers) == 0 + || CompletionUtilities.IsDebuggerTextView(args.TextView)) + return; + } + + nextCommandHandler(); + + // Buffer has changed. Update it for when we try to trigger new session. + var location = args.TextView.Caret.Position.BufferPosition; + + var trigger = new InitialTrigger(InitialTriggerReason.Insertion, typedChar); + var newSession = Broker.TriggerCompletion(args.TextView, location, typedChar, executionContext.OperationContext.UserCancellationToken); + if (newSession is IAsyncCompletionSessionOperations sessionInternal) + { + RealizeVirtualSpaceUpdateApplicableToSpan(sessionInternal, args.TextView); + location = args.TextView.Caret.Position.BufferPosition; // Buffer may have changed. Update the location. + sessionInternal.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + } + } + + CommandState IChainedCommandHandler<TabKeyCommandArgs>.GetCommandState(TabKeyCommandArgs args, Func<CommandState> nextCommandHandler) + => GetCommandStateIfCompletionIsActiveOrAvailable(args.SubjectBuffer.ContentType, args.TextView); + + bool IDynamicCommandHandler<TabKeyCommandArgs>.CanExecuteCommand(TabKeyCommandArgs args) + => Broker.IsCompletionActive(args.TextView) || Broker.IsCompletionSupported(args.SubjectBuffer.ContentType); + + void IChainedCommandHandler<TabKeyCommandArgs>.ExecuteCommand(TabKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable) + { + // In IChainedCommandHandler, we have to explicitly call the next command handler + nextCommandHandler(); + return; + } + char typedChar = '\t'; + + var session = Broker.GetSession(args.TextView); + if (session != null) + { + var commitBehavior = session.Commit(typedChar, executionContext.OperationContext.UserCancellationToken); + session.Dismiss(); + + // Mark this command as handled (return true), + // unless extender set the RaiseFurtherCommandHandlers flag - with exception of the debugger text view + if ((commitBehavior & CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers) == 0 + || CompletionUtilities.IsDebuggerTextView(args.TextView)) + return; + } + + nextCommandHandler(); + + // Buffer has changed. Update it for when we try to trigger new session. + var location = args.TextView.Caret.Position.BufferPosition; + + var trigger = new InitialTrigger(InitialTriggerReason.Insertion, typedChar); + var newSession = Broker.TriggerCompletion(args.TextView, location, typedChar, executionContext.OperationContext.UserCancellationToken); + newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + } + + CommandState IChainedCommandHandler<TypeCharCommandArgs>.GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler) + => GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView); + + bool IDynamicCommandHandler<TypeCharCommandArgs>.CanExecuteCommand(TypeCharCommandArgs args) + => CompletionAvailability.IsAvailable(args.SubjectBuffer.ContentType, args.TextView); + + void IChainedCommandHandler<TypeCharCommandArgs>.ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable) + { + // In IChainedCommandHandler, we have to explicitly call the next command handler + nextCommandHandler(); + return; + } + + var view = args.TextView; + var location = view.Caret.Position.BufferPosition; + var initialTextSnapshot = args.SubjectBuffer.CurrentSnapshot; + + // Note regarding undo: When completion and brace completion happen together, completion should be first on the undo stack. + // Effectively, we want to first undo the completion, leaving brace completion intact. Second undo should undo brace completion. + // To achieve this, we create a transaction in which we commit and reapply brace completion (via nextCommandHandler). + // Please read "Note regarding undo" comments in this method that explain the implementation choices. + // Hopefully an upcoming upgrade of the undo mechanism will allow us to undo out of order and vastly simplify this method. + + // Note regarding undo: In a corner case of typing closing brace over existing closing brace, + // Roslyn brace completion does not perform an edit. It moves the caret outside of session's applicable span, + // which dismisses the session. Put the session in a state where it will not dismiss when caret leaves the applicable span. + var sessionToCommit = Broker.GetSession(args.TextView); + if (sessionToCommit != null) + { + ((AsyncCompletionSession)sessionToCommit).IgnoreCaretMovement(ignore: true); + } + + // Execute other commands in the chain to see the change in the buffer. This includes brace completion. + // Note regarding undo: This will be 2nd in the undo stack + nextCommandHandler(); + + // if on different version than initialTextSnapshot, we will NOT rollback and we will NOT replay the nextCommandHandler + // DP to figure out why ShouldCommit returns false or Commit doesn't do anything + var braceCompletionSpecialHandling = args.SubjectBuffer.CurrentSnapshot.Version == initialTextSnapshot.Version; + + // Pass location from before calling nextCommandHandler + // so that extenders get the same view of the buffer in both ShouldCommit and Commit + if (sessionToCommit?.ShouldCommit(args.TypedChar, location, executionContext.OperationContext.UserCancellationToken) == true) + { + // Buffer has changed, update the snapshot + location = view.Caret.Position.BufferPosition; + + // Note regarding undo: this transaction will be 1st in the undo stack + using (var undoTransaction = new CaretPreservingEditTransaction("Completion", view, UndoHistoryRegistry, EditorOperationsFactoryService)) + { + if (!braceCompletionSpecialHandling) + UndoUtilities.RollbackToBeforeTypeChar(initialTextSnapshot, args.SubjectBuffer); + // Now the buffer doesn't have the commit character nor the matching brace, if any + + var commitBehavior = sessionToCommit.Commit(args.TypedChar, executionContext.OperationContext.UserCancellationToken); + + if (!braceCompletionSpecialHandling && (commitBehavior & CommitBehavior.SuppressFurtherTypeCharCommandHandlers) == 0) + nextCommandHandler(); // Replay the key, so that we get brace completion. + + // Complete the transaction before stopping it. + undoTransaction.Complete(); + } + } + + // Restore the default state where session dismisses when caret is outside of the applicable span. + if (sessionToCommit != null) + { + ((AsyncCompletionSession)sessionToCommit).IgnoreCaretMovement(ignore: false); + } + + // Buffer might have changed. Update it for when we try to trigger new session. + location = view.Caret.Position.BufferPosition; + + var trigger = new InitialTrigger(InitialTriggerReason.Insertion, args.TypedChar); + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + } + else + { + var newSession = Broker.TriggerCompletion(args.TextView, location, args.TypedChar, executionContext.OperationContext.UserCancellationToken); + newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + } + } + + CommandState ICommandHandler<DownKeyCommandArgs>.GetCommandState(DownKeyCommandArgs args) + => GetCommandStateIfCompletionIsActive(args.TextView); + + bool ICommandHandler<DownKeyCommandArgs>.ExecuteCommand(DownKeyCommandArgs args, CommandExecutionContext executionContext) + { + if (Broker.GetSession(args.TextView) is AsyncCompletionSession session) // we are accessing an internal method + { + session.SelectDown(); + return true; + } + return false; + } + + CommandState ICommandHandler<PageDownKeyCommandArgs>.GetCommandState(PageDownKeyCommandArgs args) + => GetCommandStateIfCompletionIsActive(args.TextView); + + bool ICommandHandler<PageDownKeyCommandArgs>.ExecuteCommand(PageDownKeyCommandArgs args, CommandExecutionContext executionContext) + { + if (Broker.GetSession(args.TextView) is AsyncCompletionSession session) // we are accessing an internal method + { + session.SelectPageDown(); + return true; + } + return false; + } + + CommandState ICommandHandler<PageUpKeyCommandArgs>.GetCommandState(PageUpKeyCommandArgs args) + => GetCommandStateIfCompletionIsActive(args.TextView); + + bool ICommandHandler<PageUpKeyCommandArgs>.ExecuteCommand(PageUpKeyCommandArgs args, CommandExecutionContext executionContext) + { + if (Broker.GetSession(args.TextView) is AsyncCompletionSession session) // we are accessing an internal method + { + session.SelectPageUp(); + return true; + } + return false; + } + + CommandState ICommandHandler<UpKeyCommandArgs>.GetCommandState(UpKeyCommandArgs args) + => GetCommandStateIfCompletionIsActive(args.TextView); + + bool ICommandHandler<UpKeyCommandArgs>.ExecuteCommand(UpKeyCommandArgs args, CommandExecutionContext executionContext) + { + if (Broker.GetSession(args.TextView) is AsyncCompletionSession session) // we are accessing an internal method + { + session.SelectUp(); + System.Diagnostics.Debug.WriteLine("Completions's UpKey command handler returns true (handled)"); + return true; + } + System.Diagnostics.Debug.WriteLine("Completions's UpKey command handler returns false (unhandled)"); + return false; + } + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/CompletionModel.cs b/src/Language/Impl/Language/AsyncCompletion/CompletionModel.cs new file mode 100644 index 0000000..2d75b0e --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/CompletionModel.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Immutable; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Represents an immutable snapshot of state of the async completion feature. + /// </summary> + internal sealed class CompletionModel + { + /// <summary> + /// All items, as provided by completion item sources. + /// </summary> + public readonly ImmutableArray<CompletionItem> InitialItems; + + /// <summary> + /// Sorted array of all items, as provided by the completion service. + /// </summary> + public readonly ImmutableArray<CompletionItem> SortedItems; + + /// <summary> + /// Snapshot pertinent to this completion model. + /// </summary> + public readonly ITextSnapshot Snapshot; + + /// <summary> + /// Filters involved in this completion model, including their availability and selection state. + /// </summary> + public readonly ImmutableArray<CompletionFilterWithState> Filters; + + /// <summary> + /// Items to be displayed in the UI. + /// </summary> + public readonly ImmutableArray<CompletionItemWithHighlight> PresentedItems; + + /// <summary> + /// Index of item to select. Use -1 to select nothing, when suggestion mode item should be selected. + /// </summary> + public readonly int SelectedIndex; + + /// <summary> + /// Whether selection should be displayed as soft selection. + /// </summary> + public readonly bool UseSoftSelection; + + /// <summary> + /// Whether suggestion mode item should be visible. + /// </summary> + public readonly bool DisplaySuggestionItem; + + /// <summary> + /// Whether suggestion mode item should be selected. + /// </summary> + public readonly bool SelectSuggestionItem; + + /// <summary> + /// <see cref="CompletionItem"/> which contains user-entered text. + /// Used to display and commit the suggestion mode item + /// </summary> + public readonly CompletionItem SuggestionItem; + + /// <summary> + /// <see cref="CompletionItem"/> which overrides regular unique item selection. + /// When this is null, the single item from <see cref="PresentedItems"/> is used as unique item. + /// </summary> + public readonly CompletionItem UniqueItem; + + /// <summary> + /// This flags prevents <see cref="IAsyncCompletionSession"/> from dismissing when it initially becomes empty. + /// We dismiss when this flag is set (span is empty) and user attempts to remove characters. + /// </summary> + public readonly bool ApplicableToSpanWasEmpty; + + /// <summary> + /// Indicates the state where the model received no items from the completion sources. + /// We keep the session (and its model) around to attempt getting items at the next keystroke, while preventing race conditions. + /// </summary> + public readonly bool Uninitialized; + + /// <summary> + /// Creates an instance of <see cref="CompletionModel"/> for session + /// that did not receive any <see cref="CompletionItem"/>s yet, but may receive them soon thereafter. + /// </summary> + public static CompletionModel GetUninitializedModel(ITextSnapshot snapshot) + { + return new CompletionModel(default, default, snapshot, default, default, default, default, default, default, default, default, default, uninitialized: true); + } + + /// <summary> + /// Constructor for the initial model + /// </summary> + public CompletionModel(ImmutableArray<CompletionItem> initialItems, ImmutableArray<CompletionItem> sortedItems, + ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, bool useSoftSelection, + bool displaySuggestionItem, bool selectSuggestionItem, CompletionItem suggestionItem) + { + InitialItems = initialItems; + SortedItems = sortedItems; + Snapshot = snapshot; + Filters = filters; + SelectedIndex = 0; + UseSoftSelection = useSoftSelection; + DisplaySuggestionItem = displaySuggestionItem; + SelectSuggestionItem = selectSuggestionItem; + SuggestionItem = suggestionItem; + UniqueItem = null; + ApplicableToSpanWasEmpty = false; + Uninitialized = false; + } + + /// <summary> + /// Private constructor for the With* methods + /// </summary> + private CompletionModel(ImmutableArray<CompletionItem> initialItems, ImmutableArray<CompletionItem> sortedItems, + ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, ImmutableArray<CompletionItemWithHighlight> presentedItems, + bool useSoftSelection, bool displaySuggestionItem, int selectedIndex, bool selectSuggestionItem, CompletionItem suggestionItem, + CompletionItem uniqueItem, bool applicableToSpanWasEmpty, bool uninitialized) + { + InitialItems = initialItems; + SortedItems = sortedItems; + Snapshot = snapshot; + Filters = filters; + PresentedItems = presentedItems; + SelectedIndex = selectedIndex; + UseSoftSelection = useSoftSelection; + DisplaySuggestionItem = displaySuggestionItem; + SelectSuggestionItem = selectSuggestionItem; + SuggestionItem = suggestionItem; + UniqueItem = uniqueItem; + ApplicableToSpanWasEmpty = applicableToSpanWasEmpty; + Uninitialized = uninitialized; + } + + public CompletionModel WithPresentedItems(ImmutableArray<CompletionItemWithHighlight> newPresentedItems, int newSelectedIndex) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: Filters, + presentedItems: newPresentedItems, // Updated + useSoftSelection: UseSoftSelection, + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: newSelectedIndex, // Updated + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + public CompletionModel WithSnapshot(ITextSnapshot newSnapshot) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: newSnapshot, // Updated + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: UseSoftSelection, + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + public CompletionModel WithFilters(ImmutableArray<CompletionFilterWithState> newFilters) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: newFilters, // Updated + presentedItems: PresentedItems, + useSoftSelection: UseSoftSelection, + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + public CompletionModel WithSelectedIndex(int newIndex, bool preserveSoftSelection = false) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: preserveSoftSelection ? UseSoftSelection : false, // Updated conditionally + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: newIndex, // Updated + selectSuggestionItem: false, // Explicit selection of regular item + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + public CompletionModel WithSuggestionItemSelected() + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: false, // Explicit selection and soft selection are mutually exclusive + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: -1, // Deselect regular item + selectSuggestionItem: true, // Explicit selection of suggestion item + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + public CompletionModel WithSuggestionItemVisibility(bool newDisplaySuggestionItem) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: UseSoftSelection | newDisplaySuggestionItem, // Enabling suggestion mode also enables soft selection + displaySuggestionItem: newDisplaySuggestionItem, // Updated + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + /// <summary> + /// </summary> + /// <param name="newUniqueItem">Overrides typical unique item selection. + /// Pass in null to use regular behavior: treating single <see cref="PresentedItems"/> item as the unique item.</param> + internal CompletionModel WithUniqueItem(CompletionItem newUniqueItem) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: UseSoftSelection, + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: newUniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + internal CompletionModel WithSoftSelection(bool newSoftSelection) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: newSoftSelection, // Updated + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + internal CompletionModel WithSnapshotItemsAndFilters(ITextSnapshot snapshot, ImmutableArray<CompletionItemWithHighlight> presentedItems, + CompletionItem uniqueItem, CompletionItem suggestionItem, ImmutableArray<CompletionFilterWithState> filters) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: snapshot, // Updated + filters: filters, // Updated + presentedItems: presentedItems, // Updated + useSoftSelection: UseSoftSelection, + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: suggestionItem, // Updated + uniqueItem: uniqueItem, // Updated + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } + + internal CompletionModel WithApplicableToSpanStatus(bool applicableToSpanIsEmpty) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: UseSoftSelection, + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: applicableToSpanIsEmpty, // Updated + uninitialized: Uninitialized + ); + } + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/CompletionTelemetry.cs b/src/Language/Impl/Language/AsyncCompletion/CompletionTelemetry.cs new file mode 100644 index 0000000..7faaa25 --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/CompletionTelemetry.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Telemetry data pertinent to a single <see cref="AsyncCompletionSession"/> + /// </summary> + internal class CompletionSessionTelemetry + { + private readonly CompletionTelemetryHost _telemetryHost; + + /// <summary> + /// Tracks time spent on the worker thread - getting data, filtering and sorting. Used for telemetry. + /// </summary> + internal Stopwatch ComputationStopwatch { get; } = new Stopwatch(); + + /// <summary> + /// Tracks time spent on the UI thread - either rendering or committing. Used for telemetry. + /// </summary> + internal Stopwatch UiStopwatch { get; } = new Stopwatch(); + + // Names of parts that participated in completion + internal string ItemManagerName { get; private set; } + internal string PresenterProviderName { get; private set; } + internal string CommitManagerName { get; private set; } + + // "Setup" is work done on UI thread by IAsyncCompletionBroker + // since there are many participating MEF parts, we record their names together with the time + internal Dictionary<string, long> CommitManagerSetupDuration { get; } = new Dictionary<string, long>(); + internal Dictionary<string, long> ItemSourceSetupDuration { get; } = new Dictionary<string, long>(); + + // "Get Context" is work done by IAsyncCompletionItemSource + // multiple sources may participate in a single completion session + internal Dictionary<string, long> ItemSourceGetContextDuration { get; } = new Dictionary<string, long>(); + + // "Processing" is work done by IAsyncCompletionItemManager + internal long InitialProcessingDuration { get; private set; } + internal long TotalProcessingDuration { get; private set; } + internal int TotalProcessingCount { get; private set; } + + // "Rendering" is work done on UI thread by ICompletionPresenter + internal long InitialRenderingDuration { get; private set; } + internal long TotalRenderingDuration { get; private set; } + internal int TotalRenderingCount { get; private set; } + + // "Closing" is also work done on UI thread by ICompletionPresenter + internal long ClosingDuration { get; private set; } + + // "Commit" is work done on UI thread by IAsyncCompletionCommitManager + internal long CommitDuration { get; private set; } + + // The following work is a mix of "Get Context" and "Processing" and blocks UI thread + internal long BlockingComputationDuration { get; private set; } + + // Additional parameters related to work done by IAsyncCompletionItemManager + internal bool UserEverScrolled { get; private set; } + internal bool UserEverSetFilters { get; private set; } + internal int FinalItemCount { get; private set; } + internal int NumberOfKeystrokes { get; private set; } + + public CompletionSessionTelemetry(CompletionTelemetryHost telemetryHost) + { + _telemetryHost = telemetryHost; + } + + internal void RecordProcessing(long duration, int itemCount) + { + if (TotalProcessingCount == 0) + { + InitialProcessingDuration = duration; + } + else + { + TotalProcessingDuration += duration; + FinalItemCount = itemCount; + } + TotalProcessingCount++; + } + + internal void RecordRendering(long duration) + { + if (TotalRenderingCount == 0) + InitialRenderingDuration = duration; + TotalRenderingCount++; + TotalRenderingDuration += duration; + } + + internal void RecordScrolling() + { + UserEverScrolled = true; + } + + internal void RecordChangingFilters() + { + UserEverSetFilters = true; + } + + internal void RecordKeystroke() + { + NumberOfKeystrokes++; + } + + internal void RecordCommitted(long duration, + IAsyncCompletionCommitManager manager) + { + CommitManagerName = CompletionTelemetryHost.GetCommitManagerName(manager); + CommitDuration = duration; + } + + internal void RecordClosing(long duration) + { + ClosingDuration += duration; + } + + internal void Save( + IAsyncCompletionItemManager itemManager, + ICompletionPresenterProvider presenterProvider) + { + ItemManagerName = CompletionTelemetryHost.GetItemManagerName(itemManager); + PresenterProviderName = CompletionTelemetryHost.GetPresenterProviderName(presenterProvider); + _telemetryHost.Add(this); + } + + internal void RecordObtainingCommitManagerData(IAsyncCompletionCommitManager manager, long elapsedMilliseconds) + { + var name = CompletionTelemetryHost.GetCommitManagerName(manager); + CommitManagerSetupDuration[name] = elapsedMilliseconds; + } + + internal void RecordObtainingSourceSpan(IAsyncCompletionSource source, long elapsedMilliseconds) + { + var name = CompletionTelemetryHost.GetSourceName(source); + ItemSourceSetupDuration[name] = elapsedMilliseconds; + } + + internal void RecordObtainingSourceContext(IAsyncCompletionSource source, long elapsedMilliseconds) + { + var name = CompletionTelemetryHost.GetSourceName(source); + ItemSourceGetContextDuration[name] = elapsedMilliseconds; + } + + internal void RecordBlockingWaitForComputation(long elapsedMilliseconds) + { + BlockingComputationDuration = elapsedMilliseconds; + } + } + + /// <summary> + /// Aggregates <see cref="CompletionSessionTelemetry"/>. + /// </summary> + internal class CompletionTelemetryHost + { + private class AggregateCommitManagerData + { + internal long TotalCommitTime; + internal long TotalSetupTime; + + // These values are used to calculate averages + internal long CommitCount; + internal long SetupCount; + + // We persist the slowest duration for operations on the UI thread + internal long MaxCommitTime; + internal long MaxSetupTime; + } + + private class AggregateSourceData + { + internal long TotalGetContextTime; + internal long TotalSetupTime; + + // These values are used to calculate averages + internal long GetContextCount; + internal long SetupCount; + + // We persist the slowest duration for operations on the UI thread + internal long MaxSetupTime; + } + + private class AggregateItemManagerData + { + internal long InitialProcessTime; + internal long TotalProcessTime; + internal long TotalBlockingComputationTime; + internal long MaxBlockingComputationTime; + + internal int TotalKeystrokes; + internal int UserEverScrolled; + internal int UserEverSetFilters; + internal int FinalItemCount; + + // These values are used to calculate averages + internal int SessionCount; + // This value is used to calculate average processing time. One session may have multiple processing operations. + internal int ProcessCount; + } + + private class AggregatePresenterData + { + internal long InitialRenderTime; + internal long TotalRenderTime; + internal long TotalClosingTime; + + // These values are used to calculate averages + internal int RenderCount; + internal int ClosingCount; + + // We persist the slowest duration for operations on the UI thread + internal long MaxRenderTime; + internal long MaxClosingTime; + } + + Dictionary<string, AggregateCommitManagerData> CommitManagerData = new Dictionary<string, AggregateCommitManagerData>(); + Dictionary<string, AggregateItemManagerData> ItemManagerData = new Dictionary<string, AggregateItemManagerData>(); + Dictionary<string, AggregatePresenterData> PresenterData = new Dictionary<string, AggregatePresenterData>(); + Dictionary<string, AggregateSourceData> SourceData = new Dictionary<string, AggregateSourceData>(); + + private readonly ILoggingServiceInternal _logger; + private readonly AsyncCompletionBroker _broker; + + public CompletionTelemetryHost(ILoggingServiceInternal logger, AsyncCompletionBroker broker) + { + _logger = logger; + _broker = broker; + } + + internal static string GetSourceName(IAsyncCompletionSource source) => source?.GetType().ToString() ?? string.Empty; + internal static string GetCommitManagerName(IAsyncCompletionCommitManager commitManager) => commitManager?.GetType().ToString() ?? string.Empty; + internal static string GetItemManagerName(IAsyncCompletionItemManager itemManager) => itemManager?.GetType().ToString() ?? string.Empty; + internal static string GetPresenterProviderName(ICompletionPresenterProvider provider) => provider?.GetType().ToString() ?? string.Empty; + + /// <summary> + /// Adds data from <see cref="CompletionSessionTelemetry" /> to appropriate buckets. + /// </summary> + /// <param name=""></param> + internal void Add(CompletionSessionTelemetry telemetry) + { + if (_logger == null) + return; + + AddSourceData(telemetry, SourceData); + AddItemManagerData(telemetry, ItemManagerData); + AddCommitManagerData(telemetry, CommitManagerData); + AddPresenterData(telemetry, PresenterData); + } + + /// <summary> + /// Sends batch of collected data. + /// </summary> + internal void Send() + { + if (_logger == null) + return; + + foreach (var data in ItemManagerData) + { + if (data.Value.SessionCount == 0) + continue; + if (data.Value.ProcessCount == 0) + continue; + + _logger.PostEvent(TelemetryEventType.Operation, + ItemManagerEventName, + TelemetryResult.Success, + (ItemManagerName, data.Key), + (ItemManagerAverageFinalItemCount, data.Value.FinalItemCount / (double)data.Value.SessionCount), + (ItemManagerAverageInitialProcessDuration, data.Value.InitialProcessTime / (double)data.Value.SessionCount), + (ItemManagerAverageFilterDuration, data.Value.TotalProcessTime / (double)data.Value.ProcessCount), + (ItemManagerAverageKeystrokeCount, data.Value.TotalKeystrokes / (double)data.Value.SessionCount), + (ItemManagerAverageScrolled, data.Value.UserEverScrolled / (double)data.Value.SessionCount), + (ItemManagerAverageSetFilters, data.Value.UserEverSetFilters / (double)data.Value.SessionCount), + (ItemManagerAverageBlockingComputationDuration, data.Value.TotalBlockingComputationTime / (double)data.Value.SessionCount), + (ItemManagerMaxBlockingComputationDuration, data.Value.MaxBlockingComputationTime) + ); + } + + foreach (var data in SourceData) + { + if (data.Value.SetupCount == 0) + continue; + if (data.Value.GetContextCount == 0) + data.Value.GetContextCount = 1; // the result of division will remain 0 and the division won't throw + + _logger.PostEvent(TelemetryEventType.Operation, + SourceEventName, + TelemetryResult.Success, + (SourceName, data.Key), + (SourceAverageGetContextDuration, data.Value.TotalGetContextTime / (double)data.Value.GetContextCount), + (SourceAverageSetupDuration, data.Value.TotalSetupTime / (double)data.Value.SetupCount), + (SourceMaxSetupDuration, data.Value.MaxSetupTime) + ); + } + + foreach (var data in CommitManagerData) + { + if (data.Value.CommitCount == 0) + continue; + + _logger.PostEvent(TelemetryEventType.Operation, + CommitManagerEventName, + TelemetryResult.Success, + (CommitManagerName, data.Key), + (CommitManagerAverageCommitDuration, data.Value.TotalCommitTime / (double)data.Value.CommitCount), + (CommitManagerAverageSetupDuration, data.Value.TotalSetupTime / (double)data.Value.SetupCount), + (CommitManagerMaxCommitDuration, data.Value.MaxCommitTime), + (CommitManagerMaxSetupDuration, data.Value.MaxSetupTime) + ); + } + + foreach (var data in PresenterData) + { + if (data.Value.RenderCount == 0) + continue; + + _logger.PostEvent(TelemetryEventType.Operation, + PresenterEventName, + TelemetryResult.Success, + (PresenterName, data.Key), + (PresenterAverageInitialRendering, data.Value.InitialRenderTime / (double)data.Value.ClosingCount), + (PresenterAverageRendering, data.Value.TotalRenderTime / (double)data.Value.RenderCount), + (PresenterAverageClosing, data.Value.TotalClosingTime / (double)data.Value.ClosingCount), + (PresenterMaxRendering, data.Value.MaxRenderTime), + (PresenterMaxClosing, data.Value.MaxClosingTime) + ); + } + } + + /// <summary> + /// Tracks obtaining applicable span and getting items + /// </summary> + /// <param name="telemetry">Telemetry from <see cref="IAsyncCompletionSession"/></param> + /// <param name="sourceData">Data aggregator</param> + private static void AddSourceData(CompletionSessionTelemetry telemetry, Dictionary<string, AggregateSourceData> sourceData) + { + foreach (var setupData in telemetry.ItemSourceSetupDuration) + { + if (!sourceData.ContainsKey(setupData.Key)) + sourceData[setupData.Key] = new AggregateSourceData(); + var aggregateSourceData = sourceData[setupData.Key]; + + aggregateSourceData.TotalSetupTime += setupData.Value; + aggregateSourceData.SetupCount++; + + aggregateSourceData.MaxSetupTime = Math.Max(aggregateSourceData.MaxSetupTime, setupData.Value); + } + + foreach (var getContextData in telemetry.ItemSourceGetContextDuration) + { + if (!sourceData.ContainsKey(getContextData.Key)) + sourceData[getContextData.Key] = new AggregateSourceData(); + var aggregateSourceData = sourceData[getContextData.Key]; + + aggregateSourceData.TotalGetContextTime += getContextData.Value; + aggregateSourceData.GetContextCount++; + } + } + + /// <summary> + /// Tracks sorting and filtering items + /// </summary> + /// <param name="telemetry">Telemetry from <see cref="IAsyncCompletionSession"/></param> + /// <param name="sourceData">Data aggregator</param> + private static void AddItemManagerData(CompletionSessionTelemetry telemetry, Dictionary<string, AggregateItemManagerData> itemManagerData) + { + var itemManagerKey = telemetry.ItemManagerName; + if (!itemManagerData.ContainsKey(itemManagerKey)) + itemManagerData[itemManagerKey] = new AggregateItemManagerData(); + var aggregateItemManagerData = itemManagerData[itemManagerKey]; + + aggregateItemManagerData.InitialProcessTime += telemetry.InitialProcessingDuration; + aggregateItemManagerData.TotalProcessTime += telemetry.TotalProcessingDuration; + aggregateItemManagerData.TotalBlockingComputationTime += telemetry.BlockingComputationDuration; + aggregateItemManagerData.ProcessCount += telemetry.TotalProcessingCount; + aggregateItemManagerData.TotalKeystrokes += telemetry.NumberOfKeystrokes; + aggregateItemManagerData.UserEverScrolled += telemetry.UserEverScrolled ? 1 : 0; + aggregateItemManagerData.UserEverSetFilters += telemetry.UserEverSetFilters ? 1 : 0; + aggregateItemManagerData.FinalItemCount += telemetry.FinalItemCount; + aggregateItemManagerData.SessionCount++; + + aggregateItemManagerData.MaxBlockingComputationTime = Math.Max(aggregateItemManagerData.MaxBlockingComputationTime, telemetry.BlockingComputationDuration); + } + + /// <summary> + /// Tracks obtaining commit characters and committing + /// </summary> + /// <param name="telemetry">Telemetry from <see cref="IAsyncCompletionSession"/></param> + /// <param name="sourceData">Data aggregator</param> + private static void AddCommitManagerData(CompletionSessionTelemetry telemetry, Dictionary<string, AggregateCommitManagerData> commitManagerData) + { + var commitKey = telemetry.CommitManagerName; + if (!string.IsNullOrEmpty(commitKey)) + { + // commitKey is empty when session is dismissed without committing. + if (!commitManagerData.ContainsKey(commitKey)) + commitManagerData[commitKey] = new AggregateCommitManagerData(); + var aggregateCommitManagerData = commitManagerData[commitKey]; + + aggregateCommitManagerData.TotalCommitTime += telemetry.CommitDuration; + aggregateCommitManagerData.CommitCount++; + + aggregateCommitManagerData.MaxCommitTime = Math.Max(aggregateCommitManagerData.MaxCommitTime, telemetry.CommitDuration); + } + + foreach (var commitManagerSetupData in telemetry.CommitManagerSetupDuration) + { + if (!commitManagerData.ContainsKey(commitManagerSetupData.Key)) + commitManagerData[commitManagerSetupData.Key] = new AggregateCommitManagerData(); + var aggregateCommitManagerData = commitManagerData[commitManagerSetupData.Key]; + + aggregateCommitManagerData.TotalSetupTime += commitManagerSetupData.Value; + aggregateCommitManagerData.SetupCount++; + + aggregateCommitManagerData.MaxSetupTime = Math.Max(aggregateCommitManagerData.MaxSetupTime, commitManagerSetupData.Value); + } + } + + /// <summary> + /// Tracks opening, updating and closing the GUI + /// </summary> + /// <param name="telemetry">Telemetry from <see cref="IAsyncCompletionSession"/></param> + /// <param name="sourceData">Data aggregator</param> + private static void AddPresenterData(CompletionSessionTelemetry telemetry, Dictionary<string, AggregatePresenterData> presenterData) + { + var presenterKey = telemetry.PresenterProviderName; + if (!presenterData.ContainsKey(presenterKey)) + presenterData[presenterKey] = new AggregatePresenterData(); + var aggregatePresenterData = presenterData[presenterKey]; + + aggregatePresenterData.InitialRenderTime += telemetry.InitialRenderingDuration; + aggregatePresenterData.TotalRenderTime += telemetry.TotalRenderingDuration; + aggregatePresenterData.RenderCount += telemetry.TotalRenderingCount; + aggregatePresenterData.TotalClosingTime += telemetry.ClosingDuration; + aggregatePresenterData.ClosingCount++; + + aggregatePresenterData.MaxRenderTime = Math.Max(aggregatePresenterData.MaxRenderTime, telemetry.InitialRenderingDuration); + aggregatePresenterData.MaxClosingTime = Math.Max(aggregatePresenterData.MaxClosingTime, telemetry.ClosingDuration); + } + + // Property and event names + internal const string PresenterEventName = "VS/Editor/Completion/PresenterData"; + internal const string PresenterName = "Property.Presenter.Name"; + internal const string PresenterAverageInitialRendering = "Property.Presenter.InitialRenderDuration"; + internal const string PresenterAverageRendering = "Property.Presenter.AllRenderDuration"; + internal const string PresenterAverageClosing = "Property.Presenter.AllClosingDuration"; + internal const string PresenterMaxRendering = "Property.Presenter.MaxRenderDuration"; + internal const string PresenterMaxClosing = "Property.Presenter.MaxClosingDuration"; + + internal const string ItemManagerEventName = "VS/Editor/Completion/ItemManagerData"; + internal const string ItemManagerName = "Property.ItemManager.Name"; + internal const string ItemManagerAverageFinalItemCount = "Property.ItemManager.FinalItemCount"; + internal const string ItemManagerAverageInitialProcessDuration = "Property.ItemManager.InitialDuration"; + internal const string ItemManagerAverageFilterDuration = "Property.ItemManager.AnyDuration"; + internal const string ItemManagerAverageKeystrokeCount = "Property.ItemManager.KeystrokeCount"; + internal const string ItemManagerAverageScrolled = "Property.ItemManager.Scrolled"; + internal const string ItemManagerAverageSetFilters = "Property.ItemManager.SetFilters"; + internal const string ItemManagerAverageBlockingComputationDuration = "Property.ItemManager.BlockingComputationDuration"; + internal const string ItemManagerMaxBlockingComputationDuration = "Property.ItemManager.MaxBlockingComputationDuration"; + + internal const string CommitManagerEventName = "VS/Editor/Completion/CommitManagerData"; + internal const string CommitManagerName = "Property.CommitManager.Name"; + internal const string CommitManagerAverageCommitDuration = "Property.Commit.CommitDuration"; + internal const string CommitManagerAverageSetupDuration = "Property.Commit.SetupDuration"; + internal const string CommitManagerMaxCommitDuration = "Property.Commit.MaxCommitDuration"; + internal const string CommitManagerMaxSetupDuration = "Property.Commit.MaxSetupDuration"; + + internal const string SourceEventName = "VS/Editor/Completion/SourceData"; + internal const string SourceName = "Property.Source.Name"; + internal const string SourceAverageGetContextDuration = "Property.Source.GetContextDuration"; + internal const string SourceAverageSetupDuration = "Property.Source.SetupDuration"; + internal const string SourceMaxSetupDuration = "Property.Source.MaxSetupDuration"; + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs b/src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs new file mode 100644 index 0000000..6392376 --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Linq; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + internal static class CompletionUtilities + { + /// <summary> + /// Maps given point to buffers that contain this point. Requires UI thread. + /// </summary> + /// <param name="textView"></param> + /// <param name="point"></param> + /// <returns></returns> + internal static IEnumerable<ITextBuffer> GetBuffersForPoint(ITextView textView, SnapshotPoint point) + { + // We are looking at the buffer to the left of the caret. + return textView.BufferGraph.GetTextBuffers(n => + textView.BufferGraph.MapDownToBuffer(point, PointTrackingMode.Negative, n, PositionAffinity.Predecessor) != null); + } + + /// <summary> + /// Returns whether the <see cref="ITextView"/> is furnished by the debugger, + /// e.g. it is a view in the breakpoint settings window or watch window. + /// </summary> + /// <param name="textView">View to examine</param> + /// <returns>True if the view has "DEBUGVIEW" text view role.</returns> + internal static bool IsDebuggerTextView(ITextView textView) => textView.Roles.Contains("DEBUGVIEW"); + + static readonly EditorOptionKey<bool> SuggestionModeOptionKey = new EditorOptionKey<bool>(PredefinedCompletionNames.SuggestionModeInCompletionOptionName); + static readonly EditorOptionKey<bool> SuggestionModeInDebuggerCompletionOptionKey = new EditorOptionKey<bool>(PredefinedCompletionNames.SuggestionModeInDebuggerCompletionOptionName); + private const bool UseSuggestionModeDefaultValue = false; + private const bool UseSuggestionModeInDebuggerCompletionDefaultValue = true; + + [Export(typeof(EditorOptionDefinition))] + [Name(PredefinedCompletionNames.SuggestionModeInCompletionOptionName)] + class SuggestionModeOptionDefinition : EditorOptionDefinition + { + public override object DefaultValue => UseSuggestionModeDefaultValue; + + public override Type ValueType => typeof(bool); + + public override string Name => PredefinedCompletionNames.SuggestionModeInCompletionOptionName; + } + + [Export(typeof(EditorOptionDefinition))] + [Name(PredefinedCompletionNames.SuggestionModeInDebuggerCompletionOptionName)] + class SuggestionModeInDebuggerCompletionOptionDefinition : EditorOptionDefinition + { + public override object DefaultValue => UseSuggestionModeInDebuggerCompletionDefaultValue; + + public override Type ValueType => typeof(bool); + + public override string Name => PredefinedCompletionNames.SuggestionModeInDebuggerCompletionOptionName; + } + + internal static bool GetSuggestionModeOption(ITextView textView) + { + var options = textView.Options.GlobalOptions; + if (!(options.IsOptionDefined(SuggestionModeOptionKey, localScopeOnly: false))) + options.SetOptionValue(SuggestionModeOptionKey, UseSuggestionModeDefaultValue); + return options.GetOptionValue(SuggestionModeOptionKey); + } + + internal static void SetSuggestionModeOption(ITextView textView, bool value) + { + var options = textView.Options.GlobalOptions; + options.SetOptionValue(SuggestionModeOptionKey, value); + } + + internal static bool GetSuggestionModeInDebuggerCompletionOption(ITextView textView) + { + var options = textView.Options.GlobalOptions; + if (!(options.IsOptionDefined(SuggestionModeInDebuggerCompletionOptionKey, localScopeOnly: false))) + options.SetOptionValue(SuggestionModeInDebuggerCompletionOptionKey, UseSuggestionModeInDebuggerCompletionDefaultValue); + return options.GetOptionValue(SuggestionModeInDebuggerCompletionOptionKey); + } + + internal static void SetSuggestionModeDuringDebuggingOption(ITextView textView, bool value) + { + var options = textView.Options.GlobalOptions; + options.SetOptionValue(SuggestionModeInDebuggerCompletionOptionKey, value); + } + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs b/src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs new file mode 100644 index 0000000..9ae95b5 --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Core.Imaging; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.PatternMatching; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + [Export(typeof(IAsyncCompletionItemManagerProvider))] + [Name(PredefinedCompletionNames.DefaultCompletionItemManager)] + [ContentType("text")] + internal sealed class DefaultCompletionItemManagerProvider : IAsyncCompletionItemManagerProvider + { + [Import] + public IPatternMatcherFactory PatternMatcherFactory; + + DefaultCompletionItemManager _instance; + + IAsyncCompletionItemManager IAsyncCompletionItemManagerProvider.GetOrCreate(ITextView textView) + { + if (_instance == null) + _instance = new DefaultCompletionItemManager(PatternMatcherFactory); + return _instance; + } + } + + internal sealed class DefaultCompletionItemManager : IAsyncCompletionItemManager + { + readonly IPatternMatcherFactory _patternMatcherFactory; + + internal DefaultCompletionItemManager(IPatternMatcherFactory patternMatcherFactory) + { + _patternMatcherFactory = patternMatcherFactory; + } + + Task<FilteredCompletionModel> IAsyncCompletionItemManager.UpdateCompletionListAsync + (IAsyncCompletionSession session, AsyncCompletionSessionDataSnapshot data, CancellationToken token) + { + // Filter by text + var filterText = session.ApplicableToSpan.GetText(data.Snapshot); + if (string.IsNullOrWhiteSpace(filterText)) + { + // There is no text filtering. Just apply user filters, sort alphabetically and return. + IEnumerable<CompletionItem> listFiltered = data.InitialSortedList; + if (data.SelectedFilters.Any(n => n.IsSelected)) + { + listFiltered = listFiltered.Where(n => ShouldBeInCompletionList(n, data.SelectedFilters)); + } + var listSorted = listFiltered.OrderBy(n => n.SortText); + var listHighlighted = listSorted.Select(n => new CompletionItemWithHighlight(n)).ToImmutableArray(); + return Task.FromResult(new FilteredCompletionModel(listHighlighted, 0, data.SelectedFilters)); + } + + // Pattern matcher not only filters, but also provides a way to order the results by their match quality. + // The relevant CompletionItem is match.Item1, its PatternMatch is match.Item2 + var patternMatcher = _patternMatcherFactory.CreatePatternMatcher( + filterText, + new PatternMatcherCreationOptions(System.Globalization.CultureInfo.CurrentCulture, PatternMatcherCreationFlags.IncludeMatchedSpans)); + + var matches = data.InitialSortedList + // Perform pattern matching + .Select(completionItem => (completionItem, patternMatcher.TryMatch(completionItem.FilterText))) + // Pick only items that were matched, unless length of filter text is 1 + .Where(n => (filterText.Length == 1 || n.Item2.HasValue)); + + // See which filters might be enabled based on the typed code + var textFilteredFilters = matches.SelectMany(n => n.completionItem.Filters).Distinct(); + + // When no items are available for a given filter, it becomes unavailable + var updatedFilters = ImmutableArray.CreateRange(data.SelectedFilters.Select(n => n.WithAvailability(textFilteredFilters.Contains(n.Filter)))); + + // Filter by user-selected filters. The value on availableFiltersWithSelectionState conveys whether the filter is selected. + var filterFilteredList = matches; + if (data.SelectedFilters.Any(n => n.IsSelected)) + { + filterFilteredList = matches.Where(n => ShouldBeInCompletionList(n.completionItem, data.SelectedFilters)); + } + + var bestMatch = filterFilteredList.OrderByDescending(n => n.Item2.HasValue).ThenBy(n => n.Item2).FirstOrDefault(); + var listWithHighlights = filterFilteredList.Select(n => n.Item2.HasValue ? new CompletionItemWithHighlight(n.completionItem, n.Item2.Value.MatchedSpans) : new CompletionItemWithHighlight(n.completionItem)).ToImmutableArray(); + + int selectedItemIndex = 0; + if (data.DisplaySuggestionItem) + { + selectedItemIndex = -1; + } + else + { + for (int i = 0; i < listWithHighlights.Length; i++) + { + if (listWithHighlights[i].CompletionItem == bestMatch.completionItem) + { + selectedItemIndex = i; + break; + } + } + } + + return Task.FromResult(new FilteredCompletionModel(listWithHighlights, selectedItemIndex, updatedFilters)); + } + + Task<ImmutableArray<CompletionItem>> IAsyncCompletionItemManager.SortCompletionListAsync + (IAsyncCompletionSession session, AsyncCompletionSessionInitialDataSnapshot data, CancellationToken token) + { + return Task.FromResult(data.InitialList.OrderBy(n => n.SortText).ToImmutableArray()); + } + + #region Filtering + + private static bool ShouldBeInCompletionList( + CompletionItem item, + ImmutableArray<CompletionFilterWithState> filtersWithState) + { + foreach (var filterWithState in filtersWithState.Where(n => n.IsSelected)) + { + if (item.Filters.Any(n => n == filterWithState.Filter)) + { + return true; + } + } + return false; + } + + #endregion + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs b/src/Language/Impl/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs new file mode 100644 index 0000000..d2a0a2a --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Exposes non-public functionality to commanding and tests + /// </summary> + public interface IAsyncCompletionSessionOperations : IAsyncCompletionSession + { + /// <summary> + /// Sets span applicable to this completion session. + /// The span is defined on the session's <see cref="ITextView.TextBuffer"/>. + /// </summary> + new ITrackingSpan ApplicableToSpan { get; set; } + + /// <summary> + /// Returns whether computation has begun. + /// Computation starts after calling <see cref="IAsyncCompletionSession.OpenOrUpdate(InitialTrigger, SnapshotPoint, CancellationToken)"/> + /// </summary> + bool IsStarted { get; } + + /// <summary> + /// Enqueues selection a specified item. When all queued tasks are completed, the UI updates. + /// </summary> + void SelectCompletionItem(CompletionItem item); + + /// <summary> + /// Enqueues setting suggestion mode. When all queued tasks are completed, the UI updates. + /// </summary> + void SetSuggestionMode(bool useSuggestionMode); + + /// <summary> + /// Commits unique item. If no items were computed, performs computation. If there is no unique item, shows the UI. + /// </summary> + void InvokeAndCommitIfUnique(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token); + + /// <summary> + /// Enqueues selecting the next item. When all queued tasks are completed, the UI updates. + /// </summary> + void SelectDown(); + + /// <summary> + /// Enqueues selecting the item on the next page. When all queued tasks are completed, the UI updates. + /// </summary> + void SelectPageDown(); + + /// <summary> + /// Enqueues selecting the previous item. When all queued tasks are completed, the UI updates. + /// </summary> + void SelectUp(); + + /// <summary> + /// Enqueues selecting the item on the previous page. When all queued tasks are completed, the UI updates. + /// </summary> + void SelectPageUp(); + } +} diff --git a/src/Language/Impl/Language/AsyncCompletion/IModelComputationCallbackHandler.cs b/src/Language/Impl/Language/AsyncCompletion/IModelComputationCallbackHandler.cs new file mode 100644 index 0000000..a271ca1 --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/IModelComputationCallbackHandler.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + internal interface IModelComputationCallbackHandler<TModel> + { + Task UpdateUI(TModel model, CancellationToken token); + void Dismiss(); + } +} diff --git a/src/Language/Impl/Language/Completion/ImportBucket.cs b/src/Language/Impl/Language/AsyncCompletion/ImportBucket.cs index 9876600..7d87cf0 100644 --- a/src/Language/Impl/Language/Completion/ImportBucket.cs +++ b/src/Language/Impl/Language/AsyncCompletion/ImportBucket.cs @@ -6,10 +6,10 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.Text.Utilities; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation { /// <summary> - /// Lightweight stack-like view over a readonly ordered list of command handlers. + /// Lightweight stack-like view over a readonly ordered list of imports. /// </summary> internal class ImportBucket<T, TMetadata> where T : class diff --git a/src/Language/Impl/Language/Completion/CompletionUtilities.cs b/src/Language/Impl/Language/AsyncCompletion/MetadataUtilities.cs index ec0d97f..39c7e7e 100644 --- a/src/Language/Impl/Language/Completion/CompletionUtilities.cs +++ b/src/Language/Impl/Language/AsyncCompletion/MetadataUtilities.cs @@ -2,28 +2,29 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Text.Projection; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation { internal class MetadataUtilities<T, TMetadata> - where T : class - where TMetadata : IContentTypeMetadata + where T : class + where TMetadata : IContentTypeMetadata { /// <summary> /// This method creates a collection of (T, SnapshotPoint) pairs where the SnapshotPoint is the originalPoint /// translated to the buffer whose Content Type best matches Content Type associated with T /// Each instance of T appears only once in the returned collection. + /// Must be invoked on UI thread. /// </summary> - /// <param name="textView"></param> - /// <param name="originalPoint"></param> - /// <param name="imports"></param> - /// <returns></returns> - internal static IEnumerable<(ITextBuffer buffer, SnapshotPoint point, Lazy<T, TMetadata> import)> GetOrderedBuffersAndImports(ITextView textView, SnapshotPoint location, Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<T, TMetadata>>> getImports, IComparer<IEnumerable<string>> contentTypeComparer) + internal static IEnumerable<(ITextBuffer buffer, SnapshotPoint point, Lazy<T, TMetadata> import)> GetOrderedBuffersAndImports( + IBufferGraph bufferGraph, + ITextViewRoleSet roles, + SnapshotPoint location, + Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<T, TMetadata>>> getImports, + IComparer<IEnumerable<string>> contentTypeComparer) { // This method is created based on EditorCommandHandlerService.GetOrderedBuffersAndCommandHandlers @@ -53,7 +54,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation // list of (buffer, handler) pairs: (projection buffer, projection handler), (C# buffer, C# handler), // (projection buffer, any handler). - var mappedPointsEnumeration = GetPointsOnAvailableBuffers(textView, location); + var mappedPointsEnumeration = GetPointsOnAvailableBuffers(bufferGraph, location); if (!mappedPointsEnumeration.Any()) yield break; @@ -65,7 +66,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation var importBuckets = new ImportBucket<T, TMetadata>[buffers.Length]; for (int i = 0; i < buffers.Length; i++) { - importBuckets[i] = new ImportBucket<T, TMetadata>(getImports(buffers[i].ContentType, textView.Roles)); + importBuckets[i] = new ImportBucket<T, TMetadata>(getImports(buffers[i].ContentType, roles)); } while (true) @@ -120,16 +121,16 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation } /// <summary> - /// A simpler method that returns all imports with declared content type that matches content type of subject buffers available at the given location + /// A simpler method that returns all imports with declared content type that matches content type of subject buffers available at the given location. + /// Must be invoked on UI thread. /// </summary> - /// <param name="textView"></param> - /// <param name="location"></param> - /// <param name="getImports"></param> - /// <param name="contentTypeComparer"></param> - /// <returns></returns> - internal static IEnumerable<(ITextBuffer buffer, SnapshotPoint point, Lazy<T, TMetadata> import)> GetBuffersAndImports(ITextView textView, SnapshotPoint location, Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<T, TMetadata>>> getImports) + internal static IEnumerable<(ITextBuffer buffer, SnapshotPoint point, Lazy<T, TMetadata> import)> GetBuffersAndImports( + IBufferGraph bufferGraph, + ITextViewRoleSet roles, + SnapshotPoint location, + Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<T, TMetadata>>> getImports) { - var mappedPointsEnumeration = GetPointsOnAvailableBuffers(textView, location); + var mappedPointsEnumeration = GetPointsOnAvailableBuffers(bufferGraph, location); if (!mappedPointsEnumeration.Any()) yield break; @@ -138,36 +139,23 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation // An array of per-buffer buckets, each containing cached list of matching imports, // ordered by [Order] and content type specificity - var importBuckets = new ImportBucket<T, TMetadata>[buffers.Length]; for (int i = 0; i < buffers.Length; i++) { - foreach (var import in getImports(buffers[i].ContentType, textView.Roles)) + foreach (var import in getImports(buffers[i].ContentType, roles)) yield return (buffers[i], mappedPoints[i], import); } } - private static IEnumerable<SnapshotPoint> GetPointsOnAvailableBuffers(ITextView textView, SnapshotPoint location) - { - var mappingPoint = textView.BufferGraph.CreateMappingPoint(location, PointTrackingMode.Negative); - var buffers = textView.BufferGraph.GetTextBuffers(b => mappingPoint.GetPoint(b, PositionAffinity.Predecessor) != null); - var pointsInBuffers = buffers.Select(b => mappingPoint.GetPoint(b, PositionAffinity.Predecessor).Value); - return pointsInBuffers; - } - } - - internal static class CompletionUtilities - { /// <summary> - /// Maps given point to buffers that contain this point. Requires UI thread. + /// Maps given <see cref="SnapshotPoint"/> to <see cref="SnapshotPoint"/>s on buffers available at this location. + /// Must be invoked on UI thread. /// </summary> - /// <param name="textView"></param> - /// <param name="point"></param> - /// <returns></returns> - internal static IEnumerable<ITextBuffer> GetBuffersForPoint(ITextView textView, SnapshotPoint point) + private static IEnumerable<SnapshotPoint> GetPointsOnAvailableBuffers(IBufferGraph bufferGraph, SnapshotPoint location) { - // We are looking at the buffer to the left of the caret. - return textView.BufferGraph.GetTextBuffers(n => - textView.BufferGraph.MapDownToBuffer(point, PointTrackingMode.Negative, n, PositionAffinity.Predecessor) != null); + var mappingPoint = bufferGraph.CreateMappingPoint(location, PointTrackingMode.Negative); + var buffers = bufferGraph.GetTextBuffers(b => mappingPoint.GetPoint(b, PositionAffinity.Predecessor) != null); + var pointsInBuffers = buffers.Select(b => mappingPoint.GetPoint(b, PositionAffinity.Predecessor).Value); + return pointsInBuffers; } } } diff --git a/src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs b/src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs new file mode 100644 index 0000000..d553d3e --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs @@ -0,0 +1,137 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Facilitates enqueuing tasks to be ran on a worker thread. + /// Each task takes an immutable instance of <typeparamref name="TModel"/> + /// and outputs an instance of <typeparamref name="TModel"/>. + /// The returned instance will serve as input to the next task. + /// </summary> + /// <typeparam name="TModel">Type that represents a snapshot of feature's state</typeparam> + sealed class ModelComputation<TModel> where TModel : class + { + private readonly JoinableTaskFactory _joinableTaskFactory; + private readonly TaskScheduler _computationTaskScheduler; + private readonly CancellationToken _token; + private readonly IGuardedOperations _guardedOperations; + private readonly IModelComputationCallbackHandler<TModel> _callbacks; + + private bool _terminated; + private JoinableTask<TModel> _lastJoinableTask; + private CancellationTokenSource _uiCancellation; + + internal TModel RecentModel { get; private set; } = default; + + /// <summary> + /// Creates an instance of <see cref="ModelComputation{TModel}"/> + /// and enqueues an task that will generate the initial state of the <typeparamref name="TModel"/> + /// </summary> +#pragma warning disable CA1068 // CancellationToken should be the last parameter + public ModelComputation( + TaskScheduler computationTaskScheduler, + JoinableTaskContext joinableTaskContext, + Func<TModel, CancellationToken, Task<TModel>> initialTransformation, + CancellationToken token, + IGuardedOperations guardedOperations, + IModelComputationCallbackHandler<TModel> callbacks) +#pragma warning restore CA1068 + { + _joinableTaskFactory = joinableTaskContext.Factory; + _computationTaskScheduler = computationTaskScheduler; + _token = token; + _guardedOperations = guardedOperations; + _callbacks = callbacks; + + // Start dummy tasks so that we don't need to check for null on first Enqueue + _lastJoinableTask = _joinableTaskFactory.RunAsync(() => Task.FromResult(default(TModel))); + _uiCancellation = new CancellationTokenSource(); + + // Immediately run the first transformation, to operate on proper TModel. + Enqueue(initialTransformation, updateUi: false); + } + + /// <summary> + /// Schedules work to be done on the background, + /// potentially preempted by another piece of work scheduled in the future, + /// <paramref name="updateUi" /> indicates whether a single piece of work should occue once all background work is completed. + /// </summary> + public void Enqueue(Func<TModel, CancellationToken, Task<TModel>> transformation, bool updateUi) + { + // The integrity of our sequential chain depends on this method not being called concurrently. + // So we require the UI thread. + if (!_joinableTaskFactory.Context.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + if (_token.IsCancellationRequested || _terminated) + return; // Don't enqueue after computation has stopped. + + // Attempt to commit (CommitIfUnique) will cancel the UI updates. If the commit failed, we still want to update the UI. + if (_uiCancellation.IsCancellationRequested) + _uiCancellation = new CancellationTokenSource(); + + var previousTask = _lastJoinableTask; + JoinableTask<TModel> currentTask = null; + currentTask = _joinableTaskFactory.RunAsync(async () => + { + await Task.Yield(); // Yield to guarantee that currentTask is assigned. + await _computationTaskScheduler; // Go to the above priority thread. Main thread will return as soon as possible. + try + { + var previousModel = await previousTask; + // Previous task finished processing. We are ready to execute next piece of work. + if (_token.IsCancellationRequested || _terminated) + return previousModel; + + var transformedModel = await transformation(await previousTask, _token).ConfigureAwait(true); + RecentModel = transformedModel; + + // TODO: update UI even if updateUi is false but it wasn't updated yet. + if (_lastJoinableTask == currentTask && updateUi) + { + // update UI because we're the latest task + if (!_uiCancellation.IsCancellationRequested) + _callbacks.UpdateUI(transformedModel, _uiCancellation.Token).Forget(); + } + + return transformedModel; + } + catch (Exception ex) + { + _terminated = true; + _guardedOperations.HandleException(this, ex); + _callbacks.Dismiss(); + return await previousTask; + } + }); + + _lastJoinableTask = currentTask; + } + + /// <summary> + /// Blocks, waiting for all background work to finish. + /// </summary> + /// <param name="cancelUi">Whether UI should be dismissed. If false, this method will return after UI has been rendered</param> + /// <param name="dontWaitForUpdatedModel">Returns last available model without block. Used in WYSIWYG mode.</param> + /// <param name="token">Token used to cancel the operation, unblock the thread and return null</param> + /// <returns></returns> + public TModel WaitAndGetResult(bool cancelUi, CancellationToken token) + { + if (cancelUi) + _uiCancellation.Cancel(); + + try + { + return _lastJoinableTask.Join(token); + } + catch (OperationCanceledException) + { + return null; + } + } + } +} diff --git a/src/Language/Impl/Language/Completion/PrioritizedTaskScheduler.cs b/src/Language/Impl/Language/AsyncCompletion/PrioritizedTaskScheduler.cs index 8dd5394..3c054d9 100644 --- a/src/Language/Impl/Language/Completion/PrioritizedTaskScheduler.cs +++ b/src/Language/Impl/Language/AsyncCompletion/PrioritizedTaskScheduler.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation { /// <summary> /// Clone of Roslyn's PrioritizedTaskScheduler diff --git a/src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs b/src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs new file mode 100644 index 0000000..97d031b --- /dev/null +++ b/src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation +{ + /// <summary> + /// Internal item source used during lifetime of the suggestion mode item. + /// </summary> + internal class SuggestionModeCompletionItemSource : IAsyncCompletionSource + { + private SuggestionItemOptions _options; + + internal SuggestionModeCompletionItemSource(SuggestionItemOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + Task<CompletionContext> IAsyncCompletionSource.GetCompletionContextAsync(InitialTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token) + { + throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide a tooltip."); + } + + Task<object> IAsyncCompletionSource.GetDescriptionAsync(CompletionItem item, CancellationToken token) + { + return Task.FromResult<object>(_options.ToolTipText); + } + + bool IAsyncCompletionSource.TryGetApplicableToSpan(char typedChar, SnapshotPoint triggerLocation, out SnapshotSpan applicableToSpan, CancellationToken token) + { + applicableToSpan = default; + return false; + } + } +} diff --git a/src/Language/Impl/Language/Completion/TextUndoTransactionThatRollsBackProperly.cs b/src/Language/Impl/Language/AsyncCompletion/TextUndoTransactionThatRollsBackProperly.cs index 54b6778..0fbb876 100644 --- a/src/Language/Impl/Language/Completion/TextUndoTransactionThatRollsBackProperly.cs +++ b/src/Language/Impl/Language/AsyncCompletion/TextUndoTransactionThatRollsBackProperly.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation { /// <summary> /// An implementation of <see cref="ITextUndoTransaction" /> that wraps another diff --git a/src/Language/Impl/Language/Completion/UndoUtilities.cs b/src/Language/Impl/Language/AsyncCompletion/UndoUtilities.cs index 3eb4d8f..9477d7b 100644 --- a/src/Language/Impl/Language/Completion/UndoUtilities.cs +++ b/src/Language/Impl/Language/AsyncCompletion/UndoUtilities.cs @@ -4,12 +4,12 @@ using Microsoft.VisualStudio.Text; using System.Collections.Generic; using System.Linq; -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation { /// <summary> /// Code taken from http://source.roslyn.io/#Microsoft.CodeAnalysis.EditorFeatures/Implementation/IntelliSense/Completion/Controller_Commit.cs /// </summary> - static class UndoUtilities + internal static class UndoUtilities { internal static void RollbackToBeforeTypeChar(ITextSnapshot initialTextSnapshot, ITextBuffer subjectBuffer) { diff --git a/src/Language/Impl/Language/Completion/AsyncCompletionBroker.cs b/src/Language/Impl/Language/Completion/AsyncCompletionBroker.cs deleted file mode 100644 index 8ee9e35..0000000 --- a/src/Language/Impl/Language/Completion/AsyncCompletionBroker.cs +++ /dev/null @@ -1,382 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Operations; -using Microsoft.VisualStudio.Text.Utilities; -using Microsoft.VisualStudio.Threading; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - [Export(typeof(IAsyncCompletionBroker))] - internal class AsyncCompletionBroker : IAsyncCompletionBroker - { - [Import(AllowDefault = true)] - private ILoggingServiceInternal Logger; - - [Import] - private IGuardedOperations GuardedOperations; - - [Import] - private JoinableTaskContext JoinableTaskContext; - - [Import] - private IContentTypeRegistryService ContentTypeRegistryService; - - // Used exclusively for legacy telemetry - [Import(AllowDefault = true)] - private ITextDocumentFactoryService TextDocumentFactoryService; - - [ImportMany] - private IEnumerable<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> UnorderedPresenterProviders; - - [ImportMany] - private IEnumerable<Lazy<IAsyncCompletionItemSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> UnorderedCompletionItemSourceProviders; - - [ImportMany] - private IEnumerable<Lazy<IAsyncCompletionServiceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> UnorderedCompletionServiceProviders; - - private IList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> _orderedPresenterProviders; - private IList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> OrderedPresenterProviders - => _orderedPresenterProviders ?? (_orderedPresenterProviders = Orderer.Order(UnorderedPresenterProviders)); - - private IList<Lazy<IAsyncCompletionItemSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> _orderedCompletionItemSourceProviders; - private IList<Lazy<IAsyncCompletionItemSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> OrderedCompletionItemSourceProviders - => _orderedCompletionItemSourceProviders ?? (_orderedCompletionItemSourceProviders = Orderer.Order(UnorderedCompletionItemSourceProviders)); - - private IList<Lazy<IAsyncCompletionServiceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> _orderedCompletionServiceProviders; - private IList<Lazy<IAsyncCompletionServiceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> OrderedCompletionServiceProviders - => _orderedCompletionServiceProviders ?? (_orderedCompletionServiceProviders = Orderer.Order(UnorderedCompletionServiceProviders)); - - private ImmutableDictionary<IContentType, ImmutableSortedSet<char>> _commitCharacters = ImmutableDictionary<IContentType, ImmutableSortedSet<char>>.Empty; - private ImmutableDictionary<IContentType, ImmutableArray<IAsyncCompletionItemSourceProvider>> _cachedCompletionItemSourceProviders = ImmutableDictionary<IContentType, ImmutableArray<IAsyncCompletionItemSourceProvider>>.Empty; - private ImmutableDictionary<IContentType, ImmutableArray<IAsyncCompletionServiceProvider>> _cachedCompletionServiceProviders = ImmutableDictionary<IContentType, ImmutableArray<IAsyncCompletionServiceProvider>>.Empty; - private ImmutableDictionary<IContentType, ICompletionPresenterProvider> _cachedPresenterProviders = ImmutableDictionary<IContentType, ICompletionPresenterProvider>.Empty; - private bool firstRun = true; // used only for diagnostics - private bool _firstInvocationReported; // used for "time to code" - private StableContentTypeComparer _contentTypeComparer; - private const string IsCompletionAvailableProperty = "IsCompletionAvailable"; - - private Dictionary<IContentType, bool> FeatureAvailabilityByContentType = new Dictionary<IContentType, bool>(); - - bool IAsyncCompletionBroker.IsCompletionSupported(IContentType contentType) - { - bool featureIsAvailable; - if (FeatureAvailabilityByContentType.TryGetValue(contentType, out featureIsAvailable)) - { - return featureIsAvailable; - } - - featureIsAvailable = UnorderedCompletionItemSourceProviders - .Any(n => n.Metadata.ContentTypes.Any(ct => contentType.IsOfType(ct))); - featureIsAvailable &= UnorderedCompletionServiceProviders - .Any(n => n.Metadata.ContentTypes.Any(ct => contentType.IsOfType(ct))); - - FeatureAvailabilityByContentType[contentType] = featureIsAvailable; - return featureIsAvailable; - } - - IAsyncCompletionSession IAsyncCompletionBroker.TriggerCompletion(ITextView textView, SnapshotPoint triggerLocation, char typedChar) - { - var session = GetSession(textView); - if (session != null) - { - return session; - } - - var sourcesWithData = MetadataUtilities<IAsyncCompletionItemSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>.GetBuffersAndImports(textView, triggerLocation, GetCompletionItemSourceProviders); - - // We will pass this to the next TriggerCompletion. This feels hacky, refactor the way parts are collected and built - var cachedData = new CompletionSourcesWithData(sourcesWithData); - - SnapshotSpan? applicableSpan = null; - foreach (var sourceWithData in sourcesWithData) - { - var sourceProvider = GuardedOperations.InstantiateExtension(this, sourceWithData.import); // TODO: consider caching this - var source = sourceProvider.GetOrCreate(textView); - var candidateSpan = GuardedOperations.CallExtensionPoint( - errorSource: source, - call: () => source.ShouldTriggerCompletion(typedChar, sourceWithData.point), - valueOnThrow: null - ); - - // Assume that sources are ordered. If this source is the first one to provide span, map it to the view's top buffer and use it for completion, - if (applicableSpan == null && candidateSpan.HasValue) - { - var mappingSpan = textView.BufferGraph.CreateMappingSpan(candidateSpan.Value, SpanTrackingMode.EdgeInclusive); - applicableSpan = mappingSpan.GetSpans(textView.TextBuffer)[0]; - } - } - return applicableSpan.HasValue - ? TriggerCompletion(textView, triggerLocation, applicableSpan.Value, cachedData) - : null; - } - - private IAsyncCompletionSession TriggerCompletion(ITextView textView, SnapshotPoint triggerLocation, SnapshotSpan applicableSpan, CompletionSourcesWithData sources) - { - var session = GetSession(textView); - if (session != null) - { - return session; - } - - if (!sources.Data.Any()) - { - // There is no completion source available for this buffer - return null; - } - - var potentialCommitCharsBuilder = ImmutableArray.CreateBuilder<char>(); - var sourcesWithLocations = new List<(IAsyncCompletionItemSource, SnapshotPoint)>(); - foreach (var sourceWithData in sources.Data) - { - var sourceProvider = GuardedOperations.InstantiateExtension(this, sourceWithData.import); // TODO: consider caching this - GuardedOperations.CallExtensionPoint( - errorSource: sourceProvider, - call: () => - { - var source = sourceProvider.GetOrCreate(textView); - potentialCommitCharsBuilder.AddRange(source.GetPotentialCommitCharacters()); - sourcesWithLocations.Add((source, sourceWithData.point)); - }); - } - - if (_contentTypeComparer == null) - _contentTypeComparer = new StableContentTypeComparer(ContentTypeRegistryService); - - var servicesWithLocations = MetadataUtilities<IAsyncCompletionServiceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>.GetOrderedBuffersAndImports(textView, triggerLocation, GetServiceProviders, _contentTypeComparer); - var bestServiceWithData = servicesWithLocations.FirstOrDefault(); - var serviceProvider = GuardedOperations.InstantiateExtension(this, bestServiceWithData.import); // TODO: consider caching this - var service = GuardedOperations.CallExtensionPoint(serviceProvider, () => serviceProvider.GetOrCreate(textView), null); - if (service == null) - { - // This should never happen because we provide a default and IsCompletionFeatureAvailable would have returned false - throw new InvalidOperationException("No completion services not found. Completion will be unavailable."); - } - - var presentationProvidersWithLocations = MetadataUtilities<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>.GetOrderedBuffersAndImports(textView, triggerLocation, GetPresenters, _contentTypeComparer); - var bestPresentationProviderWithLocation = presentationProvidersWithLocations.FirstOrDefault(); - var presenterProvider = GuardedOperations.InstantiateExtension(this, bestPresentationProviderWithLocation.import); // TODO: consider caching this - - if (firstRun) - { - System.Diagnostics.Debug.Assert(presenterProvider != null, $"No instance of {nameof(ICompletionPresenterProvider)} is loaded. Completion will work without the UI."); - firstRun = false; - } - var telemetry = GetOrCreateTelemetry(textView); - - session = new AsyncCompletionSession(applicableSpan, potentialCommitCharsBuilder.ToImmutable(), JoinableTaskContext.Factory, presenterProvider, sourcesWithLocations, service, this, textView, telemetry, GuardedOperations); - textView.Properties.AddProperty(typeof(IAsyncCompletionSession), session); - textView.Closed += TextView_Closed; - - // Additionally, emulate the legacy completion telemetry - EmulateLegacyCompletionTelemetry(bestServiceWithData.buffer?.ContentType, textView); - - return session; - } - - private IReadOnlyList<Lazy<IAsyncCompletionItemSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> GetCompletionItemSourceProviders(IContentType contentType, ITextViewRoleSet textViewRoles) - { - return OrderedCompletionItemSourceProviders.Where(n => n.Metadata.ContentTypes.Any(c => contentType.IsOfType(c)) && (n.Metadata.TextViewRoles == null || textViewRoles.ContainsAny(n.Metadata.TextViewRoles))).ToList(); - } - private IReadOnlyList<Lazy<IAsyncCompletionServiceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> GetServiceProviders(IContentType contentType, ITextViewRoleSet textViewRoles) - { - return OrderedCompletionServiceProviders.Where(n => n.Metadata.ContentTypes.Any(c => contentType.IsOfType(c)) && (n.Metadata.TextViewRoles == null || textViewRoles.ContainsAny(n.Metadata.TextViewRoles))).OrderBy(n => n.Metadata.ContentTypes, _contentTypeComparer).ToList(); - } - private IReadOnlyList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>> GetPresenters(IContentType contentType, ITextViewRoleSet textViewRoles) - { - return OrderedPresenterProviders.Where(n => n.Metadata.ContentTypes.Any(c => contentType.IsOfType(c)) && (n.Metadata.TextViewRoles == null || textViewRoles.ContainsAny(n.Metadata.TextViewRoles))).OrderBy(n => n.Metadata.ContentTypes, _contentTypeComparer).ToList(); - } - - // TODO: Evaluate the methods below and clean them up. We should have one reliable way to get parts in correct order - private ImmutableArray<IAsyncCompletionItemSourceProvider> GetCompletionItemSourceProviders(IContentType contentType) - { - if (_cachedCompletionItemSourceProviders.TryGetValue(contentType, out var cachedSourceProviders)) - { - return cachedSourceProviders; - } - - var providers = GuardedOperations.InvokeMatchingFactories( - lazyFactories: OrderedCompletionItemSourceProviders, - getter: n => n, - dataContentType: contentType, - errorSource: this); - - var result = providers.ToImmutableArray(); - _cachedCompletionItemSourceProviders = _cachedCompletionItemSourceProviders.Add(contentType, result); - return result; - } - - private ImmutableArray<IAsyncCompletionServiceProvider> GetCompletionServiceProviders(IContentType contentType) - { - if (_cachedCompletionServiceProviders.TryGetValue(contentType, out var serviceProvider)) - { - return serviceProvider; - } - - var providers = GuardedOperations.InvokeMatchingFactories( - lazyFactories: OrderedCompletionServiceProviders, - getter: n => n, - dataContentType: contentType, - errorSource: this); - - var result = providers.ToImmutableArray(); - _cachedCompletionServiceProviders = _cachedCompletionServiceProviders.Add(contentType, result); - return result; - } - - private ICompletionPresenterProvider GetUiFactory(IContentType contentType) - { - if (_cachedPresenterProviders.TryGetValue(contentType, out var factory)) - { - return factory; - } - - ICompletionPresenterProvider bestFactory = GuardedOperations.InvokeBestMatchingFactory( - providerHandles: OrderedPresenterProviders, - getter: n => n, - dataContentType: contentType, - contentTypeRegistryService: ContentTypeRegistryService, - errorSource: this); - - _cachedPresenterProviders = _cachedPresenterProviders.Add(contentType, bestFactory); - return bestFactory; - } - - internal bool TryGetKnownCommitCharacters(IContentType contentType, ITextView view, out ImmutableSortedSet<char> commitChars) - { - if (_commitCharacters.TryGetValue(contentType, out commitChars)) - { - return commitChars.Any(); - } - var allCommitChars = new List<char>(); - foreach (var source in - GetCompletionItemSourceProviders(contentType) - .Select(n => n.GetOrCreate(view))) - { - GuardedOperations.CallExtensionPoint( - errorSource: source, - call: () => allCommitChars.AddRange(source.GetPotentialCommitCharacters()) - ); - } - commitChars = ImmutableSortedSet.CreateRange(allCommitChars); - _commitCharacters = _commitCharacters.Add(contentType, commitChars); - return commitChars.Any(); - } - - private void TextView_Closed(object sender, EventArgs e) - { - var view = (ITextView)sender; - view.Closed -= TextView_Closed; - GetSession(view)?.Dismiss(); - try - { - SendTelemetry(view); - } - catch (Exception ex) - { - GuardedOperations.HandleException(this, ex); - } - } - - bool IAsyncCompletionBroker.IsCompletionActive(ITextView textView) - { - return textView.Properties.ContainsProperty(typeof(IAsyncCompletionSession)); - } - - public IAsyncCompletionSession GetSession(ITextView textView) - { - if (textView.Properties.TryGetProperty(typeof(IAsyncCompletionSession), out IAsyncCompletionSession session)) - { - return session; - } - return null; - } - - /// <summary> - /// This method is used by <see cref="IAsyncCompletionSession"/> to inform the broker that it should forget about the session. Used when dismissing. - /// This method does not dismiss the session! - /// </summary> - /// <param name="session">Session being dismissed</param> - internal void ForgetSession(IAsyncCompletionSession session) - { - session.TextView.Properties.RemoveProperty(typeof(IAsyncCompletionSession)); - } - - /// <summary> - /// Wrapper around complex parameters. This is a candidate for refactoring. - /// </summary> - private struct CompletionSourcesWithData - { - internal IEnumerable<(ITextBuffer buffer, SnapshotPoint point, Lazy<IAsyncCompletionItemSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata> import)> Data; - - public CompletionSourcesWithData(IEnumerable<(ITextBuffer buffer, SnapshotPoint point, Lazy<IAsyncCompletionItemSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata> import)> data) - { - Data = data; - } - } - - // Helper methods for telemetry: - private CompletionTelemetryHost GetOrCreateTelemetry(ITextView textView) - { - if (textView.Properties.TryGetProperty(typeof(CompletionTelemetryHost), out CompletionTelemetryHost telemetry)) - { - return telemetry; - } - else - { - var newTelemetry = new CompletionTelemetryHost(Logger, this); - textView.Properties.AddProperty(typeof(CompletionTelemetryHost), newTelemetry); - return newTelemetry; - } - } - - private void SendTelemetry(ITextView textView) - { - if (textView.Properties.TryGetProperty(typeof(CompletionTelemetryHost), out CompletionTelemetryHost telemetry)) - { - telemetry.Send(); - textView.Properties.RemoveProperty(typeof(CompletionTelemetryHost)); - } - } - - internal string GetItemSourceName(IAsyncCompletionItemSource source) => OrderedCompletionItemSourceProviders.FirstOrDefault(n => n.IsValueCreated && n.Value == source)?.Metadata.Name ?? string.Empty; - internal string GetCompletionServiceName(IAsyncCompletionService service) => OrderedCompletionServiceProviders.FirstOrDefault(n => n.IsValueCreated && n.Value == service)?.Metadata.Name ?? string.Empty; - internal string GetCompletionPresenterProviderName(ICompletionPresenterProvider provider) => OrderedPresenterProviders.FirstOrDefault(n => n.IsValueCreated && n.Value == provider)?.Metadata.Name ?? string.Empty; - - // Parity with legacy telemetry - private void EmulateLegacyCompletionTelemetry(IContentType contentType, ITextView textView) - { - if (Logger == null || _firstInvocationReported) - return; - - string GetFileExtension(ITextBuffer buffer) - { - var documentFactoryService = TextDocumentFactoryService; - if (buffer != null && documentFactoryService != null) - { - ITextDocument currentDocument = null; - documentFactoryService.TryGetTextDocument(buffer, out currentDocument); - if (currentDocument != null && currentDocument.FilePath != null) - { - return System.IO.Path.GetExtension(currentDocument.FilePath); - } - } - return null; - } - var fileExtension = GetFileExtension(textView.TextBuffer) ?? "Unknown"; - var reportedContentType = contentType?.ToString() ?? "Unknown"; - - _firstInvocationReported = true; - Logger.PostEvent(TelemetryEventType.Operation, "VS/Editor/IntellisenseFirstRun/Opened", TelemetryResult.Success, - ("VS.Editor.IntellisenseFirstRun.Opened.ContentType", reportedContentType), - ("VS.Editor.IntellisenseFirstRun.Opened.FileExtension", fileExtension)); - } - } -} diff --git a/src/Language/Impl/Language/Completion/AsyncCompletionSession.cs b/src/Language/Impl/Language/Completion/AsyncCompletionSession.cs deleted file mode 100644 index bda6905..0000000 --- a/src/Language/Impl/Language/Completion/AsyncCompletionSession.cs +++ /dev/null @@ -1,776 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Operations; -using Microsoft.VisualStudio.Text.Utilities; -using Microsoft.VisualStudio.Threading; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Holds a state of the session - /// and a reference to the UI element - /// </summary> - internal class AsyncCompletionSession : IAsyncCompletionSession, ICompletionComputationCallbackHandler<CompletionModel>, IPropertyOwner - { - // Available data and services - private readonly IList<(IAsyncCompletionItemSource Source, SnapshotPoint Point)> _completionSources; - private readonly IDictionary<IAsyncCompletionItemSource, SnapshotPoint> _completionSourcesWhoGaveItems; - private readonly IAsyncCompletionService _completionService; - private readonly SnapshotSpan _initialApplicableSpan; - private readonly JoinableTaskFactory _jtf; - private readonly ICompletionPresenterProvider _presenterProvider; - private readonly AsyncCompletionBroker _broker; - private readonly ITextView _textView; - private readonly IGuardedOperations _guardedOperations; - private readonly ImmutableArray<char> _potentialCommitChars; - - // Presentation: - ICompletionPresenter _gui; // Must be accessed from GUI thread - const int FirstIndex = 0; - readonly int PageStepSize; - - // Computation state machine - private ModelComputation<CompletionModel> _computation; - private readonly CancellationTokenSource _computationCancellation = new CancellationTokenSource(); - int _lastFilteringTaskId; - - // Telemetry: - - private readonly CompletionSessionTelemetry _telemetry; - - /// <summary> - /// Tracks time spent on the worker thread - getting data, filtering and sorting. Used for telemetry. - /// </summary> - internal Stopwatch ComputationStopwatch { get; } = new Stopwatch(); - - /// <summary> - /// Tracks time spent on the UI thread - either rendering or committing. Used for telemetry. - /// </summary> - internal Stopwatch UiStopwatch { get; } = new Stopwatch(); - - // When set, UI will no longer be updated - private bool _isDismissed; - - // Facilitate experience when there are no items to display - private bool _selectionModeBeforeNoResultFallback; - private bool _inNoResultFallback; - private bool _ignoreCaretMovement; - - public event EventHandler<CompletionItemEventArgs> ItemCommitted; - public event EventHandler Dismissed; - public event EventHandler<CompletionItemsWithHighlightEventArgs> ItemsUpdated; - - public ITextView TextView => _textView; - - public bool IsDismissed => _isDismissed; - - public PropertyCollection Properties { get; } - - public AsyncCompletionSession(SnapshotSpan applicableSpan, ImmutableArray<char> potentialCommitChars, JoinableTaskFactory jtf, - ICompletionPresenterProvider presenterProvider, IList<(IAsyncCompletionItemSource, SnapshotPoint)> completionSources, - IAsyncCompletionService completionService, AsyncCompletionBroker broker, ITextView view, CompletionTelemetryHost telemetryHost, IGuardedOperations guardedOperations) - { - _initialApplicableSpan = applicableSpan; - _potentialCommitChars = potentialCommitChars; - _jtf = jtf; - _presenterProvider = presenterProvider; - _broker = broker; - _completionSources = completionSources; // still prorotype at the momemnt. - _completionSourcesWhoGaveItems = new Dictionary<IAsyncCompletionItemSource, SnapshotPoint>(); // To be filled in GetInitialModel - _completionService = completionService; - _textView = view; - _guardedOperations = guardedOperations; - _telemetry = new CompletionSessionTelemetry(telemetryHost, completionService, presenterProvider); - PageStepSize = presenterProvider?.ResultsPerPage ?? 1; - _textView.Caret.PositionChanged += OnCaretPositionChanged; - Properties = new PropertyCollection(); - } - - bool IAsyncCompletionSession.CommitIfUnique(CancellationToken token) - { - if (_isDismissed) - return false; - - // Note that this will deadlock if OpenOrUpdate wasn't called ahead of time. - var lastModel = _computation.WaitAndGetResult(); - if (lastModel.UniqueItem != null) - { - Commit(default(char), lastModel.UniqueItem, token); - return true; - } - else if (!lastModel.PresentedItems.IsDefaultOrEmpty && lastModel.PresentedItems.Length == 1) - { - Commit(default(char), lastModel.PresentedItems[0].CompletionItem, token); - return true; - } - else - { - // Show the UI, because waitAndGetResult canceled showing the UI. - UpdateUiInner(lastModel); // We are on the UI thread, so we may call UpdateUiInner - return false; - } - } - - /// <summary> - /// Entry point for committing. Decides whether to commit or not. The inner commit method calls Dispose to stop this session and hide the UI. - /// </summary> - /// <param name="token">Command handler infrastructure provides a token that we should pass to the language service's custom commit method.</param> - /// <param name="typedChar">It is default(char) when commit was requested by an explcit command (e.g. hitting Tab, Enter or clicking) - /// and it is not default(char) when commit happens as a result of typing a commit character.</param> - CommitBehavior IAsyncCompletionSession.Commit(CancellationToken token, char typedChar) - { - if (_isDismissed) - return CommitBehavior.None; - - var lastModel = _computation.WaitAndGetResult(); - - if (lastModel.UseSoftSelection && !typedChar.Equals(default(char))) - { - // In soft selection mode, user commits explicitly (click, tab, e.g. not tied to a text change). Otherwise, we dismiss the session - ((IAsyncCompletionSession)this).Dismiss(); - return CommitBehavior.None; - } - else if (lastModel.SelectSuggestionMode && string.IsNullOrWhiteSpace(lastModel.SuggestionModeItem?.InsertText)) - { - // In suggestion mode, don't commit empty suggestion (suggestion item temporarily shows description of suggestion mode) - return CommitBehavior.None; - } - else if (lastModel.SelectSuggestionMode) - { - // Commit the suggestion mode item - return Commit(typedChar, lastModel.SuggestionModeItem, token); - } - else if (lastModel.PresentedItems.IsDefaultOrEmpty) - { - // There is nothing to commit - Dismiss(); - return CommitBehavior.None; - } - else - { - // Regular commit - return Commit(typedChar, lastModel.PresentedItems[lastModel.SelectedIndex].CompletionItem, token); - } - } - - private CommitBehavior Commit(char typedChar, CompletionItem itemToCommit, CancellationToken token) - { - CommitBehavior result = CommitBehavior.None; - if (_isDismissed) - return result; - - var lastModel = _computation.WaitAndGetResult(); - - if (!_jtf.Context.IsOnMainThread) - throw new InvalidOperationException($"{nameof(IAsyncCompletionSession)}.{nameof(IAsyncCompletionSession.Commit)} must be callled from UI thread."); - - UiStopwatch.Restart(); - - // Pass appropriate buffer to the item's provider - var buffer = _completionSourcesWhoGaveItems[itemToCommit.Source].Snapshot.TextBuffer; - if (itemToCommit.UseCustomCommit) - { - result = _guardedOperations.CallExtensionPoint( - errorSource: itemToCommit.Source, - call: () => itemToCommit.Source.CustomCommit(_textView, buffer, itemToCommit, lastModel.ApplicableSpan, typedChar, token), - valueOnThrow: CommitBehavior.None); - } - else - { - result = _guardedOperations.CallExtensionPoint( - errorSource: itemToCommit.Source, - call: () => itemToCommit.Source.GetDefaultCommitBehavior(_textView, buffer, itemToCommit, lastModel.ApplicableSpan, typedChar, token), - valueOnThrow: CommitBehavior.None); - - InsertIntoBuffer(_textView, lastModel, itemToCommit.InsertText, typedChar); - } - UiStopwatch.Stop(); - _telemetry.RecordCommitted(UiStopwatch.ElapsedMilliseconds, itemToCommit); - _guardedOperations.RaiseEvent(this, ItemCommitted, new CompletionItemEventArgs(itemToCommit)); - Dismiss(); - return result; - } - - private void InsertIntoBuffer(ITextView view, CompletionModel model, string insertText, char typeChar) - { - var buffer = view.TextBuffer; - var bufferEdit = buffer.CreateEdit(); - - // ApplicableSpan already contains the typeChar and brace completion. Replacing this span will cause us to lose this data. - // The command handler who invoked this code needs to re-play the type char command, such that we get these changes back. - bufferEdit.Replace(model.ApplicableSpan.GetSpan(buffer.CurrentSnapshot), insertText); - bufferEdit.Apply(); - } - - public void Dismiss() - { - if (_isDismissed) - return; - - _isDismissed = true; - _broker.ForgetSession(this); - _guardedOperations.RaiseEvent(this, Dismissed); - _textView.Caret.PositionChanged -= OnCaretPositionChanged; - _computationCancellation.Cancel(); - - if (_gui != null) - { - var copyOfGui = _gui; - _guardedOperations.CallExtensionPointAsync( - errorSource: _gui, - asyncAction: async () => - { - await _jtf.SwitchToMainThreadAsync(); - copyOfGui.FiltersChanged -= OnFiltersChanged; - copyOfGui.CommitRequested -= OnCommitRequested; - copyOfGui.CompletionItemSelected -= OnItemSelected; - copyOfGui.CompletionClosed -= OnGuiClosed; - copyOfGui.Close(); - }); - _gui = null; - } - } - - void IAsyncCompletionSession.OpenOrUpdate(CompletionTrigger trigger, SnapshotPoint triggerLocation) - { - if (_isDismissed) - return; - - if (_computation == null) - { - _computation = new ModelComputation<CompletionModel>(PrioritizedTaskScheduler.AboveNormalInstance, _computationCancellation.Token, _guardedOperations, this); - _computation.Enqueue((model, token) => GetInitialModel(_textView, trigger, triggerLocation, token), updateUi: false); - } - - var taskId = Interlocked.Increment(ref _lastFilteringTaskId); - _computation.Enqueue((model, token) => UpdateSnapshot(model, trigger, FromCompletionTriggerReason(trigger.Reason), triggerLocation, token, taskId), updateUi: true); - } - - internal void InvokeAndCommitIfUnique(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) - { - if (_isDismissed) - return; - - if (_computation == null) - { - // Do not recompute, since this may change the selection. - ((IAsyncCompletionSession)this).OpenOrUpdate(trigger, triggerLocation); - } - - if (((IAsyncCompletionSession)this).CommitIfUnique(token)) - { - ((IAsyncCompletionSession)this).Dismiss(); - } - } - - private static CompletionFilterReason FromCompletionTriggerReason(CompletionTriggerReason reason) - { - switch (reason) - { - case CompletionTriggerReason.Invoke: - case CompletionTriggerReason.InvokeAndCommitIfUnique: - return CompletionFilterReason.Initial; - case CompletionTriggerReason.Insertion: - return CompletionFilterReason.Insertion; - case CompletionTriggerReason.Deletion: - return CompletionFilterReason.Deletion; - default: - throw new ArgumentOutOfRangeException(nameof(reason)); - } - } - - #region Internal methods accessed by the command handlers - - internal void ToggleSuggestionMode() - { - _computation.Enqueue((model, token) => ToggleCompletionModeInner(model, token), updateUi: true); - } - - internal void SelectDown() - { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, +1, token), updateUi: true); - } - - internal void SelectPageDown() - { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, +PageStepSize, token), updateUi: true); - } - - internal void SelectUp() - { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, -1, token), updateUi: true); - } - - internal void SelectPageUp() - { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, -PageStepSize, token), updateUi: true); - } - - #endregion - - private void OnFiltersChanged(object sender, CompletionFilterChangedEventArgs args) - { - var taskId = Interlocked.Increment(ref _lastFilteringTaskId); - _computation.Enqueue((model, token) => UpdateFilters(model, args.Filters, token, taskId), updateUi: true); - } - - private void OnCommitRequested(object sender, CompletionItemEventArgs args) - { - Commit(default(char), args.Item, default(CancellationToken)); - } - - private void OnItemSelected(object sender, CompletionItemSelectedEventArgs args) - { - // Note 1: Use this only to react to selection changes initiated by user's mouse\touch operation in the UI, since they cancel the soft selection - // Note 2: we are not enqueuing a call to update the UI, since this would put us in infinite loop, and the UI is already updated - _computation.Enqueue((model, token) => UpdateSelectedItem(model, args.SelectedItem, args.SuggestionModeSelected, token), updateUi: false); - } - - private void OnGuiClosed(object sender, CompletionClosedEventArgs args) - { - Dismiss(); - } - - /// <summary> - /// Determines whether the commit code path should be taken. Since this method is on a typing hot path, - /// we return quickly if the character is not found in the predefined list of potential commit characters. - /// Else, we create a mapping point from the top-buffer trigger location to each source's buffer - /// and return whether any source would like to commit completion. - /// </summary> - /// <remarks>This method must run on UI thread because of mapping the point across buffers.</remarks> - bool IAsyncCompletionSession.ShouldCommit(char typeChar, SnapshotPoint triggerLocation) - { - if (!_jtf.Context.IsOnMainThread) - throw new InvalidOperationException($"This method must be callled on the UI thread."); - - if (_potentialCommitChars.Contains(typeChar)) - { - var mappingPoint = _textView.BufferGraph.CreateMappingPoint(triggerLocation, PointTrackingMode.Negative); - return _completionSourcesWhoGaveItems - .Select(p => (p, mappingPoint.GetPoint(p.Value.Snapshot.TextBuffer, PositionAffinity.Predecessor))) - .Where(n => n.Item2.HasValue) - .Any(n => _guardedOperations.CallExtensionPoint( - errorSource: n.Item1.Key, - call: () => n.Item1.Key.ShouldCommitCompletion(typeChar, n.Item2.Value), - valueOnThrow: false)); - } - return false; - } - - /// <summary> - /// Monitors when user scrolled outside of the applicable span. Note that: - /// * This event is not raised during regular typing. - /// * This event is raised by brace completion. - /// * Typing stretches the applicable span - /// </summary> - private void OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e) - { - // http://source.roslyn.io/#Microsoft.CodeAnalysis.EditorFeatures/Implementation/IntelliSense/Completion/Controller_CaretPositionChanged.cs,40 - if (_ignoreCaretMovement) - return; - - _computation.Enqueue((model, token) => HandleCaretPositionChanged(model, e.NewPosition), updateUi: true); - } - - internal void IgnoreCaretMovement(bool ignore) - { - _ignoreCaretMovement = ignore; - if (!ignore) - { - // Don't let the session exist in invalid state: ensure that the location of the session is still valid - _computation?.Enqueue((model, token) => HandleCaretPositionChanged(model, _textView.Caret.Position), updateUi: true); - } - } - - async Task ICompletionComputationCallbackHandler<CompletionModel>.UpdateUi(CompletionModel model) - { - if (_presenterProvider == null) return; - await _jtf.SwitchToMainThreadAsync(); - UpdateUiInner(model); - await TaskScheduler.Default; - } - - /// <summary> - /// Opens or updates the UI. Must be called on UI thread. - /// </summary> - /// <param name="model"></param> - private void UpdateUiInner(CompletionModel model) - { - if (_isDismissed) - return; - // TODO: Consider building CompletionPresentationViewModel in BG and passing it here - - UiStopwatch.Restart(); - if (_gui == null) - { - _gui = _guardedOperations.CallExtensionPoint(errorSource: _presenterProvider, call: () => _presenterProvider.GetOrCreate(_textView), valueOnThrow: null); - if (_gui != null) - { - _guardedOperations.CallExtensionPoint( - errorSource: _gui, - call: () => - { - _gui = _presenterProvider.GetOrCreate(_textView); - _gui.Open(new CompletionPresentationViewModel(model.PresentedItems, model.Filters, model.ApplicableSpan, model.UseSoftSelection, - model.DisplaySuggestionMode, model.SelectSuggestionMode, model.SelectedIndex, model.SuggestionModeItem, model.SuggestionModeDescription)); - _gui.FiltersChanged += OnFiltersChanged; - _gui.CommitRequested += OnCommitRequested; - _gui.CompletionItemSelected += OnItemSelected; - _gui.CompletionClosed += OnGuiClosed; - }); - } - } - else - { - _guardedOperations.CallExtensionPoint( - errorSource: _gui, - call: () => _gui.Update(new CompletionPresentationViewModel(model.PresentedItems, model.Filters, model.ApplicableSpan, model.UseSoftSelection, - model.DisplaySuggestionMode, model.SelectSuggestionMode, model.SelectedIndex, model.SuggestionModeItem, model.SuggestionModeDescription))); - } - UiStopwatch.Stop(); - _telemetry.RecordRendering(UiStopwatch.ElapsedMilliseconds); - } - - /// <summary> - /// Creates a new model and populates it with initial data - /// </summary> - private async Task<CompletionModel> GetInitialModel(ITextView view, CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) - { - // Map the trigger location to respective view for each completion provider - var getCompletionTasks = new Task<CompletionContext>[_completionSources.Count]; - for (int i = 0; i < _completionSources.Count; i++) - { - var index = i; // Capture current value of i - getCompletionTasks[i] = Task.Run(async () => - { - var source = _completionSources[index].Source; - var point = _completionSources[index].Point; - var context = await _guardedOperations.CallExtensionPointAsync( - errorSource: _completionSources[index].Source, - asyncCall: () => _completionSources[index].Source.GetCompletionContextAsync(trigger, point, _initialApplicableSpan, token), - valueOnThrow: null - ); - if (context != null && !context.Items.IsDefaultOrEmpty) - { - _completionSourcesWhoGaveItems[source] = point; - } - return context; - }); - } - var nestedResults = await Task.WhenAll(getCompletionTasks); - var initialCompletionItems = nestedResults.Where(n => n != null && !n.Items.IsDefaultOrEmpty).SelectMany(n => n.Items).ToImmutableArray(); - - // Do not continue with empty session - if (initialCompletionItems.IsEmpty) - { - ((IAsyncCompletionSession)this).Dismiss(); - return default(CompletionModel); - } - - var availableFilters = initialCompletionItems - .SelectMany(n => n.Filters) - .Distinct() - .Select(n => new CompletionFilterWithState(n, true)) - .ToImmutableArray(); - - var applicableSpan = triggerLocation.Snapshot.CreateTrackingSpan(_initialApplicableSpan, SpanTrackingMode.EdgeInclusive); - - var useSoftSelection = nestedResults.Any(n => n != null && n.UseSoftSelection); - var useSuggestionMode = nestedResults.Any(n => n != null && n.UseSuggestionMode); - var suggestionModeDescription = nestedResults.FirstOrDefault(n => !string.IsNullOrEmpty(n?.SuggestionModeDescription))?.SuggestionModeDescription ?? string.Empty; - -#if DEBUG - Debug.WriteLine("Completion session got data."); - Debug.WriteLine("Sources: " + String.Join(", ", _completionSources.Select(n => n.Source.GetType()))); - Debug.WriteLine("Service: " + _completionService.GetType()); - Debug.WriteLine("Filters: " + String.Join(", ", availableFilters.Select(n => n.Filter.DisplayText))); - Debug.WriteLine("Span: " + _initialApplicableSpan.GetText()); -#endif - - ComputationStopwatch.Restart(); - var sortedList = await _guardedOperations.CallExtensionPointAsync( - errorSource: _completionService, - asyncCall: () => _completionService.SortCompletionListAsync(initialCompletionItems, trigger.Reason, triggerLocation.Snapshot, applicableSpan, _textView, token), - valueOnThrow: initialCompletionItems); - ComputationStopwatch.Stop(); - _telemetry.RecordProcessing(ComputationStopwatch.ElapsedMilliseconds, initialCompletionItems.Length); - _telemetry.RecordKeystroke(); - - return new CompletionModel(initialCompletionItems, sortedList, applicableSpan, trigger.Reason, triggerLocation.Snapshot, - availableFilters, useSoftSelection, useSuggestionMode, suggestionModeDescription, suggestionModeItem: null); - } - - /// <summary> - /// User has moved the caret. Ensure that the caret is still within the applicable span. If not, dismiss the session. - /// </summary> - /// <returns></returns> - private Task<CompletionModel> HandleCaretPositionChanged(CompletionModel model, CaretPosition caretPosition) - { - if (!(model.ApplicableSpan.GetSpan(caretPosition.VirtualBufferPosition.Position.Snapshot).IntersectsWith(new SnapshotSpan(caretPosition.VirtualBufferPosition.Position, 0)))) - { - ((IAsyncCompletionSession)this).Dismiss(); - } - return Task.FromResult(model); - } - - /// <summary> - /// TODO what is this? - /// </summary> - /// <returns></returns> - private Task<CompletionModel> ToggleCompletionModeInner(CompletionModel model, CancellationToken token) - { - return Task.FromResult(model.WithSuggestionModeActive(!model.DisplaySuggestionMode)); - } - - /// <summary> - /// User has typed. Update the known snapshot, filter the items and update the model. - /// </summary> - private async Task<CompletionModel> UpdateSnapshot(CompletionModel model, CompletionTrigger trigger, CompletionFilterReason filterReason, SnapshotPoint triggerLocation, CancellationToken token, int thisId) - { - // Always record keystrokes, even if filtering is preempted - _telemetry.RecordKeystroke(); - - // Completion got cancelled - if (token.IsCancellationRequested || model == null) - return default(CompletionModel); - - // Dismiss if we are outside of the applicable span - var currentlyApplicableSpan = model.ApplicableSpan.GetSpan(triggerLocation.Snapshot); - if (triggerLocation < currentlyApplicableSpan.Start - || triggerLocation > currentlyApplicableSpan.End) - { - ((IAsyncCompletionSession)this).Dismiss(); - return model; - } - // Record the first time the span is empty. If it is empty the second time we're here, and user is deleting, then dismiss - if (currentlyApplicableSpan.IsEmpty && model.ApplicableSpanWasEmpty && trigger.Reason == CompletionTriggerReason.Deletion) - { - ((IAsyncCompletionSession)this).Dismiss(); - return model; - } - model = model.WithApplicableSpanEmptyRecord(currentlyApplicableSpan.IsEmpty); - - // Filtering got preempted, so store the most recent snapshot for the next time we filter - if (thisId != _lastFilteringTaskId) - return model.WithSnapshot(triggerLocation.Snapshot); - - ComputationStopwatch.Restart(); - - var filteredCompletion = await _guardedOperations.CallExtensionPointAsync( - errorSource: _completionService, - asyncCall: () => _completionService.UpdateCompletionListAsync( - model.InitialItems, - model.InitialTriggerReason, - filterReason, - triggerLocation.Snapshot, // used exclusively to resolve applicable span - model.ApplicableSpan, - model.Filters, - _textView, // Roslyn doesn't need it, and likely, can't use it - token), - valueOnThrow: null); - - // Handle error cases by logging the issue and dismissing the session. - if (filteredCompletion == null) - { - ((IAsyncCompletionSession)this).Dismiss(); - return model; - } - - // Special experience when there are no more selected items: - ImmutableArray<CompletionItemWithHighlight> returnedItems; - int selectedIndex = filteredCompletion.SelectedItemIndex; - if (filteredCompletion.Items.IsDefault) - { - // Prevent null references when service returns default(ImmutableArray) - returnedItems = ImmutableArray<CompletionItemWithHighlight>.Empty; - } - else if (filteredCompletion.Items.IsEmpty) - { - // If there are no results now, show previously visible results, but without highlighting - if (model.PresentedItems.IsDefaultOrEmpty) - { - returnedItems = ImmutableArray<CompletionItemWithHighlight>.Empty; - } - else - { - returnedItems = model.PresentedItems.Select(n => new CompletionItemWithHighlight(n.CompletionItem)).ToImmutableArray(); - _selectionModeBeforeNoResultFallback = model.UseSoftSelection; - selectedIndex = model.SelectedIndex; - _inNoResultFallback = true; - model = model.WithSoftSelection(true); - } - } - else - { - if (_inNoResultFallback) - { - model = model.WithSoftSelection(_selectionModeBeforeNoResultFallback); - _inNoResultFallback = false; - } - returnedItems = filteredCompletion.Items; - } - - ComputationStopwatch.Stop(); - _telemetry.RecordProcessing(ComputationStopwatch.ElapsedMilliseconds, returnedItems.Length); - - if (filteredCompletion.SelectionMode == CompletionItemSelection.SoftSelected) - model = model.WithSoftSelection(true); - else if (filteredCompletion.SelectionMode == CompletionItemSelection.Selected) - model = model.WithSoftSelection(false); - - // Prepare the suggestionModeItem if we ever change the mode - var enteredText = currentlyApplicableSpan.GetText(); - var suggestionModeItem = new CompletionItem(enteredText, SuggestionModeCompletionItemSource.Instance); - - _guardedOperations.RaiseEvent(this, ItemsUpdated, new CompletionItemsWithHighlightEventArgs(returnedItems)); - - return model.WithSnapshotAndItems(triggerLocation.Snapshot, returnedItems, selectedIndex, filteredCompletion.UniqueItem, suggestionModeItem); - } - - /// <summary> - /// Reacts to user toggling a filter - /// </summary> - /// <param name="newFilters">Filters with updated Selected state, as indicated by the user.</param> - private async Task<CompletionModel> UpdateFilters(CompletionModel model, ImmutableArray<CompletionFilterWithState> newFilters, CancellationToken token, int thisId) - { - _telemetry.RecordChangingFilters(); - _telemetry.RecordKeystroke(); - - // Filtering got preempted, so store the most updated filters for the next time we filter - if (token.IsCancellationRequested || thisId != _lastFilteringTaskId) - return model.WithFilters(newFilters); - - var filteredCompletion = await _guardedOperations.CallExtensionPointAsync( - errorSource: _completionService, - asyncCall: () => _completionService.UpdateCompletionListAsync( - model.InitialItems, - model.InitialTriggerReason, - CompletionFilterReason.FilterChange, - model.Snapshot, - model.ApplicableSpan, - newFilters, - _textView, - token), - valueOnThrow: null); - - // Handle error cases by logging the issue and discarding the request to filter - if (filteredCompletion == null) - return model; - if (filteredCompletion.Filters.Length != newFilters.Length) - { - _guardedOperations.HandleException( - errorSource: _completionService, - e: new InvalidOperationException("Completion service returned incorrect set of filters.")); - return model; - } - - return model.WithFilters(filteredCompletion.Filters).WithPresentedItems(filteredCompletion.Items, filteredCompletion.SelectedItemIndex); - } - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - /// <summary> - /// Reacts to user scrolling the list using keyboard - /// </summary> - private async Task<CompletionModel> UpdateSelectedItem(CompletionModel model, int offset, CancellationToken token) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - _telemetry.RecordScrolling(); - _telemetry.RecordKeystroke(); - - if (!model.PresentedItems.Any()) - { - // No-op if there are no items - if (model.DisplaySuggestionMode) - { - // Unless there is a suggestion mode item. Select it. - return model.WithSuggestionItemSelected(); - } - return model; - } - - var lastIndex = model.PresentedItems.Count() - 1; - var currentIndex = model.SelectSuggestionMode ? -1 : model.SelectedIndex; - - if (offset > 0) // Scrolling down. Stop at last index then go to first index. - { - if (currentIndex == lastIndex) - { - if (model.DisplaySuggestionMode) - return model.WithSuggestionItemSelected(); - else - return model.WithSelectedIndex(FirstIndex); - } - var newIndex = currentIndex + offset; - return model.WithSelectedIndex(Math.Min(newIndex, lastIndex)); - } - else // Scrolling up. Stop at first index then go to last index. - { - if (currentIndex < FirstIndex) - { - // Suggestion mode item is selected. Go to the last item. - return model.WithSelectedIndex(lastIndex); - } - else if (currentIndex == FirstIndex) - { - // The first item is selected. If there is a suggestion, select it. - if (model.DisplaySuggestionMode) - return model.WithSuggestionItemSelected(); - else - return model.WithSelectedIndex(lastIndex); - } - var newIndex = currentIndex + offset; - return model.WithSelectedIndex(Math.Max(newIndex, FirstIndex)); - } - } - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - /// <summary> - /// Reacts to user selecting a specific item in the list - /// </summary> - private async Task<CompletionModel> UpdateSelectedItem(CompletionModel model, CompletionItem selectedItem, bool suggestionModeSelected, CancellationToken token) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - _telemetry.RecordScrolling(); - if (suggestionModeSelected) - { - return model.WithSuggestionItemSelected(); - } - else - { - for (int i = 0; i < model.PresentedItems.Length; i++) - { - if (model.PresentedItems[i].CompletionItem == selectedItem) - { - return model.WithSelectedIndex(i); - } - } - // This item is not in the model - return model; - } - } - - public ImmutableArray<CompletionItem> GetVisibleItems(CancellationToken token) - { - // TODO: Consider returning array of CompletionItemWithHighlight so that we don't do linq here - return _computation.RecentModel.PresentedItems.Select(n => n.CompletionItem).ToImmutableArray(); - } - - public CompletionItem GetSelectedItem(CancellationToken token) - { - var model = _computation.RecentModel; - if (model.SelectSuggestionMode) - return model.SuggestionModeItem; - else - return model.PresentedItems[model.SelectedIndex].CompletionItem; - } - } -} diff --git a/src/Language/Impl/Language/Completion/CompletionCommandHandlers.cs b/src/Language/Impl/Language/Completion/CompletionCommandHandlers.cs deleted file mode 100644 index cdd8b07..0000000 --- a/src/Language/Impl/Language/Completion/CompletionCommandHandlers.cs +++ /dev/null @@ -1,431 +0,0 @@ -using System; -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Commanding; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -using Microsoft.VisualStudio.Text.Operations; -using Microsoft.VisualStudio.Text.Utilities; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Reacts to the down arrow command and attempts to scroll the completion list. - /// </summary> - [Name(KnownCompletionNames.CompletionCommandHandlers)] - [ContentType("text")] - [Export(typeof(ICommandHandler))] - internal sealed class CompletionCommandHandlers : - ICommandHandler<DownKeyCommandArgs>, - ICommandHandler<PageDownKeyCommandArgs>, - ICommandHandler<PageUpKeyCommandArgs>, - ICommandHandler<UpKeyCommandArgs>, - IChainedCommandHandler<BackspaceKeyCommandArgs>, - ICommandHandler<EscapeKeyCommandArgs>, - ICommandHandler<InvokeCompletionListCommandArgs>, - ICommandHandler<CommitUniqueCompletionListItemCommandArgs>, - ICommandHandler<InsertSnippetCommandArgs>, - ICommandHandler<ToggleCompletionModeCommandArgs>, - IChainedCommandHandler<DeleteKeyCommandArgs>, - ICommandHandler<WordDeleteToEndCommandArgs>, - ICommandHandler<WordDeleteToStartCommandArgs>, - ICommandHandler<ReturnKeyCommandArgs>, - ICommandHandler<RedoCommandArgs>, - ICommandHandler<TabKeyCommandArgs>, - IChainedCommandHandler<TypeCharCommandArgs>, - ICommandHandler<UndoCommandArgs> - { - [Import] - IAsyncCompletionBroker Broker; - - [Import] - IExperimentationServiceInternal ExperimentationService; - - [Import] - ITextUndoHistoryRegistry UndoHistoryRegistry; - - [Import] - IEditorOperationsFactoryService EditorOperationsFactoryService; - - string INamed.DisplayName => Strings.CompletionCommandHandlerName; - - /// <summary> - /// Helper method that returns command state for commands - /// that are always available - unless the completion feature is available. - /// </summary> - /// <param name="view"></param> - /// <returns></returns> - private CommandState Available(IContentType contentType) - { - return ModernCompletionFeature.GetFeatureState(ExperimentationService) - && Broker.IsCompletionSupported(contentType) - ? CommandState.Available - : CommandState.Unspecified; - } - - /// <summary> - /// Helper method that returns command state for commands - /// that are available when completion is active. - /// </summary> - /// <remarks> - /// Completion might be active only if the feature is available, so we're skipping other checks. - /// </remarks> - private CommandState AvailableIfCompletionIsUp(ITextView view) - { - return Broker.IsCompletionActive(view) - ? CommandState.Available - : CommandState.Unspecified; - } - - CommandState IChainedCommandHandler<BackspaceKeyCommandArgs>.GetCommandState(BackspaceKeyCommandArgs args, Func<CommandState> nextCommandHandler) - => AvailableIfCompletionIsUp(args.TextView); - - void IChainedCommandHandler<BackspaceKeyCommandArgs>.ExecuteCommand(BackspaceKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - { - // Execute other commands in the chain to see the change in the buffer. - nextCommandHandler(); - - var session = Broker.GetSession(args.TextView); - if (session != null) - { - var trigger = new CompletionTrigger(CompletionTriggerReason.Deletion); - var location = args.TextView.Caret.Position.BufferPosition; - session.OpenOrUpdate(trigger, location); - } - } - - CommandState ICommandHandler<EscapeKeyCommandArgs>.GetCommandState(EscapeKeyCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<EscapeKeyCommandArgs>.ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView); - if (session != null) - { - session.Dismiss(); - return true; - } - return false; - } - - CommandState ICommandHandler<InvokeCompletionListCommandArgs>.GetCommandState(InvokeCompletionListCommandArgs args) - => Available(args.SubjectBuffer.ContentType); - - bool ICommandHandler<InvokeCompletionListCommandArgs>.ExecuteCommand(InvokeCompletionListCommandArgs args, CommandExecutionContext executionContext) - { - // If the caret is buried in virtual space, we should realize this virtual space before triggering the session. - if (args.TextView.Caret.InVirtualSpace) - { - IEditorOperations editorOperations = EditorOperationsFactoryService.GetEditorOperations(args.TextView); - // We can realize virtual space by inserting nothing through the editor operations. - editorOperations?.InsertText(""); - } - - var trigger = new CompletionTrigger(CompletionTriggerReason.Invoke); - var location = args.TextView.Caret.Position.BufferPosition; - var session = Broker.TriggerCompletion(args.TextView, location, default(char)); - if (session != null) - { - session.OpenOrUpdate(trigger, location); - return true; - } - return false; - } - - CommandState ICommandHandler<CommitUniqueCompletionListItemCommandArgs>.GetCommandState(CommitUniqueCompletionListItemCommandArgs args) - => Available(args.SubjectBuffer.ContentType); - - bool ICommandHandler<CommitUniqueCompletionListItemCommandArgs>.ExecuteCommand(CommitUniqueCompletionListItemCommandArgs args, CommandExecutionContext executionContext) - { - // If the caret is buried in virtual space, we should realize this virtual space before triggering the session. - if (args.TextView.Caret.InVirtualSpace) - { - IEditorOperations editorOperations = EditorOperationsFactoryService.GetEditorOperations(args.TextView); - // We can realize virtual space by inserting nothing through the editor operations. - editorOperations?.InsertText(""); - } - - var trigger = new CompletionTrigger(CompletionTriggerReason.InvokeAndCommitIfUnique); - var location = args.TextView.Caret.Position.BufferPosition; - var session = Broker.TriggerCompletion(args.TextView, location, default(char)); - if (session != null) - { - var sessionInternal = session as AsyncCompletionSession; - sessionInternal?.InvokeAndCommitIfUnique(trigger, location, executionContext.OperationContext.UserCancellationToken); - return true; - } - return false; - } - - CommandState ICommandHandler<InsertSnippetCommandArgs>.GetCommandState(InsertSnippetCommandArgs args) - => Available(args.SubjectBuffer.ContentType); - - bool ICommandHandler<InsertSnippetCommandArgs>.ExecuteCommand(InsertSnippetCommandArgs args, CommandExecutionContext executionContext) - { - System.Diagnostics.Debug.WriteLine("!!!! InsertSnippetCommandArgs"); - return false; - } - - CommandState ICommandHandler<ToggleCompletionModeCommandArgs>.GetCommandState(ToggleCompletionModeCommandArgs args) - => Available(args.SubjectBuffer.ContentType); - - bool ICommandHandler<ToggleCompletionModeCommandArgs>.ExecuteCommand(ToggleCompletionModeCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method - if (session != null) - { - session.ToggleSuggestionMode(); - return true; // TODO: Investigate. If we return false, we get called again. No matter what we return, the button in the UI does not update. - } - return false; - } - - CommandState IChainedCommandHandler<DeleteKeyCommandArgs>.GetCommandState(DeleteKeyCommandArgs args, Func<CommandState> nextCommandHandler) - => AvailableIfCompletionIsUp(args.TextView); - - void IChainedCommandHandler<DeleteKeyCommandArgs>.ExecuteCommand(DeleteKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - { - // Execute other commands in the chain to see the change in the buffer. - nextCommandHandler(); - - var session = Broker.GetSession(args.TextView); - if (session != null) - { - var trigger = new CompletionTrigger(CompletionTriggerReason.Deletion); - var location = args.TextView.Caret.Position.BufferPosition; - session.OpenOrUpdate(trigger, location); - } - } - - CommandState ICommandHandler<WordDeleteToEndCommandArgs>.GetCommandState(WordDeleteToEndCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<WordDeleteToEndCommandArgs>.ExecuteCommand(WordDeleteToEndCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView); - if (session != null) - { - session.Dismiss(); - return false; // return false so that the editor can handle this event - } - return false; - } - - CommandState ICommandHandler<WordDeleteToStartCommandArgs>.GetCommandState(WordDeleteToStartCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<WordDeleteToStartCommandArgs>.ExecuteCommand(WordDeleteToStartCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView); - if (session != null) - { - session.Dismiss(); - return false; // return false so that the editor can handle this event - } - return false; - } - - CommandState ICommandHandler<RedoCommandArgs>.GetCommandState(RedoCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<RedoCommandArgs>.ExecuteCommand(RedoCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView); - if (session != null) - { - session.Dismiss(); - return false; // return false so that the editor can handle this event - } - return false; - } - - CommandState ICommandHandler<ReturnKeyCommandArgs>.GetCommandState(ReturnKeyCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<ReturnKeyCommandArgs>.ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView); - if (session != null) - { - var commitBehavior = session.Commit(executionContext.OperationContext.UserCancellationToken, '\n'); - session.Dismiss(); - - // Mark this command as handled (return true), unless extender set the RaiseFurtherCommandHandlers flag. - if ((commitBehavior & CommitBehavior.RaiseFurtherCommandHandlers) == 0) - return true; - } - return false; - } - - CommandState ICommandHandler<TabKeyCommandArgs>.GetCommandState(TabKeyCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<TabKeyCommandArgs>.ExecuteCommand(TabKeyCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView); - if (session != null) - { - var commitBehavior = session.Commit(executionContext.OperationContext.UserCancellationToken, '\t'); - session.Dismiss(); - - // Mark this command as handled (return true), unless extender set the RaiseFurtherCommandHandlers flag. - if ((commitBehavior & CommitBehavior.RaiseFurtherCommandHandlers) == 0) - return true; - } - return false; - } - - CommandState IChainedCommandHandler<TypeCharCommandArgs>.GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler) - => Available(args.SubjectBuffer.ContentType); - - void IChainedCommandHandler<TypeCharCommandArgs>.ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - { - var initialTextSnapshot = args.TextView.TextSnapshot; - var view = args.TextView; - var location = view.Caret.Position.BufferPosition; - - // Note regarding undo: When completion and brace completion happen together, completion should be first on the undo stack. - // Effectively, we want to first undo the completion, leaving brace completion intact. Second undo should undo brace completion. - // To achieve this, we create a transaction in which we commit and reapply brace completion (via nextCommandHandler). - // Please read "Note regarding undo" comments in this method that explain the implementation choices. - // Hopefully an upcoming upgrade of the undo mechanism will allow us to undo out of order and vastly simplify this method. - - // Note regarding undo: In a corner case of typing closing brace over existing closing brace, - // Roslyn brace completion does not perform an edit. It moves the caret outside of session's applicable span, - // which dismisses the session. Put the session in a state where it will not dismiss when caret leaves the applicable span. - /* - // Unfortunately, preserving this session means that ultimately replaying the key will insert second brace instead of overtyping. - // Actually, even obtaining session before nextCommandHandler messes this up. I don't understand this yet and for now I will keep this code commented out. Completing over closing brace is now a known bug. - if (sessionToCommit != null && args.TextView.TextBuffer == args.SubjectBuffer) - { - ((AsyncCompletionSession)sessionToCommit).IgnoreCaretMovement(ignore: true); - } - */ - // Execute other commands in the chain to see the change in the buffer. This includes brace completion. - // Note regarding undo: This will be undone second - nextCommandHandler(); - - // Pass location from before calling nextCommandHandler so that extenders - // get the same view of the buffer in both ShouldCommit and Commit - var sessionToCommit = Broker.GetSession(args.TextView); - if (sessionToCommit?.ShouldCommit(args.TypedChar, location) == true) - { - // Buffer has changed, update the snapshot - location = view.Caret.Position.BufferPosition; - - // Note regarding undo: this transaction will be undone first - using (var undoTransaction = new CaretPreservingEditTransaction("Completion", view, UndoHistoryRegistry, EditorOperationsFactoryService)) - { - UndoUtilities.RollbackToBeforeTypeChar(initialTextSnapshot, args.SubjectBuffer); - // Now the buffer doesn't have the commit character nor the matching brace, if any - - var commitBehavior = sessionToCommit.Commit(executionContext.OperationContext.UserCancellationToken, args.TypedChar); - - if ((commitBehavior & CommitBehavior.SuppressFurtherCommandHandlers) == 0) - nextCommandHandler(); // Replay the key, so that we get brace completion. - - // Complete the transaction before stopping it. - undoTransaction.Complete(); - } - } - /* - if (sessionToCommit != null) - { - ((AsyncCompletionSession)sessionToCommit).IgnoreCaretMovement(ignore: false); - } - */ - // Buffer might have changed. Update it for when we try to trigger new session. - location = view.Caret.Position.BufferPosition; - - var trigger = new CompletionTrigger(CompletionTriggerReason.Insertion, args.TypedChar); - var session = Broker.GetSession(args.TextView); - if (session != null) - { - session.OpenOrUpdate(trigger, location); - } - else - { - var newSession = Broker.TriggerCompletion(args.TextView, location, args.TypedChar); - if (newSession != null) - { - newSession?.OpenOrUpdate(trigger, location); - } - } - } - - CommandState ICommandHandler<UndoCommandArgs>.GetCommandState(UndoCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<UndoCommandArgs>.ExecuteCommand(UndoCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView); - if (session != null) - { - session.Dismiss(); - return false; // return false so that the editor can handle this event - } - return false; - } - - CommandState ICommandHandler<DownKeyCommandArgs>.GetCommandState(DownKeyCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<DownKeyCommandArgs>.ExecuteCommand(DownKeyCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method - if (session != null) - { - session.SelectDown(); - System.Diagnostics.Debug.WriteLine("Completions's DownKey command handler returns true (handled)"); - return true; - } - System.Diagnostics.Debug.WriteLine("Completions's DownKey command handler returns false (unhandled)"); - return false; - } - - CommandState ICommandHandler<PageDownKeyCommandArgs>.GetCommandState(PageDownKeyCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<PageDownKeyCommandArgs>.ExecuteCommand(PageDownKeyCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method - if (session != null) - { - session.SelectPageDown(); - return true; - } - return false; - } - - CommandState ICommandHandler<PageUpKeyCommandArgs>.GetCommandState(PageUpKeyCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<PageUpKeyCommandArgs>.ExecuteCommand(PageUpKeyCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method - if (session != null) - { - session.SelectPageUp(); - return true; - } - return false; - } - - CommandState ICommandHandler<UpKeyCommandArgs>.GetCommandState(UpKeyCommandArgs args) - => AvailableIfCompletionIsUp(args.TextView); - - bool ICommandHandler<UpKeyCommandArgs>.ExecuteCommand(UpKeyCommandArgs args, CommandExecutionContext executionContext) - { - var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method - if (session != null) - { - session.SelectUp(); - System.Diagnostics.Debug.WriteLine("Completions's UpKey command handler returns true (handled)"); - return true; - } - System.Diagnostics.Debug.WriteLine("Completions's UpKey command handler returns false (unhandled)"); - return false; - } - } -} diff --git a/src/Language/Impl/Language/Completion/CompletionModel.cs b/src/Language/Impl/Language/Completion/CompletionModel.cs deleted file mode 100644 index 84d18f3..0000000 --- a/src/Language/Impl/Language/Completion/CompletionModel.cs +++ /dev/null @@ -1,461 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - sealed class CompletionModel - { - /// <summary> - /// All items, as provided by completion item sources. - /// </summary> - public readonly ImmutableArray<CompletionItem> InitialItems; - - /// <summary> - /// Sorted array of all items, as provided by the completion service. - /// </summary> - public readonly ImmutableArray<CompletionItem> SortedItems; - - /// <summary> - /// Span pertinent to this completion model. - /// </summary> - public readonly ITrackingSpan ApplicableSpan; - - /// <summary> - /// Snapshot pertinent to this completion model. - /// </summary> - public readonly ITextSnapshot Snapshot; - - /// <summary> - /// Stores the initial reason this session was triggererd. - /// </summary> - public readonly CompletionTriggerReason InitialTriggerReason; - - /// <summary> - /// Filters involved in this completion model, including their availability and selection state. - /// </summary> - public readonly ImmutableArray<CompletionFilterWithState> Filters; - - /// <summary> - /// Items to be displayed in the UI. - /// </summary> - public readonly ImmutableArray<CompletionItemWithHighlight> PresentedItems; - - /// <summary> - /// Index of item to select. Use -1 to select nothing, when suggestion mode item should be selected. - /// </summary> - public readonly int SelectedIndex; - - /// <summary> - /// Whether selection should be displayed as soft selection. - /// </summary> - public readonly bool UseSoftSelection; - - /// <summary> - /// Whether suggestion mode item should be visible. - /// </summary> - public readonly bool DisplaySuggestionMode; - - /// <summary> - /// Whether suggestion mode item should be selected. - /// </summary> - public readonly bool SelectSuggestionMode; - - /// <summary> - /// <see cref="CompletionItem"/> which contains user-entered text. - /// Used to display and commit the suggestion mode item - /// </summary> - public readonly CompletionItem SuggestionModeItem; - - /// <summary> - /// Text to display in place of suggestion mode when filtered text is empty. - /// </summary> - public readonly string SuggestionModeDescription; - - /// <summary> - /// <see cref="CompletionItem"/> which overrides regular unique item selection. - /// When this is null, the single item from <see cref="PresentedItems"/> is used as unique item. - /// </summary> - public readonly CompletionItem UniqueItem; - - /// <summary> - /// When completion starts, its <see cref="ApplicableSpan"/> may be empty. Initially, don't dismiss. - /// Further, the span is empty if user removes characters. We would like to dismiss then. - /// </summary> - public readonly bool ApplicableSpanWasEmpty; - - /// <summary> - /// Constructor for the initial model - /// </summary> - public CompletionModel(ImmutableArray<CompletionItem> initialItems, ImmutableArray<CompletionItem> sortedItems, - ITrackingSpan applicableSpan, CompletionTriggerReason initialTriggerReason, ITextSnapshot snapshot, - ImmutableArray<CompletionFilterWithState> filters, bool useSoftSelection, bool useSuggestionMode, string suggestionModeDescription, CompletionItem suggestionModeItem) - { - InitialItems = initialItems; - SortedItems = sortedItems; - ApplicableSpan = applicableSpan; - InitialTriggerReason = initialTriggerReason; - Snapshot = snapshot; - Filters = filters; - SelectedIndex = 0; - UseSoftSelection = useSoftSelection; - DisplaySuggestionMode = useSuggestionMode; - SelectSuggestionMode = useSuggestionMode; - SuggestionModeDescription = suggestionModeDescription; - SuggestionModeItem = suggestionModeItem; - UniqueItem = null; - ApplicableSpanWasEmpty = false; - } - - /// <summary> - /// Private constructor for the With* methods - /// </summary> - private CompletionModel(ImmutableArray<CompletionItem> initialItems, ImmutableArray<CompletionItem> sortedItems, ITrackingSpan applicableSpan, CompletionTriggerReason initialTriggerReason, - ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, ImmutableArray<CompletionItemWithHighlight> presentedItems, bool useSoftSelection, bool useSuggestionMode, - string suggestionModeDescription, int selectedIndex, bool selectSuggestionMode, CompletionItem suggestionModeItem, CompletionItem uniqueItem, bool applicableSpanWasEmpty) - { - InitialItems = initialItems; - SortedItems = sortedItems; - ApplicableSpan = applicableSpan; - InitialTriggerReason = initialTriggerReason; - Snapshot = snapshot; - Filters = filters; - PresentedItems = presentedItems; - SelectedIndex = selectedIndex; - UseSoftSelection = useSoftSelection; - DisplaySuggestionMode = useSuggestionMode; - SelectSuggestionMode = selectSuggestionMode; - SuggestionModeDescription = suggestionModeDescription; - SuggestionModeItem = suggestionModeItem; - ApplicableSpanWasEmpty = applicableSpanWasEmpty; - } - - public CompletionModel WithPresentedItems(ImmutableArray<CompletionItemWithHighlight> newPresentedItems, int newSelectedIndex) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: newPresentedItems, // Updated - useSoftSelection: UseSoftSelection, - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: newSelectedIndex, // Updated - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - public CompletionModel WithSnapshot(ITextSnapshot newSnapshot) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: newSnapshot, // Updated - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: UseSoftSelection, - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: SelectedIndex, - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - public CompletionModel WithFilters(ImmutableArray<CompletionFilterWithState> newFilters) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: newFilters, // Updated - presentedItems: PresentedItems, - useSoftSelection: UseSoftSelection, - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: SelectedIndex, - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - public CompletionModel WithSelectedIndex(int newIndex) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: false, // Explicit selection and soft selection are mutually exclusive - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: newIndex, // Updated - selectSuggestionMode: false, // Explicit selection of regular item - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - public CompletionModel WithSuggestionItemSelected() - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: false, // Explicit selection and soft selection are mutually exclusive - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: -1, // Deselect regular item - selectSuggestionMode: true, // Explicit selection of suggestion item - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - public CompletionModel WithSuggestionModeActive(bool newUseSuggestionMode) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: UseSoftSelection | newUseSuggestionMode, // Enabling suggestion mode also enables soft selection - useSuggestionMode: newUseSuggestionMode, // Updated - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: SelectedIndex, - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - /// <summary> - /// </summary> - /// <param name="newSuggestionModeItem">It is ok to pass in null when there is no suggestion. UI will display SuggestsionModeDescription instead.</param> - internal CompletionModel WithSuggestionModeItem(CompletionItem newSuggestionModeItem) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: UseSoftSelection, - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: SelectedIndex, - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: newSuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - /// <summary> - /// </summary> - /// <param name="newUniqueItem">Overrides typical unique item selection. - /// Pass in null to use regular behavior: treating single <see cref="PresentedItems"/> item as the unique item.</param> - internal CompletionModel WithUniqueItem(CompletionItem newUniqueItem) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: UseSoftSelection, - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: SelectedIndex, - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: SuggestionModeItem, - uniqueItem: newUniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - internal CompletionModel WithSoftSelection(bool newSoftSelection) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: newSoftSelection, // Updated - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: SelectedIndex, - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - internal CompletionModel WithSnapshotAndItems(ITextSnapshot snapshot, ImmutableArray<CompletionItemWithHighlight> presentedItems, int selectedIndex, CompletionItem uniqueItem, CompletionItem suggestionModeItem) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: snapshot, // Updated - filters: Filters, - presentedItems: presentedItems, // Updated - useSoftSelection: UseSoftSelection, - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: selectedIndex, // Updated - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: suggestionModeItem, // Updated - uniqueItem: uniqueItem, // Updated - applicableSpanWasEmpty: ApplicableSpanWasEmpty - ); - } - - internal CompletionModel WithApplicableSpanEmptyRecord(bool applicableSpanIsEmpty) - { - return new CompletionModel( - initialItems: InitialItems, - sortedItems: SortedItems, - applicableSpan: ApplicableSpan, - initialTriggerReason: InitialTriggerReason, - snapshot: Snapshot, - filters: Filters, - presentedItems: PresentedItems, - useSoftSelection: UseSoftSelection, - useSuggestionMode: DisplaySuggestionMode, - suggestionModeDescription: SuggestionModeDescription, - selectedIndex: SelectedIndex, - selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: SuggestionModeItem, - uniqueItem: UniqueItem, - applicableSpanWasEmpty: applicableSpanIsEmpty // Updated - ); - } - } - - sealed class ModelComputation<TModel> - { - private Task<TModel> _lastTask = Task.FromResult(default(TModel)); - private Task _notifyUITask = Task.CompletedTask; - private readonly TaskScheduler _computationTaskScheduler; - private readonly CancellationToken _token; - private readonly IGuardedOperations _guardedOperations; - private CancellationTokenSource _uiCancellation; - private readonly ICompletionComputationCallbackHandler<TModel> _callbacks; - internal TModel RecentModel { get; private set; } = default(TModel); - - public ModelComputation(TaskScheduler computationTaskScheduler, CancellationToken token, IGuardedOperations guardedOperations, ICompletionComputationCallbackHandler<TModel> callbacks) - { - _computationTaskScheduler = computationTaskScheduler; - _token = token; - _guardedOperations = guardedOperations; - _uiCancellation = new CancellationTokenSource(); - _callbacks = callbacks; - } - - private Task<TModel> SafelyInvoke(Func<TModel, CancellationToken, Task<TModel>> transformation, Task<TModel> previousTask, CancellationToken token) - { - var transformedTask = transformation(previousTask.Result, token); - if (transformedTask.IsFaulted) - { - _guardedOperations.HandleException(this, transformedTask.Exception); - _callbacks.Dismiss(); - return previousTask; - } - return transformedTask; - } - - /// <summary> - /// Schedules work to be done on the background, - /// potentially preempted by another piece of work scheduled in the future, - /// <paramref name="updateUi" /> indicates whether a single piece of work should occue once all background work is completed. - /// </summary> - public void Enqueue(Func<TModel, CancellationToken, Task<TModel>> transformation, bool updateUi) - { - // This method is based on Roslyn's ModelComputation.ChainTaskAndNotifyControllerWhenFinished - var nextTask = _lastTask.ContinueWith(t => SafelyInvoke(transformation, t, _token), _computationTaskScheduler).Unwrap(); - _lastTask = nextTask; - - // If the _notifyUITask is canceled, refresh it - if (_notifyUITask.IsCanceled || _uiCancellation.IsCancellationRequested) - { - _notifyUITask = Task.CompletedTask; - _uiCancellation = new CancellationTokenSource(); - } - - _notifyUITask = Task.Factory.ContinueWhenAll( - new[] { _notifyUITask, nextTask }, - async existingTasks => - { - if (existingTasks.All(t => t.Status == TaskStatus.RanToCompletion)) - { - OnModelUpdated(nextTask.Result); - if (updateUi && nextTask == _lastTask) - { - await _callbacks.UpdateUi(nextTask.Result); - } - } - }, - _uiCancellation.Token - ); - } - - private void OnModelUpdated(TModel result) - { - RecentModel = result; - } - - /// <summary> - /// Blocks, waiting for all background work to finish. - /// Ignores the last piece of work a.k.a. "updateUI" - /// </summary> - public TModel WaitAndGetResult() - { - _uiCancellation.Cancel(); - _lastTask.Wait(); - return _lastTask.Result; - } - } -} diff --git a/src/Language/Impl/Language/Completion/CompletionTelemetry.cs b/src/Language/Impl/Language/Completion/CompletionTelemetry.cs deleted file mode 100644 index 35915a3..0000000 --- a/src/Language/Impl/Language/Completion/CompletionTelemetry.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Text.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - internal class CompletionSessionTelemetry - { - private readonly CompletionTelemetryHost _telemetryHost; - - // Collected data - internal string CompletionService { get; private set; } - internal string CompletionPresenterProvider { get; private set; } - internal string CompletionSource { get; private set; } - - internal long InitialProcessingDuration { get; private set; } - internal long TotalProcessingDuration { get; private set; } - internal int TotalProcessingCount { get; private set; } - - internal long InitialRenderingDuration { get; private set; } - internal long TotalRenderingDuration { get; private set; } - internal int TotalRenderingCount { get; private set; } - - internal long CommitDuration { get; private set; } - - internal bool UserEverScrolled { get; private set; } - internal bool UserEverSetFilters { get; private set; } - internal int FinalItemCount { get; private set; } - internal int NumberOfKeystrokes { get; private set; } - - public CompletionSessionTelemetry(CompletionTelemetryHost telemetryHost, IAsyncCompletionService completionService, ICompletionPresenterProvider presenterProvider) - { - _telemetryHost = telemetryHost; - CompletionService = _telemetryHost.GetCompletionServiceName(completionService); - CompletionPresenterProvider = _telemetryHost.GetCompletionPresenterProviderName(presenterProvider); - } - - internal void RecordProcessing(long processingTime, int itemCount) - { - if (TotalProcessingCount == 0) - { - InitialProcessingDuration = processingTime; - } - else - { - TotalProcessingDuration += processingTime; - FinalItemCount = itemCount; - } - TotalProcessingCount++; - } - - internal void RecordRendering(long processingTime) - { - if (TotalRenderingCount == 0) - InitialRenderingDuration = processingTime; - TotalRenderingCount++; - TotalRenderingDuration += processingTime; - } - - internal void RecordScrolling() - { - UserEverScrolled = true; - } - - internal void RecordChangingFilters() - { - UserEverSetFilters = true; - } - - internal void RecordKeystroke() - { - NumberOfKeystrokes++; - } - - internal void RecordCommitted(long commitDuration, CompletionItem committedItem) - { - CompletionSource = committedItem.UseCustomCommit ? _telemetryHost.GetItemSourceName(committedItem.Source) : String.Empty; - CommitDuration = commitDuration; - _telemetryHost.Add(this); - } - } - - internal class CompletionTelemetryHost - { - private class AggregateSourceData - { - internal long TotalCommitTime; - internal long CommitCount; - } - - private class AggregateServiceData - { - internal long TotalProcessTime; - internal long InitialProcessTime; - internal int ProcessCount; - internal int TotalKeystrokes; - internal int UserEverScrolled; - internal int UserEverSetFilters; - internal int FinalItemCount; - internal int DataCount; - } - - private class AggregatePresenterData - { - internal long TotalRenderTime; - internal long InitialRenderTime; - internal int RenderCount; - } - - Dictionary<string, AggregateSourceData> SourceData = new Dictionary<string, AggregateSourceData>(4); - Dictionary<string, AggregateServiceData> ServiceData = new Dictionary<string, AggregateServiceData>(4); - Dictionary<string, AggregatePresenterData> PresenterData = new Dictionary<string, AggregatePresenterData>(4); - - private readonly ILoggingServiceInternal _logger; - private readonly AsyncCompletionBroker _broker; - - public CompletionTelemetryHost(ILoggingServiceInternal logger, AsyncCompletionBroker broker) - { - _logger = logger; - _broker = broker; - } - - internal string GetItemSourceName(IAsyncCompletionItemSource source) => _broker.GetItemSourceName(source); - internal string GetCompletionServiceName(IAsyncCompletionService service) => _broker.GetCompletionServiceName(service); - internal string GetCompletionPresenterProviderName(ICompletionPresenterProvider provider) => _broker.GetCompletionPresenterProviderName(provider); - - /// <summary> - /// Adds data from <see cref="CompletionSessionTelemetry" /> to appropriate buckets. - /// </summary> - /// <param name=""></param> - internal void Add(CompletionSessionTelemetry telemetry) - { - if (_logger == null) - return; - - var presenterKey = telemetry.CompletionPresenterProvider; - if (!PresenterData.ContainsKey(presenterKey)) - PresenterData[presenterKey] = new AggregatePresenterData(); - var aggregatePresenterData = PresenterData[presenterKey]; - - var serviceKey = telemetry.CompletionService; - if (!ServiceData.ContainsKey(serviceKey)) - ServiceData[serviceKey] = new AggregateServiceData(); - var aggregateServiceData = ServiceData[serviceKey]; - - var sourceKey = telemetry.CompletionSource; - if (!SourceData.ContainsKey(sourceKey)) - SourceData[sourceKey] = new AggregateSourceData(); - var aggregateSourceData = SourceData[sourceKey]; - - aggregatePresenterData.InitialRenderTime += telemetry.InitialRenderingDuration; - aggregatePresenterData.TotalRenderTime += telemetry.TotalRenderingDuration; - aggregatePresenterData.RenderCount += telemetry.TotalRenderingCount; - - aggregateServiceData.DataCount++; - aggregateServiceData.FinalItemCount += telemetry.FinalItemCount; - aggregateServiceData.InitialProcessTime += telemetry.InitialProcessingDuration; - aggregateServiceData.ProcessCount += telemetry.TotalProcessingCount; - aggregateServiceData.TotalProcessTime += telemetry.TotalProcessingDuration; - aggregateServiceData.TotalKeystrokes += telemetry.NumberOfKeystrokes; - aggregateServiceData.TotalProcessTime += telemetry.TotalProcessingDuration; - aggregateServiceData.UserEverScrolled += telemetry.UserEverScrolled ? 1 : 0; - aggregateServiceData.UserEverSetFilters += telemetry.UserEverSetFilters ? 1 : 0; - - aggregateSourceData.TotalCommitTime += telemetry.CommitDuration; - aggregateSourceData.CommitCount++; - } - - /// <summary> - /// Sends batch of collected data. - /// </summary> - internal void Send() - { - if (_logger == null) - return; - - foreach (var data in PresenterData) - { - if (data.Value.RenderCount == 0) - continue; - - _logger.PostEvent(PresenterEventName, - (PresenterName, data.Key), - (PresenterAverageInitialRendering, data.Value.InitialRenderTime / data.Value.RenderCount), - (PresenterAverageRendering, data.Value.TotalRenderTime / data.Value.RenderCount) - ); - } - - foreach (var data in ServiceData) - { - if (data.Value.DataCount == 0) - continue; - - _logger.PostEvent(ServiceEventName, - (ServiceName, data.Key), - (ServiceAverageFinalItemCount, data.Value.FinalItemCount / data.Value.DataCount), - (ServiceAverageInitialProcessTime, data.Value.InitialProcessTime / data.Value.DataCount), - (ServiceAverageFilterTime, data.Value.TotalProcessTime / data.Value.ProcessCount), - (ServiceAverageKeystrokeCount, data.Value.TotalKeystrokes / data.Value.DataCount), - (ServiceAverageScrolled, data.Value.UserEverScrolled / data.Value.DataCount), - (ServiceAverageSetFilters, data.Value.UserEverSetFilters / data.Value.DataCount) - ); - } - - foreach (var data in SourceData) - { - if (data.Value.CommitCount == 0) - continue; - - _logger.PostEvent(SourceEventName, - (SourceName, data.Key), - (SourceAverageCommit, data.Value.TotalCommitTime / data.Value.CommitCount) - ); - } - } - - // Property and event names - internal const string PresenterEventName = "VS/Editor/Completion/PresenterData"; - internal const string PresenterName = "Property.Rendering.Name"; - internal const string PresenterAverageInitialRendering = "Property.Rendering.InitialDuration"; - internal const string PresenterAverageRendering = "Property.Rendering.AnyDuration"; - - internal const string ServiceEventName = "VS/Editor/Completion/ServiceData"; - internal const string ServiceName = "Property.Service.Name"; - internal const string ServiceAverageFinalItemCount = "Property.Service.Name"; - internal const string ServiceAverageInitialProcessTime = "Property.Service.Name"; - internal const string ServiceAverageFilterTime = "Property.Service.Name"; - internal const string ServiceAverageKeystrokeCount = "Property.Service.Name"; - internal const string ServiceAverageScrolled = "Property.Service.Name"; - internal const string ServiceAverageSetFilters = "Property.Service.Name"; - - internal const string SourceEventName = "VS/Editor/Completion/SourceData"; - internal const string SourceName = "Property.Commit.Name"; - internal const string SourceAverageCommit = "Property.Commit.Duration"; - } -} diff --git a/src/Language/Impl/Language/Completion/DefaultCompletionService.cs b/src/Language/Impl/Language/Completion/DefaultCompletionService.cs deleted file mode 100644 index 3cd3479..0000000 --- a/src/Language/Impl/Language/Completion/DefaultCompletionService.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Core.Imaging; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.PatternMatching; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - [Export(typeof(IAsyncCompletionServiceProvider))] - [Name(KnownCompletionNames.DefaultCompletionService)] - [ContentType("text")] - internal class DefaultCompletionServiceProvider : IAsyncCompletionServiceProvider - { - [Import] - public IPatternMatcherFactory PatternMatcherFactory { get; set; } - - DefaultCompletionService _instance; - - IAsyncCompletionService IAsyncCompletionServiceProvider.GetOrCreate(ITextView textView) - { - if (_instance == null) - _instance = new DefaultCompletionService(PatternMatcherFactory); - return _instance; - } - } - - internal class DefaultCompletionService : IAsyncCompletionService - { - readonly IPatternMatcherFactory _patternMatcherFactory; - - internal DefaultCompletionService(IPatternMatcherFactory patternMatcherFactory) - { - _patternMatcherFactory = patternMatcherFactory; - } - - Task<FilteredCompletionModel> IAsyncCompletionService.UpdateCompletionListAsync( - ImmutableArray<CompletionItem> sortedList, CompletionTriggerReason triggerReason, CompletionFilterReason filterReason, - ITextSnapshot snapshot, ITrackingSpan applicableSpan, ImmutableArray<CompletionFilterWithState> filters, ITextView view, CancellationToken token) - { - // Filter by text - var filterText = applicableSpan.GetText(snapshot); - if (string.IsNullOrWhiteSpace(filterText)) - { - // There is no text filtering. Just apply user filters, sort alphabetically and return. - IEnumerable<CompletionItem> listFiltered = sortedList; - if (filters.Any(n => n.IsSelected)) - { - listFiltered = sortedList.Where(n => ShouldBeInCompletionList(n, filters)); - } - var listSorted = listFiltered.OrderBy(n => n.SortText); - var listHighlighted = listSorted.Select(n => new CompletionItemWithHighlight(n)).ToImmutableArray(); - return Task.FromResult(new FilteredCompletionModel(listHighlighted, 0, filters)); - } - - // Pattern matcher not only filters, but also provides a way to order the results by their match quality. - // The relevant CompletionItem is match.Item1, its PatternMatch is match.Item2 - var patternMatcher = _patternMatcherFactory.CreatePatternMatcher( - filterText, - new PatternMatcherCreationOptions(System.Globalization.CultureInfo.CurrentCulture, PatternMatcherCreationFlags.IncludeMatchedSpans)); - - var matches = sortedList - // Perform pattern matching - .Select(completionItem => (completionItem, patternMatcher.TryMatch(completionItem.FilterText))) - // Pick only items that were matched, unless length of filter text is 1 - .Where(n => (filterText.Length == 1 || n.Item2.HasValue)); - - // See which filters might be enabled based on the typed code - var textFilteredFilters = matches.SelectMany(n => n.Item1.Filters).Distinct(); - - // When no items are available for a given filter, it becomes unavailable - var updatedFilters = ImmutableArray.CreateRange(filters.Select(n => n.WithAvailability(textFilteredFilters.Contains(n.Filter)))); - - // Filter by user-selected filters. The value on availableFiltersWithSelectionState conveys whether the filter is selected. - var filterFilteredList = matches; - if (filters.Any(n => n.IsSelected)) - { - filterFilteredList = matches.Where(n => ShouldBeInCompletionList(n.Item1, filters)); - } - - var bestMatch = filterFilteredList.OrderByDescending(n => n.Item2.HasValue).ThenBy(n => n.Item2).FirstOrDefault(); - var listWithHighlights = filterFilteredList.Select(n => n.Item2.HasValue ? new CompletionItemWithHighlight(n.Item1, n.Item2.Value.MatchedSpans) : new CompletionItemWithHighlight(n.Item1)).ToImmutableArray(); - - int selectedItemIndex = 0; - for (int i = 0; i < listWithHighlights.Length; i++) - { - if (listWithHighlights[i].CompletionItem == bestMatch.Item1) - { - selectedItemIndex = i; - break; - } - } - - return Task.FromResult(new FilteredCompletionModel(listWithHighlights, selectedItemIndex, updatedFilters)); - } - - Task<ImmutableArray<CompletionItem>> IAsyncCompletionService.SortCompletionListAsync( - ImmutableArray<CompletionItem> initialList, CompletionTriggerReason triggerReason, ITextSnapshot snapshot, - ITrackingSpan applicableToSpan, ITextView view, CancellationToken token) - { - return Task.FromResult(initialList.OrderBy(n => n.SortText).ToImmutableArray()); - } - - #region Filtering - - private static bool ShouldBeInCompletionList( - CompletionItem item, - ImmutableArray<CompletionFilterWithState> filtersWithState) - { - foreach (var filterWithState in filtersWithState.Where(n => n.IsSelected)) - { - if (item.Filters.Any(n => n == filterWithState.Filter)) - { - return true; - } - } - return false; - } - - #endregion - } - -#if DEBUG && false - [Export(typeof(IAsyncCompletionItemSourceProvider))] - [Name("Debug completion item source")] - [Order(After = "default")] - [ContentType("any")] - public class DebugCompletionItemSourceProvider : IAsyncCompletionItemSourceProvider - { - DebugCompletionItemSource _instance; - - IAsyncCompletionItemSource IAsyncCompletionItemSourceProvider.GetOrCreate(ITextView textView) - { - if (_instance == null) - _instance = new DebugCompletionItemSource(); - return _instance; - } - } - - public class DebugCompletionItemSource : IAsyncCompletionItemSource - { - private static readonly AccessibleImageId Icon1 = new AccessibleImageId(new Guid("{ae27a6b0-e345-4288-96df-5eaf394ee369}"), 666, "Icon description"); - private static readonly CompletionFilter Filter1 = new CompletionFilter("Diagnostic", "d", Icon1); - private static readonly AccessibleImageId Icon2 = new AccessibleImageId(new Guid("{ae27a6b0-e345-4288-96df-5eaf394ee369}"), 2852, "Icon description"); - private static readonly CompletionFilter Filter2 = new CompletionFilter("Snippets", "s", Icon2); - private static readonly AccessibleImageId Icon3 = new AccessibleImageId(new Guid("{ae27a6b0-e345-4288-96df-5eaf394ee369}"), 473, "Icon description"); - private static readonly CompletionFilter Filter3 = new CompletionFilter("Class", "c", Icon3); - private static readonly ImmutableArray<CompletionFilter> FilterCollection1 = ImmutableArray.Create(Filter1); - private static readonly ImmutableArray<CompletionFilter> FilterCollection2 = ImmutableArray.Create(Filter2); - private static readonly ImmutableArray<CompletionFilter> FilterCollection3 = ImmutableArray.Create(Filter3); - private static readonly ImmutableArray<char> commitCharacters = ImmutableArray.Create(' ', ';', '.', '<', '(', '['); - - CustomCommitBehavior IAsyncCompletionItemSource.CustomCommit(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar, CancellationToken token) - { - return CustomCommitBehavior.None; - } - - CommitBehavior IAsyncCompletionItemSource.GetDefaultCommitBehavior(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar, CancellationToken token) - { - return CommitBehavior.None; - } - - async Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan triggerLocation, CancellationToken token) - { - return await Task.FromResult(new CompletionContext( - ImmutableArray.Create( - new CompletionItem("SampleItem<>", this, Icon3, FilterCollection3, string.Empty, false, "SampleItem", "SampleItem<>", "SampleItem", ImmutableArray<AccessibleImageId>.Empty), - new CompletionItem("AnotherItem🐱👤", this, Icon3, FilterCollection3, string.Empty, false, "AnotherItem", "AnotherItem", "AnotherItem", ImmutableArray.Create(Icon3)), - new CompletionItem("Sampling", this, Icon1, FilterCollection1), - new CompletionItem("Sampler", this, Icon1, FilterCollection1), - new CompletionItem("Sapling", this, Icon2, FilterCollection2, "Sapling is a young tree"), - new CompletionItem("OverSampling", this, Icon1, FilterCollection1, "overload"), - new CompletionItem("AnotherSample", this, Icon2, FilterCollection2), - new CompletionItem("AnotherSampling", this, Icon2, FilterCollection2), - new CompletionItem("Simple", this, Icon3, FilterCollection3, "KISS"), - new CompletionItem("Simpler", this, Icon3, FilterCollection3, "KISS") - )));//, true, true, "Suggestion mode description!")); - } - - async Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item, CancellationToken token) - { - return await Task.FromResult("This is a tooltip for " + item.DisplayText); - } - - ImmutableArray<char> IAsyncCompletionItemSource.GetPotentialCommitCharacters() => commitCharacters; - - bool IAsyncCompletionItemSource.ShouldCommitCompletion(char typeChar, SnapshotPoint location) - { - return true; - } - - SnapshotSpan? IAsyncCompletionItemSource.ShouldTriggerCompletion(char typeChar, SnapshotPoint triggerLocation) - { - var charBeforeCaret = triggerLocation.Subtract(1).GetChar(); - if (commitCharacters.Contains(charBeforeCaret) || triggerLocation.Position == 0) - { - // skip the typed character. the applicable span starts at the caret - return new SnapshotSpan(triggerLocation, 0); - } - else - { - // include the typed character. - return new SnapshotSpan(triggerLocation - 1, 1); - } - } - } - -#endif -#if DEBUG && true - - [Export(typeof(IAsyncCompletionItemSourceProvider))] - [Name("Debug HTML completion item source")] - [Order(After = "default")] - [ContentType("RazorCSharp")] - public class DebugHtmlCompletionItemSourceProvider : IAsyncCompletionItemSourceProvider - { - DebugHtmlCompletionItemSource _instance; - - IAsyncCompletionItemSource IAsyncCompletionItemSourceProvider.GetOrCreate(ITextView textView) - { - if (_instance == null) - _instance = new DebugHtmlCompletionItemSource(); - return _instance; - } - } - - public class DebugHtmlCompletionItemSource : IAsyncCompletionItemSource - { - private static readonly ImmutableArray<char> commitCharacters = ImmutableArray.Create(' ', '>', '='); - - CommitBehavior IAsyncCompletionItemSource.CustomCommit(Text.Editor.ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar, CancellationToken token) - { - return CommitBehavior.None; - } - - CommitBehavior IAsyncCompletionItemSource.GetDefaultCommitBehavior(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar, CancellationToken token) - { - return CommitBehavior.None; - } - - async Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableSpan, CancellationToken token) - { - return await Task.FromResult(new CompletionContext(ImmutableArray.Create(new CompletionItem("html", this), new CompletionItem("head", this), new CompletionItem("body", this), new CompletionItem("header", this)))); - } - - async Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item, CancellationToken token) - { - return await Task.FromResult(item.DisplayText); - } - - ImmutableArray<char> IAsyncCompletionItemSource.GetPotentialCommitCharacters() - { - return commitCharacters; - } - - bool IAsyncCompletionItemSource.ShouldCommitCompletion(char typeChar, SnapshotPoint location) - { - return true; - } - - SnapshotSpan? IAsyncCompletionItemSource.ShouldTriggerCompletion(char typeChar, SnapshotPoint triggerLocation) - { - var charBeforeCaret = triggerLocation.Subtract(1).GetChar(); - if (commitCharacters.Contains(charBeforeCaret) || triggerLocation.Position == 0) - { - // skip the typed character. the applicable span starts at the caret - return new SnapshotSpan(triggerLocation, 0); - } - else - { - // include the typed character. - return new SnapshotSpan(triggerLocation - 1, 1); - } - } - } -#endif -} diff --git a/src/Language/Impl/Language/Completion/ICompletionComputationCallbackHandler.cs b/src/Language/Impl/Language/Completion/ICompletionComputationCallbackHandler.cs deleted file mode 100644 index a19f138..0000000 --- a/src/Language/Impl/Language/Completion/ICompletionComputationCallbackHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - internal interface ICompletionComputationCallbackHandler<TModel> - { - Task UpdateUi(TModel model); - void Dismiss(); - } -} diff --git a/src/Language/Impl/Language/Completion/ModernCompletionFeature.cs b/src/Language/Impl/Language/Completion/ModernCompletionFeature.cs deleted file mode 100644 index a5ed8cf..0000000 --- a/src/Language/Impl/Language/Completion/ModernCompletionFeature.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Diagnostics; -using Microsoft.VisualStudio.Text.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Provides information whether modern completion should be enabled, given the buffer's content type. - /// </summary> - internal static class ModernCompletionFeature - { - private const string TreatmentFlightName = "CompletionAPI"; - private static bool _treatmentFlightEnabled; - private static bool _initialized; - - /// <summary> - /// Returns whether or not modern completion should be enabled. - /// </summary> - /// <returns>true if experiment is enabled.</returns> - public static bool GetFeatureState(IExperimentationServiceInternal experimentationService) - { - if (_initialized) - { - return _treatmentFlightEnabled; - } - -#if DEBUG - _treatmentFlightEnabled = true; -#else - _treatmentFlightEnabled = experimentationService.IsCachedFlightEnabled(TreatmentFlightName); -#endif - _initialized = true; - return _treatmentFlightEnabled; - } - } -} diff --git a/src/Language/Impl/Language/Completion/SuggestionModeCompletionItemSource.cs b/src/Language/Impl/Language/Completion/SuggestionModeCompletionItemSource.cs deleted file mode 100644 index c86589c..0000000 --- a/src/Language/Impl/Language/Completion/SuggestionModeCompletionItemSource.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Internal item source used during lifetime of the suggestion mode item. - /// </summary> - internal class SuggestionModeCompletionItemSource : IAsyncCompletionItemSource - { - static IAsyncCompletionItemSource _instance; - internal static IAsyncCompletionItemSource Instance - { - get - { - if (_instance == null) - _instance = new SuggestionModeCompletionItemSource(); - return _instance; - } - } - - CommitBehavior IAsyncCompletionItemSource.CustomCommit(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar, CancellationToken token) - { - return CommitBehavior.None; - } - - CommitBehavior IAsyncCompletionItemSource.GetDefaultCommitBehavior(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar, CancellationToken token) - { - return CommitBehavior.None; - } - - Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableSpan, CancellationToken token) - { - throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide tooltip."); - } - - Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item, CancellationToken token) - { - return Task.FromResult<object>(string.Empty); - } - - ImmutableArray<char> IAsyncCompletionItemSource.GetPotentialCommitCharacters() - { - throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide tooltip."); - } - - bool IAsyncCompletionItemSource.ShouldCommitCompletion(char typeChar, SnapshotPoint location) - { - return false; // Typing should not commit the suggestion mode item. - } - - SnapshotSpan? IAsyncCompletionItemSource.ShouldTriggerCompletion(char typeChar, SnapshotPoint location) - { - return null; - } - } -} diff --git a/src/Language/Impl/Language/Strings.Designer.cs b/src/Language/Impl/Language/Strings.Designer.cs index e4e63cc..d1a0428 100644 --- a/src/Language/Impl/Language/Strings.Designer.cs +++ b/src/Language/Impl/Language/Strings.Designer.cs @@ -68,5 +68,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { return ResourceManager.GetString("CompletionCommandHandlerName", resourceCulture); } } + + /// <summary> + /// Looks up a localized string similar to Suggestion mode. Allows typing delimeters without auto completing.. + /// </summary> + public static string SuggestionModeDefaultTooltip { + get { + return ResourceManager.GetString("SuggestionModeDefaultTooltip", resourceCulture); + } + } } } diff --git a/src/Language/Impl/Language/Strings.resx b/src/Language/Impl/Language/Strings.resx index 3870d03..c391c70 100644 --- a/src/Language/Impl/Language/Strings.resx +++ b/src/Language/Impl/Language/Strings.resx @@ -120,4 +120,8 @@ <data name="CompletionCommandHandlerName" xml:space="preserve"> <value>Completion command handler</value> </data> + <data name="SuggestionModeDefaultTooltip" xml:space="preserve"> + <value>Suggestion mode. Allows typing delimeters without auto completing.</value> + <comment>Tooltip on suggestion mode completion item (visible when suggestion mode is enabled, through Ctrl+Alt+Space)</comment> + </data> </root>
\ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Text.Implementation.csproj b/src/Microsoft.VisualStudio.Text.Implementation.csproj index 2757a7c..0644a58 100644 --- a/src/Microsoft.VisualStudio.Text.Implementation.csproj +++ b/src/Microsoft.VisualStudio.Text.Implementation.csproj @@ -5,10 +5,11 @@ <SignAssembly>true</SignAssembly> <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile> <DelaySign>false</DelaySign> - <Version>15.0.25-pre</Version> + <Version>15.0.30-pre</Version> <AssemblyVersion>15.0.0.0</AssemblyVersion> - <NuGetVersionEditor>15.7.153-preview-g7d0635149a</NuGetVersionEditor> - <NuGetVersionLanguage>15.7.153-preview-g7d0635149a</NuGetVersionLanguage> + <NuGetVersionEditor>15.8.519</NuGetVersionEditor> + <NuGetVersionLanguage>15.8.519</NuGetVersionLanguage> + <LangVersion>latest</LangVersion> </PropertyGroup> <PropertyGroup> diff --git a/src/Text/Def/Internal/TextData/ExtensionMethods.cs b/src/Text/Def/Internal/TextData/ExtensionMethods.cs index dd50540..64fab7d 100644 --- a/src/Text/Def/Internal/TextData/ExtensionMethods.cs +++ b/src/Text/Def/Internal/TextData/ExtensionMethods.cs @@ -25,7 +25,7 @@ namespace Microsoft.VisualStudio.Text { if (startIndex < 0) { - throw new ArgumentOutOfRangeException("start"); + throw new ArgumentOutOfRangeException(nameof(startIndex)); } // Take advantage of performant [] operators on the ITextSnapshot diff --git a/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs b/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs deleted file mode 100644 index afe6de3..0000000 --- a/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -// This file contain internal APIs that are subject to change without notice. -// Use at your own risk. -// -namespace Microsoft.VisualStudio.Text.Structure -{ - using System; - using System.Collections.Generic; - using Microsoft.VisualStudio.Text.Editor; - using Microsoft.VisualStudio.Text.UI.Adornments; - - /// <summary> - /// Defines the interface for the <see cref="IStructureSpanningTreeManager"/> which - /// provides information about the structural hierarchy of code in an <see cref="ITextView"/>. - /// </summary> - /// <remarks> - /// You can obtain an instance of this class via the <see cref="IStructureSpanningTreeService"/>. - /// </remarks> - public interface IStructureSpanningTreeManager - { - /// <summary> - /// Event that indicates that <see cref="SpanningTreeSnapshot"/> has been updated, - /// and that any method calls on this service will now return more up to date results. - /// </summary> - event EventHandler SpanningTreeChanged; - - /// <summary> - /// Gets an immutable instance of the most up to date current code structure. - /// </summary> - IStructureElement SpanningTreeSnapshot { get; } - - /// <summary> - /// Gets an enumerable of <see cref="IStructureElement"/>s that encapsulate the given <see cref="SnapshotPoint"/>. - /// </summary> - /// <remarks> - /// This method is intended as a projection-aware means to obtain language-service provided - /// structural context for a location such as the caret position, or a structure guide line. - /// </remarks> - /// <param name="point">A <see cref="SnapshotPoint"/> indicating the position of interest.</param> - /// <returns> - /// The elements within which <paramref name="point"/> is nested, in order, from outermost to innermost. - /// </returns> - IEnumerable<IStructureElement> GetElementsEncapsulatingPoint(SnapshotPoint point); - - /// <summary> - /// Gets an enumerable of <see cref="IStructureElement"/>s that intersect with the given - /// <see cref="SnapshotSpan"/>. - /// </summary> - /// <remarks> - /// This method is intended as a projection-aware means to obtain language-service provided - /// structural context for a span. - /// </remarks> - /// <param name="spans">The spans to collect elements from.</param> - /// <returns>The elements that intersect with the given span.</returns> - IEnumerable<IStructureElement> GetElementsIntersectingSpans(NormalizedSnapshotSpanCollection spans); - } -} diff --git a/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs b/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs deleted file mode 100644 index 1ad9c28..0000000 --- a/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -// This file contain internal APIs that are subject to change without notice. -// Use at your own risk. -// -namespace Microsoft.VisualStudio.Text.Structure -{ - using System; - using Microsoft.VisualStudio.Text.Editor; - - /// <summary> - /// Defines the interface for the <see cref="IStructureSpanningTreeService"/> which can be - /// used to obtain instances of the <see cref="IStructureSpanningTreeManager"/>, which - /// provides information about the structural hierarchy of code in an <see cref="ITextView"/>. - /// </summary> - /// <remarks> - /// This interface is a MEF component part and can be imported with a MEF import attribute. - /// <code> - /// [Import] - /// internal IStructureSpanningTreeService StructureSpanningTreeService { get; } - /// </code> - /// </remarks> - public interface IStructureSpanningTreeService - { - /// <summary> - /// Gets the singleton <see cref="IStructureSpanningTreeManager"/> for the specified view. - /// </summary> - /// <param name="textView">The view to get the structure manager for.</param> - /// <exception cref="InvalidOperationException">Throw if not called from the UI thread.</exception> - /// <returns>The singleton instance of <see cref="IStructureSpanningTreeManager"/> for the view.</returns> - IStructureSpanningTreeManager GetManager(ITextView textView); - } -} diff --git a/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs b/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs index 64c0c0d..3521c28 100644 --- a/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs +++ b/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs @@ -6,6 +6,7 @@ // Use at your own risk. // using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -16,16 +17,20 @@ namespace Microsoft.VisualStudio.Utilities /// </summary> public class JoinableTaskHelper { +#pragma warning disable CA1051 // Do not declare visible instance fields + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "By design")] public readonly JoinableTaskContext Context; + + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "By design")] public readonly JoinableTaskCollection Collection; + + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "By design")] public readonly JoinableTaskFactory Factory; +#pragma warning restore CA1051 // Do not declare visible instance fields public JoinableTaskHelper(JoinableTaskContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); - - this.Context = context; + this.Context = context ?? throw new ArgumentNullException(nameof(context)); this.Collection = context.CreateCollection(); this.Factory = context.CreateFactory(this.Collection); } @@ -64,15 +69,17 @@ namespace Microsoft.VisualStudio.Utilities } } - public async Task DisposeAsync() + public Task DisposeAsync() { - await this.Collection.JoinTillEmptyAsync(); + return this.Collection.JoinTillEmptyAsync(); } public void Dispose() { - this.Context.Factory.Run(async delegate { // Not this.Factory - await this.DisposeAsync(); + this.Context.Factory.Run(async delegate + { + // Not this.Factory + await this.DisposeAsync().ConfigureAwait(false); }); } } diff --git a/src/Text/Def/Internal/TextData/LazyObservableCollection.cs b/src/Text/Def/Internal/TextData/LazyObservableCollection.cs index 2c4849f..1c2992b 100644 --- a/src/Text/Def/Internal/TextData/LazyObservableCollection.cs +++ b/src/Text/Def/Internal/TextData/LazyObservableCollection.cs @@ -157,7 +157,7 @@ namespace Microsoft.VisualStudio.Text.Utilities TWrapper wrapperObj = value as TWrapper; if ((value != null) && (wrapperObj == null)) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), "value"); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), nameof(value)); } return this.Contains(wrapperObj); @@ -173,7 +173,7 @@ namespace Microsoft.VisualStudio.Text.Utilities TWrapper wrapperObj = value as TWrapper; if ((value != null) && (wrapperObj == null)) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), "value"); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), nameof(value)); } return this.IndexOf(wrapperObj); @@ -234,7 +234,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if ((array.Length - index) < this.Count) { - throw new ArgumentException("Array not big enough", "array"); + throw new ArgumentException("Array not big enough", nameof(array)); } int i = index; @@ -395,10 +395,12 @@ namespace Microsoft.VisualStudio.Text.Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #region IDisposable Members +#pragma warning disable CA1063 // Implement IDisposable Correctly /// <summary> /// Disposes and releases all wrappers created. Also releases all references to the underlying object /// </summary> public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { if (_disposed) return; _disposed = true; @@ -560,7 +562,7 @@ namespace Microsoft.VisualStudio.Text.Utilities } // Also notify consumers of the change to the 'Count' property. - this.RaisePropertyChanged("Count"); + this.RaisePropertyChanged(nameof(Count)); } private void RaisePropertyChanged(string propertyName) diff --git a/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs b/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs index 4cb900c..26309eb 100644 --- a/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs +++ b/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs @@ -11,7 +11,7 @@ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; namespace Microsoft.VisualStudio.Text { - public class TextBufferOperationHelpers + public static class TextBufferOperationHelpers { /// <summary> /// Checks if the given <see cref="ITextSnapshotLine"/> has any non-whitespace characters diff --git a/src/Text/Def/Internal/TextData/TrackingSpanTree.cs b/src/Text/Def/Internal/TextData/TrackingSpanTree.cs index 8a7b195..acf8082 100644 --- a/src/Text/Def/Internal/TextData/TrackingSpanTree.cs +++ b/src/Text/Def/Internal/TextData/TrackingSpanTree.cs @@ -50,7 +50,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public TrackingSpanTree(ITextBuffer buffer, bool keepTrackingCurrent) { if (buffer == null) - throw new ArgumentNullException("buffer"); + throw new ArgumentNullException(nameof(buffer)); Buffer = buffer; Count = 0; @@ -77,10 +77,10 @@ namespace Microsoft.VisualStudio.Text.Utilities public TrackingSpanNode<T> TryAddItem(T item, ITrackingSpan trackingSpan) { if (trackingSpan == null) - throw new ArgumentNullException("trackingSpan"); + throw new ArgumentNullException(nameof(trackingSpan)); if (trackingSpan.TrackingMode != SpanTrackingMode.EdgeExclusive) - throw new ArgumentException("The tracking mode of the given tracking span must be SpanTrackingMode.EdgeExclusive", "trackingSpan"); + throw new ArgumentException("The tracking mode of the given tracking span must be SpanTrackingMode.EdgeExclusive", nameof(trackingSpan)); SnapshotSpan spanToAdd = trackingSpan.GetSpan(Buffer.CurrentSnapshot); TrackingSpanNode<T> node = new TrackingSpanNode<T>(item, trackingSpan); @@ -101,7 +101,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public bool RemoveItem(T item, ITrackingSpan trackingSpan) { if (trackingSpan == null) - throw new ArgumentNullException("trackingSpan"); + throw new ArgumentNullException(nameof(trackingSpan)); SnapshotSpan spanToRemove = trackingSpan.GetSpan(Buffer.CurrentSnapshot); @@ -228,7 +228,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (toVersion == null) { - throw new ArgumentNullException("toVersion"); + throw new ArgumentNullException(nameof(toVersion)); } if (toVersion.VersionNumber > this.advanceVersion) diff --git a/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs b/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs index 6ed9e0a..fc355c2 100644 --- a/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs +++ b/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs @@ -17,7 +17,9 @@ namespace Microsoft.VisualStudio.Text.Utilities /// </summary> public class LineBuffer { +#pragma warning disable CA2211 // Non-constant fields should not be visible public static int BufferSize = 1024; // this is non-constant so unit tests can change it to better test LineBuffer logic +#pragma warning restore CA2211 // Non-constant fields should not be visible private readonly ITextSnapshotLine line; @@ -63,7 +65,7 @@ namespace Microsoft.VisualStudio.Text.Utilities } } - public class UnicodeWordExtent + public static class UnicodeWordExtent { static public bool FindCurrentToken(SnapshotPoint currentPosition, out SnapshotSpan span) { diff --git a/src/Text/Def/Internal/TextLogic/IBypassUndoEditTag.cs b/src/Text/Def/Internal/TextLogic/IBypassUndoEditTag.cs new file mode 100644 index 0000000..33d07c8 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IBypassUndoEditTag.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Edit tag indicating that the edit should be ignored by the undo system. + /// </summary> + /// <remarks> + /// <para> + /// Yes this is as dangerous as it sounds. Using it will corrupt the undo stack so + /// do not use unless you are prepared to do the appropriate clean-up. + /// </para> + /// </remarks> + public interface IBypassUndoEditTag : IUndoEditTag + { + } +} diff --git a/src/Text/Def/Internal/TextLogic/IEditOnlyTextUndoPrimitive.cs b/src/Text/Def/Internal/TextLogic/IEditOnlyTextUndoPrimitive.cs new file mode 100644 index 0000000..74dec93 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IEditOnlyTextUndoPrimitive.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations +{ + /// <summary> + /// Represents undo primitive that consists only of text changes. + /// </summary> + public interface IEditOnlyTextUndoPrimitive : ITextUndoPrimitive + { + INormalizedTextChangeCollection Changes { get; } + + int? BeforeReiteratedVersionNumber { get; } + int? AfterReiteratedVersionNumber { get; } + } +} diff --git a/src/Text/Def/Internal/TextLogic/IElisionTag.cs b/src/Text/Def/Internal/TextLogic/IElisionTag.cs index eaa4502..154e5da 100644 --- a/src/Text/Def/Internal/TextLogic/IElisionTag.cs +++ b/src/Text/Def/Internal/TextLogic/IElisionTag.cs @@ -7,13 +7,15 @@ // namespace Microsoft.VisualStudio.Text.Tagging { + using Microsoft.VisualStudio.Text.Editor; + /// <summary> /// Tag indicating spans of text to be excluded from a view. /// </summary> /// <remarks> /// <para> /// IViewTaggerProviders are querried by the editor implementation with this tag type for views having - /// the <see cref="F:Microsoft.VisualStudio.Text.Editor.PredefinedTextViewRoles.Structured"/> view role. + /// the <see cref="PredefinedTextViewRoles.Structured"/> view role. /// </para> /// <para> /// These tags cause text to be hidden but do not result in any outlining UI. diff --git a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs index e78084b..dad1d94 100644 --- a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs +++ b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs @@ -12,7 +12,7 @@ using System; namespace Microsoft.VisualStudio.Text.Utilities { /// <summary> - /// Allows code in src/Platform to log events. + /// Allows code in VS-Platform to log events. /// </summary> /// <remarks> /// For example, the VS Provider of this inserts data points into the telemetry data stream. diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs b/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs index 57676ee..c560c27 100644 --- a/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs +++ b/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs @@ -13,6 +13,7 @@ namespace Microsoft.VisualStudio.Text.Operations /// </summary> public interface ITextSearchNavigator2 : ITextSearchNavigator { +#pragma warning disable CA2227 // Collection properties should be read only /// <summary> /// Indicates the ranges that should be searched (if any). /// </summary> @@ -20,5 +21,6 @@ namespace Microsoft.VisualStudio.Text.Operations /// If this value to a non-null value will effectively override the ITextSearchNavigator.SearchSpan property. /// </remarks> NormalizedSnapshotSpanCollection SearchSpans { get; set; } +#pragma warning restore CA2227 // Collection properties should be read only } } diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs b/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs index f351d11..4051ae7 100644 --- a/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs +++ b/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs @@ -60,6 +60,7 @@ namespace Microsoft.VisualStudio.Text.Operations /// </remarks> public interface ITextSearchTagger<T> : ITagger<T> where T : ITag { +#pragma warning disable CA2227 // Collection properties should be read only /// <summary> /// Limits the scope of the tagger to the provided <see cref="NormalizedSnapshotSpanCollection"/>. /// </summary> @@ -67,6 +68,7 @@ namespace Microsoft.VisualStudio.Text.Operations /// If the value is set to <c>null</c> the entire range of the buffer will be searched. /// </remarks> NormalizedSnapshotSpanCollection SearchSpans { get; set; } +#pragma warning restore CA2227 // Collection properties should be read only /// <summary> /// Starts tagging occurences of the <paramref name="searchTerm"/>. diff --git a/src/Text/Def/Internal/TextLogic/ITextUndoHistory2.cs b/src/Text/Def/Internal/TextLogic/ITextUndoHistory2.cs new file mode 100644 index 0000000..0d2d3e3 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ITextUndoHistory2.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Operations +{ + /// <summary> + /// Contains undo transactions. + /// </summary> + /// <remarks> + /// Typically only one undo transaction history at a time is availbble to the user. + /// </remarks> + public interface ITextUndoHistory2 : ITextUndoHistory + { + /// <summary> + /// Creates a new transaction, invisible, nests it in the previously current transaction, and marks it current. + /// </summary> + /// <param name="description">The description of the transaction.</param> + /// <returns>The new transaction.</returns> + /// <remarks> + /// <para>Invisible transactions are like normal undo transactions except that they are effectively invisible to the end user. They won't be displayed + /// in the undo stack and if the user does an "undo" then all the invisible transactions leading up to the 1st non-invisible transaction are "skipped".</para> + /// <para>Invisible transactions can only contain simple text edits (other types of undo actions will be lost and potentially corrupt the undo stack).</para> + /// </remarks> + ITextUndoTransaction CreateInvisibleTransaction(string description); + } +} diff --git a/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs b/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs index 77476d3..80d4351 100644 --- a/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs +++ b/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs @@ -14,7 +14,9 @@ namespace Microsoft.VisualStudio.Text.Tagging /// Tag Aggregator options. /// </summary> [Flags] +#pragma warning disable CA1714 // Flags enums should have plural names public enum TagAggregatorOptions2 +#pragma warning restore CA1714 // Flags enums should have plural names { /// <summary> /// Default behavior. The tag aggregator will map up and down through all projection buffers. @@ -56,4 +58,4 @@ namespace Microsoft.VisualStudio.Text.Tagging /// </remarks> NoProjection = 0x04 } -}
\ No newline at end of file +} diff --git a/src/Text/Def/Internal/TextLogic/TelemetryComplexProperty.cs b/src/Text/Def/Internal/TextLogic/TelemetryComplexProperty.cs new file mode 100644 index 0000000..fe35096 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/TelemetryComplexProperty.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Allows code in VS-Platform to use complex telemetry properties, which reduce boilerplate code. + /// </summary> + public class TelemetryComplexProperty + { + public object Property { get; } + + public TelemetryComplexProperty(object property) + { + Property = property; + } + } +} diff --git a/src/Text/Def/Internal/TextUI/DisplayTextRange.cs b/src/Text/Def/Internal/TextUI/DisplayTextRange.cs index 9becda5..87323fb 100644 --- a/src/Text/Def/Internal/TextUI/DisplayTextRange.cs +++ b/src/Text/Def/Internal/TextUI/DisplayTextRange.cs @@ -11,10 +11,12 @@ using Microsoft.VisualStudio.Text.Formatting; namespace Microsoft.VisualStudio.Text.Editor { +#pragma warning disable CA1710 // Identifiers should have correct suffix /// <summary> /// Represents a range in the <see cref="TextBuffer"/> that behaves relative to the view in which it lives. /// </summary> public abstract class DisplayTextRange : TextRange, IEnumerable<DisplayTextPoint> +#pragma warning restore CA1710 // Identifiers should have correct suffix { /// <summary> /// When implemented in a derived class, gets the <see cref="TextView"/> of this range. diff --git a/src/Text/Def/Internal/TextUI/IViewPrimitives.cs b/src/Text/Def/Internal/TextUI/IViewPrimitives.cs index 4130715..bf47488 100644 --- a/src/Text/Def/Internal/TextUI/IViewPrimitives.cs +++ b/src/Text/Def/Internal/TextUI/IViewPrimitives.cs @@ -21,7 +21,7 @@ namespace Microsoft.VisualStudio.Text.Editor /// <summary> /// Gets the <see cref="Selection"/> primitive used for selection manipulation. /// </summary> - Selection Selection { get; } + LegacySelection Selection { get; } /// <summary> /// Gets the <see cref="Caret"/> primitive used for caret movement. diff --git a/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs b/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs index 2408671..bed9ced 100644 --- a/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs +++ b/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs @@ -52,16 +52,16 @@ namespace Microsoft.VisualStudio.Text.Editor DisplayTextRange CreateDisplayTextRange(TextView textView, TextRange textRange); /// <summary> - /// Creates a <see cref="Selection"/> primitive. + /// Creates a <see cref="LegacySelection"/> primitive. /// </summary> /// <param name="textView">The <see cref="ITextView"/> on which to base this primitive.</param> - /// <returns>The <see cref="Selection"/> primitive for the given <see cref="ITextView"/>.</returns> + /// <returns>The <see cref="LegacySelection"/> primitive for the given <see cref="ITextView"/>.</returns> /// <remarks> /// <para> /// This method always returns the same object if the same <see cref="ITextView"/> is passed in. /// </para> /// </remarks> - Selection CreateSelection(TextView textView); + LegacySelection CreateSelection(TextView textView); /// <summary> /// Creates a <see cref="Caret"/> primitive. diff --git a/src/Text/Def/Internal/TextUI/Selection.cs b/src/Text/Def/Internal/TextUI/LegacySelection.cs index 59d27eb..ee3b9cc 100644 --- a/src/Text/Def/Internal/TextUI/Selection.cs +++ b/src/Text/Def/Internal/TextUI/LegacySelection.cs @@ -9,10 +9,12 @@ namespace Microsoft.VisualStudio.Text.Editor { using System; +#pragma warning disable CA1710 // Identifiers should have correct suffix /// <summary> /// Represents the selection on the screen. /// </summary> - public abstract class Selection : DisplayTextRange + public abstract class LegacySelection : DisplayTextRange +#pragma warning restore CA1710 // Identifiers should have correct suffix { /// <summary> /// When implemented in a derived class, selects the given text range. diff --git a/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs b/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs deleted file mode 100644 index 3cff2ac..0000000 --- a/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -// This file contain internal APIs that are subject to change without notice. -// Use at your own risk. -// -namespace Microsoft.VisualStudio.Text.OverviewMargin -{ - public static class OverviewFormatDefinitions - { - public const string ElisionColorName = "OverviewMarginCollapsedRegion"; - public const string OffScreenColorName = "OverviewMarginBackground"; - public const string VisibleColorName = "OverviewMarginVisible"; - public const string CaretColorName = "OverviewMarginCaret"; - } -} diff --git a/src/Text/Def/Internal/TextUI/TextRange.cs b/src/Text/Def/Internal/TextUI/TextRange.cs index 3b114ad..354f4d0 100644 --- a/src/Text/Def/Internal/TextUI/TextRange.cs +++ b/src/Text/Def/Internal/TextUI/TextRange.cs @@ -14,6 +14,7 @@ using Microsoft.VisualStudio.Text.Operations; namespace Microsoft.VisualStudio.Text.Editor { +#pragma warning disable CA1710 // Identifiers should have correct suffix /// <summary> /// Represents a range of text in the buffer. /// </summary> @@ -24,6 +25,7 @@ namespace Microsoft.VisualStudio.Text.Editor /// </para> /// </remarks> public abstract class TextRange : IEnumerable<TextPoint> +#pragma warning restore CA1710 // Identifiers should have correct suffix { /// <summary> /// When implemented in a derived class, gets the start point of this text range. diff --git a/src/Text/Def/Internal/TextUI/TextView.cs b/src/Text/Def/Internal/TextUI/TextView.cs index e1a3fa6..26f4446 100644 --- a/src/Text/Def/Internal/TextUI/TextView.cs +++ b/src/Text/Def/Internal/TextUI/TextView.cs @@ -149,7 +149,7 @@ namespace Microsoft.VisualStudio.Text.Editor /// <summary> /// When implemented in a derived class, gets the <see cref="Selection"/>. of this view. /// </summary> - public abstract Selection Selection + public abstract LegacySelection Selection { get; } diff --git a/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs b/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs index adbb0ca..2152d22 100644 --- a/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs +++ b/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs @@ -40,6 +40,15 @@ namespace Microsoft.VisualStudio.Text.Editor /// <summary> /// The offset with respect to the bottom of the view to the bottom of the text on the line. /// </summary> - TextBottom + TextBottom, + + /// <summary> + /// The offset is with respect to the BaseLine of the line containing bufferPosition. + /// </summary> + /// <remarks> + /// If this positioning mode is used (and only this positioning mode), then bufferPosition can be default(SnapshotPoint). + /// If a default(SnapshotPoint) is used or one is given but that line is not visible, then the view will pick an appropriate line to use. + /// </remarks> + Baseline } -}
\ No newline at end of file +} diff --git a/src/Text/Def/TextData/Differencing/ITokenizedStringList.cs b/src/Text/Def/TextData/Differencing/ITokenizedStringList.cs index 3c7dca7..d92839d 100644 --- a/src/Text/Def/TextData/Differencing/ITokenizedStringList.cs +++ b/src/Text/Def/TextData/Differencing/ITokenizedStringList.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; namespace Microsoft.VisualStudio.Text.Differencing { +#pragma warning disable CA1710 // Identifiers should have correct suffix /// <summary> /// A tokenized representation of a string into abutting and non-overlapping segments. /// </summary> @@ -17,6 +18,7 @@ namespace Microsoft.VisualStudio.Text.Differencing /// as ILists.</para> /// </remarks> public interface ITokenizedStringList : IList<string> +#pragma warning restore CA1710 // Identifiers should have correct suffix { /// <summary> /// The original string that was tokenized. @@ -40,4 +42,4 @@ namespace Microsoft.VisualStudio.Text.Differencing /// <returns>The span mapped onto the original list.</returns> Span GetSpanInOriginal(Span span); } -}
\ No newline at end of file +} diff --git a/src/Text/Def/TextData/Differencing/Match.cs b/src/Text/Def/TextData/Differencing/Match.cs index dedd673..82476e0 100644 --- a/src/Text/Def/TextData/Differencing/Match.cs +++ b/src/Text/Def/TextData/Differencing/Match.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; namespace Microsoft.VisualStudio.Text.Differencing { +#pragma warning disable CA1710 // Identifiers should have correct suffix /// <summary> /// Represents a range of matches between two sequences as a pair of spans of equal length. /// </summary> @@ -20,6 +21,7 @@ namespace Microsoft.VisualStudio.Text.Differencing /// (0, 0, 2) and (4, 4, 1) ///</remarks> public class Match : IEnumerable<Tuple<int, int>> +#pragma warning restore CA1710 // Identifiers should have correct suffix { private Span left; private Span right; diff --git a/src/Text/Def/TextData/Differencing/StringDifferenceOptions.cs b/src/Text/Def/TextData/Differencing/StringDifferenceOptions.cs index 02e9e88..d5c95c9 100644 --- a/src/Text/Def/TextData/Differencing/StringDifferenceOptions.cs +++ b/src/Text/Def/TextData/Differencing/StringDifferenceOptions.cs @@ -7,29 +7,22 @@ using System.Globalization; namespace Microsoft.VisualStudio.Text.Differencing { -// Ignore the warnings about deprecated properties + // Ignore the warnings about deprecated properties #pragma warning disable 0618 +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Options to use in computing string differences. /// </summary> public struct StringDifferenceOptions +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { - // These need to be fields and specifically in this order in order for the COM-friendly - // interfaces to be generated correctly. - private StringDifferenceTypes differenceType; - private int locality; - private bool ignoreTrimWhiteSpace; /// <summary> /// The type of string differencing to do, as a combination /// of line, word, and character differencing. /// </summary> - public StringDifferenceTypes DifferenceType - { - get { return differenceType; } - set { differenceType = value; } - } + public StringDifferenceTypes DifferenceType { get; set; } /// <summary> /// The greatest distance a differencing element (line, span, or character) can move @@ -47,20 +40,12 @@ namespace Microsoft.VisualStudio.Text.Differencing /// </para> /// </remarks> [Obsolete("This value is no longer used and will be ignored.")] - public int Locality - { - get { return locality; } - set { locality = value; } - } + public int Locality { get; set; } /// <summary> /// Gets or sets whether to ignore white space. /// </summary> - public bool IgnoreTrimWhiteSpace - { - get { return ignoreTrimWhiteSpace; } - set { ignoreTrimWhiteSpace = value; } - } + public bool IgnoreTrimWhiteSpace { get; set; } /// <summary> /// The behavior to use when splitting words, if word differencing is requested @@ -91,9 +76,9 @@ namespace Microsoft.VisualStudio.Text.Differencing /// <param name="ignoreTrimWhiteSpace">Determines whether whitespace should be ignored.</param> public StringDifferenceOptions(StringDifferenceTypes differenceType, int locality, bool ignoreTrimWhiteSpace) : this() { - this.differenceType = differenceType; - this.locality = locality; - this.ignoreTrimWhiteSpace = ignoreTrimWhiteSpace; + this.DifferenceType = differenceType; + this.Locality = locality; + this.IgnoreTrimWhiteSpace = ignoreTrimWhiteSpace; } /// <summary> @@ -102,9 +87,9 @@ namespace Microsoft.VisualStudio.Text.Differencing /// <param name="other">The <see cref="StringDifferenceOptions"/> to use in constructing a new <see cref="StringDifferenceOptions"/>.</param> public StringDifferenceOptions(StringDifferenceOptions other) : this() { - this.differenceType = other.DifferenceType; - this.locality = other.Locality; - this.ignoreTrimWhiteSpace = other.IgnoreTrimWhiteSpace; + this.DifferenceType = other.DifferenceType; + this.Locality = other.Locality; + this.IgnoreTrimWhiteSpace = other.IgnoreTrimWhiteSpace; this.WordSplitBehavior = other.WordSplitBehavior; this.DetermineLocalityCallback = other.DetermineLocalityCallback; this.ContinueProcessingPredicate = other.ContinueProcessingPredicate; @@ -119,7 +104,7 @@ namespace Microsoft.VisualStudio.Text.Differencing { return string.Format(CultureInfo.InvariantCulture, "Type: {0}, Locality: {1}, IgnoreTrimWhiteSpace: {2}, WordSplitBehavior: {3}, DetermineLocalityCallback: {4}, ContinueProcessingPredicate: {5}", - DifferenceType, Locality, IgnoreTrimWhiteSpace, WordSplitBehavior, DetermineLocalityCallback, ContinueProcessingPredicate); + this.DifferenceType, this.Locality, this.IgnoreTrimWhiteSpace, this.WordSplitBehavior, this.DetermineLocalityCallback, this.ContinueProcessingPredicate); } /// <summary> @@ -127,9 +112,9 @@ namespace Microsoft.VisualStudio.Text.Differencing /// </summary> public override int GetHashCode() { - int callbackHashCode = (DetermineLocalityCallback != null) ? DetermineLocalityCallback.GetHashCode() : 0; - int predicateHashCode = (ContinueProcessingPredicate != null)? ContinueProcessingPredicate.GetHashCode() : 0; - return (DifferenceType.GetHashCode() ^ Locality.GetHashCode() ^ IgnoreTrimWhiteSpace.GetHashCode() ^ WordSplitBehavior.GetHashCode() ^ callbackHashCode ^ predicateHashCode); + int callbackHashCode = (this.DetermineLocalityCallback != null) ? this.DetermineLocalityCallback.GetHashCode() : 0; + int predicateHashCode = (this.ContinueProcessingPredicate != null)? this.ContinueProcessingPredicate.GetHashCode() : 0; + return (this.DifferenceType.GetHashCode() ^ this.Locality.GetHashCode() ^ this.IgnoreTrimWhiteSpace.GetHashCode() ^ this.WordSplitBehavior.GetHashCode() ^ callbackHashCode ^ predicateHashCode); } /// <summary> @@ -149,7 +134,7 @@ namespace Microsoft.VisualStudio.Text.Differencing /// </summary> public static bool operator ==(StringDifferenceOptions left, StringDifferenceOptions right) { - if (object.ReferenceEquals(left, right)) + if (ReferenceEquals(left, right)) return true; if ((object)left == null || (object)right == null) diff --git a/src/Text/Def/TextData/Document/FileUtilities.cs b/src/Text/Def/TextData/Document/FileUtilities.cs deleted file mode 100644 index c176a73..0000000 --- a/src/Text/Def/TextData/Document/FileUtilities.cs +++ /dev/null @@ -1,253 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; - -namespace Microsoft.VisualStudio.Text -{ - internal class FileUtilities - { - public static void SaveSnapshot(ITextSnapshot snapshot, - FileMode fileMode, - Encoding encoding, - string filePath) - { - Debug.Assert((fileMode == FileMode.Create) || (fileMode == FileMode.CreateNew)); - - //Save the contents of the text buffer to disk. - - string temporaryFilePath = null; - try - { - FileStream originalFileStream = null; - FileStream temporaryFileStream = FileUtilities.CreateFileStream(filePath, fileMode, out temporaryFilePath, out originalFileStream); - if (originalFileStream == null) - { - //The "normal" scenario: save the snapshot directly to disk. Either: - // there are no hard links to the target file so we can write the snapshot to the temporary and use File.Replace. - // we're creating a new file (in which case, temporaryFileStream is a misnomer: it is the stream for the file we are creating). - try - { - using (StreamWriter streamWriter = new StreamWriter(temporaryFileStream, encoding)) - { - snapshot.Write(streamWriter); - } - } - finally - { - //This is somewhat redundant: disposing of streamWriter had the side-effect of disposing of temporaryFileStream - temporaryFileStream.Dispose(); - temporaryFileStream = null; - } - - if (temporaryFilePath != null) - { - //We were saving to the original file and already have a copy of the file on disk. - int remainingAttempts = 3; - do - { - try - { - //Replace the contents of filePath with the contents of the temporary using File.Replace to - //preserve the various attributes of the original file. - File.Replace(temporaryFilePath, filePath, null, true); - temporaryFilePath = null; - - return; - } - catch (FileNotFoundException) - { - // The target file doesn't exist (someone deleted it after we detected it earlier). - // This is an acceptable condition so don't throw. - File.Move(temporaryFilePath, filePath); - temporaryFilePath = null; - - return; - } - catch (IOException) - { - //There was some other exception when trying to replace the contents of the file - //(probably because some other process had the file locked). - //Wait a few ms and try again. - System.Threading.Thread.Sleep(5); - } - } - while (--remainingAttempts > 0); - - //We're giving up on replacing the file. Try overwriting it directly (this is essentially the old Dev11 behavior). - //Do not try approach we are using for hard links (copying the original & restoring it if there is a failure) since - //getting here implies something strange is going on with the file system (Git or the like locking files) so we - //want the simplest possible fallback. - - //Failing here causes the exception to be passed to the calling code. - using (FileStream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - using (StreamWriter streamWriter = new StreamWriter(stream, encoding)) - { - snapshot.Write(streamWriter); - } - } - } - } - else - { - //filePath has hard links so we need to use a different approach to save the file: - // copy the original file to the temporary - // write directly to the original - // restore the original in the event of errors (which could be encoding errors and not disk issues) if there's a problem. - try - { - // Copy the contents of the original file to the temporary. - originalFileStream.CopyTo(temporaryFileStream); - - //We've got a clean copy, try writing the snapshot directly to the original file - try - { - originalFileStream.Seek(0, SeekOrigin.Begin); - originalFileStream.SetLength(0); - - //Make sure the StreamWriter is flagged leaveOpen == true. Otherwise disposing of the StreamWriter will dispose of originalFileStream and we need to - //leave originalFileStream open so we can use it to restore the original from the temporary copy we made. - using (var streamWriter = new StreamWriter(originalFileStream, encoding, bufferSize: 1024, leaveOpen: true)) //1024 == the default buffer size for a StreamWriter. - { - snapshot.Write(streamWriter); - } - } - catch - { - //Restore the original from the temporary copy we made (but rethrow the original exception since we didn't save the file). - temporaryFileStream.Seek(0, SeekOrigin.Begin); - - originalFileStream.Seek(0, SeekOrigin.Begin); - originalFileStream.SetLength(0); - - temporaryFileStream.CopyTo(originalFileStream); - - throw; - } - } - finally - { - originalFileStream.Dispose(); - originalFileStream = null; - - temporaryFileStream.Dispose(); - temporaryFileStream = null; - } - } - } - finally - { - if (temporaryFilePath != null) - { - try - { - //We do not need the temporary any longer. - if (File.Exists(temporaryFilePath)) - { - File.Delete(temporaryFilePath); - } - } - catch - { - //Failing to clean up the temporary is an ignorable exception. - } - } - } - } - - private static FileStream CreateFileStream(string filePath, FileMode fileMode, out string temporaryPath, out FileStream originalFileStream) - { - originalFileStream = null; - - if (File.Exists(filePath)) - { - // We're writing to a file that already exists. This is an error if we're trying to do a CreateNew. - if (fileMode == FileMode.CreateNew) - { - throw new IOException(filePath + " exists"); - } - - try - { - originalFileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); - - //Even thoug SafeFileHandle is an IDisposable, we don't dispose of it since that closes the strem. - var safeHandle = originalFileStream.SafeFileHandle; - if (!(safeHandle.IsClosed || safeHandle.IsInvalid)) - { - BY_HANDLE_FILE_INFORMATION fi; - if (GetFileInformationByHandle(safeHandle, out fi)) - { - if (fi.NumberOfLinks <= 1) - { - // The file we're trying to write to doesn't have any hard links ... clear out the originalFileStream - // as a clue. - originalFileStream.Dispose(); - originalFileStream = null; - } - } - } - } - catch - { - if (originalFileStream != null) - { - originalFileStream.Dispose(); - originalFileStream = null; - } - - //We were not able to determine whether or not the file had hard links so throw here (aborting the save) - //since we don't know how to do it safely. - throw; - } - - string root = Path.GetDirectoryName(filePath); - - int count = 0; - while (++count < 20) - { - try - { - temporaryPath = Path.Combine(root, Path.GetRandomFileName() + "~"); //The ~ suffix hides the temporary file from GIT. - return new FileStream(temporaryPath, FileMode.CreateNew, (originalFileStream != null) ? FileAccess.ReadWrite : FileAccess.Write, FileShare.None); - } - catch (IOException) - { - //Ignore IOExceptions ... GetRandomFileName() came up with a duplicate so we need to try again. - } - } - - Debug.Fail("Unable to create a temporary file"); - } - - temporaryPath = null; - return new FileStream(filePath, fileMode, FileAccess.Write, FileShare.Read); - } - - [StructLayout(LayoutKind.Sequential)] - struct BY_HANDLE_FILE_INFORMATION - { - public uint FileAttributes; - public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime; - public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime; - public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime; - public uint VolumeSerialNumber; - public uint FileSizeHigh; - public uint FileSizeLow; - public uint NumberOfLinks; - public uint FileIndexHigh; - public uint FileIndexLow; - } - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool GetFileInformationByHandle( - Microsoft.Win32.SafeHandles.SafeFileHandle hFile, - out BY_HANDLE_FILE_INFORMATION lpFileInformation - ); - } -}
\ No newline at end of file diff --git a/src/Text/Def/TextData/Document/TextDocumentFileActionEventArgs.cs b/src/Text/Def/TextData/Document/TextDocumentFileActionEventArgs.cs index 9e4732b..e94e9ae 100644 --- a/src/Text/Def/TextData/Document/TextDocumentFileActionEventArgs.cs +++ b/src/Text/Def/TextData/Document/TextDocumentFileActionEventArgs.cs @@ -52,7 +52,7 @@ namespace Microsoft.VisualStudio.Text { if (filePath == null) { - throw new ArgumentNullException("filePath"); + throw new ArgumentNullException(nameof(filePath)); } _filePath = filePath; diff --git a/src/Text/Def/TextData/Model/ContentTypeChangedEventArgs.cs b/src/Text/Def/TextData/Model/ContentTypeChangedEventArgs.cs index fccc545..a18c86d 100644 --- a/src/Text/Def/TextData/Model/ContentTypeChangedEventArgs.cs +++ b/src/Text/Def/TextData/Model/ContentTypeChangedEventArgs.cs @@ -12,10 +12,8 @@ namespace Microsoft.VisualStudio.Text /// </summary> public class ContentTypeChangedEventArgs : TextSnapshotChangedEventArgs { - #region Private Members - IContentType _beforeContentType; - IContentType _afterContentType; + #region Private Members #endregion @@ -38,40 +36,18 @@ namespace Microsoft.VisualStudio.Text object editTag) : base(beforeSnapshot, afterSnapshot, editTag) { - if (beforeContentType == null) - { - throw new ArgumentNullException("beforeContentType"); - } - - if (afterContentType == null) - { - throw new ArgumentNullException("afterContentType"); - } - - _beforeContentType = beforeContentType; - _afterContentType = afterContentType; + BeforeContentType = beforeContentType ?? throw new ArgumentNullException(nameof(beforeContentType)); + AfterContentType = afterContentType ?? throw new ArgumentNullException(nameof(afterContentType)); } /// <summary> /// The <see cref="IContentType"/> before the change occurred. /// </summary> - public IContentType BeforeContentType - { - get - { - return _beforeContentType; - } - } + public IContentType BeforeContentType { get; } /// <summary> /// The <see cref="IContentType"/> after the change occurred. /// </summary> - public IContentType AfterContentType - { - get - { - return _afterContentType; - } - } + public IContentType AfterContentType { get; } } } diff --git a/src/Text/Def/TextData/Model/EditOptions.cs b/src/Text/Def/TextData/Model/EditOptions.cs index 71568ad..d8699ec 100644 --- a/src/Text/Def/TextData/Model/EditOptions.cs +++ b/src/Text/Def/TextData/Model/EditOptions.cs @@ -6,13 +6,13 @@ namespace Microsoft.VisualStudio.Text { using Microsoft.VisualStudio.Text.Differencing; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Options applicable to text editing transactions. /// </summary> public struct EditOptions +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { - private bool computeMinimalChange; - private StringDifferenceOptions differenceOptions; #region Common EditOptions values @@ -35,8 +35,8 @@ namespace Microsoft.VisualStudio.Text /// </summary> public EditOptions(StringDifferenceOptions differenceOptions) { - this.computeMinimalChange = true; - this.differenceOptions = differenceOptions; + this.ComputeMinimalChange = true; + this.DifferenceOptions = differenceOptions; } /// <summary> @@ -44,17 +44,14 @@ namespace Microsoft.VisualStudio.Text /// </summary> public EditOptions(bool computeMinimalChange, StringDifferenceOptions differenceOptions) { - this.computeMinimalChange = computeMinimalChange; - this.differenceOptions = differenceOptions; + this.ComputeMinimalChange = computeMinimalChange; + this.DifferenceOptions = differenceOptions; } /// <summary> /// True if this edit computes minimal change using the differencing option <see cref="StringDifferenceOptions"/>, false otherwise. /// </summary> - public bool ComputeMinimalChange - { - get { return this.computeMinimalChange; } - } + public bool ComputeMinimalChange { get; } /// <summary> /// The differencing options for this edit, if <see cref="ComputeMinimalChange" /> is true. @@ -63,10 +60,7 @@ namespace Microsoft.VisualStudio.Text /// <see cref="StringDifferenceOptions.IgnoreTrimWhiteSpace" /> will be /// ignored. /// </remarks> - public StringDifferenceOptions DifferenceOptions - { - get { return differenceOptions; } - } + public StringDifferenceOptions DifferenceOptions { get; } #region Overridden methods and operators @@ -81,7 +75,7 @@ namespace Microsoft.VisualStudio.Text } else { - return differenceOptions.ToString(); + return DifferenceOptions.ToString(); } } @@ -96,7 +90,7 @@ namespace Microsoft.VisualStudio.Text } else { - return differenceOptions.GetHashCode(); + return DifferenceOptions.GetHashCode(); } } @@ -114,7 +108,7 @@ namespace Microsoft.VisualStudio.Text if (!this.ComputeMinimalChange) return true; - return other.differenceOptions == this.differenceOptions; + return other.DifferenceOptions == this.DifferenceOptions; } else { diff --git a/src/Text/Def/TextData/Model/EditTags.cs b/src/Text/Def/TextData/Model/EditTags.cs new file mode 100644 index 0000000..20cec5b --- /dev/null +++ b/src/Text/Def/TextData/Model/EditTags.cs @@ -0,0 +1,66 @@ +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Interface that can be used for the <see cref="ITextBuffer.CreateEdit(EditOptions, int?, object)"/> editTag parameter. + /// </summary> + /// <remarks> + /// <para> + /// This interface, by itself, does nothing. The derived interfaces, however, can provide some context on the nature of the edit. + /// For example, the tags for edits associated with the user doing an "undo" should derive from <see cref="IUndoEditTag"/> and + /// <see cref="IUserEditTag"/>. + /// </para> + /// </remarks> + public interface IEditTag { } + + /// <summary> + /// Indicates a constraint that no additional edits should be performed in the buffer's <see cref="ITextBuffer.Changed"/> event + /// handlers called in response to this edit. + /// </summary> + /// <remarks> + /// <para> + /// This constraint is not currently enforced but that may happen in the future. + /// </para> + /// </remarks> + public interface IInviolableEditTag : IEditTag { } + + /// <summary> + /// Indicates that this edit will create an invisible undo transaction. + /// </summary> + public interface IInvisibleEditTag : IEditTag { } + + /// <summary> + /// Indicates that the edit is part of an undo or redo. + /// </summary> + public interface IUndoEditTag : IInviolableEditTag { } + + /// <summary> + /// Indicates that the edit is part of automatic formatting. + /// </summary> + public interface IFormattingEditTag : IInviolableEditTag { } + + /// <summary> + /// Indicates that the edit is from a remote collaborator. + /// </summary> + public interface IRemoteEditTag : IInviolableEditTag, IInvisibleEditTag { } + + /// <summary> + /// Indicates that the edit is a direct result of a user action (e.g. typing) as opposed to a side-effect (e.g. the + /// automatic formatting after the user types a semicolon). + /// </summary> + public interface IUserEditTag : IEditTag { } + + /// <summary> + /// Indicates that the edit is something like a "paste" where the modified text should be formatted. + /// </summary> + public interface IFormattingNeededEditTag : IEditTag { } + + /// <summary> + /// Indicates that the edit is the result of the user typing a character. + /// </summary> + public interface ITypingEditTag : IUserEditTag { } + + /// <summary> + /// Indicates that the edit is the result of the user typing hitting a backspace or delete. + /// </summary> + public interface IDeleteEditTag : IUserEditTag { } +} diff --git a/src/Text/Def/TextData/Model/NormalizedSnapshotSpanCollection.cs b/src/Text/Def/TextData/Model/NormalizedSnapshotSpanCollection.cs index 095fb71..42b60ed 100644 --- a/src/Text/Def/TextData/Model/NormalizedSnapshotSpanCollection.cs +++ b/src/Text/Def/TextData/Model/NormalizedSnapshotSpanCollection.cs @@ -7,6 +7,7 @@ namespace Microsoft.VisualStudio.Text using System; using System.Collections; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; /// <summary> /// A read-only collection of <see cref="SnapshotSpan"/> objects, all from the same snapshot. @@ -28,10 +29,11 @@ namespace Microsoft.VisualStudio.Text // over 95% of the instances of this class). If this.spans is nonnull, the collection is size two or greater. // We can't do this by subclassing because of backward compatibility with the concrete constructor. - private ITextSnapshot snapshot; - private NormalizedSpanCollection spans; - private Span span; + private readonly ITextSnapshot snapshot; + private readonly NormalizedSpanCollection spans; + private readonly Span span; + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "Type is immutable")] public readonly static NormalizedSnapshotSpanCollection Empty = new NormalizedSnapshotSpanCollection(); /// <summary> @@ -68,11 +70,11 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (spans == null) { - throw new ArgumentNullException("spans"); + throw new ArgumentNullException(nameof(spans)); } if (spans.Count > 0 && spans[spans.Count - 1].End > snapshot.Length) { @@ -101,11 +103,11 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (spans == null) { - throw new ArgumentNullException("spans"); + throw new ArgumentNullException(nameof(spans)); } using (IEnumerator<Span> spanEnumerator = spans.GetEnumerator()) @@ -151,11 +153,11 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (spans == null) { - throw new ArgumentNullException("spans"); + throw new ArgumentNullException(nameof(spans)); } if (spans.Count == 0) @@ -197,7 +199,7 @@ namespace Microsoft.VisualStudio.Text { if (snapshotSpans == null) { - throw new ArgumentNullException("snapshotSpans"); + throw new ArgumentNullException(nameof(snapshotSpans)); } using (IEnumerator<SnapshotSpan> spanEnumerator = snapshotSpans.GetEnumerator()) @@ -265,7 +267,7 @@ namespace Microsoft.VisualStudio.Text // TODO: possibly eliminate based on slight usage? if (snapshotSpans == null) { - throw new ArgumentNullException("snapshotSpans"); + throw new ArgumentNullException(nameof(snapshotSpans)); } if (snapshotSpans.Count == 0) @@ -324,7 +326,7 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (span.End > snapshot.Length) @@ -341,11 +343,11 @@ namespace Microsoft.VisualStudio.Text { if (targetSnapshot == null) { - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); } if (mode < SpanTrackingMode.EdgeExclusive || mode > SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("mode"); + throw new ArgumentOutOfRangeException(nameof(mode)); } if (this.snapshot == null) @@ -425,11 +427,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -465,11 +467,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -504,11 +506,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -543,11 +545,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -581,7 +583,7 @@ namespace Microsoft.VisualStudio.Text { if (set == null) { - throw new ArgumentNullException("set"); + throw new ArgumentNullException(nameof(set)); } else if (set.Count == 0 || this.Count == 0) { @@ -636,7 +638,7 @@ namespace Microsoft.VisualStudio.Text { if (set == null) { - throw new ArgumentNullException("set"); + throw new ArgumentNullException(nameof(set)); } else if (set.Count == 0 || this.Count == 0) { @@ -742,7 +744,10 @@ namespace Microsoft.VisualStudio.Text } else { - throw new ArgumentOutOfRangeException("index"); + // Analyzer has a bug where it is giving a false positive in this location. +#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations + throw new ArgumentOutOfRangeException(nameof(index)); +#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations } } set @@ -806,11 +811,11 @@ namespace Microsoft.VisualStudio.Text { if (array == null) { - throw new ArgumentNullException("array"); + throw new ArgumentNullException(nameof(array)); } if (arrayIndex < 0 || arrayIndex > array.Length || this.Count > array.Length - arrayIndex) { - throw new ArgumentOutOfRangeException("arrayIndex"); + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } if (this.spans != null) { @@ -1034,7 +1039,7 @@ namespace Microsoft.VisualStudio.Text } else { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } } set @@ -1060,11 +1065,11 @@ namespace Microsoft.VisualStudio.Text { if (array == null) { - throw new ArgumentNullException("array"); + throw new ArgumentNullException(nameof(array)); } if (index < 0 || index > array.Length || this.Count > array.Length - index) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } if (array.Rank != 1) { diff --git a/src/Text/Def/TextData/Model/NormalizedSpanCollection.cs b/src/Text/Def/TextData/Model/NormalizedSpanCollection.cs index aff7bdb..c118d3c 100644 --- a/src/Text/Def/TextData/Model/NormalizedSpanCollection.cs +++ b/src/Text/Def/TextData/Model/NormalizedSpanCollection.cs @@ -15,6 +15,7 @@ namespace Microsoft.VisualStudio.Text /// </summary> public class NormalizedSpanCollection : ReadOnlyCollection<Span> { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104", Justification = "Type is readonly")] public readonly static NormalizedSpanCollection Empty = new NormalizedSpanCollection(); /// <summary> @@ -59,8 +60,10 @@ namespace Microsoft.VisualStudio.Text /// <para>This constructor runs in O(N) time, where N = spans.Count.</para></remarks> /// <exception cref="ArgumentNullException"><paramref name="normalizedSpans"/> is null.</exception> /// <remarks>This constructor is private so as not to expose the misleading <paramref name="ignored"/> parameter.</remarks> +#pragma warning disable CA1801 // Parameter ignored is never used private NormalizedSpanCollection(IList<Span> normalizedSpans, bool ignored) : base(normalizedSpans) +#pragma warning restore CA1801 // Parameter ignored is never used { } @@ -93,11 +96,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -109,7 +112,7 @@ namespace Microsoft.VisualStudio.Text return left; } - List<Span> spans = new List<Span>(); + var spans = new List<Span>(); int index1 = 0; int index2 = 0; @@ -163,11 +166,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -179,7 +182,7 @@ namespace Microsoft.VisualStudio.Text return right; } - List<Span> spans = new List<Span>(); + var spans = new List<Span>(); for (int index1 = 0, index2 = 0; (index1 < left.Count) && (index2 < right.Count); ) { Span span1 = left[index1]; @@ -220,11 +223,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -236,7 +239,7 @@ namespace Microsoft.VisualStudio.Text return right; } - List<Span> spans = new List<Span>(); + var spans = new List<Span>(); for (int index1 = 0, index2 = 0; (index1 < left.Count) && (index2 < right.Count) ;) { Span span1 = left[index1]; @@ -276,11 +279,11 @@ namespace Microsoft.VisualStudio.Text { if (left == null) { - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); } if (right == null) { - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); } if (left.Count == 0) @@ -292,7 +295,7 @@ namespace Microsoft.VisualStudio.Text return left; } - List<Span> spans = new List<Span>(); + var spans = new List<Span>(); int index1 = 0; int index2 = 0; @@ -362,9 +365,9 @@ namespace Microsoft.VisualStudio.Text /// <returns><c>true</c> if the two sets are equivalent, otherwise <c>false</c>.</returns> public static bool operator ==(NormalizedSpanCollection left, NormalizedSpanCollection right) { - if (object.ReferenceEquals(left, right)) + if (ReferenceEquals(left, right)) return true; - if (object.ReferenceEquals(left, null) || object.ReferenceEquals(right, null)) + if (ReferenceEquals(left, null) || ReferenceEquals(right, null)) return false; if (left.Count != right.Count) @@ -398,7 +401,7 @@ namespace Microsoft.VisualStudio.Text { if (set == null) { - throw new ArgumentNullException("set"); + throw new ArgumentNullException(nameof(set)); } for (int index1 = 0, index2 = 0; (index1 < this.Count) && (index2 < set.Count) ;) @@ -457,7 +460,7 @@ namespace Microsoft.VisualStudio.Text { if (set == null) { - throw new ArgumentNullException("set"); + throw new ArgumentNullException(nameof(set)); } for (int index1 = 0, index2 = 0; (index1 < this.Count) && (index2 < set.Count); ) @@ -586,10 +589,10 @@ namespace Microsoft.VisualStudio.Text { if (spans == null) { - throw new ArgumentNullException("spans"); + throw new ArgumentNullException(nameof(spans)); } - List<Span> sorted = new List<Span>(spans); + var sorted = new List<Span>(spans); if (sorted.Count <= 1) { return sorted; diff --git a/src/Text/Def/TextData/Model/Projection/ElisionSourceSpansChangedEventArgs.cs b/src/Text/Def/TextData/Model/Projection/ElisionSourceSpansChangedEventArgs.cs index b7c5136..02a78b3 100644 --- a/src/Text/Def/TextData/Model/Projection/ElisionSourceSpansChangedEventArgs.cs +++ b/src/Text/Def/TextData/Model/Projection/ElisionSourceSpansChangedEventArgs.cs @@ -33,11 +33,11 @@ namespace Microsoft.VisualStudio.Text.Projection { if (elidedSpans == null) { - throw new ArgumentNullException("elidedSpans"); + throw new ArgumentNullException(nameof(elidedSpans)); } if (expandedSpans == null) { - throw new ArgumentNullException("expandedSpans"); + throw new ArgumentNullException(nameof(expandedSpans)); } this.elidedSpans = elidedSpans; this.expandedSpans = expandedSpans; diff --git a/src/Text/Def/TextData/Model/Projection/GraphBufferContentTypeChangedEventArgs.cs b/src/Text/Def/TextData/Model/Projection/GraphBufferContentTypeChangedEventArgs.cs index d977310..b3f2e49 100644 --- a/src/Text/Def/TextData/Model/Projection/GraphBufferContentTypeChangedEventArgs.cs +++ b/src/Text/Def/TextData/Model/Projection/GraphBufferContentTypeChangedEventArgs.cs @@ -29,15 +29,15 @@ namespace Microsoft.VisualStudio.Text.Projection { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (beforeContentType == null) { - throw new ArgumentNullException("beforeContentType"); + throw new ArgumentNullException(nameof(beforeContentType)); } if (afterContentType == null) { - throw new ArgumentNullException("afterContentType"); + throw new ArgumentNullException(nameof(afterContentType)); } this.textBuffer = textBuffer; this.beforeContentType = beforeContentType; diff --git a/src/Text/Def/TextData/Model/Projection/GraphBuffersChangedEventArgs.cs b/src/Text/Def/TextData/Model/Projection/GraphBuffersChangedEventArgs.cs index 98d1ce5..b142651 100644 --- a/src/Text/Def/TextData/Model/Projection/GraphBuffersChangedEventArgs.cs +++ b/src/Text/Def/TextData/Model/Projection/GraphBuffersChangedEventArgs.cs @@ -26,11 +26,11 @@ namespace Microsoft.VisualStudio.Text.Projection { if (addedBuffers == null) { - throw new ArgumentNullException("addedBuffers"); + throw new ArgumentNullException(nameof(addedBuffers)); } if (removedBuffers == null) { - throw new ArgumentNullException("removedBuffers"); + throw new ArgumentNullException(nameof(removedBuffers)); } this.addedBuffers = new ReadOnlyCollection<ITextBuffer>(addedBuffers); this.removedBuffers = new ReadOnlyCollection<ITextBuffer>(removedBuffers); diff --git a/src/Text/Def/TextData/Model/Projection/ProjectionSourceBuffersChangedEventArgs.cs b/src/Text/Def/TextData/Model/Projection/ProjectionSourceBuffersChangedEventArgs.cs index 977ce7e..ea17f1e 100644 --- a/src/Text/Def/TextData/Model/Projection/ProjectionSourceBuffersChangedEventArgs.cs +++ b/src/Text/Def/TextData/Model/Projection/ProjectionSourceBuffersChangedEventArgs.cs @@ -44,11 +44,11 @@ namespace Microsoft.VisualStudio.Text.Projection { if (addedBuffers == null) { - throw new ArgumentNullException("addedBuffers"); + throw new ArgumentNullException(nameof(addedBuffers)); } if (removedBuffers == null) { - throw new ArgumentNullException("removedBuffers"); + throw new ArgumentNullException(nameof(removedBuffers)); } this.addedBuffers = addedBuffers; this.removedBuffers = removedBuffers; diff --git a/src/Text/Def/TextData/Model/Projection/ProjectionSourceSpansChangedEventArgs.cs b/src/Text/Def/TextData/Model/Projection/ProjectionSourceSpansChangedEventArgs.cs index e219725..2497ff7 100644 --- a/src/Text/Def/TextData/Model/Projection/ProjectionSourceSpansChangedEventArgs.cs +++ b/src/Text/Def/TextData/Model/Projection/ProjectionSourceSpansChangedEventArgs.cs @@ -40,11 +40,11 @@ namespace Microsoft.VisualStudio.Text.Projection { if (insertedSpans == null) { - throw new ArgumentNullException("insertedSpans"); + throw new ArgumentNullException(nameof(insertedSpans)); } if (deletedSpans == null) { - throw new ArgumentNullException("deletedSpans"); + throw new ArgumentNullException(nameof(deletedSpans)); } this.insertedSpans = new ReadOnlyCollection<ITrackingSpan>(insertedSpans); this.deletedSpans = new ReadOnlyCollection<ITrackingSpan>(deletedSpans); diff --git a/src/Text/Def/TextData/Model/SnapshotPoint.cs b/src/Text/Def/TextData/Model/SnapshotPoint.cs index 330cc9c..de383a0 100644 --- a/src/Text/Def/TextData/Model/SnapshotPoint.cs +++ b/src/Text/Def/TextData/Model/SnapshotPoint.cs @@ -6,14 +6,15 @@ namespace Microsoft.VisualStudio.Text { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals +#pragma warning disable CA1036 // Override methods on comparable types /// <summary> /// An immutable text position in a particular text snapshot. /// </summary> public struct SnapshotPoint : IComparable<SnapshotPoint> +#pragma warning restore CA1036 // Override methods on comparable types +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { - // Member must match order in the ctor, otherwise the COM tool gets confused. - private ITextSnapshot snapshot; - private int position; /// <summary> /// Initializes a new instance of a <see cref="SnapshotPoint"/> with respect to a particular snapshot and position. @@ -24,32 +25,26 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (position < 0 || position > snapshot.Length) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } - this.snapshot = snapshot; - this.position = position; + this.Snapshot = snapshot; + this.Position = position; } /// <summary> /// Gets the position of the point. /// </summary> /// <value>A non-negative integer less than or equal to the length of the snapshot.</value> - public int Position - { - get { return this.position; } - } + public int Position { get; } /// <summary> /// Gets the <see cref="ITextSnapshot"/> to which this snapshot point refers. /// </summary> - public ITextSnapshot Snapshot - { - get { return this.snapshot; } - } + public ITextSnapshot Snapshot { get; } /// <summary> /// Implicitly converts the snapshot point to an integer equal to the position of the snapshot point in the snapshot. @@ -65,7 +60,7 @@ namespace Microsoft.VisualStudio.Text /// <returns></returns> public ITextSnapshotLine GetContainingLine() { - return this.snapshot.GetLineFromPosition(this.position); + return this.Snapshot.GetLineFromPosition(this.Position); } /// <summary> @@ -75,7 +70,7 @@ namespace Microsoft.VisualStudio.Text /// <exception cref="ArgumentOutOfRangeException"> if the position of this point is equal to the length of the snapshot.</exception> public char GetChar() { - return this.snapshot[this.position]; + return this.Snapshot[this.Position]; } /// <summary> @@ -88,7 +83,7 @@ namespace Microsoft.VisualStudio.Text /// <exception cref="ArgumentException"><paramref name="targetSnapshot"/> does not refer to the same <see cref="ITextBuffer"/> as this snapshot point.</exception> public SnapshotPoint TranslateTo(ITextSnapshot targetSnapshot, PointTrackingMode trackingMode) { - if (targetSnapshot == this.snapshot) + if (targetSnapshot == this.Snapshot) { return this; } @@ -96,16 +91,16 @@ namespace Microsoft.VisualStudio.Text { if (targetSnapshot == null) { - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); } - if (targetSnapshot.TextBuffer != this.snapshot.TextBuffer) + if (targetSnapshot.TextBuffer != this.Snapshot.TextBuffer) { throw new ArgumentException(Strings.InvalidSnapshot); } - int targetPosition = targetSnapshot.Version.VersionNumber > this.snapshot.Version.VersionNumber - ? Tracking.TrackPositionForwardInTime(trackingMode, this.position, this.snapshot.Version, targetSnapshot.Version) - : Tracking.TrackPositionBackwardInTime(trackingMode, this.position, this.snapshot.Version, targetSnapshot.Version); + int targetPosition = targetSnapshot.Version.VersionNumber > this.Snapshot.Version.VersionNumber + ? Tracking.TrackPositionForwardInTime(trackingMode, this.Position, this.Snapshot.Version, targetSnapshot.Version) + : Tracking.TrackPositionBackwardInTime(trackingMode, this.Position, this.Snapshot.Version, targetSnapshot.Version); return new SnapshotPoint(targetSnapshot, targetPosition); } @@ -116,7 +111,7 @@ namespace Microsoft.VisualStudio.Text /// </summary> public override int GetHashCode() { - return (this.snapshot != null) ? (this.position.GetHashCode() ^ this.snapshot.GetHashCode()) : 0; + return (this.Snapshot != null) ? (this.Position.GetHashCode() ^ this.Snapshot.GetHashCode()) : 0; } /// <summary> @@ -124,19 +119,18 @@ namespace Microsoft.VisualStudio.Text /// </summary> public override string ToString() { - if (this.snapshot == null) + if (this.Snapshot == null) { return "uninit"; } else { - string tag; - this.Snapshot.TextBuffer.Properties.TryGetProperty<string>("tag", out tag); + this.Snapshot.TextBuffer.Properties.TryGetProperty("tag", out string tag); return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0}_v{1}_{2}_'{3}'", tag ?? "?", this.Snapshot.Version.VersionNumber, - this.position, - position == this.Snapshot.Length ? "<end>" : this.Snapshot.GetText(position, 1)); + this.Position, + this.Position == this.Snapshot.Length ? "<end>" : this.Snapshot.GetText(this.Position, 1)); } } @@ -297,7 +291,7 @@ namespace Microsoft.VisualStudio.Text throw new ArgumentException(Strings.InvalidSnapshotPoint); } - return this.position.CompareTo(other.position); + return this.Position.CompareTo(other.Position); } #endregion diff --git a/src/Text/Def/TextData/Model/SnapshotSpan.cs b/src/Text/Def/TextData/Model/SnapshotSpan.cs index dc71c9e..5d60818 100644 --- a/src/Text/Def/TextData/Model/SnapshotSpan.cs +++ b/src/Text/Def/TextData/Model/SnapshotSpan.cs @@ -6,10 +6,12 @@ namespace Microsoft.VisualStudio.Text { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// An immutable text span in a particular text snapshot. /// </summary> public struct SnapshotSpan +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { #region Private Members @@ -30,11 +32,11 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (span.End > snapshot.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } this.start = new SnapshotPoint(snapshot, span.Start); @@ -78,7 +80,7 @@ namespace Microsoft.VisualStudio.Text } if (end.Position < start.Position) { - throw new ArgumentOutOfRangeException("end"); + throw new ArgumentOutOfRangeException(nameof(end)); } this.start = start; @@ -98,7 +100,7 @@ namespace Microsoft.VisualStudio.Text if (length < 0 || start.Position + length > start.Snapshot.Length) { - throw new ArgumentOutOfRangeException("length"); + throw new ArgumentOutOfRangeException(nameof(length)); } this.start = start; @@ -148,7 +150,7 @@ namespace Microsoft.VisualStudio.Text { if (targetSnapshot == null) { - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); } if (targetSnapshot.TextBuffer != this.Start.Snapshot.TextBuffer) { @@ -156,8 +158,8 @@ namespace Microsoft.VisualStudio.Text } Span targetSpan = targetSnapshot.Version.VersionNumber > this.Snapshot.Version.VersionNumber - ? Tracking.TrackSpanForwardInTime(spanTrackingMode, Span, this.Snapshot.Version, targetSnapshot.Version) - : Tracking.TrackSpanBackwardInTime(spanTrackingMode, Span, this.Snapshot.Version, targetSnapshot.Version); + ? Tracking.TrackSpanForwardInTime(spanTrackingMode, this.Span, this.Snapshot.Version, targetSnapshot.Version) + : Tracking.TrackSpanBackwardInTime(spanTrackingMode, this.Span, this.Snapshot.Version, targetSnapshot.Version); return new SnapshotSpan(targetSnapshot, targetSpan); } @@ -170,7 +172,7 @@ namespace Microsoft.VisualStudio.Text /// </summary> public Span Span { - get { return new Span(start, length); } + get { return new Span(this.start, this.length); } } /// <summary> @@ -180,7 +182,7 @@ namespace Microsoft.VisualStudio.Text { get { - return start; + return this.start; } } @@ -192,7 +194,7 @@ namespace Microsoft.VisualStudio.Text { get { - return start + length; + return this.start + this.length; } } @@ -436,7 +438,7 @@ namespace Microsoft.VisualStudio.Text else { string tag; - this.Snapshot.TextBuffer.Properties.TryGetProperty<string>("tag", out tag); + this.Snapshot.TextBuffer.Properties.TryGetProperty("tag", out tag); return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0}_v{1}_{2}_'{3}'", tag ?? "?", @@ -455,7 +457,7 @@ namespace Microsoft.VisualStudio.Text { if (obj is SnapshotSpan) { - SnapshotSpan other = (SnapshotSpan)obj; + var other = (SnapshotSpan)obj; return other == this; } else diff --git a/src/Text/Def/TextData/Model/Span.cs b/src/Text/Def/TextData/Model/Span.cs index e7e3940..738f8a5 100644 --- a/src/Text/Def/TextData/Model/Span.cs +++ b/src/Text/Def/TextData/Model/Span.cs @@ -6,12 +6,14 @@ namespace Microsoft.VisualStudio.Text { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// An immutable integer interval that describes a range of values from <see cref="Start"/> to <see cref="End"/> that is closed on /// the left and open on the right: [Start .. End). A zpan is usually applied to an <see cref="ITextSnapshot"/> to denote a span of text, /// but it is independent of any particular text buffer or snapshot. /// </summary> public struct Span +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { #region Private Members @@ -34,11 +36,11 @@ namespace Microsoft.VisualStudio.Text { if (start < 0) { - throw new ArgumentOutOfRangeException("start"); + throw new ArgumentOutOfRangeException(nameof(start)); } if (start + length < start) { - throw new ArgumentOutOfRangeException("length"); + throw new ArgumentOutOfRangeException(nameof(length)); } this.start = start; this.length = length; diff --git a/src/Text/Def/TextData/Model/TextBufferCreatedEventArgs.cs b/src/Text/Def/TextData/Model/TextBufferCreatedEventArgs.cs index 25c532e..93e2e69 100644 --- a/src/Text/Def/TextData/Model/TextBufferCreatedEventArgs.cs +++ b/src/Text/Def/TextData/Model/TextBufferCreatedEventArgs.cs @@ -24,7 +24,7 @@ namespace Microsoft.VisualStudio.Text { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } TextBuffer = textBuffer; } diff --git a/src/Text/Def/TextData/Model/TextContentChangingEventArgs.cs b/src/Text/Def/TextData/Model/TextContentChangingEventArgs.cs index cb828d8..b431b3c 100644 --- a/src/Text/Def/TextData/Model/TextContentChangingEventArgs.cs +++ b/src/Text/Def/TextData/Model/TextContentChangingEventArgs.cs @@ -39,7 +39,7 @@ namespace Microsoft.VisualStudio.Text { if (beforeSnapshot == null) { - throw new ArgumentNullException("beforeSnapshot"); + throw new ArgumentNullException(nameof(beforeSnapshot)); } Canceled = false; diff --git a/src/Text/Def/TextData/Model/TextImageLine.cs b/src/Text/Def/TextData/Model/TextImageLine.cs index b7e95c1..de9dbbf 100644 --- a/src/Text/Def/TextData/Model/TextImageLine.cs +++ b/src/Text/Def/TextData/Model/TextImageLine.cs @@ -5,6 +5,7 @@ namespace Microsoft.VisualStudio.Text { using System; + using System.Diagnostics.CodeAnalysis; /// <summary> /// Immutable information about a line of text from an <see cref="ITextImage"/>. @@ -36,12 +37,17 @@ namespace Microsoft.VisualStudio.Text /// <summary> /// The <see cref="ITextImage"/> in which the line appears. /// </summary> + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "Type is readonly")] +#pragma warning disable CA1051 // Do not declare visible instance fields public readonly ITextImage Image; +#pragma warning restore CA1051 // Do not declare visible instance fields /// <summary> /// The extent of the line, excluding any line break characters. /// </summary> +#pragma warning disable CA1051 // Do not declare visible instance fields public readonly Span Extent; +#pragma warning restore CA1051 // Do not declare visible instance fields /// <summary> /// The extent of the line, including any line break characters. @@ -51,7 +57,9 @@ namespace Microsoft.VisualStudio.Text /// <summary> /// The 0-origin line number of the line. /// </summary> +#pragma warning disable CA1051 // Do not declare visible instance fields public readonly int LineNumber; +#pragma warning restore CA1051 // Do not declare visible instance fields /// <summary> /// The position of the first character in the line. @@ -87,7 +95,9 @@ namespace Microsoft.VisualStudio.Text /// <summary> /// Length of line break characters (always falls in the range [0..2]). /// </summary> +#pragma warning disable CA1051 // Do not declare visible instance fields public readonly int LineBreakLength; +#pragma warning restore CA1051 // Do not declare visible instance fields /// <summary> /// The text of the line, excluding any line break characters. diff --git a/src/Text/Def/TextData/Model/TextSnapshotChangedEventArgs.cs b/src/Text/Def/TextData/Model/TextSnapshotChangedEventArgs.cs index 4581044..9582fb4 100644 --- a/src/Text/Def/TextData/Model/TextSnapshotChangedEventArgs.cs +++ b/src/Text/Def/TextData/Model/TextSnapshotChangedEventArgs.cs @@ -31,11 +31,11 @@ namespace Microsoft.VisualStudio.Text { if (beforeSnapshot == null) { - throw new ArgumentNullException("beforeSnapshot"); + throw new ArgumentNullException(nameof(beforeSnapshot)); } if (afterSnapshot == null) { - throw new ArgumentNullException("afterSnapshot"); + throw new ArgumentNullException(nameof(afterSnapshot)); } this.before = beforeSnapshot; this.after = afterSnapshot; diff --git a/src/Text/Def/TextData/Model/TextSnapshotToTextReader.cs b/src/Text/Def/TextData/Model/TextSnapshotToTextReader.cs index 35f630e..7befcc3 100644 --- a/src/Text/Def/TextData/Model/TextSnapshotToTextReader.cs +++ b/src/Text/Def/TextData/Model/TextSnapshotToTextReader.cs @@ -79,13 +79,13 @@ namespace Microsoft.VisualStudio.Text if (_currentPosition == -1) throw new ObjectDisposedException("TextSnapshotToTextReader"); if (buffer == null) - throw new ArgumentNullException("buffer"); + throw new ArgumentNullException(nameof(buffer)); if (index < 0) - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); if (count < 0) - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); if (((index + count) < 0) || ((index + count) > buffer.Length)) - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); int charactersToRead = System.Math.Min(_snapshot.Length - _currentPosition, count); _snapshot.CopyTo(_currentPosition, buffer, index, charactersToRead); @@ -159,7 +159,7 @@ namespace Microsoft.VisualStudio.Text public TextSnapshotToTextReader(ITextSnapshot textSnapshot) { if (textSnapshot == null) - throw new ArgumentNullException("textSnapshot"); + throw new ArgumentNullException(nameof(textSnapshot)); _snapshot = textSnapshot; } diff --git a/src/Text/Def/TextData/Model/Tracking.cs b/src/Text/Def/TextData/Model/Tracking.cs index 3058af0..8f515b2 100644 --- a/src/Text/Def/TextData/Model/Tracking.cs +++ b/src/Text/Def/TextData/Model/Tracking.cs @@ -3,7 +3,6 @@ // Licensed under the MIT License. See License.txt in the project root for license information. // using System; -using System.Collections.Generic; using System.Diagnostics; namespace Microsoft.VisualStudio.Text @@ -20,15 +19,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.TextBuffer != currentVersion.TextBuffer) { @@ -36,11 +35,11 @@ namespace Microsoft.VisualStudio.Text } if (targetVersion.VersionNumber < currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } if (currentPosition < 0 || currentPosition > currentVersion.Length) { - throw new ArgumentOutOfRangeException("currentPosition"); + throw new ArgumentOutOfRangeException(nameof(currentPosition)); } // track forward in time @@ -64,15 +63,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.Identifier != currentVersion.Identifier) { @@ -80,11 +79,11 @@ namespace Microsoft.VisualStudio.Text } if (targetVersion.VersionNumber < currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } if (currentPosition < 0 || currentPosition > currentVersion.Length) { - throw new ArgumentOutOfRangeException("currentPosition"); + throw new ArgumentOutOfRangeException(nameof(currentPosition)); } // track forward in time @@ -226,15 +225,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.TextBuffer != currentVersion.TextBuffer) { @@ -242,11 +241,11 @@ namespace Microsoft.VisualStudio.Text } if (targetVersion.VersionNumber > currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } if (currentPosition < 0 || currentPosition > currentVersion.Length) { - throw new ArgumentOutOfRangeException("currentPosition"); + throw new ArgumentOutOfRangeException(nameof(currentPosition)); } // track backwards in time @@ -278,15 +277,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.Identifier != currentVersion.Identifier) { @@ -294,11 +293,11 @@ namespace Microsoft.VisualStudio.Text } if (targetVersion.VersionNumber > currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } if (currentPosition < 0 || currentPosition > currentVersion.Length) { - throw new ArgumentOutOfRangeException("currentPosition"); + throw new ArgumentOutOfRangeException(nameof(currentPosition)); } // track backwards in time @@ -402,15 +401,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.TextBuffer != currentVersion.TextBuffer) { @@ -418,11 +417,11 @@ namespace Microsoft.VisualStudio.Text } if (span.End > currentVersion.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } if (targetVersion.VersionNumber < currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } int resultStart = @@ -444,15 +443,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.Identifier != currentVersion.Identifier) { @@ -460,11 +459,11 @@ namespace Microsoft.VisualStudio.Text } if (span.End > currentVersion.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } if (targetVersion.VersionNumber < currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } int resultStart = @@ -489,15 +488,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.TextBuffer != currentVersion.TextBuffer) { @@ -505,11 +504,11 @@ namespace Microsoft.VisualStudio.Text } if (span.End > currentVersion.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } if (targetVersion.VersionNumber > currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } int resultStart = @@ -533,15 +532,15 @@ namespace Microsoft.VisualStudio.Text { if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (currentVersion == null) { - throw new ArgumentNullException("currentVersion"); + throw new ArgumentNullException(nameof(currentVersion)); } if (targetVersion == null) { - throw new ArgumentNullException("targetVersion"); + throw new ArgumentNullException(nameof(targetVersion)); } if (targetVersion.Identifier != currentVersion.Identifier) { @@ -549,11 +548,11 @@ namespace Microsoft.VisualStudio.Text } if (span.End > currentVersion.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } if (targetVersion.VersionNumber > currentVersion.VersionNumber) { - throw new ArgumentOutOfRangeException("targetVersion"); + throw new ArgumentOutOfRangeException(nameof(targetVersion)); } int resultStart = diff --git a/src/Text/Def/TextData/Model/VersionedPosition.cs b/src/Text/Def/TextData/Model/VersionedPosition.cs index 8f9e036..7b921b6 100644 --- a/src/Text/Def/TextData/Model/VersionedPosition.cs +++ b/src/Text/Def/TextData/Model/VersionedPosition.cs @@ -5,14 +5,19 @@ namespace Microsoft.VisualStudio.Text { using System; + using System.Diagnostics.CodeAnalysis; /// <summary> /// Describes a location in a specific <see cref="ITextImageVersion"/>. /// </summary> public struct VersionedPosition : IEquatable<VersionedPosition> { + +#pragma warning disable CA1051 // Do not declare visible instance fields + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "Type is readonly")] public readonly ITextImageVersion Version; public readonly int Position; +#pragma warning restore CA1051 // Do not declare visible instance fields public readonly static VersionedPosition Invalid = new VersionedPosition(); @@ -82,7 +87,7 @@ namespace Microsoft.VisualStudio.Text { return (this.Version == null) ? nameof(Invalid) - : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{1}_{2}", + : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{0}_{1}", this.Version.VersionNumber, this.Position); } diff --git a/src/Text/Def/TextData/Model/VersionedSpan.cs b/src/Text/Def/TextData/Model/VersionedSpan.cs index 67de693..75f589d 100644 --- a/src/Text/Def/TextData/Model/VersionedSpan.cs +++ b/src/Text/Def/TextData/Model/VersionedSpan.cs @@ -5,14 +5,18 @@ namespace Microsoft.VisualStudio.Text { using System; + using System.Diagnostics.CodeAnalysis; /// <summary> /// Describes a span in a specific <see cref="ITextImageVersion"/>. /// </summary> public struct VersionedSpan : IEquatable<VersionedSpan> { +#pragma warning disable CA1051 // Do not declare visible instance fields + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "Type is readonly")] public readonly ITextImageVersion Version; public readonly Span Span; +#pragma warning disable CA1051 // Do not declare visible instance fields public readonly static VersionedSpan Invalid = new VersionedSpan(); @@ -82,7 +86,7 @@ namespace Microsoft.VisualStudio.Text { return (this.Version == null) ? nameof(Invalid) - : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{1}_{2}", + : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{0}_{1}", this.Version.VersionNumber, this.Span); } diff --git a/src/Text/Def/TextData/TextData.csproj b/src/Text/Def/TextData/TextData.csproj index a993f1f..e4a64d4 100644 --- a/src/Text/Def/TextData/TextData.csproj +++ b/src/Text/Def/TextData/TextData.csproj @@ -4,8 +4,6 @@ <AssemblyName>Microsoft.VisualStudio.Text.Data</AssemblyName> <TargetFramework>net46</TargetFramework> <RootNamespace>Microsoft.VisualStudio.Text</RootNamespace> - <NonShipping>false</NonShipping> - <IsPackable>true</IsPackable> <PushToPublicFeed>true</PushToPublicFeed> <NoWarn>649;436;$(NoWarn)</NoWarn> <AssemblyAttributeClsCompliant>true</AssemblyAttributeClsCompliant> @@ -15,8 +13,7 @@ <Reference Include="System.Core" /> </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.VisualStudio.Threading" Version="$(MicrosoftVisualStudioThreadingVersion)" /> - <PackageReference Include="Microsoft.VisualStudio.Validation" Version="$(MicrosoftVisualStudioValidationVersion)" /> + <PackageReference Include="Microsoft.VisualStudio.Threading" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\..\Core\Def\CoreUtility.csproj" /> diff --git a/src/Text/Def/TextLogic/Classification/ClassificationSpan.cs b/src/Text/Def/TextLogic/Classification/ClassificationSpan.cs index e952105..e8cd90a 100644 --- a/src/Text/Def/TextLogic/Classification/ClassificationSpan.cs +++ b/src/Text/Def/TextLogic/Classification/ClassificationSpan.cs @@ -29,7 +29,7 @@ namespace Microsoft.VisualStudio.Text.Classification { if (classification == null) { - throw new ArgumentNullException("classification"); + throw new ArgumentNullException(nameof(classification)); } this.span = span; this.classification = classification; diff --git a/src/Text/Def/TextLogic/Classification/ClassificationTypeAttribute.cs b/src/Text/Def/TextLogic/Classification/ClassificationTypeAttribute.cs index 21bd9fa..56b3bde 100644 --- a/src/Text/Def/TextLogic/Classification/ClassificationTypeAttribute.cs +++ b/src/Text/Def/TextLogic/Classification/ClassificationTypeAttribute.cs @@ -45,11 +45,11 @@ namespace Microsoft.VisualStudio.Text.Classification { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } if (string.IsNullOrEmpty(value)) { - throw new ArgumentOutOfRangeException("value"); + throw new ArgumentOutOfRangeException(nameof(value)); } _name = value; diff --git a/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs b/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs index 991f01c..d1e7af1 100644 --- a/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs +++ b/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsConvertTabsToSpacesEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultOptions.ConvertTabsToSpacesOptionId); } @@ -35,7 +35,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static int GetTabSize(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultOptions.TabSizeOptionId); } @@ -48,7 +48,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static int GetIndentSize(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultOptions.IndentSizeOptionId); } @@ -61,7 +61,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool GetReplicateNewLineCharacter(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultOptions.ReplicateNewLineCharacterOptionId); } @@ -74,7 +74,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static string GetNewLineCharacter(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultOptions.NewLineCharacterOptionId); } @@ -87,7 +87,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool GetTrimTrailingWhieSpace(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultOptions.TrimTrailingWhiteSpaceOptionId); } @@ -100,11 +100,24 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool GetInsertFinalNewLine(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultOptions.InsertFinalNewLineOptionId); } + /// <summary> + /// Determines appearance category for tooltips originating in this view + /// </summary> + /// <param name="options">The <see cref="IEditorOptions"/>.</param> + /// <returns>A string containing the appearance category for tooltips originating in this view.</returns> + public static string GetTooltipAppearanceCategory(this IEditorOptions options) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + return options.GetOptionValue(DefaultOptions.TooltipAppearanceCategoryOptionId); + } + #endregion } } @@ -185,6 +198,12 @@ namespace Microsoft.VisualStudio.Text.Editor public static readonly EditorOptionKey<bool> InsertFinalNewLineOptionId = new EditorOptionKey<bool>(InsertFinalNewLineOptionName); public const string InsertFinalNewLineOptionName = "InsertFinalNewLine"; + /// <summary> + /// The default option that determines appearance category for tooltips originating in this view. + /// </summary> + public static readonly EditorOptionKey<string> TooltipAppearanceCategoryOptionId = new EditorOptionKey<string>(TooltipAppearanceCategoryOptionName); + public const string TooltipAppearanceCategoryOptionName = "TooltipAppearanceCategory"; + #endregion } @@ -371,5 +390,23 @@ namespace Microsoft.VisualStudio.Text.Editor public override EditorOptionKey<bool> Key { get { return DefaultOptions.InsertFinalNewLineOptionId; } } } + /// <summary> + /// The option definition that determines whether to insert a final newline. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.TooltipAppearanceCategoryOptionName)] + public sealed class TooltipAppearanceCategory : EditorOptionDefinition<string> + { + /// <summary> + /// Gets the default value ("text"). + /// </summary> + public override string Default { get => "text"; } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<string> Key { get { return DefaultOptions.TooltipAppearanceCategoryOptionId; } } + } + #endregion } diff --git a/src/Text/Def/TextLogic/EditorOptions/DeferCreationAttribute.cs b/src/Text/Def/TextLogic/EditorOptions/DeferCreationAttribute.cs index 37ae443..ead82ee 100644 --- a/src/Text/Def/TextLogic/EditorOptions/DeferCreationAttribute.cs +++ b/src/Text/Def/TextLogic/EditorOptions/DeferCreationAttribute.cs @@ -37,7 +37,7 @@ namespace Microsoft.VisualStudio.Text.Editor { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } this.optionName = value; } diff --git a/src/Text/Def/TextLogic/EditorOptions/EditorOptionDefinition.cs b/src/Text/Def/TextLogic/EditorOptions/EditorOptionDefinition.cs index 7894122..ab99820 100644 --- a/src/Text/Def/TextLogic/EditorOptions/EditorOptionDefinition.cs +++ b/src/Text/Def/TextLogic/EditorOptions/EditorOptionDefinition.cs @@ -68,8 +68,8 @@ namespace Microsoft.VisualStudio.Text.Editor /// <returns><c>true</c> if the two objects are the same, otherwise <c>false</c>.</returns> public override bool Equals(object obj) { - EditorOptionDefinition other = obj as EditorOptionDefinition; - return other != null && other.Name == this.Name; + var other = obj as EditorOptionDefinition; + return other != null && string.Equals(other.Name, this.Name, StringComparison.Ordinal); } /// <summary> @@ -97,12 +97,12 @@ namespace Microsoft.VisualStudio.Text.Editor /// <summary> /// Gets the name of the option. /// </summary> - public sealed override string Name { get { return Key.Name; } } + public sealed override string Name { get { return this.Key.Name; } } /// <summary> /// Gets the default value of the option. /// </summary> - public sealed override object DefaultValue { get { return Default; } } + public sealed override object DefaultValue { get { return this.Default; } } /// <summary>Determines whether the proposed value is valid. /// </summary> @@ -113,10 +113,8 @@ namespace Microsoft.VisualStudio.Text.Editor /// The implementer of this method may modify the value.</remarks> public sealed override bool IsValid(ref object proposedValue) { - if (proposedValue is T) + if (proposedValue is T value) { - T value = (T)proposedValue; - var result = IsValid(ref value); proposedValue = value; diff --git a/src/Text/Def/TextLogic/EditorOptions/EditorOptionKey.cs b/src/Text/Def/TextLogic/EditorOptions/EditorOptionKey.cs index 4c9f1cf..cda9259 100644 --- a/src/Text/Def/TextLogic/EditorOptions/EditorOptionKey.cs +++ b/src/Text/Def/TextLogic/EditorOptions/EditorOptionKey.cs @@ -4,26 +4,24 @@ // namespace Microsoft.VisualStudio.Text.Editor { +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Represents a type-safe key for editor options. /// </summary> /// <typeparam name="T">The type of the option value.</typeparam> public struct EditorOptionKey<T> +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { - #region Private data - private string _name; - #endregion - /// <summary> /// Initializes a new instance of <see cref="EditorOptionKey<T>"/>. /// </summary> /// <param name="name">The name of the option key.</param> - public EditorOptionKey(string name) { _name = name; } + public EditorOptionKey(string name) { this.Name = name; } /// <summary> /// Gets the name of this key. /// </summary> - public string Name { get { return _name; } } + public string Name { get; } #region Object overrides @@ -36,8 +34,8 @@ namespace Microsoft.VisualStudio.Text.Editor { if (obj is EditorOptionKey<T>) { - EditorOptionKey<T> other = (EditorOptionKey<T>)obj; - return other.Name == this.Name; + var other = (EditorOptionKey<T>)obj; + return string.Equals(other.Name, this.Name, System.StringComparison.Ordinal); } return false; @@ -66,7 +64,7 @@ namespace Microsoft.VisualStudio.Text.Editor /// </summary> public static bool operator ==(EditorOptionKey<T> left, EditorOptionKey<T> right) { - return left.Name == right.Name; + return string.Equals(left.Name, right.Name, System.StringComparison.Ordinal); } /// <summary> diff --git a/src/Text/Def/TextLogic/Find/FindData.cs b/src/Text/Def/TextLogic/Find/FindData.cs index fe96bcb..c0a5cd2 100644 --- a/src/Text/Def/TextLogic/Find/FindData.cs +++ b/src/Text/Def/TextLogic/Find/FindData.cs @@ -6,15 +6,15 @@ using System; namespace Microsoft.VisualStudio.Text.Operations { +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Represents the set of data used in a search by the <see cref="ITextSearchService"/>. /// </summary> public struct FindData +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { private string _searchString; private ITextSnapshot _textSnapshotToSearch; - private FindOptions _findOptions; - private ITextStructureNavigator _textStructureNavigator; /// <summary> /// Initializes a new instance of <see cref="FindData"/> with the specified search pattern, text snapshot, @@ -30,22 +30,17 @@ namespace Microsoft.VisualStudio.Text.Operations { if (searchPattern == null) { - throw new ArgumentNullException("searchPattern"); + throw new ArgumentNullException(nameof(searchPattern)); } if (searchPattern.Length == 0) { - throw new ArgumentOutOfRangeException("searchPattern"); - } - - if (textSnapshot == null) - { - throw new ArgumentNullException("textSnapshot"); + throw new ArgumentOutOfRangeException(nameof(searchPattern)); } _searchString = searchPattern; - _textSnapshotToSearch = textSnapshot; - _findOptions = findOptions; - _textStructureNavigator = textStructureNavigator; + _textSnapshotToSearch = textSnapshot ?? throw new ArgumentNullException(nameof(textSnapshot)); + FindOptions = findOptions; + TextStructureNavigator = textStructureNavigator; } /// <summary> @@ -63,8 +58,8 @@ namespace Microsoft.VisualStudio.Text.Operations { _searchString = null; _textSnapshotToSearch = textSnapshot; - _findOptions = FindOptions.None; - _textStructureNavigator = null; + FindOptions = FindOptions.None; + TextStructureNavigator = null; } /// <summary> @@ -79,11 +74,11 @@ namespace Microsoft.VisualStudio.Text.Operations { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } if (value.Length == 0) { - throw new ArgumentOutOfRangeException("value"); + throw new ArgumentOutOfRangeException(nameof(value)); } _searchString = value; } @@ -100,10 +95,10 @@ namespace Microsoft.VisualStudio.Text.Operations { FindData other = (FindData)obj; - return (_searchString == other._searchString) && - (_findOptions == other._findOptions) && + return (string.Equals(_searchString, other._searchString, StringComparison.Ordinal)) && + (FindOptions == other.FindOptions) && object.ReferenceEquals(_textSnapshotToSearch, other._textSnapshotToSearch) && - object.ReferenceEquals(_textStructureNavigator, other._textStructureNavigator); + object.ReferenceEquals(TextStructureNavigator, other.TextStructureNavigator); } return false; } @@ -151,11 +146,7 @@ namespace Microsoft.VisualStudio.Text.Operations /// <summary> /// Gets or sets the options that are used for the search. /// </summary> - public FindOptions FindOptions - { - get { return _findOptions; } - set { _findOptions = value; } - } + public FindOptions FindOptions { get; set; } /// <summary> /// Gets or sets the <see cref="ITextSnapshot"/> on which to perform the search. @@ -166,21 +157,13 @@ namespace Microsoft.VisualStudio.Text.Operations get { return _textSnapshotToSearch; } set { - if (value == null) - { - throw new ArgumentNullException("value"); - } - _textSnapshotToSearch = value; + _textSnapshotToSearch = value ?? throw new ArgumentNullException(nameof(value)); } } /// <summary> /// Gets or sets the <see cref="ITextStructureNavigator"/> to use in determining word boundaries. /// </summary> - public ITextStructureNavigator TextStructureNavigator - { - get { return _textStructureNavigator; } - set { _textStructureNavigator = value; } - } + public ITextStructureNavigator TextStructureNavigator { get; set; } } } diff --git a/src/Text/Def/TextLogic/Find/ITextSearchService2.cs b/src/Text/Def/TextLogic/Find/ITextSearchService2.cs index 905d0b3..226683e 100644 --- a/src/Text/Def/TextLogic/Find/ITextSearchService2.cs +++ b/src/Text/Def/TextLogic/Find/ITextSearchService2.cs @@ -65,7 +65,7 @@ namespace Microsoft.VisualStudio.Text.Operations SnapshotSpan? Find(SnapshotSpan searchRange, SnapshotPoint startingPosition, string searchPattern, FindOptions options); /// <summary> - /// Searches for the next occurence of <paramref name="searchPattern"/> and sets <paramref name="expandedReplacePattern"/> to the result of + /// Searches for the next occurrence of <paramref name="searchPattern"/> and sets <paramref name="expandedReplacePattern"/> to the result of /// the text replacement. /// </summary> /// <param name="startingPosition"> @@ -101,7 +101,7 @@ namespace Microsoft.VisualStudio.Text.Operations SnapshotSpan? FindForReplace(SnapshotPoint startingPosition, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern); /// <summary> - /// Searches for the next occurence of <paramref name="searchPattern"/> and sets <paramref name="expandedReplacePattern"/> to the result of + /// Searches for the next occurrence of <paramref name="searchPattern"/> and sets <paramref name="expandedReplacePattern"/> to the result of /// the text replacement. /// </summary> /// <param name="searchRange"> @@ -136,7 +136,7 @@ namespace Microsoft.VisualStudio.Text.Operations SnapshotSpan? FindForReplace(SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern); /// <summary> - /// Finds all occurences of the <paramref name="searchPattern"/> in <paramref name="searchRange"/>. + /// Finds all occurrences of the <paramref name="searchPattern"/> in <paramref name="searchRange"/>. /// </summary> /// <param name="searchRange"> /// The range to search in. @@ -148,7 +148,7 @@ namespace Microsoft.VisualStudio.Text.Operations /// The options to use while performing the search operation. /// </param> /// <returns> - /// An <see cref="IEnumerable{SnapshotSpan}"/> containing all occurences of the <paramref name="searchPattern"/>. + /// An <see cref="IEnumerable{SnapshotSpan}"/> containing all occurrences of the <paramref name="searchPattern"/>. /// </returns> /// <remarks> /// This method is safe to execute on any thread. @@ -156,7 +156,7 @@ namespace Microsoft.VisualStudio.Text.Operations IEnumerable<SnapshotSpan> FindAll(SnapshotSpan searchRange, string searchPattern, FindOptions options); /// <summary> - /// Finds all occurences of the <paramref name="searchPattern"/> in <paramref name="searchRange"/> starting from + /// Finds all occurrences of the <paramref name="searchPattern"/> in <paramref name="searchRange"/> starting from /// <paramref name="startingPosition"/>. /// </summary> /// <param name="searchRange"> @@ -172,7 +172,7 @@ namespace Microsoft.VisualStudio.Text.Operations /// The options to use while performing the search operation. /// </param> /// <returns> - /// An <see cref="IEnumerable{SnapshotSpan}"/> containing all occurences of the <paramref name="searchPattern"/>. + /// An <see cref="IEnumerable{SnapshotSpan}"/> containing all occurrences of the <paramref name="searchPattern"/>. /// </returns> /// <remarks> /// This method is safe to execute on any thread. @@ -180,7 +180,7 @@ namespace Microsoft.VisualStudio.Text.Operations IEnumerable<SnapshotSpan> FindAll(SnapshotSpan searchRange, SnapshotPoint startingPosition, string searchPattern, FindOptions options); /// <summary> - /// Searches for all occurences of the <paramref name="searchPattern"/> and calculates all + /// Searches for all occurrences of the <paramref name="searchPattern"/> and calculates all /// the corresponding replacement results for every match according to the <paramref name="replacePattern"/>. /// </summary> /// <param name="searchRange"> diff --git a/src/Text/Def/TextLogic/Navigation/TextExtent.cs b/src/Text/Def/TextLogic/Navigation/TextExtent.cs index 1d416f6..8ca62e4 100644 --- a/src/Text/Def/TextLogic/Navigation/TextExtent.cs +++ b/src/Text/Def/TextLogic/Navigation/TextExtent.cs @@ -4,10 +4,12 @@ // namespace Microsoft.VisualStudio.Text.Operations { +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Represents the extent of a word. /// </summary> public struct TextExtent +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { #region Private Members diff --git a/src/Text/Def/TextLogic/PatternMatching/PatternMatch.cs b/src/Text/Def/TextLogic/PatternMatching/PatternMatch.cs index dd907d0..f48141d 100644 --- a/src/Text/Def/TextLogic/PatternMatching/PatternMatch.cs +++ b/src/Text/Def/TextLogic/PatternMatching/PatternMatch.cs @@ -4,7 +4,11 @@ using System.Linq; namespace Microsoft.VisualStudio.Text.PatternMatching { +#pragma warning disable CA1815 // Override equals and operator equals on value types +#pragma warning disable CA1036 // Override methods on comparable types public struct PatternMatch : IComparable<PatternMatch> +#pragma warning restore CA1036 // Override methods on comparable types +#pragma warning restore CA1815 // Override equals and operator equals on value types { /// <summary> /// True if this was a case sensitive match. diff --git a/src/Text/Def/TextLogic/PatternMatching/PatternMatcherCreationOptions.cs b/src/Text/Def/TextLogic/PatternMatching/PatternMatcherCreationOptions.cs index 5693a3f..dbe5236 100644 --- a/src/Text/Def/TextLogic/PatternMatching/PatternMatcherCreationOptions.cs +++ b/src/Text/Def/TextLogic/PatternMatching/PatternMatcherCreationOptions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Microsoft.VisualStudio.Text.PatternMatching @@ -11,12 +12,17 @@ namespace Microsoft.VisualStudio.Text.PatternMatching /// <summary> /// Used to tailor character comparisons to the correct culture. /// </summary> +#pragma warning disable CA1051 // Do not declare visible instance fields + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "CultureInfo is immutable")] public readonly CultureInfo CultureInfo; +#pragma warning restore CA1051 // Do not declare visible instance fields /// <summary> /// A set of biniary options, used to control options like case-sensitivity. /// </summary> +#pragma warning disable CA1051 // Do not declare visible instance fields public readonly PatternMatcherCreationFlags Flags; +#pragma warning disable CA1051 // Do not declare visible instance fields /// <summary> /// Characters that should be considered as describing a container/contained boundary. When matching types, this can be the '.' character @@ -25,7 +31,10 @@ namespace Microsoft.VisualStudio.Text.PatternMatching /// /// <see langword="null"/> signifies no characters are container boundaries. /// </summary> +#pragma warning disable CA1051 // Do not declare visible instance fields + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "Cannot make a breaking change")] public readonly IReadOnlyCollection<char> ContainerSplitCharacters; +#pragma warning restore CA1051 // Do not declare visible instance fields /// <summary> /// Creates an instance of <see cref="PatternMatcherCreationOptions"/>. diff --git a/src/Text/Def/TextLogic/Tagging/BatchedTagsChangedEventArgs.cs b/src/Text/Def/TextLogic/Tagging/BatchedTagsChangedEventArgs.cs index df661ac..bc865ff 100644 --- a/src/Text/Def/TextLogic/Tagging/BatchedTagsChangedEventArgs.cs +++ b/src/Text/Def/TextLogic/Tagging/BatchedTagsChangedEventArgs.cs @@ -24,7 +24,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public BatchedTagsChangedEventArgs(IList<IMappingSpan> spans) { if (spans == null) - throw new ArgumentNullException("spans"); + throw new ArgumentNullException(nameof(spans)); //Make a copy of spans so we don't need to worry about it changing. _spans = new ReadOnlyCollection<IMappingSpan>(new List<IMappingSpan>(spans)); diff --git a/src/Text/Def/TextLogic/Tagging/ITag.cs b/src/Text/Def/TextLogic/Tagging/ITag.cs index 21fe434..a8696f8 100644 --- a/src/Text/Def/TextLogic/Tagging/ITag.cs +++ b/src/Text/Def/TextLogic/Tagging/ITag.cs @@ -4,10 +4,12 @@ // namespace Microsoft.VisualStudio.Text.Tagging { +#pragma warning disable CA1040 // Avoid empty interfaces /// <summary> /// The base interface of all tags. /// </summary> public interface ITag +#pragma warning restore CA1040 // Avoid empty interfaces { } -}
\ No newline at end of file +} diff --git a/src/Text/Def/TextLogic/Tagging/ITagAggregator.cs b/src/Text/Def/TextLogic/Tagging/ITagAggregator.cs index e54c146..41ec3bd 100644 --- a/src/Text/Def/TextLogic/Tagging/ITagAggregator.cs +++ b/src/Text/Def/TextLogic/Tagging/ITagAggregator.cs @@ -72,7 +72,7 @@ namespace Microsoft.VisualStudio.Text.Tagging /// <para> /// This is a batched version of the TagsChanged event. One or more TagsChanged events /// are accumulated and then raised as a single BatchedTagsChanged event on idle using the - /// <see cref="T:System.Windows.Threading.Dispatcher.CurrentDispatcher" /> that was active when the ITagAggregator was + /// Dispatcher.CurrentDispatcher that was active when the ITagAggregator was /// created. /// </para> /// <para> diff --git a/src/Text/Def/TextLogic/Tagging/MappingTagSpan.cs b/src/Text/Def/TextLogic/Tagging/MappingTagSpan.cs index 562999c..350e95a 100644 --- a/src/Text/Def/TextLogic/Tagging/MappingTagSpan.cs +++ b/src/Text/Def/TextLogic/Tagging/MappingTagSpan.cs @@ -55,9 +55,9 @@ namespace Microsoft.VisualStudio.Text.Tagging public MappingTagSpan(IMappingSpan span, T tag) { if (span == null) - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); if (tag == null) - throw new ArgumentNullException("tag"); + throw new ArgumentNullException(nameof(tag)); Span = span; Tag = tag; diff --git a/src/Text/Def/TextLogic/Tagging/SimpleTagger.cs b/src/Text/Def/TextLogic/Tagging/SimpleTagger.cs index 40048ea..1ffe420 100644 --- a/src/Text/Def/TextLogic/Tagging/SimpleTagger.cs +++ b/src/Text/Def/TextLogic/Tagging/SimpleTagger.cs @@ -95,9 +95,9 @@ namespace Microsoft.VisualStudio.Text.Tagging public TrackingTagSpan<T> CreateTagSpan(ITrackingSpan span, T tag) { if (span == null) - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); if (tag == null) - throw new ArgumentNullException("tag"); + throw new ArgumentNullException(nameof(tag)); var tagSpan = new TrackingTagSpan<T>(span, tag); @@ -127,7 +127,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public bool RemoveTagSpan(TrackingTagSpan<T> tagSpan) { if (tagSpan == null) - throw new ArgumentNullException("tagSpan"); + throw new ArgumentNullException(nameof(tagSpan)); bool removed = false; @@ -162,7 +162,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public int RemoveTagSpans(Predicate<TrackingTagSpan<T>> match) { if (match == null) - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); int removedCount = 0; @@ -283,7 +283,7 @@ namespace Microsoft.VisualStudio.Text.Tagging { if (tagger == null) { - throw new ArgumentNullException("tagger"); + throw new ArgumentNullException(nameof(tagger)); } _tagger = tagger; _tagger.StartBatch(); diff --git a/src/Text/Def/TextLogic/Tagging/TagSpan.cs b/src/Text/Def/TextLogic/Tagging/TagSpan.cs index cd105aa..5636a7c 100644 --- a/src/Text/Def/TextLogic/Tagging/TagSpan.cs +++ b/src/Text/Def/TextLogic/Tagging/TagSpan.cs @@ -55,7 +55,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public TagSpan(SnapshotSpan span, T tag) { if (tag == null) - throw new ArgumentNullException("tag"); + throw new ArgumentNullException(nameof(tag)); Span = span; Tag = tag; diff --git a/src/Text/Def/TextLogic/Tagging/TagTypeAttribute.cs b/src/Text/Def/TextLogic/Tagging/TagTypeAttribute.cs index 228e1ba..3c5539c 100644 --- a/src/Text/Def/TextLogic/Tagging/TagTypeAttribute.cs +++ b/src/Text/Def/TextLogic/Tagging/TagTypeAttribute.cs @@ -24,9 +24,9 @@ namespace Microsoft.VisualStudio.Text.Tagging public TagTypeAttribute(Type tagType) { if (tagType == null) - throw new ArgumentNullException("tagType"); + throw new ArgumentNullException(nameof(tagType)); if (!typeof(ITag).IsAssignableFrom(tagType)) - throw new ArgumentException("Given type must derive from ITag", "tagType"); + throw new ArgumentException("Given type must derive from ITag", nameof(tagType)); this.type = tagType; } diff --git a/src/Text/Def/TextLogic/Tagging/TagsChangedEventArgs.cs b/src/Text/Def/TextLogic/Tagging/TagsChangedEventArgs.cs index 78f33b2..dd1055a 100644 --- a/src/Text/Def/TextLogic/Tagging/TagsChangedEventArgs.cs +++ b/src/Text/Def/TextLogic/Tagging/TagsChangedEventArgs.cs @@ -24,7 +24,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public TagsChangedEventArgs(IMappingSpan span) { if (span == null) - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); Span = span; } diff --git a/src/Text/Def/TextLogic/Tagging/TrackingTagSpan.cs b/src/Text/Def/TextLogic/Tagging/TrackingTagSpan.cs index 5ebfe29..9fca753 100644 --- a/src/Text/Def/TextLogic/Tagging/TrackingTagSpan.cs +++ b/src/Text/Def/TextLogic/Tagging/TrackingTagSpan.cs @@ -32,9 +32,9 @@ namespace Microsoft.VisualStudio.Text.Tagging public TrackingTagSpan(ITrackingSpan span, T tag) { if (span == null) - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); if (tag == null) - throw new ArgumentNullException("tag"); + throw new ArgumentNullException(nameof(tag)); Span = span; Tag = tag; diff --git a/src/Text/Def/TextLogic/Tags/ClassificationTag.cs b/src/Text/Def/TextLogic/Tags/ClassificationTag.cs index d8e37c5..32994ab 100644 --- a/src/Text/Def/TextLogic/Tags/ClassificationTag.cs +++ b/src/Text/Def/TextLogic/Tags/ClassificationTag.cs @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public ClassificationTag(IClassificationType type) { if (type == null) - throw new ArgumentNullException("type"); + throw new ArgumentNullException(nameof(type)); ClassificationType = type; } diff --git a/src/Text/Def/TextLogic/Tags/UrlTag.cs b/src/Text/Def/TextLogic/Tags/UrlTag.cs index 83e086e..b8ed521 100644 --- a/src/Text/Def/TextLogic/Tags/UrlTag.cs +++ b/src/Text/Def/TextLogic/Tags/UrlTag.cs @@ -20,7 +20,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public UrlTag(Uri url) { if (url == null) - throw new ArgumentNullException("url"); + throw new ArgumentNullException(nameof(url)); Url = url; } diff --git a/src/Text/Def/TextLogic/TextLogic.csproj b/src/Text/Def/TextLogic/TextLogic.csproj index f7b3b78..15d863a 100644 --- a/src/Text/Def/TextLogic/TextLogic.csproj +++ b/src/Text/Def/TextLogic/TextLogic.csproj @@ -3,8 +3,6 @@ <AssemblyName>Microsoft.VisualStudio.Text.Logic</AssemblyName> <RootNamespace>$(AssemblyName)</RootNamespace> <TargetFramework>net46</TargetFramework> - <NonShipping>false</NonShipping> - <IsPackable>true</IsPackable> <PushToPublicFeed>true</PushToPublicFeed> <NoWarn>649;436;$(NoWarn)</NoWarn> <AssemblyAttributeClsCompliant>true</AssemblyAttributeClsCompliant> @@ -15,7 +13,7 @@ <Reference Include="System.Core" /> </ItemGroup> <ItemGroup> - <PackageReference Include="System.Collections.Immutable" Version="$(SystemCollectionsImmutableVersion)" /> + <PackageReference Include="System.Collections.Immutable" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\TextData\TextData.csproj" /> diff --git a/src/Text/Def/TextLogic/TextModel/VirtualSnapshotPoint.cs b/src/Text/Def/TextLogic/TextModel/VirtualSnapshotPoint.cs index 7828fb4..d4d7762 100644 --- a/src/Text/Def/TextLogic/TextModel/VirtualSnapshotPoint.cs +++ b/src/Text/Def/TextLogic/TextModel/VirtualSnapshotPoint.cs @@ -6,10 +6,12 @@ namespace Microsoft.VisualStudio.Text { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Represents a <see cref="SnapshotPoint"/> that may have virtual spaces. /// </summary> public struct VirtualSnapshotPoint : IComparable<VirtualSnapshotPoint> +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { private readonly SnapshotPoint _position; private readonly int _virtualSpaces; @@ -48,7 +50,7 @@ namespace Microsoft.VisualStudio.Text public VirtualSnapshotPoint(SnapshotPoint position, int virtualSpaces) { if (virtualSpaces < 0) - throw new ArgumentOutOfRangeException("virtualSpaces"); + throw new ArgumentOutOfRangeException(nameof(virtualSpaces)); //Treat trying to set virtual spaces in the middle of a line as a soft error. It is easy to do if some 3rd party does an unexpected edit on a text change //and setting virtualSpaces to 0 is a reasonable fallback behavior. @@ -73,9 +75,9 @@ namespace Microsoft.VisualStudio.Text public VirtualSnapshotPoint(ITextSnapshotLine line, int offset) { if (line == null) - throw new ArgumentNullException("line"); + throw new ArgumentNullException(nameof(line)); if (offset < 0) - throw new ArgumentOutOfRangeException("offset"); + throw new ArgumentOutOfRangeException(nameof(offset)); if (offset <= line.Length) { @@ -152,12 +154,12 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (snapshot.Version.VersionNumber < _position.Snapshot.Version.VersionNumber) { - throw new ArgumentException("VirtualSnapshotPoints can only be translated to later snapshots", "snapshot"); + throw new ArgumentException("VirtualSnapshotPoints can only be translated to later snapshots", nameof(snapshot)); } else if (snapshot == _position.Snapshot) { diff --git a/src/Text/Def/TextLogic/TextModel/VirtualSnapshotSpan.cs b/src/Text/Def/TextLogic/TextModel/VirtualSnapshotSpan.cs index c8478f7..03c09de 100644 --- a/src/Text/Def/TextLogic/TextModel/VirtualSnapshotSpan.cs +++ b/src/Text/Def/TextLogic/TextModel/VirtualSnapshotSpan.cs @@ -6,10 +6,12 @@ namespace Microsoft.VisualStudio.Text { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Represents two <see cref="VirtualSnapshotPoint" />s /// </summary> public struct VirtualSnapshotSpan +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { private readonly VirtualSnapshotPoint _start; private readonly VirtualSnapshotPoint _end; @@ -47,7 +49,7 @@ namespace Microsoft.VisualStudio.Text } if (end < start) { - throw new ArgumentOutOfRangeException("end"); + throw new ArgumentOutOfRangeException(nameof(end)); } _start = start; @@ -290,12 +292,12 @@ namespace Microsoft.VisualStudio.Text { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (snapshot.Version.VersionNumber < _start.Position.Snapshot.Version.VersionNumber) { - throw new ArgumentException("VirtualSnapshotSpans can only be translated to later snapshots", "snapshot"); + throw new ArgumentException("VirtualSnapshotSpans can only be translated to later snapshots", nameof(snapshot)); } else if (snapshot == _start.Position.Snapshot) { diff --git a/src/Text/Def/TextUI/Adornments/ToolTipService/IViewElementFactoryService.cs b/src/Text/Def/TextUI/Adornments/ToolTipService/IViewElementFactoryService.cs index c5d324a..0ed776c 100644 --- a/src/Text/Def/TextUI/Adornments/ToolTipService/IViewElementFactoryService.cs +++ b/src/Text/Def/TextUI/Adornments/ToolTipService/IViewElementFactoryService.cs @@ -11,7 +11,7 @@ /// This is a MEF service that can be obtained via the <see cref="ImportAttribute"/> in a MEF exported class. /// </para> /// <para> - /// The editor supports <see cref="ClassifiedTextElement"/>s, <see cref="ImageElement"/>s, and <see cref="object"/> + /// The editor supports <see cref="ClassifiedTextElement"/>s, <see cref="ContainerElement"/>, <see cref="ImageElement"/>s, and <see cref="object"/> /// on all platforms. Text and image elements are converted to colorized text and images respectively and /// other objects are displayed as the <see cref="string"/> returned by <see cref="object.ToString()"/> /// unless an extender exports a <see cref="IViewElementFactory"/> for that type. diff --git a/src/Text/Def/TextUI/Adornments/ToolTipService/ToolTipParameters.cs b/src/Text/Def/TextUI/Adornments/ToolTipService/ToolTipParameters.cs index daad688..87bf4f4 100644 --- a/src/Text/Def/TextUI/Adornments/ToolTipService/ToolTipParameters.cs +++ b/src/Text/Def/TextUI/Adornments/ToolTipService/ToolTipParameters.cs @@ -1,6 +1,7 @@ namespace Microsoft.VisualStudio.Text.Adornments { using System; + using System.Diagnostics.CodeAnalysis; /// <summary> /// Determines behavior for a <see cref="IToolTipPresenter"/>. @@ -12,6 +13,7 @@ /// <summary> /// Default options for a mouse tracking tooltip. /// </summary> + [SuppressMessage("Microsoft.Security", "CA2104", Justification = "Type is readonly")] public static readonly ToolTipParameters Default = new ToolTipParameters(); /// <summary> diff --git a/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ContainerElementStyle.cs b/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ContainerElementStyle.cs index d9f8b22..69c3600 100644 --- a/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ContainerElementStyle.cs +++ b/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ContainerElementStyle.cs @@ -1,18 +1,28 @@ namespace Microsoft.VisualStudio.Text.Adornments { + using System; + +#pragma warning disable CA1714 // Flags enums should have plural names /// <summary> /// The layout style for a <see cref="ContainerElement"/>. /// </summary> + [Flags] public enum ContainerElementStyle +#pragma warning restore CA1714 // Flags enums should have plural names { /// <summary> /// Contents are end-to-end, and wrapped when the control becomes too wide. /// </summary> - Wrapped, + Wrapped = 0b_0000, /// <summary> /// Contents are stacked vertically. /// </summary> - Stacked + Stacked = 0b_0001, + + /// <summary> + /// Additional padding above and below content. + /// </summary> + VerticalPadding = 0b_0010 } } diff --git a/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ImageElement.cs b/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ImageElement.cs index f4c477c..079449a 100644 --- a/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ImageElement.cs +++ b/src/Text/Def/TextUI/Adornments/ToolTipService/ViewElementFactories/ImageElement.cs @@ -1,29 +1,47 @@ namespace Microsoft.VisualStudio.Text.Adornments { + using System; using Microsoft.VisualStudio.Core.Imaging; /// <summary> - /// Represents an image in an <see cref="IToolTipService"/> <see cref="IToolTipPresenter"/>. + /// Represents cross platform compatible image. /// </summary> /// /// <remarks> /// <see cref="ImageElement"/>s should be constructed with <see cref="Microsoft.VisualStudio.Core.Imaging.ImageId"/>s /// that correspond to an image on that platform. /// </remarks> - public sealed class ImageElement + public class ImageElement { /// <summary> /// Creates a new instance of an image element. /// </summary> - /// <param name="iamgeId"> A unique identifier for an image.</param> + /// <param name="imageId"> A unique identifier for an image</param> public ImageElement(ImageId imageId) { this.ImageId = imageId; } /// <summary> + /// Creates a new instance of an image element. + /// </summary> + /// <param name="imageId"> A unique identifier for an image</param> + /// <param name="automationName"> Localized description of the image</param> + public ImageElement(ImageId imageId, string automationName) + : this(imageId) + { + // Let's allow empty strings, as long as they are not null references + this.AutomationName = automationName ?? throw new ArgumentNullException(nameof(automationName)); + } + + /// <summary> /// A unique identifier for an image. /// </summary> public ImageId ImageId { get; } + + /// <summary> + /// Localized description of the image + /// </summary> + public string AutomationName { get; } } } diff --git a/src/Text/Def/TextUI/Editor/CaretPosition.cs b/src/Text/Def/TextUI/Editor/CaretPosition.cs index 8b51e56..0297473 100644 --- a/src/Text/Def/TextUI/Editor/CaretPosition.cs +++ b/src/Text/Def/TextUI/Editor/CaretPosition.cs @@ -6,10 +6,12 @@ namespace Microsoft.VisualStudio.Text.Editor { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Represents the position of a caret in an <see cref="ITextView"/>. /// </summary> public struct CaretPosition +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { #region Private Members VirtualSnapshotPoint _bufferPosition; @@ -28,7 +30,7 @@ namespace Microsoft.VisualStudio.Text.Editor { if (mappingPoint == null) { - throw new ArgumentNullException("mappingPoint"); + throw new ArgumentNullException(nameof(mappingPoint)); } _bufferPosition = bufferPosition; diff --git a/src/Text/Def/Internal/TextUI/ITextView2.cs b/src/Text/Def/TextUI/Editor/ITextView2.cs index 38ff665..ed797ee 100644 --- a/src/Text/Def/Internal/TextUI/ITextView2.cs +++ b/src/Text/Def/TextUI/Editor/ITextView2.cs @@ -1,39 +1,39 @@ -// +// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // -// This file contain internal APIs that are subject to change without notice. -// Use at your own risk. -// + +using System; + namespace Microsoft.VisualStudio.Text.Editor { - using System; - /// <summary> - /// An extension of the ITextView that exposes some internal hooks. + /// Extensions to <see cref="ITextView"/>, augmenting functionality. For every member here + /// there should also be an extension method in <see cref="TextViewExtensions"/>. /// </summary> public interface ITextView2 : ITextView { /// <summary> - /// The MaxTextRightCoordinate of the view based only on the text contained in the view. + /// Determines whether the view is in the process of being laid out or is preparing to be laid out. /// </summary> - double RawMaxTextRightCoordinate + /// <remarks> + /// As opposed to <see cref="ITextView.InLayout"/>, it is safe to get the <see cref="ITextView.TextViewLines"/> + /// but attempting to queue another layout will cause a reentrant layout exception. + /// </remarks> + bool InOuterLayout { get; } /// <summary> - /// The minimum value for the view's MaxTextRightCoordinate. + /// Gets an object for managing selections within the view. /// </summary> - /// <remarks> - /// If setting this value changes the view's MaxTextRightCoordinate, the view will raise a layout changed event. - /// </remarks> - double MinMaxTextRightCoordinate + IMultiSelectionBroker MultiSelectionBroker { get; - set; } + /// <summary> /// Raised whenever the view's MaxTextRightCoordinate is changed. /// </summary> diff --git a/src/Text/Def/TextUI/Editor/ITextViewRoleSet.cs b/src/Text/Def/TextUI/Editor/ITextViewRoleSet.cs index b3fba65..ae66092 100644 --- a/src/Text/Def/TextUI/Editor/ITextViewRoleSet.cs +++ b/src/Text/Def/TextUI/Editor/ITextViewRoleSet.cs @@ -7,10 +7,12 @@ namespace Microsoft.VisualStudio.Text.Editor using System; using System.Collections.Generic; +#pragma warning disable CA1710 // Identifiers should have correct suffix /// <summary> /// Set of text view roles. /// </summary> public interface ITextViewRoleSet : IEnumerable<string> +#pragma warning restore CA1710 // Identifiers should have correct suffix { /// <summary> /// Compute whether the given text view role is a member of the set. diff --git a/src/Text/Def/TextUI/Editor/MarginContainerAttribute.cs b/src/Text/Def/TextUI/Editor/MarginContainerAttribute.cs index 815b804..aa89687 100644 --- a/src/Text/Def/TextUI/Editor/MarginContainerAttribute.cs +++ b/src/Text/Def/TextUI/Editor/MarginContainerAttribute.cs @@ -26,7 +26,7 @@ namespace Microsoft.VisualStudio.Text.Editor public MarginContainerAttribute(string marginContainer) { if (marginContainer == null) - throw new ArgumentNullException("marginContainer"); + throw new ArgumentNullException(nameof(marginContainer)); if (marginContainer.Length == 0) throw new ArgumentException("marginContainer is an empty string."); diff --git a/src/Text/Def/TextUI/Editor/MouseHoverEventArgs.cs b/src/Text/Def/TextUI/Editor/MouseHoverEventArgs.cs index 3a1b1da..1a94192 100644 --- a/src/Text/Def/TextUI/Editor/MouseHoverEventArgs.cs +++ b/src/Text/Def/TextUI/Editor/MouseHoverEventArgs.cs @@ -33,12 +33,12 @@ namespace Microsoft.VisualStudio.Text.Editor public MouseHoverEventArgs(ITextView view, int position, IMappingPoint textPosition) { if (view == null) - throw new ArgumentNullException("view"); + throw new ArgumentNullException(nameof(view)); #pragma warning suppress 56506 // ToDo: Add a comment on why it is not necessary to check view.TextSnapshot if ((position < 0) || (position > view.TextSnapshot.Length)) // Allow positions at the end of the file - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); if (textPosition == null) - throw new ArgumentNullException("textPosition"); + throw new ArgumentNullException(nameof(textPosition)); // we could be very paranoid and check: //if (textPosition.AnchorBuffer != view.TextBuffer) // throw new ArgumentException(); diff --git a/src/Text/Def/TextUI/Editor/ReplacesAttribute.cs b/src/Text/Def/TextUI/Editor/ReplacesAttribute.cs index 7b9b738..a842509 100644 --- a/src/Text/Def/TextUI/Editor/ReplacesAttribute.cs +++ b/src/Text/Def/TextUI/Editor/ReplacesAttribute.cs @@ -29,7 +29,7 @@ namespace Microsoft.VisualStudio.Text.Editor public ReplacesAttribute(string replaces) { if (replaces == null) - throw new ArgumentNullException("replaces"); + throw new ArgumentNullException(nameof(replaces)); if (replaces.Length == 0) throw new ArgumentException("replaces is an empty string."); @@ -47,4 +47,4 @@ namespace Microsoft.VisualStudio.Text.Editor } } } -}
\ No newline at end of file +} diff --git a/src/Text/Def/TextUI/Editor/TextViewCreatedEventArgs.cs b/src/Text/Def/TextUI/Editor/TextViewCreatedEventArgs.cs index 21527c3..009cf3d 100644 --- a/src/Text/Def/TextUI/Editor/TextViewCreatedEventArgs.cs +++ b/src/Text/Def/TextUI/Editor/TextViewCreatedEventArgs.cs @@ -24,7 +24,7 @@ namespace Microsoft.VisualStudio.Text.Editor { if (textView == null) { - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); } TextView = textView; } diff --git a/src/Text/Def/TextUI/Editor/TextViewExtensions.cs b/src/Text/Def/TextUI/Editor/TextViewExtensions.cs new file mode 100644 index 0000000..ae2cfab --- /dev/null +++ b/src/Text/Def/TextUI/Editor/TextViewExtensions.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System; + +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Utility <see cref="ITextView"/> extension methods. + /// </summary> + public static class TextViewExtensions + { + /// <summary> + /// Gets whether given <see cref="ITextView"/> is embedded in another <see cref="ITextView"/>. + /// </summary> + /// <param name="textView">The <see cref="ITextView"/> for which to determine if it's embedded.</param> + /// <returns><c>true</c> if given <see cref="ITextView"/> is embedded, <c>false</c> otherwise.</returns> + public static bool IsEmbeddedTextView(this ITextView textView) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + return textView.Roles.Contains(PredefinedTextViewRoles.EmbeddedPeekTextView); + } + + /// <summary> + /// Gets containing <see cref="ITextView"/> for given embedded <see cref="ITextView"/>. + /// </summary> + /// <param name="textView">An embedded <see cref="ITextView"/>, for which to get a containing <see cref="ITextView"/>.</param> + /// <param name="containingTextView">A <see cref="ITextView"/> that contains given <see cref="ITextView"/> or null if + /// given <see cref="ITextView"/> is not embedded in another <see cref="ITextView"/>.</param> + /// <returns><c>true</c> if containing <see cref="ITextView"/> was found, <c>false</c> otherwise.</returns> + public static bool TryGetContainingTextView(this ITextView textView, out ITextView containingTextView) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + // Extra scrutiny because Peek is on a different layer and we cannot just rely on it doing the right thing + if (textView.IsEmbeddedTextView()) + { + bool success = textView.Properties.TryGetProperty("PeekContainingTextView", out containingTextView); + if (!success || containingTextView == null) + { + throw new InvalidOperationException("Unexpected failure to obtain containing text view of an embedded text view."); + } + + return true; + } + + containingTextView = null; + return false; + } + + /// <summary> + /// Determines whether a view is in the process of being laid out or is preparing to be laid out. + /// </summary> + /// <param name="textView">The <see cref="ITextView"/> to check.</param> + /// <remarks> + /// As opposed to <see cref="ITextView.InLayout"/>, it is safe to get the <see cref="ITextView.TextViewLines"/> + /// but attempting to queue another layout will cause a reentrant layout exception. + /// </remarks> + public static bool GetInOuterLayout(this ITextView textView) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + return ((ITextView2)textView).InOuterLayout; + } + + /// <summary> + /// Gets an object for managing selections within the view. + /// </summary> + public static IMultiSelectionBroker GetMultiSelectionBroker(this ITextView textView) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + return ((ITextView2)textView).MultiSelectionBroker; + } + } +} diff --git a/src/Text/Def/TextUI/Editor/TextViewLayoutChangedEventArgs.cs b/src/Text/Def/TextUI/Editor/TextViewLayoutChangedEventArgs.cs index e26769c..9bb0376 100644 --- a/src/Text/Def/TextUI/Editor/TextViewLayoutChangedEventArgs.cs +++ b/src/Text/Def/TextUI/Editor/TextViewLayoutChangedEventArgs.cs @@ -63,13 +63,13 @@ namespace Microsoft.VisualStudio.Text.Editor IList<ITextViewLine> translatedLines) { if (oldState == null) - throw new ArgumentNullException("oldState"); + throw new ArgumentNullException(nameof(oldState)); if (newState == null) - throw new ArgumentNullException("newState"); + throw new ArgumentNullException(nameof(newState)); if (translatedLines == null) - throw new ArgumentNullException("translatedLines"); + throw new ArgumentNullException(nameof(translatedLines)); if (newOrReformattedLines == null) - throw new ArgumentNullException("newOrReformattedLines"); + throw new ArgumentNullException(nameof(newOrReformattedLines)); _oldViewState = oldState; _newViewState = newState; diff --git a/src/Text/Def/TextUI/Editor/TextViewRoleAttribute.cs b/src/Text/Def/TextUI/Editor/TextViewRoleAttribute.cs index e17c675..e630939 100644 --- a/src/Text/Def/TextUI/Editor/TextViewRoleAttribute.cs +++ b/src/Text/Def/TextUI/Editor/TextViewRoleAttribute.cs @@ -23,7 +23,7 @@ namespace Microsoft.VisualStudio.Text.Editor { if (string.IsNullOrEmpty(role)) { - throw new ArgumentNullException("role"); + throw new ArgumentNullException(nameof(role)); } this.roles = role; } @@ -36,4 +36,4 @@ namespace Microsoft.VisualStudio.Text.Editor get { return this.roles; } } } -}
\ No newline at end of file +} diff --git a/src/Text/Def/TextUI/Editor/ViewRelativePosition.cs b/src/Text/Def/TextUI/Editor/ViewRelativePosition.cs index 41f2861..78a1e82 100644 --- a/src/Text/Def/TextUI/Editor/ViewRelativePosition.cs +++ b/src/Text/Def/TextUI/Editor/ViewRelativePosition.cs @@ -16,6 +16,6 @@ namespace Microsoft.VisualStudio.Text.Editor /// <summary> /// The offset with respect to the bottom of the view. /// </summary> - Bottom + Bottom } -}
\ No newline at end of file +} diff --git a/src/Text/Def/TextUI/Editor/ViewState.cs b/src/Text/Def/TextUI/Editor/ViewState.cs index 30d677c..505bb86 100644 --- a/src/Text/Def/TextUI/Editor/ViewState.cs +++ b/src/Text/Def/TextUI/Editor/ViewState.cs @@ -61,7 +61,7 @@ namespace Microsoft.VisualStudio.Text.Editor public ViewState(ITextView view, double effectiveViewportWidth, double effectiveViewportHeight) { if (view == null) - throw new ArgumentNullException("view"); + throw new ArgumentNullException(nameof(view)); this.ViewportLeft = view.ViewportLeft; this.ViewportTop = view.ViewportTop; diff --git a/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs b/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs index 0255860..b35cb6f 100644 --- a/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs +++ b/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs @@ -23,7 +23,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsVirtualSpaceEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewOptions.UseVirtualSpaceId); } @@ -36,7 +36,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsOverwriteModeEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewOptions.OverwriteModeId); } @@ -49,7 +49,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsAutoScrollEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewOptions.AutoScrollId); } @@ -62,7 +62,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static WordWrapStyles WordWrapStyle(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<WordWrapStyles>(DefaultTextViewOptions.WordWrapStyleId); } @@ -75,7 +75,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsVisibleWhitespaceEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewOptions.UseVisibleWhitespaceId); } @@ -89,7 +89,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool DoesViewProhibitUserInput(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewOptions.ViewProhibitUserInputId); } @@ -102,7 +102,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsOutliningUndoEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId); } @@ -115,7 +115,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsDragDropEditingEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue(DefaultTextViewOptions.DragDropEditingId); } @@ -128,7 +128,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsViewportLeftClipped(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewOptions.IsViewportLeftClippedId); } @@ -150,7 +150,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsVerticalScrollBarEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.VerticalScrollBarId); } @@ -163,7 +163,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsHorizontalScrollBarEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.HorizontalScrollBarId); } @@ -176,7 +176,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsGlyphMarginEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.GlyphMarginId); } @@ -189,7 +189,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsSelectionMarginEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.SelectionMarginId); } @@ -202,7 +202,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsLineNumberMarginEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.LineNumberMarginId); } @@ -215,7 +215,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsChangeTrackingEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.ChangeTrackingId); } @@ -229,7 +229,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsOutliningMarginEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.OutliningMarginId); } @@ -242,7 +242,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods public static bool IsZoomControlEnabled(this IEditorOptions options) { if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); return options.GetOptionValue<bool>(DefaultTextViewHostOptions.ZoomControlId); } @@ -254,7 +254,7 @@ namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods /// <returns><c>true</c> if the editor is in either "Extra Contrast" or "High Contrast" modes, otherwise <c>false</c>.</returns> public static bool IsInContrastMode(this IEditorOptions options) { - if(options == null) + if (options == null) { throw new ArgumentNullException(nameof(options)); } @@ -336,6 +336,18 @@ namespace Microsoft.VisualStudio.Text.Editor public const string ShowBlockStructureName = "TextView/ShowBlockStructure"; /// <summary> + /// Should the carets be rendered. + /// </summary> + public static readonly EditorOptionKey<bool> ShouldCaretsBeRenderedId = new EditorOptionKey<bool>(ShouldCaretsBeRenderedName); + public const string ShouldCaretsBeRenderedName = "TextView/ShouldCaretsBeRendered"; + + /// <summary> + /// Should the selections be rendered. + /// </summary> + public static readonly EditorOptionKey<bool> ShouldSelectionsBeRenderedId = new EditorOptionKey<bool>(ShouldSelectionsBeRenderedName); + public const string ShouldSelectionsBeRenderedName = "TextView/ShouldSelectionsBeRendered"; + + /// <summary> /// Whether or not to replace the coding characters and special symbols (such as (,),{,},etc.) with their textual representation /// for automated objects to produce friendly text for screen readers. /// </summary> @@ -365,6 +377,12 @@ namespace Microsoft.VisualStudio.Text.Editor /// </summary> public const string BraceCompletionEnabledOptionName = "BraceCompletion/Enabled"; public readonly static EditorOptionKey<bool> BraceCompletionEnabledOptionId = new EditorOptionKey<bool>(BraceCompletionEnabledOptionName); + + /// <summary> + /// Defines how wide the caret should be rendered. This is typically used to support accessibility requirements. + /// </summary> + public const string CaretWidthOptionName = "TextView/CaretWidth"; + public readonly static EditorOptionKey<double> CaretWidthId = new EditorOptionKey<double>(CaretWidthOptionName); #endregion } @@ -685,6 +703,42 @@ namespace Microsoft.VisualStudio.Text.Editor } /// <summary> + /// Defines the Should Carets Be Rendered option. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultTextViewOptions.ShouldCaretsBeRenderedName)] + public sealed class ShouldCaretsBeRendered : ViewOptionDefinition<bool> + { + /// <summary> + /// Gets the default value, which is <c>true</c>. + /// </summary> + public override bool Default { get { return true; } } + + /// <summary> + /// Gets the default text view host value. + /// </summary> + public override EditorOptionKey<bool> Key { get { return DefaultTextViewOptions.ShouldCaretsBeRenderedId; } } + } + + /// <summary> + /// Defines the Should Selection Be Rendered option. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultTextViewOptions.ShouldSelectionsBeRenderedName)] + public sealed class ShouldSelectionsBeRendered : ViewOptionDefinition<bool> + { + /// <summary> + /// Gets the default value, which is <c>true</c>. + /// </summary> + public override bool Default { get { return true; } } + + /// <summary> + /// Gets the default text view host value. + /// </summary> + public override EditorOptionKey<bool> Key { get { return DefaultTextViewOptions.ShouldSelectionsBeRenderedId; } } + } + + /// <summary> /// Defines the option to enable providing annotated text in automation controls so that screen readers can properly /// read contents of code. /// </summary> @@ -930,4 +984,22 @@ namespace Microsoft.VisualStudio.Text.Editor /// </summary> public override EditorOptionKey<bool> Key { get { return DefaultTextViewOptions.DisplayUrlsAsHyperlinksId; } } } + + /// <summary> + /// The option definition that determines how wide the caret should be rendered. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultTextViewOptions.CaretWidthOptionName)] + public sealed class CaretWidthOption : EditorOptionDefinition<double> + { + /// <summary> + /// Gets the default value <c>1.0</c>. + /// </summary> + public override double Default => 1.0; + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<double> Key => DefaultTextViewOptions.CaretWidthId; + } } diff --git a/src/Text/Def/TextUI/Find/IncrementalSearchResult.cs b/src/Text/Def/TextUI/Find/IncrementalSearchResult.cs index d027b12..3b4f0f4 100644 --- a/src/Text/Def/TextUI/Find/IncrementalSearchResult.cs +++ b/src/Text/Def/TextUI/Find/IncrementalSearchResult.cs @@ -5,6 +5,7 @@ namespace Microsoft.VisualStudio.Text.IncrementalSearch { +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Consolidates the result of an incremental search operation. /// </summary> @@ -14,6 +15,7 @@ namespace Microsoft.VisualStudio.Text.IncrementalSearch /// the position of the first result. /// </remarks> public struct IncrementalSearchResult +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { #region Public Properties @@ -108,4 +110,4 @@ namespace Microsoft.VisualStudio.Text.IncrementalSearch #endregion //Object Overrides } -}
\ No newline at end of file +} diff --git a/src/Text/Def/TextUI/Formatting/LineTransform.cs b/src/Text/Def/TextUI/Formatting/LineTransform.cs index 6f377c0..25a7fe2 100644 --- a/src/Text/Def/TextUI/Formatting/LineTransform.cs +++ b/src/Text/Def/TextUI/Formatting/LineTransform.cs @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.Text.Formatting { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// Represents the transform from a formatted text line to a rendered text line. /// </summary> @@ -27,6 +28,7 @@ namespace Microsoft.VisualStudio.Text.Formatting /// corresponds to one pixel on the display.</para> /// </remarks> public struct LineTransform +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { private readonly double _topSpace; private readonly double _bottomSpace; @@ -87,16 +89,16 @@ namespace Microsoft.VisualStudio.Text.Formatting public LineTransform(double topSpace, double bottomSpace, double verticalScale, double right) { if (double.IsNaN(topSpace)) - throw new ArgumentOutOfRangeException("topSpace"); + throw new ArgumentOutOfRangeException(nameof(topSpace)); if (double.IsNaN(bottomSpace)) - throw new ArgumentOutOfRangeException("bottomSpace"); + throw new ArgumentOutOfRangeException(nameof(bottomSpace)); if ((verticalScale <= 0.0) || double.IsNaN(verticalScale)) - throw new ArgumentOutOfRangeException("verticalScale"); + throw new ArgumentOutOfRangeException(nameof(verticalScale)); if ((right < 0.0) || double.IsNaN(right)) - throw new ArgumentOutOfRangeException("right"); + throw new ArgumentOutOfRangeException(nameof(right)); _topSpace = topSpace; _bottomSpace = bottomSpace; diff --git a/src/Text/Def/TextUI/Formatting/TextAndAdornmentSequenceChangedEventArgs.cs b/src/Text/Def/TextUI/Formatting/TextAndAdornmentSequenceChangedEventArgs.cs index 7a7fee3..e1e7b41 100644 --- a/src/Text/Def/TextUI/Formatting/TextAndAdornmentSequenceChangedEventArgs.cs +++ b/src/Text/Def/TextUI/Formatting/TextAndAdornmentSequenceChangedEventArgs.cs @@ -25,7 +25,7 @@ namespace Microsoft.VisualStudio.Text.Formatting public TextAndAdornmentSequenceChangedEventArgs(IMappingSpan span) { if (span == null) - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); this.Span = span; } diff --git a/src/Text/Def/TextUI/Formatting/TextBounds.cs b/src/Text/Def/TextUI/Formatting/TextBounds.cs index 58113da..519eb3a 100644 --- a/src/Text/Def/TextUI/Formatting/TextBounds.cs +++ b/src/Text/Def/TextUI/Formatting/TextBounds.cs @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.Text.Formatting { using System; +#pragma warning disable CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals /// <summary> /// The bounds of a span of text in a given text line. /// </summary> @@ -27,6 +28,7 @@ namespace Microsoft.VisualStudio.Text.Formatting /// corresponds to one pixel on the display.</para> /// </remarks> public struct TextBounds +#pragma warning restore CA1066 // Type {0} should implement IEquatable<T> because it overrides Equals { #region Private Members private readonly double _leading; @@ -64,17 +66,17 @@ namespace Microsoft.VisualStudio.Text.Formatting { // Validate if (double.IsNaN(leading)) - throw new ArgumentOutOfRangeException("leading"); + throw new ArgumentOutOfRangeException(nameof(leading)); if (double.IsNaN(top)) - throw new ArgumentOutOfRangeException("top"); + throw new ArgumentOutOfRangeException(nameof(top)); if (double.IsNaN(bidiWidth)) - throw new ArgumentOutOfRangeException("bidiWidth"); + throw new ArgumentOutOfRangeException(nameof(bidiWidth)); if (double.IsNaN(height) || (height < 0.0)) - throw new ArgumentOutOfRangeException("height"); + throw new ArgumentOutOfRangeException(nameof(height)); if (double.IsNaN(textTop)) - throw new ArgumentOutOfRangeException("textTop"); + throw new ArgumentOutOfRangeException(nameof(textTop)); if (double.IsNaN(textHeight) || (textHeight < 0.0)) - throw new ArgumentOutOfRangeException("textHeight"); + throw new ArgumentOutOfRangeException(nameof(textHeight)); _leading = leading; _top = top; diff --git a/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs b/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs new file mode 100644 index 0000000..351d733 --- /dev/null +++ b/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// + +using Microsoft.VisualStudio.Text.Formatting; + +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Provides UI specific properties about an <see cref="Selection"/>. + /// </summary> + public abstract class AbstractSelectionPresentationProperties + { + /// <summary> + /// Gets the position that the caret prefers to occupy on a given line. This position may not be honored + /// if virtual space is off and the line is insufficiently long. See <see cref="CaretBounds"/> for the + /// actual location. + /// </summary> + public virtual double PreferredXCoordinate { get; protected set; } + + /// <summary> + /// Gets the position that the caret prefers to occupy vertically in the view. This position is used for operations + /// such as page up/down, but may not be honored if there is an adornment at the desired location. See + /// <see cref="CaretBounds"/> for the actual location. + /// </summary> + public virtual double PreferredYCoordinate { get; protected set; } + + /// <summary> + /// Gets the caret location and size. + /// </summary> + public virtual TextBounds CaretBounds { get; } + + /// <summary> + /// Gets whether the caret is shown in its entirety on the screen. + /// </summary> + public virtual bool IsWithinViewport { get; } + + /// <summary> + /// Gets whether the caret should be rendered as overwrite. + /// </summary> + public virtual bool IsOverwriteMode { get; } + + /// <summary> + /// Gets the <see cref="ITextViewLine"/> that contains the <see cref="Selection.InsertionPoint"/>. + /// </summary> + public virtual ITextViewLine ContainingTextViewLine { get; } + } +} diff --git a/src/Text/Def/TextUI/MultiCaret/IMultiSelectionBroker.cs b/src/Text/Def/TextUI/MultiCaret/IMultiSelectionBroker.cs new file mode 100644 index 0000000..5ca3d8d --- /dev/null +++ b/src/Text/Def/TextUI/MultiCaret/IMultiSelectionBroker.cs @@ -0,0 +1,292 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; + +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Manages all the caret and selecting behavior for an <see cref="ITextView"/>. + /// Handles multiple selections, and box selection. Throughout this namespace carets + /// are considered to be part of Selections, and are represented by <see cref="Selection.InsertionPoint"/>. + /// </summary> + public interface IMultiSelectionBroker + { + /// <summary> + /// Gets the view for which this broker manages selections. + /// </summary> + ITextView TextView { get; } + + /// <summary> + /// Gets the current <see cref="ITextSnapshot"/> that is associated with anchor, + /// active, and insertion points for everything managed by this broker. This snapshot + /// will always be based in the <see cref="ITextViewModel.EditBuffer"/> for the associated + /// <see cref="ITextView"/>. + /// </summary> + ITextSnapshot CurrentSnapshot { get; } + + #region Add/Remove/Get Selections + /// <summary> + /// Gets a list of all selections associated with <see cref="TextView" />. They will + /// be sorted in the order of appearence in the underlying snapshot. This property is + /// intended for edit operations and may be computationally expensive. If not all + /// selections are required, use <see cref="GetSelectionsIntersectingSpan(SnapshotSpan)"/> instead. + /// + /// This returns a selection as an <see cref="Selection"/>. + /// </summary> + IReadOnlyList<Selection> AllSelections { get; } + + /// <summary> + /// Gets whether there are multiple selections in <see cref="AllSelections"/>. + /// </summary> + bool HasMultipleSelections { get; } + + /// <summary> + /// Gets a list of all the selections that intersect the given span. Virtual whitespace is ignored for this method. + /// </summary> + /// <param name="span">The span of interest.</param> + /// <returns>The list of <see cref="Selection"/> objects.</returns> + IReadOnlyList<Selection> GetSelectionsIntersectingSpan(SnapshotSpan span); + + /// <summary> + /// Gets a list of all the selections that intersect the given span collection. Virtual whitespace is ignored for this method. + /// </summary> + /// <param name="spanCollection"></param> + /// <returns></returns> + IReadOnlyList<Selection> GetSelectionsIntersectingSpans(NormalizedSnapshotSpanCollection spanCollection); + + /// <summary> + /// Adds a selection to <see cref="AllSelections"/>. + /// </summary> + /// <param name="selection">The selection to add</param>. + /// <remarks>This will throw if it not based on <see cref="CurrentSnapshot"/>.</remarks> + void AddSelection(Selection selection); + + /// <summary> + /// Adds a list of selections to <see cref="AllSelections"/>. + /// </summary> + /// <param name="range">The list of selections to add.</param> + /// <remarks>This will throw if any of the selections are not based on <see cref="CurrentSnapshot"/>.</remarks> + void AddSelectionRange(IEnumerable<Selection> range); + + /// <summary> + /// Clears the current selections and adds one as the new value. This also becomes the <see cref="PrimarySelection"/>. + /// </summary> + /// <param name="selection">The selection to leave as the value of <see cref="PrimarySelection"/> and sole + /// member of <see cref="AllSelections"/>.</param> + /// <remarks>This will throw if it not based on <see cref="CurrentSnapshot"/>.</remarks> + void SetSelection(Selection selection); + + /// <summary> + /// Clears the current selections, adds the provided range, and sets the primary selection. + /// </summary> + /// <param name="range">Selections that should be part of <see cref="AllSelections"/>.</param> + /// <param name="primary">The selection that should be set as <see cref="PrimarySelection"/>.</param> + /// <remarks> + /// If range is null or does not contain primary, primary will also be added to <see cref="AllSelections"/>. + /// This will throw if any of the selections are not based on <see cref="CurrentSnapshot"/>. + /// </remarks> + void SetSelectionRange(IEnumerable<Selection> range, Selection primary); + + /// <summary> + /// Removes a selection from the view. + /// </summary> + /// <param name="selection">The selection to remove.</param> + /// <returns><c>true</c> if successful. <c>false</c> otherwise. This can fail if either the selection passed in does not exist in the view, or + /// it is the last one.</returns> + bool TryRemoveSelection(Selection selection); + + /// <summary> + /// Gets the primary selection which should remain after invoking <see cref="ClearSecondarySelections"/>. + /// </summary> + Selection PrimarySelection { get; } + + /// <summary> + /// Attempts to set the provided selection to be the new <see cref="PrimarySelection"/>. + /// </summary> + /// <param name="candidate">The new candidate for primary selection.</param> + /// <returns>Whether the set operation was successful. This will return <c>false</c> if the candidate is not + /// found in <see cref="AllSelections"/>.</returns> + bool TrySetAsPrimarySelection(Selection candidate); + + /// <summary> + /// Removes all but the <see cref="PrimarySelection"/> from the session. + /// </summary> + void ClearSecondarySelections(); + + /// <summary> + /// Performs a predefined manipulation on all <see cref="Selection"/>s contained by <see cref="TextView"/>. + /// </summary> + /// <param name="action">The manipulation to perform.</param> + /// <remarks>Overlapping selections will be merged after all manipulations have been applied.</remarks> + void PerformActionOnAllSelections(PredefinedSelectionTransformations action); + + /// <summary> + /// Performs a custom action on all <see cref="Selection"/>s contained by <see cref="TextView"/>. + /// </summary> + /// <param name="action">The action to perform. This will be called once per Selection + /// and the supplied <see cref="ISelectionTransformer"/> contains methods to adjust an individual Selection.</param> + /// <remarks>Overlapping selections will be merged after all actions have been performed.</remarks> + void PerformActionOnAllSelections(Action<ISelectionTransformer> action); + + /// <summary> + /// Attempts to perform a predefined action on a single <see cref="Selection"/>. + /// </summary> + /// <param name="before">The selection on which to perform the manipulation</param> + /// <param name="action">The manipulation to perform.</param> + /// <param name="after">Overlapping selections will be merged after the manipulation has been performed. + /// This parameter reports back the Selection post manipulation and post merge.</param> + /// <returns><c>true</c> if the manipulation was performed. <c>false</c> otherwise. Typically, <c>false</c> implies that the + /// before Selection did not exist in <see cref="AllSelections"/>.</returns> + bool TryPerformActionOnSelection(Selection before, PredefinedSelectionTransformations action, out Selection after); + + /// <summary> + /// Attempts to perform a custom action on a single <see cref="Selection"/>. + /// </summary> + /// <param name="before">The selection on which to perform the action.</param> + /// <param name="action">The action to perform.</param> + /// <param name="after">Overlapping selections will be merged after the action has been performed. + /// This parameter reports back the Selection post action and post merge.</param> + /// <returns><c>true</c> if the action was performed. <c>false</c> otherwise. Typically, <c>false</c> implies that the + /// beforeSelection did not exist in <see cref="AllSelections"/>.</returns> + bool TryPerformActionOnSelection(Selection before, Action<ISelectionTransformer> action, out Selection after); + + /// <summary> + /// Attempts to make the given Selection visible in the view. + /// </summary> + /// <param name="selection">The selection to ensure visiblity on.</param> + /// <param name="options">How the selection span should be made visible.</param> + /// <returns><c>true</c> if the selection was in <see cref="AllSelections"/> and is now in view. <c>false</c> otherwise.</returns> + /// /// <remarks> + /// This will first ensure that the selection span is visible, erring on the side of showing the <see cref="Selection.ActivePoint"/>. + /// Then if the <see cref="Selection.InsertionPoint"/> is different than the <see cref="Selection.ActivePoint"/>, the + /// <see cref="Selection.InsertionPoint"/> will be ensured visible. + /// </remarks> + bool TryEnsureVisible(Selection selection, EnsureSpanVisibleOptions options); + + + /// <summary> + /// Adds a box of selections with the given points as its corners. + /// </summary> + /// <param name="selection">A selection defining the characteristics of the box.</param> + /// <remarks> + /// Calling this method will clear all existing selections. + /// </remarks> + void SetBoxSelection(Selection selection); + + /// <summary> + /// If <see cref="IsBoxSelection"/> is <c>true</c>, returns an instantiated <see cref="Selection"/> + /// which the caller can interrogate or manipulate to work with the box itself. Calls to + /// <see cref="AllSelections"/> or <see cref="GetSelectionsIntersectingSpan(SnapshotSpan)"/> will return individual + /// per line entries rather than the full box. + /// + /// If <see cref="IsBoxSelection"/> is <c>false</c>, this will return null. + /// </summary> + Selection BoxSelection { get; } + + /// <summary> + /// Returns <c>true</c> if <see cref="SetBoxSelection(Selection)"/> has been + /// called, and selections are being managed by the box geometry, instead of manually by the user. <see cref="ClearSecondarySelections"/> + /// and <see cref="BreakBoxSelection"/> will both revert this to <c>false</c>, and several other methods like + /// <see cref="AddSelection(Selection)"/> will indirectly also set this back to <c>false</c>. + /// </summary> + bool IsBoxSelection { get; } + + /// <summary> + /// Clears <see cref="BoxSelection"/>, but retains the current state of selections. This is a useful utility when performing gestures like End and Home + /// where each selection moves, but the result is not necessarily a box. + /// </summary> + void BreakBoxSelection(); + + #endregion + + #region Get Selections + + /// <summary> + /// Gets the list of spans within <see cref="CurrentSnapshot"/> that are selected. While two selections cannot + /// overlap, they may inhabit virtual space, and selections may be adjacent. This will merge those spans and return + /// the minimum set of spans that could be used to describe the selection. This can be a costly operation + /// and should only be run when needed. + /// </summary> + NormalizedSnapshotSpanCollection SelectedSpans { get; } + + /// <summary> + /// Gives the set of spans selected. There is exactly one span per selection, but it may be empty. + /// They will be sorted in the order of appearence in the document. + /// </summary> + IReadOnlyList<VirtualSnapshotSpan> VirtualSelectedSpans { get; } + + /// <summary> + /// Gets the span containing all selections, complete with virtual space. + /// </summary> + VirtualSnapshotSpan SelectionExtent { get; } + + #endregion + + #region Environment Integration + + /// <summary> + /// Whether or not selections are active within <see cref="TextView"/>. + /// </summary> + /// <remarks> + /// <para> + /// If <see cref="ActivationTracksFocus"/> is <c>true</c>, this property is automatically + /// updated when the <see cref="ITextView"/> gains and loses aggregate focus. You can still + /// override it while <see cref="ActivationTracksFocus"/> is <c>false</c>, but the value will change + /// whenever focus changes. + /// </para> + /// </remarks> + bool AreSelectionsActive { get; set; } + + /// <summary> + /// Determines whether <see cref="AreSelectionsActive"/> should track when the <see cref="ITextView"/> gains and + /// loses aggregate focus. The default is <c>true</c>. + /// </summary> + /// <remarks> + /// <para> + /// While the value of this property is <c>true</c>, the value of <see cref="AreSelectionsActive"/> will track + /// <see cref="ITextView.HasAggregateFocus"/>. When the value of this property changes to <c>true</c>, + /// the value of <see cref="AreSelectionsActive"/> will be immediately updated. + /// </para> + /// </remarks> + bool ActivationTracksFocus { get; set; } + + /// <summary> + /// Occurs when selections are added/removed/updated. Also when the primary selection is changed, and when + /// box selection mode is entered/exited. + /// </summary> + event EventHandler MultiSelectionSessionChanged; + + /// <summary> + /// Temporarily disables <see cref="MultiSelectionSessionChanged"/>, but instead queues up all actions + /// to be included in the resultant <see cref="MultiSelectionChangedEventArgs"/> once the operation + /// is completed. Selection merges will also be deferred until the end of batch operations. + /// </summary> + /// <returns>An object that should be disposed once the batch operation is complete.</returns> + /// </param> + IDisposable BeginBatchOperation(); + + #endregion + + /// <summary> + /// Trys to get the UI properties associated with the given Selection. + /// </summary> + /// <param name="selection">The selection of interest.</param> + /// <param name="properties">Returns out the properties if successful.</param> + /// <returns><c>true</c> if the supplied selection was found and the properties returned. <c>false</c> otherwise.</returns> + bool TryGetSelectionPresentationProperties(Selection selection, out AbstractSelectionPresentationProperties properties); + + /// <summary> + /// Performs the given transformation on the given Selection without updating <see cref="AllSelections"/>. + /// The behavior of Preferred X and Y coordinates for selections that are already in the broker is undefined. + /// </summary> + /// <param name="source">The selection to transform</param> + /// <param name="">The transformation to perform</param> + /// <returns>The transformed selection</returns> + Selection TransformSelection(Selection source, PredefinedSelectionTransformations transformation); + } +} diff --git a/src/Text/Def/TextUI/MultiCaret/ISelectionTransformer.cs b/src/Text/Def/TextUI/MultiCaret/ISelectionTransformer.cs new file mode 100644 index 0000000..5c842ad --- /dev/null +++ b/src/Text/Def/TextUI/MultiCaret/ISelectionTransformer.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Allows changing existing <see cref="ISelection"/> objects as part of <see cref="IMultiSelectionBroker.PerformActionOnAllSelections(System.Action{ISelectionTransformer})" + /// and <see cref="IMultiSelectionBroker.TryPerformActionOnRegion(ISelection, out ISelection, System.Action{ISelectionTransformer})"/>./> + /// </summary> + public interface ISelectionTransformer + { + /// <summary> + /// Gets the Selection to transform. This will change through calls to <see cref="PerformAction(PredefinedSelectionTransformations)"/>, + /// <see cref="MoveTo(VirtualSnapshotPoint, bool, PositionAffinity)"/>, and + /// <see cref="MoveTo(VirtualSnapshotPoint, VirtualSnapshotPoint, VirtualSnapshotPoint, PositionAffinity)"/>. + /// </summary> + Selection Selection { get; } + + /// <summary> + /// Moves the insertion and active points to the given location. + /// </summary> + /// <param name="point">The point to move to.</param> + /// <param name="select">If <c>true</c>, leaves the anchor point where it is. If <c>false</c>, moves the anchor point too.</param> + /// <param name="insertionPointAffinity"> + /// The affinity of the insertion point. This is used in places like word-wrap where one buffer position can represent both the + /// end of one line and the beginning of the next. + /// </param> + void MoveTo(VirtualSnapshotPoint point, bool select, PositionAffinity insertionPointAffinity); + + /// <summary> + /// Sets the anchor, active, and insertion points to the specified locations. + /// </summary> + /// <param name="anchorPoint">Specifies the stationary end of the selection span.</param> + /// <param name="activePoint">Specifies the mobile end of the selection span.</param> + /// <param name="insertionPoint">Specifies the location of the caret.</param> + /// <param name="insertionPointAffinity"> + /// Specifies the affinity of the insertion point. This is used in places like word-wrap where one buffer position can represent both the + /// end of one line and the beginning of the next. + /// </param> + void MoveTo(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint, VirtualSnapshotPoint insertionPoint, PositionAffinity insertionPointAffinity); + + /// <summary> + /// Updates internal state to cache the current location as the desired reference point for navigation events. + /// </summary> + /// <remarks> + /// This affects events like <see cref="PredefinedSelectionTransformations.MoveToPreviousLine"/> where the current + /// X location of the rendered caret is used to project to the new location. Typically this method should be called + /// in cases where the user is stating where they want to focus. Since this grabs the current state, there is no + /// equivalent release method. + /// </remarks> + void CapturePreferredReferencePoint(); + + /// <summary> + /// Updates internal state to cache the current x location as the desired reference point for navigation events. + /// </summary> + /// <remarks> + /// This affects events like <see cref="PredefinedSelectionTransformations.MoveToPreviousLine"/> where the current + /// X location of the rendered caret is used to project to the new location. Typically this method should be called + /// in cases where the user is stating where they want to focus. Since this grabs the current state, there is no + /// equivalent release method. + /// </remarks> + void CapturePreferredXReferencePoint(); + + /// <summary> + /// Updates internal state to cache the current y location as the desired reference point for navigation events. + /// </summary> + /// <remarks> + /// This affects events like <see cref="PredefinedSelectionTransformations.MovePageUp"/> where the current + /// Y location of the rendered caret is used to project to the new location. Typically this method should be called + /// in cases where the user is stating where they want to focus. Since this grabs the current state, there is no + /// equivalent release method. + /// </remarks> + void CapturePreferredYReferencePoint(); + + /// <summary> + /// Transforms <see cref="Selection"/> in a predefined way. + /// </summary> + /// <param name="action">The kind of transformation to perform</param> + void PerformAction(PredefinedSelectionTransformations action); + } +} diff --git a/src/Text/Def/TextUI/MultiCaret/PredefinedSelectionTransformations.cs b/src/Text/Def/TextUI/MultiCaret/PredefinedSelectionTransformations.cs new file mode 100644 index 0000000..d8ced81 --- /dev/null +++ b/src/Text/Def/TextUI/MultiCaret/PredefinedSelectionTransformations.cs @@ -0,0 +1,158 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Defines a set of actions that are predefined for manipulating selections within a view. For custom manipulations see the usage + /// of <see cref="ISelectionTransformer"/>. These transformations can be passed in to + /// <see cref="IMultiSelectionBroker.PerformActionOnAllSelections(PredefinedSelectionTransformations)"/>, + /// <see cref="IMultiSelectionBroker.TryPerformActionOnSelection(Selection, PredefinedSelectionTransformations, out Selection)"/>, + /// and <see cref="ISelectionTransformer.PerformAction(PredefinedSelectionTransformations)"/>. + /// </summary> +#pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names + public enum PredefinedSelectionTransformations +#pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names + { + /// <summary> + /// Resets the active and anchor points to be at the insertion point. + /// </summary> + ClearSelection, + + /// <summary> + /// Moves the active, anchor, and insertion points ahead one position in the view. + /// </summary> + MoveToNextCaretPosition, + + /// <summary> + /// Moves the active and insertion points ahead one position in the view, keeping the anchor point where it is. + /// </summary> + SelectToNextCaretPosition, + + /// <summary> + /// Moves the active, anchor, and insertion points back one position in the view. + /// </summary> + MoveToPreviousCaretPosition, + + /// <summary> + /// Moves the active and insertion points back one position in the view, keeping the anchor point where it is. + /// </summary> + SelectToPreviousCaretPosition, + + /// <summary> + /// Moves the active, anchor, and insertion points ahead to the beginning of the next word. + /// </summary> + MoveToNextWord, + + /// <summary> + /// Moves the active and insertion points ahead to the beginning of the next word, keeping the anchor point where it is. + /// </summary> + SelectToNextWord, + + /// <summary> + /// Moves the active, anchor, and insertion points back to the end of the previous word. + /// </summary> + MoveToPreviousWord, + + /// <summary> + /// Moves the active and insertion points back to the end of the previous word, keeping the anchor point where it is. + /// </summary> + SelectToPreviousWord, + + /// <summary> + /// Moves the active, anchor, and insertion points back to the beginning of the current line. + /// </summary> + MoveToBeginningOfLine, + + /// <summary> + /// Moves the active and insertion points back to the beginning of the current line, keeping the anchor point where it is. + /// </summary> + SelectToBeginningOfLine, + + /// <summary> + /// Moves the active, anchor, and insertion points alternately between the beginning of the line, and the first non-whitespace character. + /// </summary> + MoveToHome, + + /// <summary> + /// Moves the active and insertion points alternately between the beginning of the line, and the first non-whitespace character, keeping the anchor point where it is. + /// </summary> + SelectToHome, + + /// <summary> + /// Moves the active, anchor, and insertion points ahead to the end of the current line. + /// </summary> + MoveToEndOfLine, + + /// <summary> + /// Moves the active and insertion points ahead to the end of the current line, keeping the anchor point where it is. + /// </summary> + SelectToEndOfLine, + + /// <summary> + /// Moves the active, anchor, and insertion points ahead to next line, staying as close to the user's preferred x-coordinate in the view as possible. + /// </summary> + MoveToNextLine, + + /// <summary> + /// Moves the active and insertion points ahead to next line, staying as close to the user's preferred x-coordinate in the view as possible, keeping the anchor point where it is. + /// </summary> + SelectToNextLine, + + /// <summary> + /// Moves the active, anchor, and insertion points back to the previous line, staying as close to the user's preferred x-coordinate in the view as possible. + /// </summary> + MoveToPreviousLine, + + /// <summary> + /// Moves the active and insertion points back to the previous line, staying as close to the user's preferred x-coordinate in the view as possible, keeping the anchor point where it is. + /// </summary> + SelectToPreviousLine, + + /// <summary> + /// Moves the active, anchor, and insertion points back one viewport height, staying as close to the user's preferred x and y coordinates in the view as possible. + /// </summary> + MovePageUp, + + /// <summary> + /// Moves the active and insertion points back one viewport height, staying as close to the user's preferred x and y coordinates in the view as possible, keeping the anchor point where it is. + /// </summary> + SelectPageUp, + + /// <summary> + /// Moves the active, anchor, and insertion points ahead one viewport height, staying as close to the user's preferred x and y coordinates in the view as possible. + /// </summary> + MovePageDown, + + /// <summary> + /// Moves the active and insertion points ahead one viewport height, staying as close to the user's preferred x and y coordinates in the view as possible, keeping the anchor point where it is. + /// </summary> + SelectPageDown, + + /// <summary> + /// Moves the active, anchor, and insertion points back to the beginning of the document. + /// </summary> + MoveToStartOfDocument, + + /// <summary> + /// Moves the active and insertion points back to the beginning of the document, keeping the anchor point where it is. + /// </summary> + SelectToStartOfDocument, + + /// <summary> + /// Moves the active, anchor, and insertion points ahead to the end of the document. + /// </summary> + MoveToEndOfDocument, + + /// <summary> + /// Moves the active and insertion points ahead to the end of the document, keeping the anchor point where it is. + /// </summary> + SelectToEndOfDocument, + + /// <summary> + /// Moves the anchor point to the beginning of the current word. Moves the active and insertion points to the end of the current word. + /// </summary> + SelectCurrentWord + } +} diff --git a/src/Text/Def/TextUI/MultiCaret/Selection.cs b/src/Text/Def/TextUI/MultiCaret/Selection.cs new file mode 100644 index 0000000..7f27834 --- /dev/null +++ b/src/Text/Def/TextUI/MultiCaret/Selection.cs @@ -0,0 +1,313 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System; + +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Manages the insertion, anchor, and active points for a single caret and its associated + /// selection. + /// </summary> + public struct Selection : IEquatable<Selection> + { + /// <summary> + /// A static instance of a selection that is invalid and can be used to check for instantiation. + /// </summary> + public static readonly Selection Invalid = new Selection(); + + /// <summary> + /// Instantiates a new Selection with a zero-width extent at the provided insertion point. + /// </summary> + /// <param name="insertionPoint">The location where a caret should be rendered and edits performed.</param> + /// <param name="insertionPointAffinity"> + /// The affinity of the insertion point. This is used in places like word-wrap where one buffer position can represent both the + /// end of one line and the beginning of the next. + /// </param> + public Selection(VirtualSnapshotPoint insertionPoint, PositionAffinity insertionPointAffinity = PositionAffinity.Successor) + : this(insertionPoint, insertionPoint, insertionPoint, insertionPointAffinity) + { } + + /// <summary> + /// Instantiates a new Selection with a zero-width extent at the provided insertion point. + /// </summary> + /// <param name="insertionPoint">The location where a caret should be rendered and edits performed.</param> + /// <param name="insertionPointAffinity"> + /// The affinity of the insertion point. This is used in places like word-wrap where one buffer position can represent both the + /// end of one line and the beginning of the next. + /// </param> + public Selection(SnapshotPoint insertionPoint, PositionAffinity insertionPointAffinity = PositionAffinity.Successor) + : this(new VirtualSnapshotPoint(insertionPoint), + new VirtualSnapshotPoint(insertionPoint), + new VirtualSnapshotPoint(insertionPoint), + insertionPointAffinity) + { } + + /// <summary> + /// Instantiates a new Selection with the given extent. Anchor and active points are defined by isReversed, and the + /// insertion point is located at the active point. + /// </summary> + /// <param name="extent">The span that the selection covers.</param> + /// <param name="isReversed"> + /// True implies that <see cref="ActivePoint"/> comes before <see cref="AnchorPoint"/>. + /// The <see cref="InsertionPoint"/> is set to the <see cref="ActivePoint"/>. + /// <see cref="InsertionPointAffinity"/> is set to <see cref="PositionAffinity.Predecessor"/> when isReversed is true. + /// <see cref="PositionAffinity.Successor"/> otherwise. + /// </param> + public Selection(VirtualSnapshotSpan extent, bool isReversed = false) + { + if (isReversed) + { + AnchorPoint = extent.End; + ActivePoint = InsertionPoint = extent.Start; + InsertionPointAffinity = PositionAffinity.Successor; + } + else + { + AnchorPoint = extent.Start; + ActivePoint = InsertionPoint = extent.End; + + // The goal here is to keep the caret with the selection box. If we're wordwrapped, and the + // box is at the end of a line, Predecessor will keep the caret on the previous line. + InsertionPointAffinity = PositionAffinity.Predecessor; + } + } + + /// <summary> + /// Instantiates a new Selection with the given extent. Anchor and active points are defined by isReversed, and the + /// insertion point is located at the active point. + /// </summary> + /// <param name="extent">The span that the selection covers.</param> + /// <param name="isReversed"> + /// True implies that <see cref="ActivePoint"/> comes before <see cref="AnchorPoint"/>. + /// The <see cref="InsertionPoint"/> is set to the <see cref="ActivePoint"/>. + /// <see cref="InsertionPointAffinity"/> is set to <see cref="PositionAffinity.Predecessor"/> when isReversed is true. + /// <see cref="PositionAffinity.Successor"/> otherwise. + /// </param> + public Selection(SnapshotSpan extent, bool isReversed = false) + : this(new VirtualSnapshotSpan(extent), isReversed) + { } + + /// <summary> + /// Instantiates a new Selection with the given anchor and active points, and the + /// insertion point is located at the active point. + /// </summary> + /// <param name="anchorPoint">The location of the fixed selection endpoint, meaning if a user were to hold shift and click, + /// this point would remain where it is.</param> + /// <param name="activePoint">location of the movable selection endpoint, meaning if a user were to hold shift and click, + /// this point would be changed to the location of the click.</param> + public Selection(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint) + : this(insertionPoint: activePoint, + anchorPoint: anchorPoint, + activePoint: activePoint, + insertionPointAffinity: (anchorPoint < activePoint) ? PositionAffinity.Predecessor : PositionAffinity.Successor) + { + } + + /// <summary> + /// Instantiates a new Selection with the given anchor and active points, and the + /// insertion point is located at the active point. + /// </summary> + /// <param name="anchorPoint">The location of the fixed selection endpoint, meaning if a user were to hold shift and click, + /// this point would remain where it is.</param> + /// <param name="activePoint">location of the movable selection endpoint, meaning if a user were to hold shift and click, + /// this point would be changed to the location of the click.</param> + public Selection(SnapshotPoint anchorPoint, SnapshotPoint activePoint) + : this(anchorPoint: new VirtualSnapshotPoint(anchorPoint), + activePoint: new VirtualSnapshotPoint(activePoint)) + { + } + + /// <summary> + /// Instantiates a new Selection. + /// </summary> + /// <param name="insertionPoint">The location where a caret should be rendered and edits performed.</param> + /// <param name="anchorPoint">The location of the fixed selection endpoint, meaning if a user were to hold shift and click, + /// this point would remain where it is.</param> + /// <param name="activePoint">location of the movable selection endpoint, meaning if a user were to hold shift and click, + /// this point would be changed to the location of the click.</param> + /// <param name="insertionPointAffinity"> + /// The affinity of the insertion point. This is used in places like word-wrap where one buffer position can represent both the + /// end of one line and the beginning of the next. + /// </param> + public Selection(VirtualSnapshotPoint insertionPoint, + VirtualSnapshotPoint anchorPoint, + VirtualSnapshotPoint activePoint, + PositionAffinity insertionPointAffinity = PositionAffinity.Successor) + { + if (insertionPoint.Position.Snapshot != anchorPoint.Position.Snapshot || insertionPoint.Position.Snapshot != activePoint.Position.Snapshot) + { + throw new ArgumentException("All points must be on the same snapshot."); + } + + InsertionPoint = insertionPoint; + AnchorPoint = anchorPoint; + ActivePoint = activePoint; + InsertionPointAffinity = insertionPointAffinity; + } + + /// <summary> + /// Instantiates a new Selection. + /// </summary> + /// <param name="insertionPoint">The location where a caret should be rendered and edits performed.</param> + /// <param name="anchorPoint">The location of the fixed selection endpoint, meaning if a user were to hold shift and click, + /// this point would remain where it is.</param> + /// <param name="activePoint">location of the movable selection endpoint, meaning if a user were to hold shift and click, + /// this point would be changed to the location of the click.</param> + /// <param name="insertionPointAffinity"> + /// The affinity of the insertion point. This is used in places like word-wrap where one buffer position can represent both the + /// end of one line and the beginning of the next. + /// </param> + public Selection(SnapshotPoint insertionPoint, + SnapshotPoint anchorPoint, + SnapshotPoint activePoint, + PositionAffinity insertionPointAffinity = PositionAffinity.Successor) + : this(new VirtualSnapshotPoint(insertionPoint), + new VirtualSnapshotPoint(anchorPoint), + new VirtualSnapshotPoint(activePoint), + insertionPointAffinity) + { } + + /// <summary> + /// Gets whether this selection contains meaningful data. + /// </summary> + public bool IsValid + { + get + { + return this != Invalid && this.InsertionPoint.Position.Snapshot != null; + } + } + + /// <summary> + /// Gets the location where a caret should be rendered and edits performed. + /// </summary> + public VirtualSnapshotPoint InsertionPoint { get; } + + /// <summary> + /// Gets the location of the fixed selection endpoint, meaning if a user were to hold shift and click, + /// this point would remain where it is. If this is an empty selection, this will be at the + /// <see cref="InsertionPoint"/>. + /// </summary> + public VirtualSnapshotPoint AnchorPoint { get; } + + /// <summary> + /// Gets the location of the movable selection endpoint, meaning if a user were to hold shift and click, + /// this point would be changed to the location of the click. If this is an empty selection, this will be at the + /// <see cref="InsertionPoint"/>. + /// </summary> + public VirtualSnapshotPoint ActivePoint { get; } + + /// <summary> + /// Gets the affinity of the insertion point. + /// This is used in places like word-wrap where one buffer position can represent both the + /// end of one line and the beginning of the next. + /// </summary> + public PositionAffinity InsertionPointAffinity { get; } + + /// <summary> + /// True if <see cref="AnchorPoint"/> is later in the document than <see cref="ActivePoint"/>. False otherwise. + /// </summary> + public bool IsReversed + { + get + { + return ActivePoint < AnchorPoint; + } + } + + /// <summary> + /// True if <see cref="AnchorPoint"/> equals <see cref="ActivePoint"/>. False otherwise. + /// </summary> + public bool IsEmpty + { + get + { + return ActivePoint == AnchorPoint; + } + } + + /// <summary> + /// Returns the smaller of <see cref="ActivePoint"/> and <see cref="AnchorPoint"/>. + /// </summary> + public VirtualSnapshotPoint Start + { + get + { + return IsReversed ? ActivePoint : AnchorPoint; + } + } + + /// <summary> + /// Returns the larger of <see cref="ActivePoint"/> and <see cref="AnchorPoint"/>. + /// </summary> + public VirtualSnapshotPoint End + { + get + { + return IsReversed ? AnchorPoint : ActivePoint; + } + } + + /// <summary> + /// Returns the span from <see cref="Start"/> to <see cref="End"/>. + /// </summary> + public VirtualSnapshotSpan Extent + { + get + { + return new VirtualSnapshotSpan(Start, End); + } + } + + public override int GetHashCode() + { + // We are fortunate enough to have 3 interesting points here. If you xor an even number of snapshot point hashcodes + // together, the snapshot component gets cancelled out. + + // However, the common case is that ActivePoint and InsertionPoint are exactly equal, so we need to do something to change that. + // Invert the bytes in InsertionPoint.GetHashCode(). + var insertionHash = (uint)InsertionPoint.GetHashCode(); + insertionHash = (((0x0000FFFF & insertionHash) << 16) | ((0xFFFF0000 & insertionHash) >> 16)); + + int pointHashes = AnchorPoint.GetHashCode() ^ ActivePoint.GetHashCode() ^ (int)insertionHash; + + // InsertionPointAffinity.GetHashCode() returns either 0 or 1 which can get stomped on by the rest of the hash codes. + // Generate more interesting hash code values below: + int affinityHash = InsertionPointAffinity == PositionAffinity.Predecessor + ? affinityHash = 04122013 + : affinityHash = 10172014; + + return pointHashes ^ affinityHash; + } + + public override bool Equals(object obj) + { + return obj is Selection && Equals((Selection)obj); + } + + public bool Equals(Selection other) + { + return this.ActivePoint == other.ActivePoint + && this.AnchorPoint == other.AnchorPoint + && this.InsertionPoint == other.InsertionPoint + && this.InsertionPointAffinity == other.InsertionPointAffinity; + } + + public static bool operator ==(Selection left, Selection right) + { + return left.Equals(right); + } + + public static bool operator !=(Selection left, Selection right) + { + return !left.Equals(right); + } + + public override string ToString() + { + return $"Ins:{InsertionPoint} Anc:{AnchorPoint} Act:{ActivePoint} Aff:{InsertionPointAffinity}"; + } + } +} diff --git a/src/Text/Def/TextUI/Operations/IUndoMetadataEditTag.cs b/src/Text/Def/TextUI/Operations/IUndoMetadataEditTag.cs new file mode 100644 index 0000000..b965983 --- /dev/null +++ b/src/Text/Def/TextUI/Operations/IUndoMetadataEditTag.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Text.Operations +{ +#if false // Work in progress + public interface IUndoMetadataEditTag : IEditTag + { + /// <summary> + /// The view from which the edit was initiated. May be null. + /// </summary> + ITextView InitiatingView { get; } + + /// <summary> + /// A localized description of the edit (which can be displayed in the undo list). + /// </summary> + string Description { get; } + + /// <summary> + /// Consecutive edits with the same, non-null, may be merged. + /// </summary> + object MergeType { get; } + } +#endif +} diff --git a/src/Text/Def/TextUI/Strings.Designer.cs b/src/Text/Def/TextUI/Strings.Designer.cs deleted file mode 100644 index f993ce8..0000000 --- a/src/Text/Def/TextUI/Strings.Designer.cs +++ /dev/null @@ -1,81 +0,0 @@ -//------------------------------------------------------------------------------ -// <auto-generated> -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// </auto-generated> -//------------------------------------------------------------------------------ - -namespace Microsoft.VisualStudio.Text.Editor { - using System; - - - /// <summary> - /// A strongly-typed resource class, for looking up localized strings, etc. - /// </summary> - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// <summary> - /// Returns the cached ResourceManager instance used by this class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Editor.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// <summary> - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// <summary> - /// Looks up a localized string similar to Buffer mismatch between oldSnapshot and newSnapshot.. - /// </summary> - internal static string BufferMismatch { - get { - return ResourceManager.GetString("BufferMismatch", resourceCulture); - } - } - - /// <summary> - /// Looks up a localized string similar to oldSnapshot's version is not older than newSnapshot's version.. - /// </summary> - internal static string VersionError { - get { - return ResourceManager.GetString("VersionError", resourceCulture); - } - } - } -} diff --git a/src/Text/Def/TextUI/Strings.resx b/src/Text/Def/TextUI/Strings.resx deleted file mode 100644 index 279c501..0000000 --- a/src/Text/Def/TextUI/Strings.resx +++ /dev/null @@ -1,126 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<root> - <!-- - Microsoft ResX Schema - - Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes - associated with the data types. - - Example: - - ... ado.net/XML headers & schema ... - <resheader name="resmimetype">text/microsoft-resx</resheader> - <resheader name="version">2.0</resheader> - <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> - <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> - <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> - <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> - <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> - <value>[base64 mime encoded serialized .NET Framework object]</value> - </data> - <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> - <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> - <comment>This is a comment</comment> - </data> - - There are any number of "resheader" rows that contain simple - name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the - mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not - extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can - read any of the formats listed below. - - mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with - : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter - : and then encoded with base64 encoding. - - mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with - : System.Runtime.Serialization.Formatters.Soap.SoapFormatter - : and then encoded with base64 encoding. - - mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array - : using a System.ComponentModel.TypeConverter - : and then encoded with base64 encoding. - --> - <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> - <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> - <xsd:element name="root" msdata:IsDataSet="true"> - <xsd:complexType> - <xsd:choice maxOccurs="unbounded"> - <xsd:element name="metadata"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" /> - </xsd:sequence> - <xsd:attribute name="name" use="required" type="xsd:string" /> - <xsd:attribute name="type" type="xsd:string" /> - <xsd:attribute name="mimetype" type="xsd:string" /> - <xsd:attribute ref="xml:space" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="assembly"> - <xsd:complexType> - <xsd:attribute name="alias" type="xsd:string" /> - <xsd:attribute name="name" type="xsd:string" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="data"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> - <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> - </xsd:sequence> - <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> - <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> - <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> - <xsd:attribute ref="xml:space" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="resheader"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> - </xsd:sequence> - <xsd:attribute name="name" type="xsd:string" use="required" /> - </xsd:complexType> - </xsd:element> - </xsd:choice> - </xsd:complexType> - </xsd:element> - </xsd:schema> - <resheader name="resmimetype"> - <value>text/microsoft-resx</value> - </resheader> - <resheader name="version"> - <value>2.0</value> - </resheader> - <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> - </resheader> - <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> - </resheader> - <data name="BufferMismatch" xml:space="preserve"> - <value>Buffer mismatch between oldSnapshot and newSnapshot.</value> - </data> - <data name="VersionError" xml:space="preserve"> - <value>oldSnapshot's version is not older than newSnapshot's version.</value> - </data> -</root>
\ No newline at end of file diff --git a/src/Text/Def/TextUI/Tags/ErrorTag.cs b/src/Text/Def/TextUI/Tags/ErrorTag.cs index 2867819..9b2b029 100644 --- a/src/Text/Def/TextUI/Tags/ErrorTag.cs +++ b/src/Text/Def/TextUI/Tags/ErrorTag.cs @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public ErrorTag(string errorType, object toolTipContent) { if (errorType == null) - throw new ArgumentNullException("errorType"); + throw new ArgumentNullException(nameof(errorType)); ErrorType = errorType; ToolTipContent = toolTipContent; diff --git a/src/Text/Def/TextUI/Tags/TextMarkerTag.cs b/src/Text/Def/TextUI/Tags/TextMarkerTag.cs index a36ec85..fd3284c 100644 --- a/src/Text/Def/TextUI/Tags/TextMarkerTag.cs +++ b/src/Text/Def/TextUI/Tags/TextMarkerTag.cs @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.Text.Tagging public TextMarkerTag(string type) { if (type == null) - throw new ArgumentNullException("type"); + throw new ArgumentNullException(nameof(type)); Type = type; } diff --git a/src/Text/Def/TextUI/TextUI.csproj b/src/Text/Def/TextUI/TextUI.csproj index 18cf4e5..c5839d2 100644 --- a/src/Text/Def/TextUI/TextUI.csproj +++ b/src/Text/Def/TextUI/TextUI.csproj @@ -1,13 +1,12 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <AssemblyName>Microsoft.VisualStudio.Text.UI</AssemblyName> <RootNamespace>Microsoft.VisualStudio.Text.Editor</RootNamespace> <TargetFramework>net46</TargetFramework> - <NonShipping>false</NonShipping> - <IsPackable>true</IsPackable> <PushToPublicFeed>true</PushToPublicFeed> <NoWarn>649;436;$(NoWarn)</NoWarn> <AssemblyAttributeClsCompliant>true</AssemblyAttributeClsCompliant> + <LangVersion>latest</LangVersion> </PropertyGroup> <ItemGroup> <Reference Include="System" /> diff --git a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs index 8047106..05a522b 100644 --- a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs +++ b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs @@ -5,10 +5,12 @@ using System.Threading; namespace Microsoft.VisualStudio.Utilities { +#pragma warning disable CA1063 // Implement IDisposable Correctly /// <summary> /// Abstract base implementation of the <see cref="IUIThreadOperationContext"/> interface. /// </summary> public abstract class AbstractUIThreadOperationContext : IUIThreadOperationContext +#pragma warning restore CA1063 // Implement IDisposable Correctly { private List<IUIThreadOperationScope> _scopes; private bool _allowCancellation; @@ -137,11 +139,14 @@ namespace Microsoft.VisualStudio.Utilities { } +#pragma warning disable CA1063 // Implement IDisposable Correctly /// <summary> /// Disposes this instance. /// </summary> public virtual void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { + GC.SuppressFinalize(this); } /// <summary> @@ -193,7 +198,7 @@ namespace Microsoft.VisualStudio.Utilities get { return _description; } set { - if (_description != value) + if (!string.Equals(_description, value, StringComparison.Ordinal)) { _description = value; _context.OnScopeChanged(this); diff --git a/src/Text/Def/TextUI/Utilities/IUIThreadOperationScope.cs b/src/Text/Def/TextUI/Utilities/IUIThreadOperationScope.cs index 8f1d05f..4995d63 100644 --- a/src/Text/Def/TextUI/Utilities/IUIThreadOperationScope.cs +++ b/src/Text/Def/TextUI/Utilities/IUIThreadOperationScope.cs @@ -29,10 +29,12 @@ namespace Microsoft.VisualStudio.Utilities IProgress<ProgressInfo> Progress { get; } } +#pragma warning disable CA1815 // Override equals and operator equals on value types /// <summary> /// Represents an update of a progress. /// </summary> public struct ProgressInfo +#pragma warning restore CA1815 // Override equals and operator equals on value types { /// <summary> /// A number of already completed items. diff --git a/src/Text/Def/TextUI/Utilities/UIThreadOperationStatus.cs b/src/Text/Def/TextUI/Utilities/UIThreadOperationStatus.cs index b7e47ae..b8b505d 100644 --- a/src/Text/Def/TextUI/Utilities/UIThreadOperationStatus.cs +++ b/src/Text/Def/TextUI/Utilities/UIThreadOperationStatus.cs @@ -1,9 +1,11 @@ namespace Microsoft.VisualStudio.Utilities { +#pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names /// <summary> /// Represents a status of executing a potentially long running operation on the UI thread. /// </summary> public enum UIThreadOperationStatus +#pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names { /// <summary> /// An operation was successfully completed. diff --git a/src/Text/Impl/BraceCompletion/BraceCompletionAggregator.cs b/src/Text/Impl/BraceCompletion/BraceCompletionAggregator.cs index fa25233..dbf866a 100644 --- a/src/Text/Impl/BraceCompletion/BraceCompletionAggregator.cs +++ b/src/Text/Impl/BraceCompletion/BraceCompletionAggregator.cs @@ -249,7 +249,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation /// This checks the type against all others until it finds one that it is /// a type of. List.Sort() does not work here since most types are unrelated. /// </summary> - private List<IContentType> SortContentTypes(List<IContentType> contentTypes) + private static List<IContentType> SortContentTypes(List<IContentType> contentTypes) { List<IContentType> sorted = new List<IContentType>(contentTypes.Count); diff --git a/src/Text/Impl/BraceCompletion/BraceCompletionAggregatorFactory.cs b/src/Text/Impl/BraceCompletion/BraceCompletionAggregatorFactory.cs index b2179b3..4795373 100644 --- a/src/Text/Impl/BraceCompletion/BraceCompletionAggregatorFactory.cs +++ b/src/Text/Impl/BraceCompletion/BraceCompletionAggregatorFactory.cs @@ -26,7 +26,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation internal IContentTypeRegistryService ContentTypeRegistryService { get; private set; } internal ITextBufferUndoManagerProvider UndoManager { get; private set; } internal IEditorOperationsFactoryService EditorOperationsFactoryService { get; private set; } - internal GuardedOperations GuardedOperations { get; private set; } + internal IGuardedOperations GuardedOperations { get; private set; } #endregion @@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation IContentTypeRegistryService contentTypeRegistryService, ITextBufferUndoManagerProvider undoManager, IEditorOperationsFactoryService editorOperationsFactoryService, - GuardedOperations guardedOperations) + IGuardedOperations guardedOperations) { SessionProviders = sessionProviders; ContextProviders = contextProviders; diff --git a/src/Text/Impl/BraceCompletion/BraceCompletionDefaultSession.cs b/src/Text/Impl/BraceCompletion/BraceCompletionDefaultSession.cs index 6877b4e..f58b3e1 100644 --- a/src/Text/Impl/BraceCompletion/BraceCompletionDefaultSession.cs +++ b/src/Text/Impl/BraceCompletion/BraceCompletionDefaultSession.cs @@ -12,6 +12,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; using System.Diagnostics; + using System.Globalization; /// <summary> /// BraceCompletionDefaultSession is a language neutral brace completion session @@ -102,7 +103,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation // insert the closing brace using (ITextEdit edit = _subjectBuffer.CreateEdit()) { - edit.Insert(closingSnapshotPoint, _closingBrace.ToString()); + edit.Insert(closingSnapshotPoint, _closingBrace.ToString(CultureInfo.CurrentCulture)); if (edit.HasFailedChanges) { @@ -125,7 +126,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation _closingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(_closingPoint.GetPoint(snapshot), PointTrackingMode.Negative); Debug.Assert(_closingPoint.GetPoint(snapshot).Position > 0 && (new SnapshotSpan(_closingPoint.GetPoint(snapshot).Subtract(1), 1)) - .GetText().Equals(_closingBrace.ToString()), "The closing point does not match the closing brace character"); + .GetText().Equals(_closingBrace.ToString(CultureInfo.CurrentCulture), System.StringComparison.Ordinal), "The closing point does not match the closing brace character"); // move the caret back between the braces _textView.Caret.MoveTo(beforePoint); diff --git a/src/Text/Impl/BraceCompletion/Options.cs b/src/Text/Impl/BraceCompletion/BraceCompletionEnabledOption.cs index ced4d14..b36dd5f 100644 --- a/src/Text/Impl/BraceCompletion/Options.cs +++ b/src/Text/Impl/BraceCompletion/BraceCompletionEnabledOption.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // @@ -7,11 +7,9 @@ // namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { - using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; using System.ComponentModel.Composition; - using System.Windows; [Export(typeof(EditorOptionDefinition))] [Name(DefaultTextViewOptions.BraceCompletionEnabledOptionName)] diff --git a/src/Text/Impl/BraceCompletion/BraceCompletionManager.cs b/src/Text/Impl/BraceCompletion/BraceCompletionManager.cs index 7cfe59c..b5f9689 100644 --- a/src/Text/Impl/BraceCompletion/BraceCompletionManager.cs +++ b/src/Text/Impl/BraceCompletion/BraceCompletionManager.cs @@ -9,6 +9,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; using System; using System.Diagnostics; @@ -24,7 +25,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation private readonly IBraceCompletionAggregatorFactory _sessionFactory; private readonly IBraceCompletionAggregator _sessionAggregator; private readonly ITextView _textView; - private readonly GuardedOperations _guardedOperations; + private readonly IGuardedOperations _guardedOperations; private bool _braceCompletionEnabled; @@ -36,7 +37,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation #region Constructors - internal BraceCompletionManager(ITextView textView, IBraceCompletionStack stack, IBraceCompletionAggregatorFactory sessionFactory, GuardedOperations guardedOperations) + internal BraceCompletionManager(ITextView textView, IBraceCompletionStack stack, IBraceCompletionAggregatorFactory sessionFactory, IGuardedOperations guardedOperations) { _textView = textView; _stack = stack; @@ -450,7 +451,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation } } - private bool IsSingleLine(ITrackingPoint openingPoint, ITrackingPoint closingPoint) + private static bool IsSingleLine(ITrackingPoint openingPoint, ITrackingPoint closingPoint) { if (openingPoint != null && closingPoint != null) { diff --git a/src/Text/Impl/BraceCompletion/BraceCompletionStack.cs b/src/Text/Impl/BraceCompletion/BraceCompletionStack.cs index 311d493..ac1ff29 100644 --- a/src/Text/Impl/BraceCompletion/BraceCompletionStack.cs +++ b/src/Text/Impl/BraceCompletion/BraceCompletionStack.cs @@ -11,6 +11,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation using Microsoft.VisualStudio.Text.BraceCompletion; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -31,11 +32,11 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation private IBraceCompletionAdornmentServiceFactory _adornmentServiceFactory; private IBraceCompletionAdornmentService _adornmentService; - private GuardedOperations _guardedOperations; + private IGuardedOperations _guardedOperations; #endregion #region Constructors - public BraceCompletionStack(ITextView textView, IBraceCompletionAdornmentServiceFactory adornmentFactory, GuardedOperations guardedOperations) + public BraceCompletionStack(ITextView textView, IBraceCompletionAdornmentServiceFactory adornmentFactory, IGuardedOperations guardedOperations) { _adornmentServiceFactory = adornmentFactory; _stack = new Stack<IBraceCompletionSession>(); diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs index b10eee8..95b3bdf 100644 --- a/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs +++ b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs @@ -37,15 +37,15 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation // Validate. if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (bufferTagAggregatorFactory == null) { - throw new ArgumentNullException("bufferTagAggregatorFactory"); + throw new ArgumentNullException(nameof(bufferTagAggregatorFactory)); } if (classificationTypeRegistry == null) { - throw new ArgumentNullException("classificationTypeRegistry"); + throw new ArgumentNullException(nameof(classificationTypeRegistry)); } _textBuffer = textBuffer; @@ -63,15 +63,15 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation // Validate. if (textView == null) { - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); } if (viewTagAggregatorFactory == null) { - throw new ArgumentNullException("viewTagAggregatorFactory"); + throw new ArgumentNullException(nameof(viewTagAggregatorFactory)); } if (classificationTypeRegistry == null) { - throw new ArgumentNullException("classificationTypeRegistry"); + throw new ArgumentNullException(nameof(classificationTypeRegistry)); } _textBuffer = textView.TextBuffer; @@ -336,7 +336,7 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation return results; } - private int Compare(PointData a, PointData b) + private static int Compare(PointData a, PointData b) { if (a.Position == b.Position) return (b.IsStart.CompareTo(a.IsStart)); // startpoints go before end points when positions are tied diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs b/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs index 653e768..4871c38 100644 --- a/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs +++ b/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs @@ -70,7 +70,9 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation #region IDisposable members +#pragma warning disable CA1063 // Implement IDisposable Correctly public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { foreach(var classifier in Classifiers) { diff --git a/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs b/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs index 35a5835..b2a65a3 100644 --- a/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs +++ b/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs @@ -9,12 +9,9 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation { using System; using System.Collections.Generic; - using System.Collections.ObjectModel; using System.ComponentModel.Composition; - using Microsoft.VisualStudio.Text.Differencing; using Microsoft.VisualStudio.Text.Projection; using Microsoft.VisualStudio.Text.Tagging; - using Microsoft.VisualStudio.Text.Utilities; using Microsoft.VisualStudio.Utilities; [Export(typeof(ITaggerProvider))] @@ -22,16 +19,13 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation [TagType(typeof(ClassificationTag))] internal class ProjectionWorkaroundProvider : ITaggerProvider { - [Import] - internal IDifferenceService diffService { get; set; } - public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag { IProjectionBuffer projectionBuffer = buffer as IProjectionBuffer; if (projectionBuffer == null) return null; - return new ProjectionWorkaroundTagger(projectionBuffer, diffService) as ITagger<T>; + return new ProjectionWorkaroundTagger(projectionBuffer) as ITagger<T>; } } @@ -45,82 +39,69 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation internal class ProjectionWorkaroundTagger : ITagger<ClassificationTag> { IProjectionBuffer ProjectionBuffer { get; set; } - IDifferenceService diffService; - internal ProjectionWorkaroundTagger(IProjectionBuffer projectionBuffer, IDifferenceService diffService) + internal ProjectionWorkaroundTagger(IProjectionBuffer projectionBuffer) { this.ProjectionBuffer = projectionBuffer; - this.diffService = diffService; this.ProjectionBuffer.SourceBuffersChanged += SourceSpansChanged; } #region ITagger<ClassificationTag> members public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans) { - yield break; + return Array.Empty<ITagSpan<ClassificationTag>>(); } public event EventHandler<SnapshotSpanEventArgs> TagsChanged; - #endregion #region Source span differencing + change event private void SourceSpansChanged(object sender, ProjectionSourceSpansChangedEventArgs e) { - if (e.Changes.Count == 0) + var handler = TagsChanged; + if ((handler != null) && (e.Changes.Count == 0)) { // If there weren't text changes, but there were span changes, then // send out a classification changed event over the spans that changed. - ProjectionSpanDifference difference = ProjectionSpanDiffer.DiffSourceSpans(this.diffService, e.Before, e.After); - int pos = 0; - int start = int.MaxValue; - int end = int.MinValue; - foreach (var diff in difference.DifferenceCollection) - { - pos += GetMatchSize(difference.DeletedSpans, diff.Before); - start = Math.Min(start, pos); - - // Now, for every span added in the new snapshot that replaced - // the deleted spans, add it to our span to raise changed events - // over. - for (int i = diff.Right.Start; i < diff.Right.End; i++) - { - pos += difference.InsertedSpans[i].Length; - } - - end = Math.Max(end, pos); - } + // + // We're raising a single event here so all we need is the start of the first changed span + // to the end of the last changed span (or, as we calculate it, the end of the first identical + // spans to the start of the last identical spans). + // + // Note that we are being generous in the span we raise. For example if I change the projection buffer + // from projecting (V0:[0,10)) and (V0:[10,15)) to projecting (V0:[0,5)) and (V0:[5,15)) we'll raise a snapshot changed + // event over the entire buffer even though neither the projected text nor the content type of its buffer + // changed. This case shouldn't happen very often and the cost of (falsely) raising a classification changed + // event is pretty small so this is a net perf win compared to doing a more expensive diff to get the actual + // changed span. + var leftSpans = e.Before.GetSourceSpans(); + var rightSpans = e.After.GetSourceSpans(); + var spansToCompare = Math.Min(leftSpans.Count, rightSpans.Count); - if (start != int.MaxValue && end != int.MinValue) + int start = 0; + int identicalSpansAtStart = 0; + while ((identicalSpansAtStart < spansToCompare) && (leftSpans[identicalSpansAtStart] == rightSpans[identicalSpansAtStart])) { - RaiseTagsChangedEvent(new SnapshotSpan(e.After, Span.FromBounds(start, end))); + start += rightSpans[identicalSpansAtStart].Length; + ++identicalSpansAtStart; } - } - } - private static int GetMatchSize(ReadOnlyCollection<SnapshotSpan> spans, Match match) - { - int size = 0; - if (match != null) - { - Span extent = match.Left; - for (int s = extent.Start; s < extent.End; ++s) + if ((identicalSpansAtStart < leftSpans.Count) || (identicalSpansAtStart < rightSpans.Count)) { - size += spans[s].Length; - } - } - return size; - } + // There are at least some span differences between leftSpans and rightSpans so we don't need to worry about running over. + spansToCompare -= identicalSpansAtStart; //No need to compare spans in the starting identical block. + int end = e.After.Length; + int identicalSpansAtEndPlus1 = 1; + while ((identicalSpansAtEndPlus1 <= spansToCompare) && (leftSpans[leftSpans.Count - identicalSpansAtEndPlus1] == rightSpans[rightSpans.Count - identicalSpansAtEndPlus1])) + { + end -= rightSpans[rightSpans.Count - identicalSpansAtEndPlus1].Length; + ++identicalSpansAtEndPlus1; + } - private void RaiseTagsChangedEvent(SnapshotSpan span) - { - var handler = TagsChanged; - if (handler != null) - { - handler(this, new SnapshotSpanEventArgs(span)); + handler(this, new SnapshotSpanEventArgs(new SnapshotSpan(e.After, Span.FromBounds(start, end)))); + } } } - #endregion } } diff --git a/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs b/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs index 6f3409f..5a47305 100644 --- a/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs +++ b/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs @@ -38,7 +38,7 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation public bool IsOfType(string type) { - if (this.name == type) + if (string.Equals(this.name, type, System.StringComparison.Ordinal)) return true; else if (this.baseTypes != null) { diff --git a/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs b/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs index d890eb4..6f746a0 100644 --- a/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs +++ b/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs @@ -59,12 +59,12 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation { if (type == null) { - throw new ArgumentNullException("type"); + throw new ArgumentNullException(nameof(type)); } if (baseTypes == null) { - throw new ArgumentNullException("baseTypes"); + throw new ArgumentNullException(nameof(baseTypes)); } if (ClassificationTypes.ContainsKey(type)) { @@ -94,7 +94,7 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation // Validate if (baseTypes == null) { - throw new ArgumentNullException("baseTypes"); + throw new ArgumentNullException(nameof(baseTypes)); } if (!baseTypes.GetEnumerator().MoveNext()) { @@ -115,7 +115,7 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation // Validate if (baseTypes == null) { - throw new ArgumentNullException("baseTypes"); + throw new ArgumentNullException(nameof(baseTypes)); } if (baseTypes.Length == 0) { @@ -150,7 +150,7 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation { if (_classificationTypes == null) { - _classificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.InvariantCultureIgnoreCase); + _classificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.OrdinalIgnoreCase); BuildClassificationTypes(_classificationTypes); } return _classificationTypes; @@ -209,7 +209,7 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation // Lazily init if (_transientClassificationTypes == null) { - _transientClassificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.InvariantCultureIgnoreCase); + _transientClassificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.OrdinalIgnoreCase); } List<IClassificationType> sortedBaseTypes = new List<IClassificationType>(baseTypes); diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs index 315e55f..4ced5c3 100644 --- a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs +++ b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs @@ -9,6 +9,7 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Threading; using ICommandHandlerAndMetadata = System.Lazy<Microsoft.VisualStudio.Commanding.ICommandHandler, Microsoft.VisualStudio.UI.Text.Commanding.Implementation.ICommandHandlerMetadata>; +using System.Runtime.CompilerServices; namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation { @@ -124,20 +125,38 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation handlerChain(); } + if (handler is IDynamicCommandHandler<T> dynamicCommandHandler && + !dynamicCommandHandler.CanExecuteCommand(args)) + { + // Skip this one as it cannot execute the command. + continue; + } + if (commandExecutionContext == null) { commandExecutionContext = CreateCommandExecutionContext(); } - handlerChain = () => _guardedOperations.CallExtensionPoint(handler, () => handler.ExecuteCommand(args, nextHandler, commandExecutionContext)); + handlerChain = () => _guardedOperations.CallExtensionPoint(handler, + () => handler.ExecuteCommand(args, nextHandler, commandExecutionContext), + // Do not guard against cancellation exceptions, they are handled by ExecuteCommandHandlerChain + exceptionGuardFilter: (e) => !IsOperationCancelledException(e)); } ExecuteCommandHandlerChain(commandExecutionContext, handlerChain, nextCommandHandler); } } - private void ExecuteCommandHandlerChain(CommandExecutionContext commandExecutionContext, - Action handlerChain, Action nextCommandHandler) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOperationCancelledException(Exception e) + { + return e is OperationCanceledException || e is AggregateException aggregate && aggregate.InnerExceptions.All(ie => ie is OperationCanceledException); + } + + private static void ExecuteCommandHandlerChain( + CommandExecutionContext commandExecutionContext, + Action handlerChain, + Action nextCommandHandler) { try { diff --git a/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs b/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs index 85f1156..4a0d84e 100644 --- a/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs +++ b/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs @@ -53,7 +53,7 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation } else { - throw new ArgumentOutOfRangeException("differenceOptions"); + throw new ArgumentOutOfRangeException(nameof(differenceOptions)); } return DiffText(left, right, type, differenceOptions); @@ -92,7 +92,7 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation } else { - throw new ArgumentOutOfRangeException("differenceOptions"); + throw new ArgumentOutOfRangeException(nameof(differenceOptions)); } return DiffText(left, right, type, differenceOptions); @@ -103,7 +103,7 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation StringDifferenceOptions nextOptions = new StringDifferenceOptions(differenceOptions); nextOptions.DifferenceType &= ~type; - var diffCollection = ComputeMatches(type, differenceOptions, left, right); + var diffCollection = ComputeMatches(differenceOptions, left, right); return new HierarchicalDifferenceCollection(diffCollection, left, right, this, nextOptions); } @@ -133,13 +133,13 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation return line.GetTextIncludingLineBreak(); } - static IDifferenceCollection<string> ComputeMatches(StringDifferenceTypes differenceType, StringDifferenceOptions differenceOptions, + static IDifferenceCollection<string> ComputeMatches(StringDifferenceOptions differenceOptions, IList<string> leftSequence, IList<string> rightSequence) { - return ComputeMatches(differenceType, differenceOptions, leftSequence, rightSequence, leftSequence, rightSequence); + return ComputeMatches(differenceOptions, leftSequence, rightSequence, leftSequence, rightSequence); } - static IDifferenceCollection<string> ComputeMatches(StringDifferenceTypes differenceType, StringDifferenceOptions differenceOptions, + static IDifferenceCollection<string> ComputeMatches(StringDifferenceOptions differenceOptions, IList<string> leftSequence, IList<string> rightSequence, IList<string> originalLeftSequence, IList<string> originalRightSequence) { diff --git a/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs b/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs index 33d06cb..93f5d32 100644 --- a/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs +++ b/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs @@ -46,11 +46,11 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation StringDifferenceOptions options) { if (differenceCollection == null) - throw new ArgumentNullException("differenceCollection"); + throw new ArgumentNullException(nameof(differenceCollection)); if (left == null) - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); if (right == null) - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); if (!object.ReferenceEquals(left, differenceCollection.LeftSequence)) throw new ArgumentException("left must equal differenceCollection.LeftSequence"); if (!object.ReferenceEquals(right, differenceCollection.RightSequence)) diff --git a/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs b/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs index a40aa72..2f664d9 100644 --- a/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs +++ b/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs @@ -20,7 +20,7 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation internal sealed class MaximalSubsequenceAlgorithm : IDifferenceService { #region IDifferenceService Members - static readonly Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] Empty = new Microsoft.TeamFoundation.Diff.Copy.IDiffChange[0]; + static readonly Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] Empty = Array.Empty<TeamFoundation.Diff.Copy.IDiffChange>(); public IDifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right) { @@ -37,9 +37,9 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation internal static DifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right, IList<T> originalLeft, IList<T> originalRight, ContinueProcessingPredicate<T> continueProcessingPredicate) { if (left == null) - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); if (right == null) - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] changes; if ((left.Count == 0) || (right.Count == 0)) diff --git a/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs b/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs index 47df212..20e92c3 100644 --- a/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs +++ b/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs @@ -27,7 +27,7 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation public SnapshotLineList(SnapshotSpan snapshotSpan, Func<ITextSnapshotLine, string> getLineTextCallback, StringDifferenceOptions options) { if (getLineTextCallback == null) - throw new ArgumentNullException("getLineTextCallback"); + throw new ArgumentNullException(nameof(getLineTextCallback)); if ((options.DifferenceType & StringDifferenceTypes.Line) == 0) throw new InvalidOperationException("This collection can only be used for line differencing"); @@ -96,7 +96,7 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation SnapshotSpan GetSpanOfIndex(int index) { if (index < 0 || index >= _lineSpan.Length) - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); ITextSnapshotLine line = _snapshotSpan.Snapshot.GetLineFromLineNumber(_lineSpan.Start + index); SnapshotSpan? lineSpan = line.ExtentIncludingLineBreak.Intersection(_snapshotSpan); diff --git a/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs b/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs index 6e2bfd3..e401345 100644 --- a/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs +++ b/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs @@ -515,7 +515,7 @@ namespace Microsoft.TeamFoundation.Diff.Copy /// the constructed changes. /// </summary> //************************************************************************* - internal class DiffChangeHelper : IDisposable + internal sealed class DiffChangeHelper : IDisposable { //********************************************************************* /// <summary> @@ -657,6 +657,7 @@ namespace Microsoft.TeamFoundation.Diff.Copy /// A base for classes which compute the differences between two input sequences. /// </summary> //************************************************************************* +#pragma warning disable CA1063 // Implement IDisposable Correctly public abstract class DiffFinder<T> : IDisposable { //************************************************************************* @@ -689,12 +690,14 @@ namespace Microsoft.TeamFoundation.Diff.Copy get { return m_elementComparer; } } + //************************************************************************* /// <summary> /// Disposes resources used by this DiffFinder /// </summary> //************************************************************************* public virtual void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { if (m_originalIds != null) { @@ -914,7 +917,7 @@ namespace Microsoft.TeamFoundation.Diff.Copy Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); // Identical sequences - No differences - changes = new IDiffChange[0]; + changes = Array.Empty<IDiffChange>(); } return changes; @@ -948,7 +951,9 @@ namespace Microsoft.TeamFoundation.Diff.Copy /// </summary> //************************************************************************* [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lcs")] +#pragma warning disable CA1000 // Do not declare static members on generic types public static DiffFinder<T> LcsDiff +#pragma warning restore CA1000 // Do not declare static members on generic types { get { return new LcsDiff<T>(); } } @@ -971,4 +976,4 @@ namespace Microsoft.TeamFoundation.Diff.Copy //Early termination predicate private ContinueDifferencePredicate<T> m_predicate; } -}
\ No newline at end of file +} diff --git a/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs b/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs index c70ca9d..9fd9720 100644 --- a/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs +++ b/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs @@ -6,10 +6,8 @@ // Use at your own risk. // using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.Text; //************************************************************************* // The code from this point on is a soure-port of the TFS diff algorithm, to be available @@ -54,7 +52,6 @@ namespace Microsoft.TeamFoundation.Diff.Copy { m_reverseHistory = null; } - GC.SuppressFinalize(this); } //************************************************************************* @@ -138,7 +135,7 @@ namespace Microsoft.TeamFoundation.Diff.Copy Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); // Identical sequences - No differences - changes = new IDiffChange[0]; + changes = Array.Empty<IDiffChange>(); } return changes; @@ -162,7 +159,7 @@ namespace Microsoft.TeamFoundation.Diff.Copy // Second Half: (midOriginal + 1, minModified + 1) to (originalEnd, modifiedEnd) // NOTE: ComputeDiff() is inclusive, therefore the second range starts on the next point IDiffChange[] leftChanges = ComputeDiffRecursive(originalStart, midOriginal, modifiedStart, midModified, out quitEarly); - IDiffChange[] rightChanges = new IDiffChange[0]; + IDiffChange[] rightChanges = Array.Empty<IDiffChange>(); if (!quitEarly) { @@ -671,7 +668,7 @@ namespace Microsoft.TeamFoundation.Diff.Copy /// <param name="right">The right changes</param> /// <returns>The concatenated list</returns> //************************************************************************* - private IDiffChange[] ConcatenateChanges(IDiffChange[] left, IDiffChange[] right) + private static IDiffChange[] ConcatenateChanges(IDiffChange[] left, IDiffChange[] right) { IDiffChange mergedChange; @@ -713,7 +710,7 @@ namespace Microsoft.TeamFoundation.Diff.Copy /// null otherwise</param> /// <returns>True if the two changes overlap</returns> //************************************************************************* - private bool ChangesOverlap(IDiffChange left, IDiffChange right, out IDiffChange mergedChange) + private static bool ChangesOverlap(IDiffChange left, IDiffChange right, out IDiffChange mergedChange) { Debug.Assert(left.OriginalStart <= right.OriginalStart, "Left change is not less than or equal to right change"); Debug.Assert(left.ModifiedStart <= right.ModifiedStart, "Left change is not less than or equal to right change"); @@ -760,10 +757,11 @@ namespace Microsoft.TeamFoundation.Diff.Copy /// <param name="numDiagonals">The total number of diagonals.</param> /// <returns>The clipped diagonal index.</returns> //************************************************************************* - private int ClipDiagonalBound(int diagonal, - int numDifferences, - int diagonalBaseIndex, - int numDiagonals) + private static int ClipDiagonalBound( + int diagonal, + int numDifferences, + int diagonalBaseIndex, + int numDiagonals) { if (diagonal >= 0 && diagonal < numDiagonals) { diff --git a/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs b/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs index f7afc1c..01e6895 100644 --- a/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs +++ b/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs @@ -42,7 +42,7 @@ namespace Microsoft.VisualStudio.Text.Differencing.Implementation protected TokenizedStringList(string original) { if (original == null) - throw new ArgumentNullException("original"); + throw new ArgumentNullException(nameof(original)); this.original = original; } diff --git a/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs b/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs index 97d1369..d19da59 100644 --- a/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs +++ b/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs @@ -19,10 +19,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { // Think twice before adding any fields here! These objects are long-lived and consume considerable space. // Unusual cases should be handled by the GeneralAfterTextBufferChangedUndoPrimitive class below. - protected ITextUndoHistory _undoHistory; - protected int _newCaretIndex; - protected byte _newCaretAffinityByte; - protected bool _canUndo; + private readonly ITextUndoHistory _undoHistory; + public readonly SelectionState State; + private bool _canUndo; /// <summary> /// Constructs a AfterTextBufferChangeUndoPrimitive. @@ -39,57 +38,21 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (textView == null) { - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); } if (undoHistory == null) { - throw new ArgumentNullException("undoHistory"); + throw new ArgumentNullException(nameof(undoHistory)); } - // Store the ITextView for these changes in the ITextUndoHistory properties so we can retrieve it later. - if (!undoHistory.Properties.ContainsProperty(typeof(ITextView))) - { - undoHistory.Properties[typeof(ITextView)] = textView; - } - - IMapEditToData map = BeforeTextBufferChangeUndoPrimitive.GetMap(textView); - - CaretPosition caret = textView.Caret.Position; - int newCaretIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, caret.BufferPosition); - int newCaretVirtualSpaces = caret.VirtualBufferPosition.VirtualSpaces; - - VirtualSnapshotPoint anchor = textView.Selection.AnchorPoint; - int newSelectionAnchorIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, anchor.Position); - int newSelectionAnchorVirtualSpaces = anchor.VirtualSpaces; - - VirtualSnapshotPoint active = textView.Selection.ActivePoint; - int newSelectionActiveIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, active.Position); - int newSelectionActiveVirtualSpaces = active.VirtualSpaces; + return new AfterTextBufferChangeUndoPrimitive(textView, undoHistory); - TextSelectionMode newSelectionMode = textView.Selection.Mode; - - if (newCaretVirtualSpaces != 0 || - newSelectionAnchorIndex != newCaretIndex || - newSelectionAnchorVirtualSpaces != 0 || - newSelectionActiveIndex != newCaretIndex || - newSelectionActiveVirtualSpaces != 0 || - newSelectionMode != TextSelectionMode.Stream) - { - return new GeneralAfterTextBufferChangeUndoPrimitive - (undoHistory, newCaretIndex, caret.Affinity, newCaretVirtualSpaces, newSelectionAnchorIndex, - newSelectionAnchorVirtualSpaces, newSelectionActiveIndex, newSelectionActiveVirtualSpaces, newSelectionMode); - } - else - { - return new AfterTextBufferChangeUndoPrimitive(undoHistory, newCaretIndex, caret.Affinity); - } } - protected AfterTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, int caretIndex, PositionAffinity caretAffinity) + protected AfterTextBufferChangeUndoPrimitive(ITextView textView, ITextUndoHistory undoHistory) { _undoHistory = undoHistory; - _newCaretIndex = caretIndex; - _newCaretAffinityByte = (byte)caretAffinity; + this.State = new SelectionState(textView); _canUndo = true; } @@ -106,15 +69,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return view; } - internal int CaretIndex - { - get { return _newCaretIndex; } - } - - internal virtual int CaretVirtualSpace - { - get { return 0; } - } #region ITextUndoPrimitive Members @@ -151,7 +105,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation Debug.Assert(view == null || !view.IsClosed, "Attempt to undo/redo on a closed view? This shouldn't happen."); if (view != null && !view.IsClosed) { - DoMoveCaretAndSelect(view, BeforeTextBufferChangeUndoPrimitive.GetMap(view)); + this.State.Restore(view); view.Caret.EnsureVisible(); } @@ -159,17 +113,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } /// <summary> - /// Move the caret and restore the selection as part of the Redo operation. - /// </summary> - protected virtual void DoMoveCaretAndSelect(ITextView view, IMapEditToData map) - { - SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newCaretIndex)); - - view.Caret.MoveTo(newCaret, (PositionAffinity)_newCaretAffinityByte); - view.Selection.Clear(); - } - - /// <summary> /// Undo the action. /// </summary> /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> @@ -197,65 +140,4 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } #endregion } - - /// <summary> - /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the general - /// version of the primitive that handles all cases, including those involving selections and virtual space. - /// </summary> - internal class GeneralAfterTextBufferChangeUndoPrimitive : AfterTextBufferChangeUndoPrimitive - { - private int _newCaretVirtualSpaces; - private int _newSelectionAnchorIndex; - private int _newSelectionAnchorVirtualSpaces; - private int _newSelectionActiveIndex; - private int _newSelectionActiveVirtualSpaces; - private TextSelectionMode _newSelectionMode; - - public GeneralAfterTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, - int newCaretIndex, - PositionAffinity newCaretAffinity, - int newCaretVirtualSpaces, - int newSelectionAnchorIndex, - int newSelectionAnchorVirtualSpaces, - int newSelectionActiveIndex, - int newSelectionActiveVirtualSpaces, - TextSelectionMode newSelectionMode) - : base(undoHistory, newCaretIndex, newCaretAffinity) - { - _newCaretVirtualSpaces = newCaretVirtualSpaces; - _newSelectionAnchorIndex = newSelectionAnchorIndex; - _newSelectionAnchorVirtualSpaces = newSelectionAnchorVirtualSpaces; - _newSelectionActiveIndex = newSelectionActiveIndex; - _newSelectionActiveVirtualSpaces = newSelectionActiveVirtualSpaces; - _newSelectionMode = newSelectionMode; - } - - internal override int CaretVirtualSpace - { - get { return _newCaretVirtualSpaces; } - } - - /// <summary> - /// Move the caret and restore the selection as part of the Redo operation. - /// </summary> - protected override void DoMoveCaretAndSelect(ITextView view, IMapEditToData map) - { - SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newCaretIndex)); - SnapshotPoint newAnchor = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newSelectionAnchorIndex)); - SnapshotPoint newActive = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newSelectionActiveIndex)); - - view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret, _newCaretVirtualSpaces), (PositionAffinity)_newCaretAffinityByte); - - view.Selection.Mode = _newSelectionMode; - - var virtualAnchor = new VirtualSnapshotPoint(newAnchor, _newSelectionAnchorVirtualSpaces); - var virtualActive = new VirtualSnapshotPoint(newActive, _newSelectionActiveVirtualSpaces); - - // Buffer may have been changed by one of the listeners on the caret move event. - virtualAnchor = virtualAnchor.TranslateTo(view.TextSnapshot); - virtualActive = virtualActive.TranslateTo(view.TextSnapshot); - - view.Selection.Select(virtualAnchor, virtualActive); - } - } } diff --git a/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs b/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs index 47abd9b..baf7c70 100644 --- a/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs +++ b/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs @@ -19,10 +19,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { // Think twice before adding any fields here! These objects are long-lived and consume considerable space. // Unusual cases should be handled by the GeneralAfterTextBufferChangedUndoPrimitive class below. - protected ITextUndoHistory _undoHistory; - protected int _oldCaretIndex; - protected byte _oldCaretAffinityByte; - protected bool _canUndo; + private readonly ITextUndoHistory _undoHistory; + public readonly SelectionState State; + private bool _canUndo; /// <summary> /// Constructs a BeforeTextBufferChangeUndoPrimitive. @@ -39,86 +38,20 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (textView == null) { - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); } if (undoHistory == null) { - throw new ArgumentNullException("undoHistory"); + throw new ArgumentNullException(nameof(undoHistory)); } - // Store the ITextView for these changes in the ITextUndoHistory properties so we can retrieve it later. - if (!undoHistory.Properties.ContainsProperty(typeof(ITextView))) - { - undoHistory.Properties[typeof(ITextView)] = textView; - } - - CaretPosition caret = textView.Caret.Position; - - IMapEditToData map = BeforeTextBufferChangeUndoPrimitive.GetMap(textView); - - int oldCaretIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, caret.BufferPosition); - int oldCaretVirtualSpaces = caret.VirtualBufferPosition.VirtualSpaces; - - VirtualSnapshotPoint anchor = textView.Selection.AnchorPoint; - int oldSelectionAnchorIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, anchor.Position); - int oldSelectionAnchorVirtualSpaces = anchor.VirtualSpaces; - - VirtualSnapshotPoint active = textView.Selection.ActivePoint; - int oldSelectionActiveIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, active.Position); - int oldSelectionActiveVirtualSpaces = active.VirtualSpaces; - - TextSelectionMode oldSelectionMode = textView.Selection.Mode; - - if (oldCaretVirtualSpaces != 0 || - oldSelectionAnchorIndex != oldCaretIndex || - oldSelectionAnchorVirtualSpaces != 0 || - oldSelectionActiveIndex != oldCaretIndex || - oldSelectionActiveVirtualSpaces != 0 || - oldSelectionMode != TextSelectionMode.Stream) - { - return new GeneralBeforeTextBufferChangeUndoPrimitive - (undoHistory, oldCaretIndex, caret.Affinity, oldCaretVirtualSpaces, oldSelectionAnchorIndex, - oldSelectionAnchorVirtualSpaces, oldSelectionActiveIndex, oldSelectionActiveVirtualSpaces, oldSelectionMode); - } - else - { - return new BeforeTextBufferChangeUndoPrimitive(undoHistory, oldCaretIndex, caret.Affinity); - } - } - - //Get the map -- if any -- used to map points in the view's edit buffer to the data buffer. The map is needed because the undo history - //typically lives on the data buffer, but is used by the view on the edit buffer and a view (if any) on the data buffer. If there isn't - //a contract that guarantees that the contents of the edit and databuffers are the same, undoing an action on the edit buffer view and - //then undoing it on the data buffer view will cause cause the undo to try and restore caret/selection (in the data buffer) the coorinates - //saved in the edit buffer. This isn't good. - internal static IMapEditToData GetMap(ITextView view) - { - IMapEditToData map = null; - if (view.TextViewModel.EditBuffer != view.TextViewModel.DataBuffer) - { - view.Properties.TryGetProperty(typeof(IMapEditToData), out map); - } - - return map; - } - - //Map point from a position in the edit buffer to a position in the data buffer (== if there is no map, otherwise ask the map). - internal static int MapToData(IMapEditToData map, int point) - { - return (map != null) ? map.MapEditToData(point) : point; - } - - //Map point from a position in the data buffer to a position in the edit buffer (== if there is no map, otherwise ask the map). - internal static int MapToEdit(IMapEditToData map, int point) - { - return (map != null) ? map.MapDataToEdit(point) : point; + return new BeforeTextBufferChangeUndoPrimitive(textView, undoHistory); } - protected BeforeTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, int caretIndex, PositionAffinity caretAffinity) + private BeforeTextBufferChangeUndoPrimitive(ITextView textView, ITextUndoHistory undoHistory) { _undoHistory = undoHistory; - _oldCaretIndex = caretIndex; - _oldCaretAffinityByte = (byte)caretAffinity; + this.State = new SelectionState(textView); _canUndo = true; } @@ -192,34 +125,18 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation Debug.Assert(view == null || !view.IsClosed, "Attempt to undo/redo on a closed view? This shouldn't happen."); if (view != null && !view.IsClosed) { - UndoMoveCaretAndSelect(view, BeforeTextBufferChangeUndoPrimitive.GetMap(view)); + this.State.Restore(view); view.Caret.EnsureVisible(); } _canUndo = false; } - /// <summary> - /// Move the caret and restore the selection as part of the Undo operation. - /// </summary> - protected virtual void UndoMoveCaretAndSelect(ITextView view, IMapEditToData map) - { - SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, MapToEdit(map, _oldCaretIndex)); - - view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret), (PositionAffinity)_oldCaretAffinityByte); - view.Selection.Clear(); - } - - protected virtual int OldCaretVirtualSpaces - { - get { return 0; } - } - public override bool CanMerge(ITextUndoPrimitive older) { if (older == null) { - throw new ArgumentNullException("older"); + throw new ArgumentNullException(nameof(older)); } AfterTextBufferChangeUndoPrimitive olderPrimitive = older as AfterTextBufferChangeUndoPrimitive; @@ -229,70 +146,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return false; } - return (olderPrimitive.CaretIndex == _oldCaretIndex) && (olderPrimitive.CaretVirtualSpace == OldCaretVirtualSpaces); + return olderPrimitive.State.Matches(this.State); } #endregion } - - /// <summary> - /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the general - /// version of the primitive that handles all cases, including those involving selections and virtual space. - /// </summary> - internal class GeneralBeforeTextBufferChangeUndoPrimitive : BeforeTextBufferChangeUndoPrimitive - { - private int _oldCaretVirtualSpaces; - private int _oldSelectionAnchorIndex; - private int _oldSelectionAnchorVirtualSpaces; - private int _oldSelectionActiveIndex; - private int _oldSelectionActiveVirtualSpaces; - private TextSelectionMode _oldSelectionMode; - - public GeneralBeforeTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, - int oldCaretIndex, - PositionAffinity oldCaretAffinity, - int oldCaretVirtualSpaces, - int oldSelectionAnchorIndex, - int oldSelectionAnchorVirtualSpaces, - int oldSelectionActiveIndex, - int oldSelectionActiveVirtualSpaces, - TextSelectionMode oldSelectionMode) - : base(undoHistory, oldCaretIndex, oldCaretAffinity) - { - _oldCaretVirtualSpaces = oldCaretVirtualSpaces; - _oldSelectionAnchorIndex = oldSelectionAnchorIndex; - _oldSelectionAnchorVirtualSpaces = oldSelectionAnchorVirtualSpaces; - _oldSelectionActiveIndex = oldSelectionActiveIndex; - _oldSelectionActiveVirtualSpaces = oldSelectionActiveVirtualSpaces; - _oldSelectionMode = oldSelectionMode; - } - - /// <summary> - /// Move the caret and restore the selection as part of the Undo operation. - /// </summary> - protected override void UndoMoveCaretAndSelect(ITextView view, IMapEditToData map) - { - SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldCaretIndex)); - SnapshotPoint newAnchor = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldSelectionAnchorIndex)); - SnapshotPoint newActive = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldSelectionActiveIndex)); - - view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret, _oldCaretVirtualSpaces), (PositionAffinity)_oldCaretAffinityByte); - - view.Selection.Mode = _oldSelectionMode; - - var virtualAnchor = new VirtualSnapshotPoint(newAnchor, _oldSelectionAnchorVirtualSpaces); - var virtualActive = new VirtualSnapshotPoint(newActive, _oldSelectionActiveVirtualSpaces); - - // Buffer may have been changed by one of the listeners on the caret move event. - virtualAnchor = virtualAnchor.TranslateTo(view.TextSnapshot); - virtualActive = virtualActive.TranslateTo(view.TextSnapshot); - - view.Selection.Select(virtualAnchor, virtualActive); - } - - protected override int OldCaretVirtualSpaces - { - get { return _oldCaretVirtualSpaces; } - } - } } diff --git a/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs b/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs index 44fa09e..aa065e4 100644 --- a/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs +++ b/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs @@ -126,17 +126,17 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (textView == null) { - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); } if (outliningManager == null) { - throw new ArgumentNullException("outliningManager"); + throw new ArgumentNullException(nameof(outliningManager)); } if (collaspedSpans == null) { - throw new ArgumentNullException("collaspedSpans"); + throw new ArgumentNullException(nameof(collaspedSpans)); } _outliningManager = outliningManager; diff --git a/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionCommandHandler.cs b/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionCommandHandler.cs deleted file mode 100644 index c7ec9b1..0000000 --- a/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionCommandHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Commanding; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Text.Operations.Implementation -{ - [Export(typeof(ICommandHandler))] - [Name(nameof(ExpandContractSelectionCommandHandler))] - [ContentType("any")] - [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] - [TextViewRole(PredefinedTextViewRoles.EmbeddedPeekTextView)] - internal sealed class ExpandContractSelectionCommandHandler - : ICommandHandler<ExpandSelectionCommandArgs>, ICommandHandler<ContractSelectionCommandArgs> - { - [ImportingConstructor] - public ExpandContractSelectionCommandHandler( - IEditorOptionsFactoryService editorOptionsFactoryService, - ITextStructureNavigatorSelectorService navigatorSelectorService) - { - this.EditorOptionsFactoryService = editorOptionsFactoryService; - this.NavigatorSelectorService = navigatorSelectorService; - } - - public IEditorOptionsFactoryService EditorOptionsFactoryService { get; } - - private readonly ITextStructureNavigatorSelectorService NavigatorSelectorService; - - public string DisplayName => Strings.ExpandContractSelectionCommandHandlerName; - - public CommandState GetCommandState(ExpandSelectionCommandArgs args) - { - var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState( - args.TextView, - this.EditorOptionsFactoryService, - this.NavigatorSelectorService); - return storedCommandState.GetExpandCommandState(args.TextView); - } - - public CommandState GetCommandState(ContractSelectionCommandArgs args) - { - var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState( - args.TextView, - this.EditorOptionsFactoryService, - this.NavigatorSelectorService); - return storedCommandState.GetContractCommandState(args.TextView); - } - - public bool ExecuteCommand(ExpandSelectionCommandArgs args, CommandExecutionContext context) - { - var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState( - args.TextView, - this.EditorOptionsFactoryService, - this.NavigatorSelectorService); - return storedCommandState.ExpandSelection(args.TextView); - } - - public bool ExecuteCommand(ContractSelectionCommandArgs args, CommandExecutionContext context) - { - var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState( - args.TextView, - this.EditorOptionsFactoryService, - this.NavigatorSelectorService); - return storedCommandState.ContractSelection(args.TextView); - } - } -} diff --git a/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionImplementation.cs b/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionImplementation.cs deleted file mode 100644 index a26c025..0000000 --- a/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionImplementation.cs +++ /dev/null @@ -1,134 +0,0 @@ -namespace Microsoft.VisualStudio.Text.Operations.Implementation -{ - using System; - using System.Collections.Generic; - using Microsoft.VisualStudio.Commanding; - using Microsoft.VisualStudio.Text.Editor; - - internal class ExpandContractSelectionImplementation - { - private readonly IEditorOptions editorOptions; - private readonly ITextStructureNavigatorSelectorService navigatorSelectorService; - private bool ignoreSelectionChangedEvent; - - public static ExpandContractSelectionImplementation GetOrCreateExpandContractState( - ITextView textView, - IEditorOptionsFactoryService editorOptionsFactoryService, - ITextStructureNavigatorSelectorService navigator) - { - return textView.Properties.GetOrCreateSingletonProperty<ExpandContractSelectionImplementation>( - typeof(ExpandContractSelectionImplementation), - () => new ExpandContractSelectionImplementation( - navigator, - editorOptionsFactoryService.GetOptions(textView), - textView)); - } - - private ExpandContractSelectionImplementation( - ITextStructureNavigatorSelectorService navigatorSelectorService, - IEditorOptions editorOptions, - ITextView textView) - { - this.editorOptions = editorOptions; - this.navigatorSelectorService = navigatorSelectorService; - textView.Selection.SelectionChanged += this.OnSelectionChanged; - } - - // Internal for testing. - internal readonly Stack<Tuple<VirtualSnapshotSpan, TextSelectionMode>> previousExpansionsStack - = new Stack<Tuple<VirtualSnapshotSpan, TextSelectionMode>>(); - - public CommandState GetExpandCommandState(ITextView textView) => CommandState.Available; - - public CommandState GetContractCommandState(ITextView textView) - { - if (this.previousExpansionsStack.Count > 0) - { - return CommandState.Available; - } - - return CommandState.Unavailable; - } - - public bool ExpandSelection(ITextView textView) - { - try - { - this.ignoreSelectionChangedEvent = true; - - var navigator = this.GetNavigator(textView); - VirtualSnapshotSpan currentSelection = textView.Selection.StreamSelectionSpan; - previousExpansionsStack.Push(Tuple.Create(currentSelection, textView.Selection.Mode)); - - SnapshotSpan newSelection; - - // If the current language has opt-ed out, return the span of the current word instead. - if (this.editorOptions.GetOptionValue(ExpandContractSelectionOptions.ExpandContractSelectionEnabledKey)) - { - // On first invocation, select the current word. - if (currentSelection.Length == 0) - { - newSelection = this.GetNavigator(textView).GetExtentOfWord(currentSelection.Start.Position).Span; - } - else - { - newSelection = this.GetNavigator(textView).GetSpanOfEnclosing(currentSelection.SnapshotSpan); - } - } - else - { - // Since the span of the current word can be left or right associative relative to the caret - // in different contexts, to avoid different selections on subsequent invocations of Expand - // Selection, always use the center point in the selection to compute the span of the current word. - var centerPoint = currentSelection.Start.Position.Add( - (currentSelection.End.Position.Position - currentSelection.Start.Position.Position) / 2); - newSelection = navigator.GetExtentOfWord(centerPoint).Span; - } - - textView.Selection.Mode = TextSelectionMode.Stream; - textView.Selection.Select(newSelection, isReversed: false); - } - finally - { - this.ignoreSelectionChangedEvent = false; - } - - return true; //return true if command is handled - } - - public bool ContractSelection(ITextView textView) - { - try - { - this.ignoreSelectionChangedEvent = true; - - if (this.previousExpansionsStack.Count > 0) - { - Tuple<VirtualSnapshotSpan, TextSelectionMode> previousExpansion = this.previousExpansionsStack.Pop(); - VirtualSnapshotSpan previousExpansionSpan = previousExpansion.Item1; - TextSelectionMode previousExpansionSelectionMode = previousExpansion.Item2; - - textView.Selection.Mode = previousExpansionSelectionMode; - textView.Selection.Select(previousExpansionSpan.Start, previousExpansionSpan.End); - } - } - finally - { - this.ignoreSelectionChangedEvent = false; - } - - return true;//return true if command is handled - } - - private void OnSelectionChanged(object sender, EventArgs eventArgs) - { - if (!this.ignoreSelectionChangedEvent) - { - this.previousExpansionsStack.Clear(); - } - } - - private ITextStructureNavigator GetNavigator(ITextView textView) - => this.navigatorSelectorService.GetTextStructureNavigator(textView.TextBuffer); - } -} diff --git a/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionOptionDefinitions.cs b/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionOptionDefinitions.cs deleted file mode 100644 index b39c3f8..0000000 --- a/src/Text/Impl/EditorOperations/Commands/ExpandContractSelectionOptionDefinitions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Microsoft.VisualStudio.Text.Operations.Implementation -{ - using System.ComponentModel.Composition; - using Microsoft.VisualStudio.Text.Editor; - using Microsoft.VisualStudio.Text.Operations; - using Microsoft.VisualStudio.Utilities; - - /// <summary> - /// Defines Expand Contract Selection Option. - /// </summary> - [Export(typeof(EditorOptionDefinition))] - [Name(ExpandContractSelectionOptions.ExpandContractSelectionEnabledOptionId)] - internal sealed class ExpandContractSelectionEnabled : EditorOptionDefinition<bool> - { - /// <summary> - /// Gets the default value, which is <c>false</c>. - /// </summary> - public override bool Default => true; - - /// <summary> - /// Gets the default text view host value. - /// </summary> - public override EditorOptionKey<bool> Key => ExpandContractSelectionOptions.ExpandContractSelectionEnabledKey; - } -} diff --git a/src/Text/Impl/EditorOperations/Commands/NavigateToNextIssueCommandHandler.cs b/src/Text/Impl/EditorOperations/Commands/NavigateToNextIssueCommandHandler.cs new file mode 100644 index 0000000..25948cf --- /dev/null +++ b/src/Text/Impl/EditorOperations/Commands/NavigateToNextIssueCommandHandler.cs @@ -0,0 +1,156 @@ +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using System.Diagnostics; + using System.Linq; + using Microsoft.VisualStudio.Commanding; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ICommandHandler))] + [Name("default " + nameof(NavigateToNextIssueCommandHandler))] + [ContentType("any")] + [TextViewRole(PredefinedTextViewRoles.Analyzable)] + internal sealed class NavigateToNextIssueCommandHandler : ICommandHandler<NavigateToNextIssueInDocumentCommandArgs>, ICommandHandler<NavigateToPreviousIssueInDocumentCommandArgs> + { + [Import] + private Lazy<IBufferTagAggregatorFactoryService> tagAggregatorFactoryService; + + public string DisplayName => Strings.NextIssue; + + #region Previous Issue + + public CommandState GetCommandState(NavigateToPreviousIssueInDocumentCommandArgs args) => CommandState.Available; + + public bool ExecuteCommand(NavigateToPreviousIssueInDocumentCommandArgs args, CommandExecutionContext executionContext) + { + var snapshot = args.TextView.TextSnapshot; + var spans = this.GetTagSpansCollection(snapshot, args.ErrorTagTypeNames); + + if (spans.Count == 0) + { + return true; + } + + (int indexOfErrorSpan, bool containsPoint) = IndexOfTagSpanNearPoint(spans, args.TextView.Caret.Position.BufferPosition.Position); + + int nextIndex = indexOfErrorSpan - 1; + if (containsPoint && (spans.Count == 1)) + { + // There is only one error tag and it contains the caret. Ensure it stays put. + return true; + } + + // Wrap if needed. + if (nextIndex < 0) + { + nextIndex = (spans.Count - 1); + } + + args.TextView.Caret.MoveTo(new SnapshotPoint(snapshot, spans[nextIndex].Start)); + args.TextView.Caret.EnsureVisible(); + return true; + } + + #endregion + + #region Next Issue + public CommandState GetCommandState(NavigateToNextIssueInDocumentCommandArgs args) => CommandState.Available; + + public bool ExecuteCommand(NavigateToNextIssueInDocumentCommandArgs args, CommandExecutionContext executionContext) + { + var snapshot = args.TextView.TextSnapshot; + var spans = this.GetTagSpansCollection(snapshot, args.ErrorTagTypeNames); + + if (spans.Count == 0) + { + return true; + } + + (int indexOfErrorSpan, bool containsPoint) = IndexOfTagSpanNearPoint(spans, args.TextView.Caret.Position.BufferPosition.Position); + + int nextIndex = indexOfErrorSpan + 1; + if (containsPoint) + { + if (spans.Count == 1) + { + // There is only one error tag and it contains the caret. Ensure it stays put. + return true; + } + } + else + { + nextIndex = indexOfErrorSpan; + } + + // Wrap if needed. + if ((indexOfErrorSpan == -1) || (nextIndex >= spans.Count)) + { + nextIndex = 0; + } + + args.TextView.Caret.MoveTo(new SnapshotPoint(snapshot, spans[nextIndex].Start)); + args.TextView.Caret.EnsureVisible(); + return true; + } + + #endregion + + private static (int index, bool containsPoint) IndexOfTagSpanNearPoint(NormalizedSpanCollection spans, int point) + { + Debug.Assert(spans.Count > 0); + Span? tagBefore = null; + Span? tagAfter = null; + + for (int i = 0; i < spans.Count; i++) + { + tagBefore = tagAfter; + tagAfter = spans[i]; + + // Case 0: point falls within error tag. We use explicit comparisons instead + // of 'Contains' so that we match a tag even if the caret at the end of it. + if ((point >= tagAfter.Value.Start) && (point <= tagAfter.Value.End)) + { + // Return tag containing the point. + return (i, true); + } + + // Case 1: point falls between two tags. + if ((tagBefore != null) && (tagBefore.Value.End < point) && (tagAfter.Value.Start > point)) + { + // Return tag following the point. + return (i, false); + } + } + + // Case 2: point falls after all tags. + return (-1, false); + } + + private NormalizedSpanCollection GetTagSpansCollection(ITextSnapshot snapshot, IEnumerable<string> errorTagTypeNames) + { + using (var tagger = this.tagAggregatorFactoryService.Value.CreateTagAggregator<IErrorTag>(snapshot.TextBuffer)) + { + var rawTags = tagger.GetTags(new SnapshotSpan(snapshot, 0, snapshot.Length)); + var curatedTags = (errorTagTypeNames?.Any() ?? false) ? + rawTags.Where(tag => errorTagTypeNames.Contains(tag.Tag.ErrorType)) : + rawTags; + + // In this case we only grab the first span that the IMappingTagSpan maps to because we always + // want to place the caret at the start of the error, and so, don't care about possibly disjoint + // subspans after mapping to the view's buffer. NormalizedSpanCollection takes care of sorting + // and joining overlapping spans together for us. It's possible for a tag to map to zero spans + // in projection scenarios in which the tag exists entirely within a region that doesn't map to + // visible space. + return new NormalizedSpanCollection( + curatedTags.Select(tagSpan => tagSpan.Span.GetSpans(snapshot)) + .Where(spanCollection => spanCollection.Count > 0) + .Select(spanCollection => spanCollection[0].Span)); + } + } + } +} diff --git a/src/Text/Impl/EditorOperations/EditorOperations.cs b/src/Text/Impl/EditorOperations/EditorOperations.cs index 5ac282a..369273b 100644 --- a/src/Text/Impl/EditorOperations/EditorOperations.cs +++ b/src/Text/Impl/EditorOperations/EditorOperations.cs @@ -16,7 +16,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation using System.IO; using System.Linq; using System.Text.RegularExpressions; - using System.Threading; using System.Windows; using Microsoft.VisualStudio.Text; @@ -56,7 +55,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation ClearVirtualSpace }; - #region Private Members +#region Private Members ITextView _textView; EditorOperationsFactoryService _factory; @@ -65,6 +64,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation ITextUndoHistory _undoHistory; IViewPrimitives _editorPrimitives; IEditorOptions _editorOptions; + IMultiSelectionBroker _multiSelectionBroker; private ITrackingSpan _immProvisionalComposition; @@ -80,7 +80,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </summary> private const string _boxSelectionCutCopyTag = "MSDEVColumnSelect"; - #endregion // Private Members +#endregion // Private Members /// <summary> /// Constructs an <see cref="EditorOperations"/> bound to a given <see cref="ITextView"/>. @@ -93,13 +93,13 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { // Validate if (textView == null) - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); if (factory == null) - throw new ArgumentNullException("factory"); + throw new ArgumentNullException(nameof(factory)); _textView = textView; _factory = factory; - + _multiSelectionBroker = _textView.GetMultiSelectionBroker(); _editorPrimitives = factory.EditorPrimitivesProvider.GetViewPrimitives(textView); // Get the TextStructure Navigator _textStructureNavigator = factory.TextStructureNavigatorFactory.GetTextStructureNavigator(_textView.TextBuffer); @@ -121,7 +121,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } - #region IEditorOperations2 Members +#region IEditorOperations2 Members public bool MoveSelectedLinesUp() { @@ -129,15 +129,13 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { bool success = false; - var view = _textView as ITextView; - // find line start - var startViewLine = GetLineStart(view, view.Selection.Start.Position); + ITextViewLine startViewLine = GetLineStart(_textView, _textView.Selection.Start.Position); SnapshotPoint start = startViewLine.Start; ITextSnapshotLine startLine = start.GetContainingLine(); // find the last line view - var endViewLine = GetLineEnd(view, view.Selection.End.Position); + ITextViewLine endViewLine = GetLineEnd(_textView, _textView.Selection.End.Position); SnapshotPoint end = endViewLine.EndIncludingLineBreak; ITextSnapshotLine endLine = endViewLine.End.GetContainingLine(); @@ -146,24 +144,24 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // Handle the case where multiple lines are selected and the caret is sitting just after the line break on the next line. // Shortening the selection here handles the case where the last line is a collapsed region. Using endLine.End will give // a line within the collapsed region instead of skipping it all together. - if (GetLineEnd(view, startViewLine.Start) != endViewLine - && view.Selection.End.Position == GetLineStart(view, view.Selection.End.Position).Start - && !view.Selection.End.IsInVirtualSpace) + if (GetLineEnd(_textView, startViewLine.Start) != endViewLine + && _textView.Selection.End.Position == GetLineStart(_textView, _textView.Selection.End.Position).Start + && !_textView.Selection.End.IsInVirtualSpace) { endLine = snapshot.GetLineFromLineNumber(endLine.LineNumber - 1); end = endLine.EndIncludingLineBreak; - endViewLine = view.GetTextViewLineContainingBufferPosition(view.Selection.End.Position - 1); + endViewLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.End.Position - 1); } - #region Initial Asserts +#region Initial Asserts - Debug.Assert(view.Selection.Start.Position.Snapshot == view.TextSnapshot, "Selection is out of sync with view."); + Debug.Assert(_textView.Selection.Start.Position.Snapshot == _textView.TextSnapshot, "Selection is out of sync with view."); - Debug.Assert(view.TextSnapshot == view.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer."); + Debug.Assert(_textView.TextSnapshot == _textView.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer."); - Debug.Assert(view.TextSnapshot == snapshot, "Text view lines are out of sync with the view"); + Debug.Assert(_textView.TextSnapshot == snapshot, "Text view lines are out of sync with the view"); - #endregion +#endregion // check if we are at the top of the file, or trying to move a blank line if (startLine.LineNumber < 1 || start == end) @@ -177,11 +175,11 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation ITextSnapshotLine prevLine = snapshot.GetLineFromLineNumber(startLine.LineNumber - 1); // prevLineExtent is different from prevLine.Extent and avoids issues around collapsed regions - SnapshotPoint prevLineStart = GetLineStart(view, prevLine.Start).Start; + SnapshotPoint prevLineStart = GetLineStart(_textView, prevLine.Start).Start; SnapshotSpan prevLineExtent = new SnapshotSpan(prevLineStart, prevLine.End); SnapshotSpan prevLineExtentIncludingLineBreak = new SnapshotSpan(prevLineStart, prevLine.EndIncludingLineBreak); - using (ITextEdit edit = view.TextBuffer.CreateEdit()) + using (ITextEdit edit = _textView.TextBuffer.CreateEdit()) { int offset; @@ -193,7 +191,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation bool hasCollapsedRegions = false; IOutliningManager outliningManager = (_factory.OutliningManagerService != null) - ? _factory.OutliningManagerService.GetOutliningManager(view) + ? _factory.OutliningManagerService.GetOutliningManager(_textView) : null; if (outliningManager != null) @@ -208,7 +206,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesUp)) { - BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, view, collapsedSpansInCurLine); + BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, _textView, collapsedSpansInCurLine); undoTransaction.AddUndo(undoPrim); undoTransaction.Complete(); } @@ -237,11 +235,11 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation if (!edit.HasFailedChanges) { // store the position before the edit is applied - int anchorPos = view.Selection.AnchorPoint.Position.Position; - int anchorVirtualSpace = view.Selection.AnchorPoint.VirtualSpaces; - int activePos = view.Selection.ActivePoint.Position.Position; - int activeVirtualSpace = view.Selection.ActivePoint.VirtualSpaces; - var selectionMode = view.Selection.Mode; + int anchorPos = _textView.Selection.AnchorPoint.Position.Position; + int anchorVirtualSpace = _textView.Selection.AnchorPoint.VirtualSpaces; + int activePos = _textView.Selection.ActivePoint.Position.Position; + int activeVirtualSpace = _textView.Selection.ActivePoint.VirtualSpaces; + var selectionMode = _textView.Selection.Mode; // apply the edit ITextSnapshot newSnapshot = edit.Apply(); @@ -266,8 +264,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { // This comes from adhocoutliner.cs in env\editor\pkg\impl\outlining and will not be available outside of VS SimpleTagger<IOutliningRegionTag> simpleTagger = - view.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>( - () => new SimpleTagger<IOutliningRegionTag>(view.TextBuffer)); + _textView.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>( + () => new SimpleTagger<IOutliningRegionTag>(_textView.TextBuffer)); if (simpleTagger != null) { @@ -316,7 +314,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // we need to recollapse after a redo using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesUp)) { - AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, view, spansForUndo); + AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, _textView, spansForUndo); undoTransaction.AddUndo(undoPrim); undoTransaction.Complete(); } @@ -341,18 +339,15 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { Func<bool> action = () => { - bool success = false; - var view = _textView as ITextView; - // find line start - var startViewLine = GetLineStart(view, view.Selection.Start.Position); + ITextViewLine startViewLine = GetLineStart(_textView, _textView.Selection.Start.Position); SnapshotPoint start = startViewLine.Start; ITextSnapshotLine startLine = start.GetContainingLine(); // find the last line view - var endViewLine = GetLineEnd(view, view.Selection.End.Position); + ITextViewLine endViewLine = GetLineEnd(_textView, _textView.Selection.End.Position); ITextSnapshotLine endLine = endViewLine.End.GetContainingLine(); ITextSnapshot snapshot = endLine.Snapshot; @@ -360,23 +355,23 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // Handle the case where multiple lines are selected and the caret is sitting just after the line break on the next line. // Shortening the selection here handles the case where the last line is a collapsed region. Using endLine.End will give // a line within the collapsed region instead of skipping it all together. - if (GetLineEnd(view, startViewLine.Start) != endViewLine - && view.Selection.End.Position == GetLineStart(view, view.Selection.End.Position).Start - && !view.Selection.End.IsInVirtualSpace) + if (GetLineEnd(_textView, startViewLine.Start) != endViewLine + && _textView.Selection.End.Position == GetLineStart(_textView, _textView.Selection.End.Position).Start + && !_textView.Selection.End.IsInVirtualSpace) { endLine = snapshot.GetLineFromLineNumber(endLine.LineNumber - 1); - endViewLine = view.GetTextViewLineContainingBufferPosition(view.Selection.End.Position - 1); + endViewLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.End.Position - 1); } - #region Initial Asserts +#region Initial Asserts - Debug.Assert(view.Selection.Start.Position.Snapshot == view.TextSnapshot, "Selection is out of sync with view."); + Debug.Assert(_textView.Selection.Start.Position.Snapshot == _textView.TextSnapshot, "Selection is out of sync with view."); - Debug.Assert(view.TextSnapshot == view.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer."); + Debug.Assert(_textView.TextSnapshot == _textView.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer."); - Debug.Assert(view.TextSnapshot == snapshot, "Text view lines are out of sync with the view"); + Debug.Assert(_textView.TextSnapshot == snapshot, "Text view lines are out of sync with the view"); - #endregion +#endregion // check if we are at the end of the file if ((endLine.LineNumber + 1) >= snapshot.LineCount) @@ -387,11 +382,11 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation else { // nextLineExtent is different from prevLine.Extent and avoids issues around collapsed regions - var lastNextLine = GetLineEnd(view, endViewLine.EndIncludingLineBreak); + ITextViewLine lastNextLine = GetLineEnd(_textView, endViewLine.EndIncludingLineBreak); SnapshotSpan nextLineExtent = new SnapshotSpan(endViewLine.EndIncludingLineBreak, lastNextLine.End); SnapshotSpan nextLineExtentIncludingLineBreak = new SnapshotSpan(endViewLine.EndIncludingLineBreak, lastNextLine.EndIncludingLineBreak); - using (ITextEdit edit = view.TextBuffer.CreateEdit()) + using (ITextEdit edit = _textView.TextBuffer.CreateEdit()) { SnapshotSpan curLineExtent = new SnapshotSpan(startViewLine.Start, endViewLine.End); SnapshotSpan curLineExtentIncLineBreak = new SnapshotSpan(startViewLine.Start, endViewLine.EndIncludingLineBreak); @@ -410,7 +405,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation bool hasCollapsedRegions = false; IOutliningManager outliningManager = (_factory.OutliningManagerService != null) - ? _factory.OutliningManagerService.GetOutliningManager(view) + ? _factory.OutliningManagerService.GetOutliningManager(_textView) : null; if (outliningManager != null) @@ -425,7 +420,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesDown)) { - BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, view, collapsedSpansInCurLine); + BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, _textView, collapsedSpansInCurLine); undoTransaction.AddUndo(undoPrim); undoTransaction.Complete(); } @@ -455,11 +450,11 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } else { - int anchorPos = view.Selection.AnchorPoint.Position.Position; - int anchorVirtualSpace = view.Selection.AnchorPoint.VirtualSpaces; - int activePos = view.Selection.ActivePoint.Position.Position; - int activeVirtualSpace = view.Selection.ActivePoint.VirtualSpaces; - var selectionMode = view.Selection.Mode; + int anchorPos = _textView.Selection.AnchorPoint.Position.Position; + int anchorVirtualSpace = _textView.Selection.AnchorPoint.VirtualSpaces; + int activePos = _textView.Selection.ActivePoint.Position.Position; + int activeVirtualSpace = _textView.Selection.ActivePoint.VirtualSpaces; + var selectionMode = _textView.Selection.Mode; ITextSnapshot newSnapshot = edit.Apply(); if (newSnapshot == snapshot) @@ -483,7 +478,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { // This comes from adhocoutliner.cs in env\editor\pkg\impl\outlining and will not be available outside of VS SimpleTagger<IOutliningRegionTag> simpleTagger = - view.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>(() => new SimpleTagger<IOutliningRegionTag>(view.TextBuffer)); + _textView.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>(() => new SimpleTagger<IOutliningRegionTag>(_textView.TextBuffer)); if (simpleTagger != null) { @@ -532,7 +527,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // we need to recollapse after a redo using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesDown)) { - AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, view, spansForUndo); + AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, _textView, spansForUndo); undoTransaction.AddUndo(undoPrim); undoTransaction.Complete(); } @@ -555,7 +550,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation private static ITextViewLine GetLineStart(ITextView view, SnapshotPoint snapshotPoint) { - var line = view.GetTextViewLineContainingBufferPosition(snapshotPoint); + ITextViewLine line = view.GetTextViewLineContainingBufferPosition(snapshotPoint); while (!line.IsFirstTextViewLineForSnapshotLine) { line = view.GetTextViewLineContainingBufferPosition(line.Start - 1); @@ -565,7 +560,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation private static ITextViewLine GetLineEnd(ITextView view, SnapshotPoint snapshotPoint) { - var line = view.GetTextViewLineContainingBufferPosition(snapshotPoint); + ITextViewLine line = view.GetTextViewLineContainingBufferPosition(snapshotPoint); while (!line.IsLastTextViewLineForSnapshotLine) { line = view.GetTextViewLineContainingBufferPosition(line.EndIncludingLineBreak); @@ -573,10 +568,10 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return line; } - #endregion +#endregion - #region IEditorOperations Members +#region IEditorOperations Members public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint) { @@ -592,41 +587,14 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { bool empty = (anchorPoint == activePoint); - // TODO: Whenever caret/selection is updated to offer a way to set both simultaneously without either eventing before - // the other is updated, we should update this method to use that. There are potential bugs below in how clients - // react to things like selection moving. For example, if someone reacts to moving the selection by moving the caret, - // the logic below will override that caret position, which may not be desirable. - - // The order of operations here is important: - // 1) We need to move the selection first. Clients (like VB) who listen for caret change need the selection to be correct, - // and we have yet to have clients that require the opposite order. See Dev10 #793198 for what happens when we do this selection-first. - // - // 2) Then we move the caret. This behaves differently, depending on if the new selection is empty or not (explained below). - - if (empty) + var selection = new Selection(anchorPoint, activePoint); + if (selectionMode == TextSelectionMode.Box) { - _textView.Selection.Clear(); - _textView.Selection.Mode = selectionMode; - - // Since the selection is empty, move the caret to the provided active point and translate that point - // to the view's text snapshot (in case someone was listening to the selection changed event and made a text edit). - // The empty selection will track the caret. - // See Dev10 #785792 for an example of what happens when we get this wrong by moving the caret to the active point - // of the selection when the selection is being cleared. - _textView.Caret.MoveTo(activePoint.TranslateTo(_textView.TextSnapshot)); + _multiSelectionBroker.SetBoxSelection(selection); } else { - _textView.Selection.Select(anchorPoint, activePoint); - _textView.Selection.Mode = selectionMode; - - // Move the caret to the active point of the selection (don't use activePoint since someone -- on the selection changed event -- might have - // moved the selection). - // But if the selection is empty (it shouldn't be since anchorPoint != activePoint, but those points could be normalized to an empty span - // or someone could have moved it), move the caret to the requested activePoint. - _textView.Caret.MoveTo(_textView.Selection.IsEmpty - ? activePoint.TranslateTo(_textView.TextSnapshot) - : _textView.Selection.ActivePoint); + _multiSelectionBroker.SetSelection(selection); } // 3) If scrollOptions were provided, we're going to try and make the span visible using the provided options. @@ -657,7 +625,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveToNextCharacter(bool select) { - _editorPrimitives.Caret.MoveToNextCharacter(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToNextCaretPosition : PredefinedSelectionTransformations.MoveToNextCaretPosition); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -668,18 +637,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveToPreviousCharacter(bool select) { - bool isCaretAtStartOfViewLine = (!_textView.Caret.InVirtualSpace) && - (_textView.Caret.Position.BufferPosition == _textView.Caret.ContainingTextViewLine.Start); - - //Prevent the caret from moving from column 0 to the end of the previous line if either: - // virtual space is turned on or - // the user is extending a box selection. - if (isCaretAtStartOfViewLine && (_editorOptions.IsVirtualSpaceEnabled() || (select && (_textView.Selection.Mode == TextSelectionMode.Box)))) - { - return; - } - - _editorPrimitives.Caret.MoveToPreviousCharacter(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToPreviousCaretPosition : PredefinedSelectionTransformations.MoveToPreviousCaretPosition); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -690,7 +649,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveToNextWord(bool select) { - _editorPrimitives.Caret.MoveToNextWord(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToNextWord : PredefinedSelectionTransformations.MoveToNextWord); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -701,15 +661,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveToPreviousWord(bool select) { - // In extending a box selection, we don't want this to jump to the previous line (if - // we are on the beginning of a line) - if (select && _textView.Selection.Mode == TextSelectionMode.Box && !_textView.Caret.InVirtualSpace) - { - if (_editorPrimitives.Caret.CurrentPosition == _editorPrimitives.Caret.StartOfViewLine) - return; - } - - _editorPrimitives.Caret.MoveToPreviousWord(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToPreviousWord : PredefinedSelectionTransformations.MoveToPreviousWord); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -720,7 +673,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveToStartOfDocument(bool select) { - _editorPrimitives.Caret.MoveToStartOfDocument(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToStartOfDocument : PredefinedSelectionTransformations.MoveToStartOfDocument); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -731,7 +685,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveToEndOfDocument(bool select) { - _editorPrimitives.Caret.MoveToEndOfDocument(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToEndOfDocument : PredefinedSelectionTransformations.MoveToEndOfDocument); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -928,110 +883,267 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </summary> public bool Backspace() { - bool emptyBox = IsEmptyBoxSelection(); - NormalizedSnapshotSpanCollection boxDeletions = null; + bool success = true; - // First, handle cases that don't require edits - if (_textView.Selection.IsEmpty) + if (WillBackspaceCreateEdit()) { - if (_textView.Caret.InVirtualSpace) - { - this.MoveCaretToPreviousIndentStopInVirtualSpace(); + var selections = _multiSelectionBroker.AllSelections; + var boxSelection = _multiSelectionBroker.BoxSelection; + var primarySelection = _multiSelectionBroker.PrimarySelection; - _textView.Caret.EnsureVisible(); - return true; - } - if (_textView.Caret.Position.BufferPosition.Position == 0) + Func<bool> action = () => { - return true; - } + using (_multiSelectionBroker.BeginBatchOperation()) + { + if (TryBackspaceEdit(selections)) + { + return TryPostBackspaceSelectionUpdate(selections, primarySelection, boxSelection); + } + } + return false; + }; + + success = ExecuteAction(Strings.DeleteCharToLeft, action, SelectionUpdate.Ignore, ensureVisible: false); } - // If the entire selection is in virtual space, clear it - else if (_textView.Selection.VirtualSelectedSpans.All(s => s.SnapshotSpan.IsEmpty && s.IsInVirtualSpace)) + else { - this.ResetVirtualSelection(); - _textView.Caret.EnsureVisible(); - return true; + success = TryBackspaceSelections(); } - else if (emptyBox) // empty box selection, make sure it is valid + + if (success) { - List<SnapshotSpan> spans = new List<SnapshotSpan>(); + _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll); + } + + return success; + } + + private bool TryPostBackspaceSelectionUpdate(IReadOnlyList<Selection> selections, Selection primarySelection, Selection boxSelection) + { + // Throughout this method, the parameters passed in are the OLD values, and the parameters on _multiSelectionBroker are the NEW ones - foreach (var span in _textView.Selection.VirtualSelectedSpans.Where(s => !s.IsInVirtualSpace).Select(s => s.SnapshotSpan)) + if (boxSelection != Selection.Invalid) + { + // If this is an empty box, we may need to capture the new active/anchor points, as points in virtual space + // won't track as we want them to through the edit. + VirtualSnapshotPoint anchorPoint = _multiSelectionBroker.BoxSelection.AnchorPoint; + VirtualSnapshotPoint activePoint = _multiSelectionBroker.BoxSelection.ActivePoint; + + if (primarySelection.IsEmpty) { - var line = span.Start.GetContainingLine(); - if (span.Start > line.Start) + if (boxSelection.AnchorPoint.IsInVirtualSpace) + { + anchorPoint = new VirtualSnapshotPoint(_multiSelectionBroker.BoxSelection.AnchorPoint.Position, boxSelection.AnchorPoint.VirtualSpaces - 1); + } + if (boxSelection.ActivePoint.IsInVirtualSpace) { - spans.Add(_textView.GetTextElementSpan(span.Start - 1)); + activePoint = new VirtualSnapshotPoint(_multiSelectionBroker.BoxSelection.ActivePoint.Position, boxSelection.ActivePoint.VirtualSpaces - 1); } } + else + { + // Just take the starting points in the first and last selections + activePoint = selections[boxSelection.IsReversed ? 0 : selections.Count - 1].Start; + anchorPoint = selections[boxSelection.IsReversed ? selections.Count - 1 : 0].Start; + } - // If there is nothing to delete, clear the selection - if (spans.Count == 0) + VirtualSnapshotPoint newAnchor = anchorPoint.TranslateTo(_textView.TextSnapshot); + VirtualSnapshotPoint newActive = activePoint.TranslateTo(_textView.TextSnapshot); + + var newSelection = new Selection(insertionPoint: newActive, anchorPoint: newAnchor, activePoint: newActive, boxSelection.InsertionPointAffinity); + if (_multiSelectionBroker.BoxSelection != newSelection) { - _textView.Caret.MoveTo(_textView.Selection.Start); - _textView.Selection.Clear(); - _textView.Caret.EnsureVisible(); - return true; + _multiSelectionBroker.SetBoxSelection(newSelection); } + } + else + { + // Perf: This is actually an n^2 algorithm here, since TryPerform... also loops through all the selections. Try to avoid copying this code + // elsewhere. We need it here because we're actually modifying each one based on its context AND because merges can happen with backspace so we + // can't do anything funny like caching the transformers. + for (int i = 0; i < selections.Count; i++) + { + //Some could have merged away, ignore return values here intentionally. + _multiSelectionBroker.TryPerformActionOnSelection(selections[i], transformer => + { + // We can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted). + // VirtualSnapshotPoint.TranslateTo doesn't know what to do with virtual whitespace, so we have to do this ourselves. + if (selections[i].IsEmpty && selections[i].InsertionPoint.IsInVirtualSpace) + { + // Move the caret back one if we have an empty selection + transformer.MoveTo(new VirtualSnapshotPoint(transformer.Selection.InsertionPoint.Position, selections[i].InsertionPoint.VirtualSpaces - 1), + select: false, + insertionPointAffinity: PositionAffinity.Successor); + } + else + { + //Move the caret to the start of the selection. + transformer.MoveTo(new VirtualSnapshotPoint(transformer.Selection.InsertionPoint.Position, selections[i].Start.VirtualSpaces), + select: false, + PositionAffinity.Successor); + } + }, out _); + } + } + + return true; + } - boxDeletions = new NormalizedSnapshotSpanCollection(spans); + private bool WillBackspaceCreateEdit() + { + if (_multiSelectionBroker.IsBoxSelection) + { + // Edits can not happen if we're a box selection at the beginning of a line + var primary = _multiSelectionBroker.PrimarySelection; + if (primary.IsEmpty && primary.Start.Position == primary.Start.Position.GetContainingLine().Start) + { + return false; + } } - // Now, handle cases that require edits - Func<bool> action = () => + var selections = _multiSelectionBroker.AllSelections; + for (int i = 0; i < selections.Count; i++) { - // 1. An empty selection mean backspace the caret - if (_textView.Selection.IsEmpty) - return _editorPrimitives.Caret.DeletePrevious(); + if ((!selections[i].Extent.SnapshotSpan.IsEmpty) || + (selections[i].IsEmpty + && !selections[i].InsertionPoint.IsInVirtualSpace + && selections[i].InsertionPoint.Position.Position != 0)) + { + return true; + } + } - // 2. If this is an empty box, we may need to capture the new active/anchor points, as points in virtual space - // won't track as we want them to through the edit. - VirtualSnapshotPoint? anchorPoint = null; - VirtualSnapshotPoint? activePoint = null; + return false; + } - if (emptyBox) + private bool TryBackspaceEdit(IReadOnlyList<Selection> selections) + { + using (var edit = _textView.TextBuffer.CreateEdit()) + { + for (int i = (selections.Count - 1); i >= 0; i--) { - if (_textView.Selection.AnchorPoint.IsInVirtualSpace) + var selection = selections[i]; + + if (selection.IsEmpty) { - anchorPoint = new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position, _textView.Selection.AnchorPoint.VirtualSpaces - 1); + if (selection.Extent.IsInVirtualSpace) + { + continue; + } + + if (!TryBackspaceEmptySelection(selection, edit)) + { + return false; + } } - if (_textView.Selection.ActivePoint.IsInVirtualSpace) + else if (!edit.Delete(selection.Extent.SnapshotSpan)) { - activePoint = new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position, _textView.Selection.ActivePoint.VirtualSpaces - 1); + return false; } } - // 3. The selection is non-empty, so delete the selected spans (unless this is an empty box selection: An empty box selection means treat this as a backspace on each line) - NormalizedSnapshotSpanCollection deletion = boxDeletions ?? _textView.Selection.SelectedSpans; + edit.Apply(); + return !edit.Canceled; + } + } + + private bool TryBackspaceEmptySelection(Selection selection, ITextEdit edit) + { + // Assumptions: + // We should have already validated this before calling. + Debug.Assert(selection.IsEmpty); - int selectionStartVirtualSpaces = _textView.Selection.Start.VirtualSpaces; + // This method is only written to perform edits on text. Virtual space operations should be performed separately and not passed here. + Debug.Assert(!selection.InsertionPoint.IsInVirtualSpace); - if (!DeleteHelper(deletion)) - return false; + // Performing deletion: + // Identify what should be deleted + if (selection.InsertionPoint.Position.Position == 0) + { + // We're at the beginning of the document, we're done. + return true; + } - // 5. Now, fix up the start and end points if this is an empty box - if (emptyBox && (anchorPoint.HasValue || activePoint.HasValue)) - { - VirtualSnapshotPoint newAnchor = (anchorPoint.HasValue) ? anchorPoint.Value.TranslateTo(_textView.TextSnapshot) : _textView.Selection.AnchorPoint; - VirtualSnapshotPoint newActive = (activePoint.HasValue) ? activePoint.Value.TranslateTo(_textView.TextSnapshot) : _textView.Selection.ActivePoint; + // Get the span of the previous element + SnapshotSpan previousElementSpan = TextView.GetTextElementSpan(selection.InsertionPoint.Position - 1); + + // Here we have some interesting decisions to make. If this is a collapsed region, we want to delete the whole thing. + // If this is a multi-byte character, we typically want to delete just one byte to allow for easier typing in chinese and other languages. + // However, if that multi-byte character is a surrogate pair or newline, we want to delete the whole thing. + + // We start by looking to see if this is a collapsed region or something like one. + if ((previousElementSpan.Length > 0) && + (_textView.TextViewModel.IsPointInVisualBuffer(selection.InsertionPoint.Position, PositionAffinity.Successor)) && + (!_textView.TextViewModel.IsPointInVisualBuffer(previousElementSpan.End - 1, PositionAffinity.Successor))) + { + // Since the previous character is not visible but the current one is, delete + // the entire previous text element span. + return edit.Delete(previousElementSpan); + } + else + { + //Next we test for surrogate pairs and newline: + ITextSnapshot snapshot = edit.Snapshot; + int previousPosition = selection.InsertionPoint.Position.Position - 1; + + int index = previousPosition; + char currentCharacter = snapshot[previousPosition]; - _textView.Caret.MoveTo(_textView.Selection.ActivePoint); - _textView.Selection.Select(newAnchor, newActive); + // By default VS (and many other apps) will delete only the last character + // of a combining character sequence. The one exception to this rule is + // surrogate pais which we are handling here. + if (char.GetUnicodeCategory(currentCharacter) == UnicodeCategory.Surrogate) + { + index--; } - else if (_textView.Selection.Mode != TextSelectionMode.Box) + + if ((index > 0) && + (currentCharacter == '\n') && + (snapshot[previousPosition - 1] == '\r')) { - //Move the caret to the start of the selection (this doesn't happen automatically if the caret was in virtual space). - //But we can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted). - _textView.Caret.MoveTo(new VirtualSnapshotPoint(_textView.Selection.Start.Position, selectionStartVirtualSpaces)); - _textView.Selection.Clear(); + index--; } - _textView.Caret.EnsureVisible(); - return true; - }; + // With index moved back in the cases of newline and surrogate pairs, this delete should handle all other cases. + return edit.Delete(new Span(index, previousPosition - index + 1)); + } + } - return ExecuteAction(Strings.DeleteCharToLeft, action, SelectionUpdate.ResetUnlessEmptyBox, true); + private bool TryBackspaceSelections() + { + if (_multiSelectionBroker.IsBoxSelection && _multiSelectionBroker.PrimarySelection.InsertionPoint.IsInVirtualSpace) + { + _multiSelectionBroker.SetSelection(new Selection(_multiSelectionBroker.PrimarySelection.Start)); + } + else if (!_multiSelectionBroker.IsBoxSelection) + { + _multiSelectionBroker.PerformActionOnAllSelections(transformer => + { + if (transformer.Selection.IsEmpty) + { + if (transformer.Selection.InsertionPoint.IsInVirtualSpace) + { + MoveToPreviousTabStop(transformer); + } + else + { + transformer.PerformAction(PredefinedSelectionTransformations.MoveToPreviousCaretPosition); + } + } + else + { + transformer.MoveTo(transformer.Selection.Start, select: false, PositionAffinity.Successor); + } + }); + } + + return true; + } + + private void MoveToPreviousTabStop(ISelectionTransformer transformer) + { + var previousStop = GetPreviousIndentStopInVirtualSpace(transformer.Selection.InsertionPoint); + transformer.MoveTo(previousStop, select: false, PositionAffinity.Successor); } private void ResetVirtualSelection() @@ -1048,8 +1160,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation ITextViewLine activeLine = (_textView.Selection.IsReversed) ? startLine : endLine; VirtualSnapshotPoint newCaret = activeLine.GetInsertionBufferPositionFromXCoordinate(leftEdge); - _textView.Caret.MoveTo(newCaret); - _textView.Selection.Clear(); + _multiSelectionBroker.ClearSecondarySelections(); + Selection unused; + _multiSelectionBroker.TryPerformActionOnSelection(_multiSelectionBroker.PrimarySelection, transformer => + { + transformer.MoveTo(newCaret, select: false, PositionAffinity.Successor); + }, out unused); } public bool DeleteFullLine() @@ -1255,7 +1371,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (textLine == null) { - throw new ArgumentNullException("textLine"); + throw new ArgumentNullException(nameof(textLine)); } if (extendSelection) @@ -1288,7 +1404,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveLineUp(bool select) { - _editorPrimitives.Caret.MoveToPreviousLine(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToPreviousLine : PredefinedSelectionTransformations.MoveToPreviousLine); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -1299,7 +1416,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveLineDown(bool select) { - _editorPrimitives.Caret.MoveToNextLine(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToNextLine : PredefinedSelectionTransformations.MoveToNextLine); + _textView.Caret.EnsureVisible(); } /// <summary> @@ -1310,7 +1428,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void PageUp(bool select) { - _editorPrimitives.Caret.MovePageUp(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectPageUp : PredefinedSelectionTransformations.MovePageUp); + } /// <summary> @@ -1321,7 +1440,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void PageDown(bool select) { - _editorPrimitives.Caret.MovePageDown(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectPageDown : PredefinedSelectionTransformations.MovePageDown); } /// <summary> @@ -1332,34 +1451,14 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </param> public void MoveToEndOfLine(bool select) { - // If the caret is at the start of an empty line, respond by trying to position - // the caret at the smart indent location. - if (_textView.Caret.Position.BufferPosition.GetContainingLine().Extent.IsEmpty && - !_textView.Caret.InVirtualSpace) - { - if (PositionCaretWithSmartIndent(useOnlyVirtualSpace: true, extendSelection: select)) - { - _editorPrimitives.Caret.EnsureVisible(); - return; - } - } - - _editorPrimitives.Caret.MoveToEndOfViewLine(select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToEndOfLine : PredefinedSelectionTransformations.MoveToEndOfLine); + _textView.Caret.EnsureVisible(); } public void MoveToHome(bool select) { - int newPosition = _editorPrimitives.Caret.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition; - - // If the caret is already at the first non-whitespace character or - // the line is entirely whitepsace, move to the start of the view line. - if (newPosition == _editorPrimitives.Caret.CurrentPosition || - newPosition == _editorPrimitives.Caret.EndOfViewLine) - { - newPosition = _editorPrimitives.Caret.StartOfViewLine; - } - - _editorPrimitives.Caret.MoveTo(newPosition, select); + _multiSelectionBroker.PerformActionOnAllSelections(select ? PredefinedSelectionTransformations.SelectToHome : PredefinedSelectionTransformations.MoveToHome); + _textView.Caret.EnsureVisible(); } public void MoveToStartOfLine(bool select) @@ -1374,117 +1473,103 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { Func<bool> action = () => { - VirtualSnapshotPoint caret = _textView.Caret.Position.VirtualBufferPosition; - ITextSnapshotLine line = caret.Position.GetContainingLine(); - ITextSnapshot snapshot = line.Snapshot; - - // todo: the following logic is duplicated in DefaultTextPointPrimitive.InsertNewLine() - // didn't call that method here because it would result in two text transactions - // ultimately everything here should probably move into primitives. - string textToInsert = TextBufferOperationHelpers.GetNewLineCharacterToInsert(line, _editorOptions); + bool editSucceeded = true; + ITextSnapshot snapshot = _textView.TextViewModel.EditBuffer.CurrentSnapshot; - bool succeeded = false; - bool caretMoved = false; - EventHandler<CaretPositionChangedEventArgs> caretWatcher = delegate (object sender, CaretPositionChangedEventArgs e) + using (var batchOp = _multiSelectionBroker.BeginBatchOperation()) { - caretMoved = true; - }; + var toIndent = new HashSet<object>(); - // Indent unless the caret is at column 0 or the current line is empty. - // This appears to be added as a fix for Venus; which combined with our implementation of - // PositionCaretWithSmartIndent does not indent correctly on NewLine when Caret is at column 0. - bool doIndent = caret.IsInVirtualSpace || (caret.Position != _textView.Caret.ContainingTextViewLine.Start) - || (_textView.Caret.ContainingTextViewLine.Extent.Length == 0); - - try - { using (var edit = _textView.TextBuffer.CreateEdit()) { - _textView.Caret.PositionChanged += caretWatcher; - int searchIndexforPreviousWhitespaces = -1; - var lineContainingTrimTrailingWhitespacesSearchindex = line; // usually is the line containing caret. + if (_multiSelectionBroker.IsBoxSelection) + { + _multiSelectionBroker.BreakBoxSelection(); + } - if (_textView.Selection.Mode == TextSelectionMode.Stream) + _multiSelectionBroker.PerformActionOnAllSelections(transformer => { + bool doIndent = false; + + VirtualSnapshotPoint caret = transformer.Selection.InsertionPoint; + ITextSnapshotLine line = caret.Position.GetContainingLine(); + ITextViewLine viewLine = _textView.GetTextViewLineContainingBufferPosition(caret.Position); + + // todo: the following logic is duplicated in DefaultTextPointPrimitive.InsertNewLine() + // didn't call that method here because it would result in two text transactions + // ultimately everything here should probably move into primitives. + string textToInsert = TextBufferOperationHelpers.GetNewLineCharacterToInsert(line, _editorOptions); + + // Indent unless the caret is at column 0 or the current line is empty. + // This appears to be added as a fix for Venus; which combined with our implementation of + // PositionCaretWithSmartIndent does not indent correctly on NewLine when Caret is at column 0. + doIndent = caret.IsInVirtualSpace || (caret.Position != viewLine.Extent.Start) + || (viewLine.Extent.Length == 0); + + int searchIndexforPreviousWhitespaces = -1; + var lineContainingTrimTrailingWhitespacesSearchindex = line; // usually is the line containing caret. + // This ignores virtual space - Span selection = _textView.Selection.StreamSelectionSpan.SnapshotSpan; - succeeded = edit.Replace(selection, textToInsert); + Span selection = transformer.Selection.Extent.SnapshotSpan; + + editSucceeded = editSucceeded && edit.Replace(selection, textToInsert); // For stream selection you should always look for trimming whitespaces previous to selection.start instead of caret position lineContainingTrimTrailingWhitespacesSearchindex = snapshot.GetLineFromPosition(selection.Start); searchIndexforPreviousWhitespaces = selection.Start - lineContainingTrimTrailingWhitespacesSearchindex.Start.Position; - } - else - { - var isDeleteSuccessfull = true; - searchIndexforPreviousWhitespaces = caret.Position.Position - line.Start.Position; - foreach (var span in _textView.Selection.SelectedSpans) + // Trim traling whitespaces as we insert the new line as well if the editor option is set + if (_editorOptions.GetOptionValue<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId)) { - // In a box selection if the caret is forward positioned then - //we should search for whitespaces from the start of the last span since the spans are not yet deleted - if (span.End.Position == caret.Position.Position) - { - searchIndexforPreviousWhitespaces = span.Start.Position - line.Start.Position; - } - if (!edit.Delete(span)) - { - isDeleteSuccessfull = false; - } - } - if (!isDeleteSuccessfull) - return false; - succeeded = edit.Replace(new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0), - textToInsert); - } - // Trim traling whitespaces as we insert the new line as well if the editor option is set - if (_editorOptions.GetOptionValue<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId)) - { - var previousNonWhitespaceCharacterIndex = lineContainingTrimTrailingWhitespacesSearchindex.IndexOfPreviousNonWhiteSpaceCharacter(searchIndexforPreviousWhitespaces); + var previousNonWhitespaceCharacterIndex = lineContainingTrimTrailingWhitespacesSearchindex.IndexOfPreviousNonWhiteSpaceCharacter(searchIndexforPreviousWhitespaces); - // Note: If previousNonWhiteSpaceCharacter index is -1 this will automatically default to line.start.position - var startIndexForTrailingWhitespaceSpan = lineContainingTrimTrailingWhitespacesSearchindex.Start.Position + previousNonWhitespaceCharacterIndex + 1; - var lengthOfTrailingWhitespaceSpan = searchIndexforPreviousWhitespaces - previousNonWhitespaceCharacterIndex - 1; + // Note: If previousNonWhiteSpaceCharacter index is -1 this will automatically default to line.start.position + var startIndexForTrailingWhitespaceSpan = lineContainingTrimTrailingWhitespacesSearchindex.Start.Position + previousNonWhitespaceCharacterIndex + 1; + var lengthOfTrailingWhitespaceSpan = searchIndexforPreviousWhitespaces - previousNonWhitespaceCharacterIndex - 1; - if (lengthOfTrailingWhitespaceSpan != 0) // If there are any whitespaces before the caret delete them - edit.Delete(new Span(startIndexForTrailingWhitespaceSpan, lengthOfTrailingWhitespaceSpan)); - } + if (lengthOfTrailingWhitespaceSpan != 0) // If there are any whitespaces before the caret delete them + edit.Delete(new Span(startIndexForTrailingWhitespaceSpan, lengthOfTrailingWhitespaceSpan)); + } + + if (doIndent) + { + // WARNING: We're caching the transformers here because we are both inside a batch operation + // and we're inserting text, so we know that there will be no merging of selections going on. + // We're using them as a perf optimization so we can avoid searching through the list of selections + // later, since we already know what we need. + // + // When writing multiple selection-aware code, do everything you can to avoid saving transformers. + toIndent.Add(transformer); + } + }); // Apply all changes - succeeded = (edit.Apply() != snapshot); + editSucceeded = editSucceeded && (edit.Apply() != snapshot); } - } - finally - { - _textView.Caret.PositionChanged -= caretWatcher; - } - if (succeeded) - { - if (doIndent) + if (editSucceeded && toIndent.Count > 0) { - caret = _textView.Caret.Position.VirtualBufferPosition; - line = caret.Position.GetContainingLine(); - - //Only attempt to auto indent if -- after the edit above -- no one moved the caret on the buffer change - //and the caret is at the start of its new line (no one did any funny edits to the buffer on the buffer change). - if ((!caretMoved) && (caret.Position == line.Start)) - { - caretMoved = PositionCaretWithSmartIndent(useOnlyVirtualSpace: false, extendSelection: false); - if (!caretMoved && caret.IsInVirtualSpace) - { - //No smart indent logic so make sure the caret is not in virtual space. - _textView.Caret.MoveTo(caret.Position); - } - } + // Need to move carets to indented location after the edit has completed, so we put them at the correct indentation in the new snapshot. + _multiSelectionBroker.PerformActionOnAllSelections(transformer => + { + if (toIndent.Contains(transformer)) + { + var caretMoved = PositionCaretWithSmartIndent(transformer, useOnlyVirtualSpace: false, extendSelection: false); + if (!caretMoved && transformer.Selection.InsertionPoint.IsInVirtualSpace) + { + //No smart indent logic so make sure the caret is not in virtual space. + transformer.MoveTo(new VirtualSnapshotPoint(transformer.Selection.InsertionPoint.Position), select: false, PositionAffinity.Successor); + } + transformer.PerformAction(PredefinedSelectionTransformations.ClearSelection); + transformer.CapturePreferredReferencePoint(); + } + }); } - ResetSelection(); } - return succeeded; + + return editSucceeded; }; return ExecuteAction(Strings.InsertNewLine, action, SelectionUpdate.Ignore, true); } - - public bool OpenLineAbove() { Func<bool> action = () => @@ -1767,11 +1852,10 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { Debug.Assert(startLine.LineNumber <= endLine.LineNumber); - var view = _textView as ITextView; bool isEditMade = false; bool success = true; - using (ITextEdit edit = view.TextBuffer.CreateEdit()) + using (ITextEdit edit = _textView.TextBuffer.CreateEdit()) { var currentSnapshot = _textView.TextBuffer.CurrentSnapshot; for (int i = startLine.LineNumber; i <= endLine.LineNumber; i++) @@ -1793,7 +1877,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return success; } - private Span? GetTrailingWhitespaceSpanToDelete(ITextSnapshotLine line) + private static Span? GetTrailingWhitespaceSpanToDelete(ITextSnapshotLine line) { int indexOfLastNonWhitespaceCharacter = -1; @@ -1864,7 +1948,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation public void SelectLine(ITextViewLine viewLine, bool extendSelection) { if (viewLine == null) - throw new ArgumentNullException("viewLine"); + throw new ArgumentNullException(nameof(viewLine)); SnapshotPoint anchor; SnapshotPoint active; @@ -1917,91 +2001,205 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// </summary> public bool Delete() { - bool emptyBox = IsEmptyBoxSelection(); - NormalizedSnapshotSpanCollection boxDeletions = null; + bool success = true; - // First, handle cases that don't require edits - if (_textView.Selection.IsEmpty) + if (WillDeleteCreateEdit()) { - if (_textView.Caret.Position.BufferPosition.Position == _textView.TextSnapshot.Length) + var selections = _multiSelectionBroker.AllSelections; + var boxSelection = _multiSelectionBroker.BoxSelection; + var primarySelection = _multiSelectionBroker.PrimarySelection; + + Func<bool> action = () => { - return true; - } + using (_multiSelectionBroker.BeginBatchOperation()) + { + if (TryDeleteEdit(selections)) + { + return TryPostDeleteSelectionUpdate(selections, primarySelection, boxSelection); + } + } + return false; + }; + + success = ExecuteAction(Strings.DeleteCharToRight, action, SelectionUpdate.Ignore, ensureVisible: false); } - // If the entire selection is empty and in virtual space, clear it - else if (_textView.Selection.VirtualSelectedSpans.All(s => s.SnapshotSpan.IsEmpty && s.IsInVirtualSpace)) + else { - this.ResetVirtualSelection(); - return true; + success = TryDeleteSelections(); } - else if (emptyBox) // empty box selection, make sure it is valid + + if (success) { - List<SnapshotSpan> spans = new List<SnapshotSpan>(); + _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll); + } - foreach (var span in _textView.Selection.SelectedSpans) + return success; + } + + private bool TryDeleteSelections() + { + if (_multiSelectionBroker.IsBoxSelection && _multiSelectionBroker.PrimarySelection.InsertionPoint.IsInVirtualSpace) + { + _multiSelectionBroker.SetSelection(new Selection(_multiSelectionBroker.PrimarySelection.Start)); + } + else if (!_multiSelectionBroker.IsBoxSelection) + { + _multiSelectionBroker.PerformActionOnAllSelections(transformer => { - var line = span.Start.GetContainingLine(); - if (span.Start < line.End) + if (!transformer.Selection.IsEmpty) { - spans.Add(_textView.GetTextElementSpan(span.Start)); + transformer.MoveTo(transformer.Selection.Start, select: false, PositionAffinity.Successor); } - } + }); + } + + return true; + } + + private bool TryPostDeleteSelectionUpdate(IReadOnlyList<Selection> selections, Selection primarySelection, Selection boxSelection) + { + // Throughout this method, the parameters passed in are the OLD values, and the parameters on _multiSelectionBroker are the NEW ones + if (boxSelection != Selection.Invalid) + { + // If this is an empty box, we may need to capture the new active/anchor points, as points in virtual space + // won't track as we want them to through the edit. + VirtualSnapshotPoint anchorPoint = _multiSelectionBroker.BoxSelection.AnchorPoint; + VirtualSnapshotPoint activePoint = _multiSelectionBroker.BoxSelection.ActivePoint; - // If there is nothing to delete, clear the selection - if (spans.Count == 0) + if (primarySelection.IsEmpty) { - _textView.Caret.MoveTo(_textView.Selection.Start); - _textView.Selection.Clear(); - return true; + if (boxSelection.AnchorPoint.IsInVirtualSpace) + { + anchorPoint = new VirtualSnapshotPoint(_multiSelectionBroker.BoxSelection.AnchorPoint.Position, boxSelection.AnchorPoint.VirtualSpaces); + } + if (boxSelection.ActivePoint.IsInVirtualSpace) + { + activePoint = new VirtualSnapshotPoint(_multiSelectionBroker.BoxSelection.ActivePoint.Position, boxSelection.ActivePoint.VirtualSpaces); + } } + else + { + // Just take the starting points in the first and last selections + activePoint = selections[boxSelection.IsReversed ? 0 : selections.Count - 1].Start; + anchorPoint = selections[boxSelection.IsReversed ? selections.Count - 1 : 0].Start; + } + + VirtualSnapshotPoint newAnchor = anchorPoint.TranslateTo(_textView.TextSnapshot); + VirtualSnapshotPoint newActive = activePoint.TranslateTo(_textView.TextSnapshot); - boxDeletions = new NormalizedSnapshotSpanCollection(spans); + var newSelection = new Selection(insertionPoint: newActive, anchorPoint: newAnchor, activePoint: newActive, boxSelection.InsertionPointAffinity); + if (_multiSelectionBroker.BoxSelection != newSelection) + { + _multiSelectionBroker.SetBoxSelection(newSelection); + } + } + else + { + // Perf: This is actually an n^2 algorithm here, since TryPerform... also loops through all the selections. Try to avoid copying this code + // elsewhere. We need it here because we're actually modifying each one based on its context AND because merges can happen with backspace so we + // can't do anything funny like caching the transformers. + for (int i = 0; i < selections.Count; i++) + { + //Some could have merged away, ignore return values here intentionally. + _multiSelectionBroker.TryPerformActionOnSelection(selections[i], transformer => + { + // We can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted). + // VirtualSnapshotPoint.TranslateTo doesn't know what to do with virtual whitespace, so we have to do this ourselves. + if (selections[i].IsEmpty && selections[i].InsertionPoint.IsInVirtualSpace) + { + // Move the caret back one if we have an empty selection + transformer.MoveTo(new VirtualSnapshotPoint(transformer.Selection.InsertionPoint.Position, selections[i].InsertionPoint.VirtualSpaces - 1), + select: false, + insertionPointAffinity: PositionAffinity.Successor); + } + else + { + //Move the caret to the start of the selection. + transformer.MoveTo(new VirtualSnapshotPoint(transformer.Selection.InsertionPoint.Position, selections[i].Start.VirtualSpaces), + select: false, + PositionAffinity.Successor); + } + }, out _); + } } + return true; + } - // Now handle cases that require edits - Func<bool> action = () => + private bool TryDeleteEdit(IReadOnlyList<Selection> selections) + { + using (var edit = _textView.TextBuffer.CreateEdit()) { - if (_textView.Selection.IsEmpty) + for (int i = (selections.Count - 1); i >= 0; i--) { - CaretPosition position = _textView.Caret.Position; - if (position.VirtualBufferPosition.IsInVirtualSpace) + var selection = selections[i]; + + if (selection.IsEmpty) { - string whitespace = GetWhitespaceForVirtualSpace(position.VirtualBufferPosition); - SnapshotSpan span = _textView.GetTextElementSpan(_textView.Caret.Position.VirtualBufferPosition.Position); + if (_multiSelectionBroker.IsBoxSelection) + { + var endOfLine = selection.InsertionPoint.Position.GetContainingLine().End; + + if (selection.InsertionPoint.Position == endOfLine) + { + continue; + } + } + + if (selection.InsertionPoint.IsInVirtualSpace) + { + var whitespace = GetWhitespaceForVirtualSpace(selection.InsertionPoint); + var span = _textView.GetTextElementSpan(selection.InsertionPoint.Position); - return ReplaceHelper(span, whitespace); + if (!edit.Replace(span, whitespace)) + { + return false; + } + } + else if (!edit.Delete(_textView.GetTextElementSpan(selection.InsertionPoint.Position))) + { + return false; + } } - else + else if (!edit.Delete(selection.Extent.SnapshotSpan)) { - return DeleteHelper(_textView.GetTextElementSpan(position.VirtualBufferPosition.Position)); + return false; } } - else - { - // The selection is non-empty, so delete selected spans - NormalizedSnapshotSpanCollection deletion = _textView.Selection.SelectedSpans; - // Unless it is an empty box selection, so treat it as a delete on each line - if (emptyBox && boxDeletions != null) - deletion = boxDeletions; + edit.Apply(); + return !edit.Canceled; + } + } - int selectionStartVirtualSpaces = _textView.Selection.Start.VirtualSpaces; - bool succeeded = DeleteHelper(deletion); + private bool WillDeleteCreateEdit() + { + var selections = _multiSelectionBroker.AllSelections; - if (succeeded && (_textView.Selection.Mode != TextSelectionMode.Box)) + if (_multiSelectionBroker.IsBoxSelection) + { + // Edits can not happen if we're a box selection at the end of every line + for (int i = 0; i < selections.Count; i++) + { + if (selections[i].Start.Position < selections[i].Start.Position.GetContainingLine().End) { - //Move the caret to the start of the selection (this doesn't happen automatically if the caret was in virtual space). - //But we can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted). - _textView.Caret.MoveTo(new VirtualSnapshotPoint(_textView.Selection.Start.Position, selectionStartVirtualSpaces)); - _textView.Selection.Clear(); + return true; } - - return succeeded; } - }; + } + else + { + for (int i = 0; i < selections.Count; i++) + { + if ((!selections[i].Extent.SnapshotSpan.IsEmpty) || + (selections[i].IsEmpty && selections[i].InsertionPoint.Position.Position != _multiSelectionBroker.CurrentSnapshot.Length)) + { + return true; + } + } + } - return ExecuteAction(Strings.DeleteText, action, SelectionUpdate.ResetUnlessEmptyBox, true); + return false; } /// <summary> @@ -2016,7 +2214,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // Validate if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } Func<bool> action = () => @@ -2042,7 +2240,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // Validate if (span.End > _textView.TextSnapshot.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } Func<bool> action = () => @@ -2077,7 +2275,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (searchText == null) { - throw new ArgumentNullException("searchText"); + throw new ArgumentNullException(nameof(searchText)); } FindData findData = new FindData(searchText, _textView.TextSnapshot); @@ -2341,7 +2539,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } string streamText = string.Join(_editorOptions.GetNewLineCharacter() + whitespace, lines); - return this.InsertText(streamText.ToString(), true, Strings.Paste, isOverwriteModeEnabled: false); + return this.InsertText(streamText.ToString(CultureInfo.CurrentCulture), true, Strings.Paste, isOverwriteModeEnabled: false); } else { @@ -2438,7 +2636,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { // Validate if (lineNumber < 0 || lineNumber > _textView.TextSnapshot.LineCount - 1) - throw new ArgumentOutOfRangeException("lineNumber"); + throw new ArgumentOutOfRangeException(nameof(lineNumber)); ITextSnapshotLine line = _textView.TextSnapshot.GetLineFromLineNumber(lineNumber); @@ -2775,9 +2973,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation #endif } - #endregion // IEditorOperations Members +#endregion // IEditorOperations Members - #region Virtual Space to Whitespace helpers +#region Virtual Space to Whitespace helpers public string GetWhitespaceForVirtualSpace(VirtualSnapshotPoint point) { @@ -2866,9 +3064,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return textToInsert; } - #endregion +#endregion - #region Text insertion helpers +#region Text insertion helpers private bool InsertText(string text, bool final) { @@ -2880,7 +3078,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // Validate if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } if ((text.Length == 0) && !final) @@ -2937,7 +3135,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } else { - replaceSpans = _textView.Selection.VirtualSelectedSpans; + replaceSpans = _multiSelectionBroker.VirtualSelectedSpans; } // The provisional composition span should be null here (the IME should @@ -2983,18 +3181,21 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } else { - VirtualSnapshotPoint insertionPoint = _textView.Caret.Position.VirtualBufferPosition; - if (isOverwriteModeEnabled && !insertionPoint.IsInVirtualSpace) + var spans = new List<VirtualSnapshotSpan>(); + foreach (var caret in _multiSelectionBroker.GetSelectionsIntersectingSpan(new SnapshotSpan(_multiSelectionBroker.CurrentSnapshot, 0, _multiSelectionBroker.CurrentSnapshot.Length))) { - SnapshotPoint point = insertionPoint.Position; - replaceSpans = new VirtualSnapshotSpan[] { new VirtualSnapshotSpan( - new SnapshotSpan(point, _textView.GetTextElementSpan(point).End)) }; - } - else - { - replaceSpans = new VirtualSnapshotSpan[] { - new VirtualSnapshotSpan(insertionPoint, insertionPoint) }; + var insertionPoint = caret.InsertionPoint; + if (isOverwriteModeEnabled && !insertionPoint.IsInVirtualSpace) + { + SnapshotPoint point = insertionPoint.Position; + spans.Add(new VirtualSnapshotSpan(new SnapshotSpan(point, _textView.GetTextElementSpan(point).End))); + } + else + { + spans.Add(new VirtualSnapshotSpan(insertionPoint, insertionPoint)); + } } + replaceSpans = spans; } ITextVersion currentVersion = _textView.TextSnapshot.Version; @@ -3053,17 +3254,28 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation if (editSuccessful) { - // Get rid of virtual space if there is any, - // since we've just made it non-virtual - _textView.Caret.MoveTo(_textView.Caret.Position.BufferPosition); - _textView.Selection.Select( - new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position), - new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position)); + if (_multiSelectionBroker.IsBoxSelection) + { + _textView.Caret.MoveTo(_textView.Caret.Position.BufferPosition); + _textView.Selection.Select( + new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position), + new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position)); + + // If the selection ends up being non-empty (meaning not an empty + // single selection *or* an empty box), then clear it. + if (_textView.Selection.VirtualSelectedSpans.Any(s => !s.IsEmpty)) + _textView.Selection.Clear(); - // If the selection ends up being non-empty (meaning not an empty - // single selection *or* an empty box), then clear it. - if (_textView.Selection.VirtualSelectedSpans.Any(s => !s.IsEmpty)) - _textView.Selection.Clear(); + } + else + { + _multiSelectionBroker.PerformActionOnAllSelections(transformer => + { + // We've done the edit now. We need to both remove virtual space and clear selections. + var newInsertion = new VirtualSnapshotPoint(transformer.Selection.InsertionPoint.Position, 0); + transformer.MoveTo(newInsertion, select: false, PositionAffinity.Successor); + }); + } _textView.Caret.EnsureVisible(); @@ -3118,7 +3330,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation public bool InsertTextAsBox(string text, out VirtualSnapshotPoint boxStart, out VirtualSnapshotPoint boxEnd, string undoText) { if (text == null) - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); boxStart = boxEnd = _textView.Caret.Position.VirtualBufferPosition; @@ -3290,9 +3502,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return succeeded; } - #endregion +#endregion - #region Clipboard and RTF helpers +#region Clipboard and RTF helpers private Func<bool> PrepareClipboardSelectionCopy() { @@ -3376,7 +3588,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // which causes the OS to send out two almost simultaneous clipboard open/close notification pairs // which confuse applications that try to synchronize clipboard data between multiple machines such // as MagicMouse or remote desktop. - Clipboard.SetDataObject(dataObject, false); #endif @@ -3392,11 +3603,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation private string GenerateRtf(NormalizedSnapshotSpanCollection spans) { #if WINDOWS - if (_factory.RtfBuilderService == null) - { - return null; - } - //Don't generate RTF for large spans (since it is expensive and probably not wanted). int length = spans.Sum((span) => span.Length); if (length < _textView.Options.GetOptionValue(MaxRtfCopyLength.OptionKey)) @@ -3426,9 +3632,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return GenerateRtf(new NormalizedSnapshotSpanCollection(span)); } - #endregion +#endregion - #region Horizontal whitespace helpers +#region Horizontal whitespace helpers private bool DeleteHorizontalWhitespace() { @@ -3610,9 +3816,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return startPoint; } - #endregion +#endregion - #region Indent/unindent helpers +#region Indent/unindent helpers // Perform the given indent action (indent/unindent) on each line at the first non-whitespace // character, skipping lines that are either empty or just whitespace. @@ -3912,9 +4118,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return new VirtualSnapshotPoint(point.Position); } - #endregion +#endregion - #region Box Selection indent/unindent helpers +#region Box Selection indent/unindent helpers /// <summary> /// Given a "fix-up" anchor/active point determined before the box operation, fix up the current selection's @@ -4011,7 +4217,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { textPoint.MoveTo(i); string character = textPoint.GetNextCharacter(); - if (character != " " && character != "\t") + if (!string.Equals(character, " ", StringComparison.Ordinal) && !string.Equals(character, "\t", StringComparison.Ordinal)) break; column = textPoint.Column; @@ -4028,9 +4234,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return maxColumnUnindent; } - #endregion +#endregion - #region Miscellaneous line helpers +#region Miscellaneous line helpers private DisplayTextRange GetFullLines() { @@ -4094,9 +4300,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation return firstTextColumn.CurrentPosition == displayTextPoint.EndOfViewLine; } - #endregion +#endregion - #region Tabs <-> spaces +#region Tabs <-> spaces private bool ConvertSpacesAndTabsHelper(bool toTabs) { @@ -4214,16 +4420,16 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } Span replaceSpan = Span.FromBounds(whiteSpaceStart, whiteSpaceEnd); - if ((replaceSpan.Length != textToInsert.Length) || (textToInsert != textEdit.Snapshot.GetText(replaceSpan))) //performance hack: don't get the text if we know they'll be different. + if ((replaceSpan.Length != textToInsert.Length) || (!string.Equals(textToInsert, textEdit.Snapshot.GetText(replaceSpan), StringComparison.Ordinal))) //performance hack: don't get the text if we know they'll be different. return textEdit.Replace(replaceSpan, textToInsert); } return true; } - #endregion +#endregion - #region Edit/Replace/Delete helpers +#region Edit/Replace/Delete helpers internal bool EditHelper(Func<ITextEdit, bool> editAction) { @@ -4300,7 +4506,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation }); } - #endregion +#endregion internal bool IsEmptyBoxSelection() { @@ -4318,9 +4524,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// <param name="extendSelection">If <c>true</c>, extend the current selection, from the existing anchor point, /// to the new caret position.</param> /// <returns><c>true</c> if the caret was positioned in virtual space.</returns> - private bool PositionCaretWithSmartIndent(bool useOnlyVirtualSpace = true, bool extendSelection = false) + private bool PositionCaretWithSmartIndent(ISelectionTransformer transformer, bool useOnlyVirtualSpace = true, bool extendSelection = false) { - var caretPosition = _textView.Caret.Position.VirtualBufferPosition; + var caretPosition = transformer.Selection.InsertionPoint; var caretLine = caretPosition.Position.GetContainingLine(); int? indentation = _factory.SmartIndentationService.GetDesiredIndentation(_textView, caretLine); @@ -4330,8 +4536,8 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { //Position the caret in virtual space at the appropriate indentation. var newCaretPoint = new VirtualSnapshotPoint(caretPosition.Position, Math.Max(0, indentation.Value - caretLine.Length)); - var anchorPoint = (extendSelection) ? _textView.Selection.AnchorPoint : newCaretPoint; - SelectAndMoveCaret(anchorPoint, newCaretPoint, selectionMode: TextSelectionMode.Stream, scrollOptions: null); + var anchorPoint = (extendSelection) ? transformer.Selection.AnchorPoint : newCaretPoint; + transformer.MoveTo(anchorPoint, newCaretPoint, newCaretPoint, PositionAffinity.Successor); return true; } else if (!useOnlyVirtualSpace) @@ -4355,7 +4561,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// <param name="line">Which line to evaluate</param> /// <param name="startPosition">Position where the count starts</param> /// <returns>Number of leading whitespace characters located after startPosition</returns> - private int GetLeadingWhitespaceChars(ITextSnapshotLine line, SnapshotPoint startPosition) + private static int GetLeadingWhitespaceChars(ITextSnapshotLine line, SnapshotPoint startPosition) { int whitespace = 0; for (int i = startPosition.Position; i < line.End; ++i) @@ -4390,7 +4596,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation SnapshotSpan currentWhiteSpace = new SnapshotSpan(line.Start, firstNonWhitespaceCharacter.AdvancedTextPoint); - if (whitespace != currentWhiteSpace.GetText()) + if (!string.Equals(whitespace, currentWhiteSpace.GetText(), StringComparison.Ordinal)) { if (!textEdit.Replace(currentWhiteSpace, whitespace)) return false; @@ -4572,7 +4778,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation if (line.LineBreakLength != 0) { string breakText = line.GetLineBreakText(); - if (breakText != replacement) + if (!string.Equals(breakText, replacement, StringComparison.Ordinal)) { if (!edit.Replace(line.End, line.LineBreakLength, replacement)) return false; diff --git a/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs b/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs index 7b1d693..a61fee1 100644 --- a/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs +++ b/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs @@ -22,6 +22,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation internal sealed class EditorOperationsFactoryService : IEditorOperationsFactoryService { [Import] + public IMultiSelectionBrokerFactory MultiSelectionBrokerFactory { get; set; } + + [Import] internal ITextStructureNavigatorSelectorService TextStructureNavigatorFactory { get; set; } #if WINDOWS @@ -76,7 +79,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // Validate if (textView == null) { - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); } // Only one EditorOperations should be created per ITextView diff --git a/src/Text/Impl/EditorOperations/Strings.Designer.cs b/src/Text/Impl/EditorOperations/Strings.Designer.cs index ffebf8e..fd4a317 100644 --- a/src/Text/Impl/EditorOperations/Strings.Designer.cs +++ b/src/Text/Impl/EditorOperations/Strings.Designer.cs @@ -241,15 +241,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { } /// <summary> - /// Looks up a localized string similar to Expand/Contract Selection Command Handler. - /// </summary> - internal static string ExpandContractSelectionCommandHandlerName { - get { - return ResourceManager.GetString("ExpandContractSelectionCommandHandlerName", resourceCulture); - } - } - - /// <summary> /// Looks up a localized string similar to Increase line indent. /// </summary> internal static string IncreaseLineIndent { @@ -349,6 +340,15 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { } /// <summary> + /// Looks up a localized string similar to Next/Previous Issue. + /// </summary> + internal static string NextIssue { + get { + return ResourceManager.GetString("NextIssue", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Make Line Endings Consistent. /// </summary> internal static string NormalizeLineEndings { diff --git a/src/Text/Impl/EditorOperations/Strings.resx b/src/Text/Impl/EditorOperations/Strings.resx index 4792155..b53056d 100644 --- a/src/Text/Impl/EditorOperations/Strings.resx +++ b/src/Text/Impl/EditorOperations/Strings.resx @@ -261,10 +261,10 @@ <data name="DuplicateSelection" xml:space="preserve"> <value>Duplicate Selection</value> </data> - <data name="ExpandContractSelectionCommandHandlerName" xml:space="preserve"> - <value>Expand/Contract Selection Command Handler</value> - </data> <data name="DuplicateSelectionCommandHandlerName" xml:space="preserve"> <value>Duplicate Selection Command Handler</value> </data> + <data name="NextIssue" xml:space="preserve"> + <value>Next/Previous Issue</value> + </data> </root>
\ No newline at end of file diff --git a/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs b/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs index d913c0d..9c8086a 100644 --- a/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs +++ b/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs @@ -48,12 +48,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation // Validate if (newTransaction == null) { - throw new ArgumentNullException("newTransaction"); + throw new ArgumentNullException(nameof(newTransaction)); } if (oldTransaction == null) { - throw new ArgumentNullException("oldTransaction"); + throw new ArgumentNullException(nameof(oldTransaction)); } TextTransactionMergePolicy oldPolicy = oldTransaction.MergePolicy as TextTransactionMergePolicy; @@ -71,7 +71,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation } // Only merge text transactions that have the same description - if (newTransaction.Description != oldTransaction.Description) + if (!string.Equals(newTransaction.Description, oldTransaction.Description, StringComparison.Ordinal)) { return false; } @@ -92,9 +92,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction) { if (existingTransaction == null) - throw new ArgumentNullException("existingTransaction"); + throw new ArgumentNullException(nameof(existingTransaction)); if (newTransaction == null) - throw new ArgumentNullException("newTransaction"); + throw new ArgumentNullException(nameof(newTransaction)); // Remove trailing AfterTextBufferChangeUndoPrimitive from previous transaction and skip copying // initial BeforeTextBufferChangeUndoPrimitive from newTransaction, as they are unnecessary. @@ -128,7 +128,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (other == null) { - throw new ArgumentNullException("other"); + throw new ArgumentNullException(nameof(other)); } // Only merge transaction if they are both a text transaction diff --git a/src/Text/Impl/EditorOptions/EditorOptions.cs b/src/Text/Impl/EditorOptions/EditorOptions.cs index 59a6e55..cb97410 100644 --- a/src/Text/Impl/EditorOptions/EditorOptions.cs +++ b/src/Text/Impl/EditorOptions/EditorOptions.cs @@ -10,6 +10,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation using System; using System.Collections.Generic; using System.Collections.Specialized; + using System.Globalization; using System.Linq; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Utilities; @@ -60,7 +61,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation throw new InvalidOperationException("Cannot change the Parent of the global options."); if (value == null) - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); if (value == this) throw new ArgumentException("The Parent of this instance of IEditorOptions cannot be set to itself."); @@ -124,7 +125,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation object value; if (!TryGetOption(definition, out value)) - throw new ArgumentException(string.Format("The specified option is not valid in this scope: {0}", definition.Name)); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The specified option is not valid in this scope: {0}", definition.Name)); return value; } @@ -136,12 +137,12 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation // Make sure the type of the provided value is correct if (!definition.ValueType.IsAssignableFrom(value.GetType())) { - throw new ArgumentException("Specified option value is of an invalid type", "value"); + throw new ArgumentException("Specified option value is of an invalid type", nameof(value)); } // Make sure the option is valid, also else if(!definition.IsValid(ref value)) { - throw new ArgumentException("The supplied value failed validation for the option.", "value"); + throw new ArgumentException("The supplied value failed validation for the option.", nameof(value)); } // Finally, set the option value locally else diff --git a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs index 733450b..a7f2686 100644 --- a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs +++ b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs @@ -35,7 +35,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation public IEditorOptions GetOptions(IPropertyOwner scope) { if (scope == null) - throw new ArgumentNullException("scope"); + throw new ArgumentNullException(nameof(scope)); return scope.Properties.GetOrCreateSingletonProperty<IEditorOptions>(() => new EditorOptions(this.GlobalOptions as EditorOptions, scope, this)); } @@ -133,7 +133,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation { var definition = this.GetOptionDefinition(optionId); if (definition == null) - throw new ArgumentException(string.Format("No EditorOptionDefinition export found for the given option name: {0}", optionId), "optionId"); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "No EditorOptionDefinition export found for the given option name: {0}", optionId), nameof(optionId)); return definition; } diff --git a/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs index 19923d5..9e383d9 100644 --- a/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs +++ b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs @@ -30,7 +30,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if ((position < 0) || (position > _textBuffer.CurrentSnapshot.Length)) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } return _bufferPrimitivesFactory.CreateTextPoint(this, position); } @@ -39,14 +39,14 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if ((line < 0) || (line > _textBuffer.CurrentSnapshot.LineCount)) { - throw new ArgumentOutOfRangeException("line"); + throw new ArgumentOutOfRangeException(nameof(line)); } ITextSnapshotLine snapshotLine = _textBuffer.CurrentSnapshot.GetLineFromLineNumber(line); if ((column < 0) || (column > snapshotLine.Length)) { - throw new ArgumentOutOfRangeException("column"); + throw new ArgumentOutOfRangeException(nameof(column)); } return _bufferPrimitivesFactory.CreateTextPoint(this, snapshotLine.Start + column); } @@ -55,7 +55,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if ((line < 0) || (line > _textBuffer.CurrentSnapshot.LineCount)) { - throw new ArgumentOutOfRangeException("line"); + throw new ArgumentOutOfRangeException(nameof(line)); } ITextSnapshotLine snapshotLine = _textBuffer.CurrentSnapshot.GetLineFromLineNumber(line); @@ -67,11 +67,11 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if (startPoint == null) { - throw new ArgumentNullException("startPoint"); + throw new ArgumentNullException(nameof(startPoint)); } if (endPoint == null) { - throw new ArgumentNullException("endPoint"); + throw new ArgumentNullException(nameof(endPoint)); } if (!object.ReferenceEquals(startPoint.TextBuffer, this)) @@ -91,12 +91,12 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if ((startPosition < 0) || (startPosition > _textBuffer.CurrentSnapshot.Length)) { - throw new ArgumentOutOfRangeException("startPosition"); + throw new ArgumentOutOfRangeException(nameof(startPosition)); } if ((endPosition < 0) || (endPosition > _textBuffer.CurrentSnapshot.Length)) { - throw new ArgumentOutOfRangeException("endPosition"); + throw new ArgumentOutOfRangeException(nameof(endPosition)); } TextPoint startPoint = GetTextPoint(startPosition); diff --git a/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs index 0b64bc4..8ca865a 100644 --- a/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs +++ b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs @@ -82,7 +82,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if (!object.ReferenceEquals(this.TextBuffer, otherPoint.TextBuffer)) { - throw new ArgumentException("The other point must have the same TextBuffer as this one", "otherPoint"); + throw new ArgumentException("The other point must have the same TextBuffer as this one", nameof(otherPoint)); } return TextView.GetTextRange(this, otherPoint); } @@ -184,7 +184,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } return _bufferPoint.InsertText(text); diff --git a/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs index 35231be..d9ce555 100644 --- a/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs +++ b/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs @@ -18,7 +18,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; - internal sealed class DefaultSelectionPrimitive : Selection + internal sealed class DefaultSelectionPrimitive : Text.Editor.LegacySelection { private TextView _textView; private IEditorOptions _editorOptions; diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs index be6d268..69088f7 100644 --- a/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs +++ b/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs @@ -37,7 +37,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation if ((position < 0) || (position > textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length)) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } _textBuffer = textBuffer; @@ -78,7 +78,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation for (int i = 0; i < lineTextInfo.LengthInTextElements; i++) { string textElement = lineTextInfo.SubstringByTextElements(i, 1); - if (textElement == "\t") + if (string.Equals(textElement, "\t", StringComparison.Ordinal)) { // If there is a tab in the text, then the column automatically jumps // to the next tab stop. @@ -291,7 +291,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if (otherPoint == null) { - throw new ArgumentNullException("otherPoint"); + throw new ArgumentNullException(nameof(otherPoint)); } if (otherPoint.TextBuffer != TextBuffer) @@ -306,7 +306,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if ((otherPosition < 0) || (otherPosition > TextBuffer.AdvancedTextBuffer.CurrentSnapshot.Length)) { - throw new ArgumentOutOfRangeException("otherPosition"); + throw new ArgumentOutOfRangeException(nameof(otherPosition)); } TextPoint otherPoint = this.Clone(); @@ -354,7 +354,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } if (text.Length > 0) @@ -410,7 +410,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { newPoint.MoveTo(i); string character = newPoint.GetNextCharacter(); - if (character != " " && character != "\t") + if (!string.Equals(character, " ", StringComparison.Ordinal) && !string.Equals(character, "\t", StringComparison.Ordinal)) { break; } @@ -510,7 +510,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if ((lineNumber < 0) || (lineNumber > _textBuffer.AdvancedTextBuffer.CurrentSnapshot.LineCount)) { - throw new ArgumentOutOfRangeException("lineNumber"); + throw new ArgumentOutOfRangeException(nameof(lineNumber)); } ITextSnapshot currentSnapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot; @@ -737,11 +737,11 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if (pattern == null) { - throw new ArgumentNullException("pattern"); + throw new ArgumentNullException(nameof(pattern)); } if (endPoint == null) { - throw new ArgumentNullException("endPoint"); + throw new ArgumentNullException(nameof(endPoint)); } if (endPoint.TextBuffer != TextBuffer) { @@ -769,7 +769,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation if ((position < 0) || (position > snapshot.Length)) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } // If this is the end of the snapshot, we don't need to check anything. @@ -872,7 +872,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation if ((lineNumber < 0) || (lineNumber > _textBuffer.AdvancedTextBuffer.CurrentSnapshot.LineCount)) { - throw new ArgumentOutOfRangeException("lineNumber"); + throw new ArgumentOutOfRangeException(nameof(lineNumber)); } ITextSnapshotLine line = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber); diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs index 961db4f..e0abb1b 100644 --- a/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs +++ b/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs @@ -167,7 +167,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation newChar = char.ToUpper(newChar, CultureInfo.CurrentCulture); } - if (!textEdit.Replace(i, 1, newChar.ToString())) + if (!textEdit.Replace(i, 1, newChar.ToString(CultureInfo.CurrentCulture))) { textEdit.Cancel(); return false; // break out early if any edit fails to reduce the time of the failure case @@ -296,7 +296,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { if (string.IsNullOrEmpty(newText)) { - throw new ArgumentNullException("newText"); + throw new ArgumentNullException(nameof(newText)); } int startPoint = _startPoint.CurrentPosition; diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs index 80715de..4dca517 100644 --- a/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs +++ b/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs @@ -10,12 +10,13 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Formatting; + using LegacySelection = Microsoft.VisualStudio.Text.Editor.LegacySelection; internal sealed class DefaultTextViewPrimitive : TextView { private ITextView _textView; private Caret _caret; - private Selection _selection; + private LegacySelection _selection; private TextBuffer _textBuffer; private IViewPrimitivesFactoryService _viewPrimitivesFactory; @@ -144,7 +145,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation get { return _caret; } } - public override Selection Selection + public override LegacySelection Selection { get { return _selection; } } diff --git a/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs b/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs index 09e9d25..99400b7 100644 --- a/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs +++ b/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs @@ -44,7 +44,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation return new DefaultDisplayTextRangePrimitive(textView, textRange); } - public Selection CreateSelection(TextView textView) + public LegacySelection CreateSelection(TextView textView) { if (textView.Selection == null) { diff --git a/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs b/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs index 9b2b1dc..081fdb4 100644 --- a/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs +++ b/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs @@ -12,7 +12,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation internal sealed class ViewPrimitives : IViewPrimitives { private TextView _textView; - private Selection _selection; + private LegacySelection _selection; private Caret _caret; private TextBuffer _textBuffer; @@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation get { return _textView; } } - public Selection Selection + public LegacySelection Selection { get { return _selection; } } diff --git a/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs b/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs index e59bc8d..ae08b4f 100644 --- a/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs +++ b/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs @@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } ITextStructureNavigator navigator = null; @@ -55,11 +55,11 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } return CreateNavigator(textBuffer, contentType); } diff --git a/src/Text/Impl/Outlining/OutliningManager.cs b/src/Text/Impl/Outlining/OutliningManager.cs index 71418ec..28decbe 100644 --- a/src/Text/Impl/Outlining/OutliningManager.cs +++ b/src/Text/Impl/Outlining/OutliningManager.cs @@ -244,7 +244,7 @@ namespace Microsoft.VisualStudio.Text.Outlining if (internalCollapsed == null) { throw new ArgumentException("The given collapsed region was not created by this outlining manager.", - "collapsed"); + nameof(collapsed)); } if (!internalCollapsed.IsValid) @@ -272,7 +272,7 @@ namespace Microsoft.VisualStudio.Text.Outlining internal IEnumerable<ICollapsed> InternalCollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken? cancel) { if (match == null) - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); EnsureValid(span); @@ -312,7 +312,7 @@ namespace Microsoft.VisualStudio.Text.Outlining public IEnumerable<ICollapsible> ExpandAllInternal(bool removalPending, SnapshotSpan span, Predicate<ICollapsed> match) { if (match == null) - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); EnsureValid(span); @@ -676,19 +676,19 @@ namespace Microsoft.VisualStudio.Text.Outlining if (spans == null) { - throw new ArgumentNullException("spans"); + throw new ArgumentNullException(nameof(spans)); } if (spans.Count == 0) { - throw new ArgumentException("The given span collection is empty.", "spans"); + throw new ArgumentException("The given span collection is empty.", nameof(spans)); } if (spans[0].Snapshot.TextBuffer != this.editBuffer) { throw new ArgumentException("The given span collection is on an invalid buffer." + "Spans must be generated against the view model's edit buffer", - "spans"); + nameof(spans)); } } @@ -705,7 +705,7 @@ namespace Microsoft.VisualStudio.Text.Outlining { throw new ArgumentException("The given span is on an invalid buffer." + "Spans must be generated against the view model's edit buffer", - "span"); + nameof(span)); } } @@ -725,9 +725,9 @@ namespace Microsoft.VisualStudio.Text.Outlining public int Compare(ICollapsible x, ICollapsible y) { if (x == null) - throw new ArgumentNullException("x"); + throw new ArgumentNullException(nameof(x)); if (y == null) - throw new ArgumentNullException("y"); + throw new ArgumentNullException(nameof(y)); ITextSnapshot current = SourceBuffer.CurrentSnapshot; SnapshotSpan left = x.Extent.GetSpan(current); diff --git a/src/Text/Impl/Outlining/OutliningManagerService.cs b/src/Text/Impl/Outlining/OutliningManagerService.cs index 5e8a795..081672e 100644 --- a/src/Text/Impl/Outlining/OutliningManagerService.cs +++ b/src/Text/Impl/Outlining/OutliningManagerService.cs @@ -28,7 +28,7 @@ namespace Microsoft.VisualStudio.Text.Outlining public IOutliningManager GetOutliningManager(ITextView textView) { if (textView == null) - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); if (!textView.Roles.Contains(PredefinedTextViewRoles.Structured)) return null; diff --git a/src/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs b/src/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs index adfacc8..6106223 100644 --- a/src/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs +++ b/src/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs @@ -1,9 +1,8 @@ // 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.Collections.Immutable; -using Microsoft.VisualStudio.Text; +using System.Globalization; using Microsoft.VisualStudio.Text.Utilities; using TextSpan = Microsoft.VisualStudio.Text.Span; @@ -66,8 +65,8 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation return GetKind(result.Value); } - private PatternMatchKind GetKind(CamelCaseResult result) - => PatternMatcher.GetCamelCaseKind(result, _candidateHumps); + private static PatternMatchKind GetKind(CamelCaseResult result) + => PatternMatcher.GetCamelCaseKind(result); private CamelCaseResult? TryMatch( int patternIndex, int candidateHumpIndex, bool? contiguous, int chunkOffset) @@ -99,7 +98,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation } var candidateHump = _candidateHumps[humpIndex]; - if (char.ToLower(_candidate[candidateHump.Start]) == patternCharacter) + if (char.ToLower(_candidate[candidateHump.Start], CultureInfo.CurrentCulture) == patternCharacter) { // Found a hump in the candidate string that matches the current pattern // character we're on. i.e. we matched the c in cofipro against the C in @@ -204,7 +203,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation /// If 'weight' is better than 'bestWeight' and matchSpanToAdd is not null, then /// matchSpanToAdd will be added to matchedSpansInReverse. /// </summary> - private bool UpdateBestResultIfBetter( + private static bool UpdateBestResultIfBetter( CamelCaseResult result, ref CamelCaseResult? bestResult, TextSpan? matchSpanToAdd) { if (matchSpanToAdd != null) @@ -234,7 +233,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation return GetKind(result) == PatternMatchKind.CamelCaseExact; } - private bool IsBetter(CamelCaseResult result, CamelCaseResult? currentBestResult) + private static bool IsBetter(CamelCaseResult result, CamelCaseResult? currentBestResult) { if (currentBestResult == null) { @@ -245,12 +244,12 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation return GetKind(result) < GetKind(currentBestResult.Value); } - private bool LowercaseSubstringsMatch( + private static bool LowercaseSubstringsMatch( string s1, int start1, string s2, int start2, int length) { for (var i = 0; i < length; i++) { - if (char.ToLower(s1[start1 + i]) != char.ToLower(s2[start2 + i])) + if (char.ToLower(s1[start1 + i], CultureInfo.CurrentCulture) != char.ToLower(s2[start2 + i], CultureInfo.CurrentCulture)) { return false; } diff --git a/src/Text/Impl/PatternMatching/ArraySlice.cs b/src/Text/Impl/PatternMatching/ArraySlice.cs index 6e7455e..1da2b03 100644 --- a/src/Text/Impl/PatternMatching/ArraySlice.cs +++ b/src/Text/Impl/PatternMatching/ArraySlice.cs @@ -41,12 +41,12 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation { if (start < 0) { - throw new ArgumentException(nameof(start), $"{start} < {0}"); + throw new ArgumentException($"{start} < {0}", nameof(start)); } if (start > _array.Length) { - throw new ArgumentException(nameof(start), $"{start} > {_array.Length}"); + throw new ArgumentException($"{start} > {_array.Length}", nameof(start)); } CheckLength(start, length); @@ -59,12 +59,12 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation { if (length < 0) { - throw new ArgumentException(nameof(length), $"{length} < {0}"); + throw new ArgumentException($"{length} < {0}", nameof(length)); } if (start + length > _array.Length) { - throw new ArgumentException(nameof(start), $"{start} + {length} > {_array.Length}"); + throw new ArgumentException($"{start} + {length} > {_array.Length}", nameof(start)); } } diff --git a/src/Text/Impl/PatternMatching/CamelCaseResult.cs b/src/Text/Impl/PatternMatching/CamelCaseResult.cs index f67572b..f11c61b 100644 --- a/src/Text/Impl/PatternMatching/CamelCaseResult.cs +++ b/src/Text/Impl/PatternMatching/CamelCaseResult.cs @@ -47,7 +47,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation } } - private static PatternMatchKind GetCamelCaseKind(CamelCaseResult result, StringBreaks candidateHumps) + private static PatternMatchKind GetCamelCaseKind(CamelCaseResult result) { /* CamelCase PatternMatchKind truth table: * | FromStart | ToEnd | Contiguous || PatternMatchKind | diff --git a/src/Text/Impl/PatternMatching/ContainerPatternMatcher.cs b/src/Text/Impl/PatternMatching/ContainerPatternMatcher.cs index 6048e0f..55aa996 100644 --- a/src/Text/Impl/PatternMatching/ContainerPatternMatcher.cs +++ b/src/Text/Impl/PatternMatching/ContainerPatternMatcher.cs @@ -43,6 +43,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation _invalidPattern = _patternSegments.Length == 0 || _patternSegments.Any(s => s.IsInvalid); } +#pragma warning disable CA1063 public override void Dispose() { base.Dispose(); @@ -52,6 +53,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation segment.Dispose(); } } +#pragma warning restore CA1063 public override PatternMatch? TryMatch(string candidate) { diff --git a/src/Text/Impl/PatternMatching/EditDistance.cs b/src/Text/Impl/PatternMatching/EditDistance.cs index 5eb25d2..d350be5 100644 --- a/src/Text/Impl/PatternMatching/EditDistance.cs +++ b/src/Text/Impl/PatternMatching/EditDistance.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Text; using System.Threading; namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation { +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional ///<summary> /// NOTE: Only use if you truly need an edit distance. /// @@ -24,7 +26,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation /// Specifically, this implementation satisfies the following inequality: D(x, y) + D(y, z) >= D(x, z) /// (where D is the edit distance). ///</summary> - internal class EditDistance : IDisposable + internal sealed class EditDistance : IDisposable { // Our edit distance algorithm makes use of an 'infinite' value. A value so high that it // could never participate in an edit distance (and effectively means the path through it @@ -556,7 +558,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation for (var i = 0; i < width; i++) { var v = matrix[i + 2, j + 2]; - sb.Append((v == Infinity ? "∞" : v.ToString()) + " "); + sb.Append((v == Infinity ? "∞" : v.ToString(CultureInfo.CurrentCulture)) + " "); } sb.AppendLine(); } @@ -666,4 +668,5 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation } } } +#pragma warning restore CA1814 // Prefer jagged arrays over multidimensional } diff --git a/src/Text/Impl/PatternMatching/PatternMatcher.cs b/src/Text/Impl/PatternMatching/PatternMatcher.cs index f781db3..793a632 100644 --- a/src/Text/Impl/PatternMatching/PatternMatcher.cs +++ b/src/Text/Impl/PatternMatching/PatternMatcher.cs @@ -58,7 +58,9 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation _allowSimpleSubstringMatching = allowSimpleSubstringMatching; } +#pragma warning disable CA1063 public virtual void Dispose() +#pragma warning restore CA1063 { foreach (var kvp in _stringToWordSpans) { @@ -138,7 +140,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation : NonFuzzyMatchPatternChunk(candidate, patternChunk, punctuationStripped, chunkOffset); } - private PatternMatch? FuzzyMatchPatternChunk( + private static PatternMatch? FuzzyMatchPatternChunk( string candidate, TextChunk patternChunk, bool punctuationStripped) @@ -167,7 +169,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation // a) Check if the part matches the candidate entirely, in an case insensitive or // sensitive manner. If it does, return that there was an exact match. return new PatternMatch( - PatternMatchKind.Exact, punctuationStripped, isCaseSensitive: candidate == patternChunk.Text, + PatternMatchKind.Exact, punctuationStripped, isCaseSensitive: string.Equals(candidate, patternChunk.Text, StringComparison.Ordinal), matchedSpans: GetMatchedSpans(chunkOffset, candidate.Length)); } else @@ -541,7 +543,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation matchedSpansInReverse: null, chunkOffset: chunkOffset ); - return GetCamelCaseKind(camelCaseResult, candidateHumps); + return GetCamelCaseKind(camelCaseResult); } else if (currentCandidateHump == candidateHumpCount) { diff --git a/src/Text/Impl/PatternMatching/WordSimilarityChecker.cs b/src/Text/Impl/PatternMatching/WordSimilarityChecker.cs index d049e6b..d2ad8b6 100644 --- a/src/Text/Impl/PatternMatching/WordSimilarityChecker.cs +++ b/src/Text/Impl/PatternMatching/WordSimilarityChecker.cs @@ -123,7 +123,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation return false; } - if (_lastAreSimilarResult.CandidateText == candidateText) + if (string.Equals(_lastAreSimilarResult.CandidateText, candidateText, StringComparison.Ordinal)) { similarityWeight = _lastAreSimilarResult.SimilarityWeight; return _lastAreSimilarResult.AreSimilar; diff --git a/src/Text/Impl/StandaloneUndo/AutoEnclose.cs b/src/Text/Impl/StandaloneUndo/AutoEnclose.cs index 42af7d4..ad92cd3 100644 --- a/src/Text/Impl/StandaloneUndo/AutoEnclose.cs +++ b/src/Text/Impl/StandaloneUndo/AutoEnclose.cs @@ -20,7 +20,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone this.end = end; } +#pragma warning disable CA1063 // Implement IDisposable Correctly public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { if (end != null) end(); GC.SuppressFinalize(this); diff --git a/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs b/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs index b39446d..ace7773 100644 --- a/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs +++ b/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs @@ -28,7 +28,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone history.ForwardToUndoOperation(primitive); } +#pragma warning disable CA1063 // Implement IDisposable Correctly public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { history.EndForwardToUndoOperation(primitive); primitive.State = DelegatedUndoPrimitiveState.Inactive; diff --git a/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs index 87c83f9..3e678f9 100644 --- a/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs +++ b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs @@ -110,10 +110,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone } } +#pragma warning disable CA1822 // Mark members as static public bool MergeWithPreviousOnly { get { return true; } } +#pragma warning restore CA1822 // Mark members as static public bool CanMerge(ITextUndoPrimitive primitive) { @@ -125,4 +127,4 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone throw new InvalidOperationException("Strings.DelegatedUndoPrimitiveCannotMerge"); } } -}
\ No newline at end of file +} diff --git a/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs b/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs index 9f5bac5..47fec01 100644 --- a/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs +++ b/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs @@ -8,13 +8,11 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Collections.ObjectModel; -using System.ComponentModel.Composition; using Microsoft.VisualStudio.Utilities; namespace Microsoft.VisualStudio.Text.Operations.Standalone { - internal class UndoHistoryImpl : ITextUndoHistory + internal class UndoHistoryImpl : ITextUndoHistory2 { public event EventHandler<TextUndoRedoEventArgs> UndoRedoHappened; public event EventHandler<TextUndoTransactionCompletedEventArgs> UndoTransactionCompleted; @@ -25,7 +23,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone private Stack<ITextUndoTransaction> undoStack; private Stack<ITextUndoTransaction> redoStack; private DelegatedUndoPrimitiveImpl activeUndoOperationPrimitive; - private TextUndoHistoryState state; + internal TextUndoHistoryState state; private PropertyCollection properties; #endregion @@ -188,6 +186,13 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone get { return this.state; } } + public ITextUndoTransaction CreateInvisibleTransaction(string description) + { + // Standalone undo doesn't support invisible transactions so simply return + // a normal transaction. + return this.CreateTransaction(description); + } + /// <summary> /// Creates a new transaction, nests it in the previously current transaction, and marks it current. /// If there is a redo stack, it gets cleared. @@ -198,9 +203,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone /// <returns></returns> public ITextUndoTransaction CreateTransaction(string description) { - if (String.IsNullOrEmpty(description)) + if (string.IsNullOrEmpty(description)) { - throw new ArgumentNullException("description", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "CreateTransaction", "description")); + throw new ArgumentNullException(nameof(description)); } // If there is a pending transaction that has already been completed, we should not be permitted @@ -244,12 +249,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (count <= 0) { - throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, "Strings.RedoAndUndoAcceptOnlyPositiveCounts", "Undo", count), "count"); + throw new ArgumentOutOfRangeException(nameof(count)); } if (!IsThereEnoughVisibleTransactions(this.undoStack, count)) { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Strings.CannotUndoMoreTransactionsThanExist", "undo", count)); + throw new InvalidOperationException("Cannot undo more transactions than exist"); } TextUndoHistoryState originalState = this.state; @@ -321,12 +326,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (count <= 0) { - throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, "Strings.RedoAndUndoAcceptOnlyPositiveCounts", "Redo", count), "count"); + throw new ArgumentOutOfRangeException(nameof(count)); } if (!IsThereEnoughVisibleTransactions(this.redoStack, count)) { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Strings.CannotUndoMoreTransactionsThanExist", "redo", count)); + throw new InvalidOperationException("Cannot redo more transactions than exist"); } TextUndoHistoryState originalState = this.state; @@ -424,15 +429,19 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone throw new InvalidOperationException("Strings.EndTransactionOutOfOrder"); } + // Note that the VS undo history actually "pops" the nested undo stack on the Complete/Cancel + // (instead of in the Dispose). This shouldn't affect anything but we should consider adapting + // this code to follow the model in VS undo. + this.currentTransaction = (UndoTransactionImpl)(transaction.Parent); + // only add completed transactions to their parents (or the stack) - if (this.currentTransaction.State == UndoTransactionState.Completed) + if (transaction.State == UndoTransactionState.Completed) { - if (this.currentTransaction.Parent == null) // stack bottomed out! + if (transaction.Parent == null) // stack bottomed out! { - MergeOrPushToUndoStack(this.currentTransaction); + MergeOrPushToUndoStack((UndoTransactionImpl)transaction); } } - this.currentTransaction = this.currentTransaction.Parent as UndoTransactionImpl; } /// <summary> @@ -471,7 +480,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone transactionAdded = transaction; transactionResult = TextUndoTransactionCompletionResult.TransactionAdded; } - RaiseUndoTransactionCompleted(transactionAdded, transactionResult); + RaiseUndoTransactionCompleted(transactionAdded, transactionResult); } public bool ValidTransactionForMarkers(ITextUndoTransaction transaction) diff --git a/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs b/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs index 2e4742d..5aef378 100644 --- a/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs +++ b/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs @@ -18,7 +18,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone internal class UndoHistoryRegistryImpl : ITextUndoHistoryRegistry { #region Private Fields - private Dictionary<ITextUndoHistory, int> histories; + internal Dictionary<ITextUndoHistory, int> histories; private Dictionary<WeakReferenceForDictionaryKey, ITextUndoHistory> weakContextMapping; private Dictionary<object, ITextUndoHistory> strongContextMapping; #endregion // Private Fields @@ -50,7 +50,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (context == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RegisterHistory", "context")); + throw new ArgumentNullException(nameof(context)); } return RegisterHistory(context, false); @@ -66,7 +66,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (context == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RegisterHistory", "context")); + throw new ArgumentNullException(nameof(context)); } ITextUndoHistory result; @@ -118,7 +118,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (context == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "GetHistory", "context")); + throw new ArgumentNullException(nameof(context)); } ITextUndoHistory result; @@ -149,7 +149,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (context == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "TryGetHistory", "context")); + throw new ArgumentNullException(nameof(context)); } ITextUndoHistory result = null; @@ -176,12 +176,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (context == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "context")); + throw new ArgumentNullException(nameof(context)); } if (history == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "history")); + throw new ArgumentNullException(nameof(history)); } AttachHistory(context, history, false); @@ -197,12 +197,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (context == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "context")); + throw new ArgumentNullException(nameof(context)); } if (history == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "history")); + throw new ArgumentNullException(nameof(history)); } if (strongContextMapping.ContainsKey(context) || weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context))) @@ -237,7 +237,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (history == null) { - throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RemoveHistory", "history")); + throw new ArgumentNullException(nameof(history)); } if (!histories.ContainsKey(history)) diff --git a/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs b/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs index aae1d06..7bc7db8 100644 --- a/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs +++ b/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs @@ -17,13 +17,14 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { #region Private Fields - private readonly UndoHistoryImpl history; + private UndoHistoryImpl history; private readonly UndoTransactionImpl parent; private string description; private UndoTransactionState state; private List<ITextUndoPrimitive> primitives; private IMergeTextUndoTransactionPolicy mergePolicy; + internal bool _isDisposed = false; #endregion @@ -31,12 +32,12 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (history == null) { - throw new ArgumentNullException("history", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "UndoTransactionImpl", "history")); + throw new ArgumentNullException(nameof(history)); } - if (String.IsNullOrEmpty(description)) + if (string.IsNullOrEmpty(description)) { - throw new ArgumentNullException("description", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "UndoTransactionImpl", "description")); + throw new ArgumentNullException(nameof(description)); } this.history = history as UndoHistoryImpl; @@ -363,35 +364,44 @@ namespace Microsoft.VisualStudio.Text.Operations.Standalone { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } this.mergePolicy = value; } } - /// <summary> - /// Closes a transaction and disposes it. - /// </summary> +#pragma warning disable CA1063 // Implement IDisposable Correctly + /// <summary> + /// Closes a transaction and disposes it. + /// </summary> public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { - GC.SuppressFinalize(this); - switch (this.State) + if (!_isDisposed) { - case UndoTransactionState.Open: - Cancel(); - break; + _isDisposed = true; - case UndoTransactionState.Canceled: - case UndoTransactionState.Completed: - break; + GC.SuppressFinalize(this); + switch (this.State) + { + case UndoTransactionState.Open: + Cancel(); + break; + + case UndoTransactionState.Canceled: + case UndoTransactionState.Completed: + break; + + case UndoTransactionState.Redoing: + case UndoTransactionState.Undoing: + case UndoTransactionState.Undone: + throw new InvalidOperationException("Strings.ClosingAnOpenTransactionThatAppearsToBeUndoneOrUndoing"); + } - case UndoTransactionState.Redoing: - case UndoTransactionState.Undoing: - case UndoTransactionState.Undone: - throw new InvalidOperationException("Strings.ClosingAnOpenTransactionThatAppearsToBeUndoneOrUndoing"); + this.history.EndTransaction(this); } - history.EndTransaction(this); } + } } diff --git a/src/Text/Impl/TagAggregator/TagAggregator.cs b/src/Text/Impl/TagAggregator/TagAggregator.cs index 2f84741..90005ff 100644 --- a/src/Text/Impl/TagAggregator/TagAggregator.cs +++ b/src/Text/Impl/TagAggregator/TagAggregator.cs @@ -128,7 +128,7 @@ namespace Microsoft.VisualStudio.Text.Tagging.Implementation public IEnumerable<IMappingTagSpan<T>> GetTags(IMappingSpan span) { if (span == null) - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); if (this.disposed) throw new ObjectDisposedException("TagAggregator"); @@ -186,7 +186,7 @@ namespace Microsoft.VisualStudio.Text.Tagging.Implementation public IEnumerable<IMappingTagSpan<T>> GetAllTags(IMappingSpan span, CancellationToken cancel) { if (span == null) - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); if (this.disposed) throw new ObjectDisposedException("TagAggregator"); diff --git a/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs b/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs index 1b743a5..d4a62b3 100644 --- a/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs +++ b/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs @@ -57,7 +57,7 @@ namespace Microsoft.VisualStudio.Text.Tagging.Implementation public ITagAggregator<T> CreateTagAggregator<T>(ITextBuffer textBuffer, TagAggregatorOptions options) where T : ITag { if (textBuffer == null) - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); return new TagAggregator<T>(this, null, this.BufferGraphFactoryService.CreateBufferGraph(textBuffer), options); @@ -75,7 +75,7 @@ namespace Microsoft.VisualStudio.Text.Tagging.Implementation public ITagAggregator<T> CreateTagAggregator<T>(ITextView textView, TagAggregatorOptions options) where T : ITag { if (textView == null) - throw new ArgumentNullException("textView"); + throw new ArgumentNullException(nameof(textView)); return new TagAggregator<T>(this, textView, textView.BufferGraph, options); } diff --git a/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs b/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs index 5d442b7..1f71b36 100644 --- a/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs +++ b/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 +// Runtime Version:2.0.50727.1426 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Strings { @@ -39,8 +39,7 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Implementation.Text.Impl.TextBufferUndoManager.String" + - "s", typeof(Strings).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Implementation.Text.Impl.TextBufferUndoManager.Strings", typeof(Strings).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs index 277c0a0..28b5233 100644 --- a/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs +++ b/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs @@ -8,16 +8,14 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { using System; - using System.Text; + using System.Diagnostics; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Operations; - using System.Collections.Generic; - using System.Diagnostics; - + /// <summary> /// The UndoPrimitive for a text buffer change operation. /// </summary> - public class TextBufferChangeUndoPrimitive : TextUndoPrimitive + public class TextBufferChangeUndoPrimitive : TextUndoPrimitive, IEditOnlyTextUndoPrimitive { #region Private Data Members @@ -26,9 +24,9 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation private readonly ITextUndoHistory _undoHistory; private WeakReference _weakBufferReference; - private readonly INormalizedTextChangeCollection _textChanges; - private int? _beforeVersion; - private int? _afterVersion; + public INormalizedTextChangeCollection Changes { get; } + public int? BeforeReiteratedVersionNumber { get; private set; } + public int? AfterReiteratedVersionNumber { get; private set; } #if DEBUG private int _bufferLengthAfterChange; #endif @@ -52,17 +50,17 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation // Verify input parameters if (undoHistory == null) { - throw new ArgumentNullException("undoHistory"); + throw new ArgumentNullException(nameof(undoHistory)); } if (textVersion == null) { - throw new ArgumentNullException("textVersion"); + throw new ArgumentNullException(nameof(textVersion)); } - _textChanges = textVersion.Changes; - _beforeVersion = textVersion.ReiteratedVersionNumber; - _afterVersion = textVersion.Next.VersionNumber; + this.Changes = textVersion.Changes; + this.BeforeReiteratedVersionNumber = textVersion.ReiteratedVersionNumber; + this.AfterReiteratedVersionNumber = textVersion.Next.VersionNumber; Debug.Assert(textVersion.Next.VersionNumber == textVersion.Next.ReiteratedVersionNumber, "Creating a TextBufferChangeUndoPrimitive for a change that has previously been undone? This is probably wrong."); @@ -124,14 +122,14 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { AttachedToNewBuffer = false; - _beforeVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; - _afterVersion = null; + this.BeforeReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber; + this.AfterReiteratedVersionNumber = null; } bool editCanceled = false; - using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, _afterVersion, typeof(TextBufferChangeUndoPrimitive))) + using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, this.AfterReiteratedVersionNumber, UndoTag.Tag)) { - foreach (ITextChange textChange in _textChanges) + foreach (ITextChange textChange in this.Changes) { if (!edit.Replace(new Span(textChange.OldPosition, textChange.OldLength), textChange.NewText)) { @@ -157,9 +155,9 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation throw new OperationCanceledException("Redo failed due to readonly regions or canceled edit."); } - if (_afterVersion == null) + if (this.AfterReiteratedVersionNumber == null) { - _afterVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; + this.AfterReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber; } #if DEBUG @@ -195,14 +193,14 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { AttachedToNewBuffer = false; - _beforeVersion = null; - _afterVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; + this.BeforeReiteratedVersionNumber = null; + this.AfterReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber; } bool editCanceled = false; - using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, _beforeVersion, typeof(TextBufferChangeUndoPrimitive))) + using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, this.BeforeReiteratedVersionNumber, UndoTag.Tag)) { - foreach (ITextChange textChange in _textChanges) + foreach (ITextChange textChange in this.Changes) { if (!edit.Replace(new Span(textChange.NewPosition, textChange.NewLength), textChange.OldText)) { @@ -228,9 +226,9 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation throw new OperationCanceledException("Undo failed due to readonly regions or canceled edit."); } - if (_beforeVersion == null) + if (this.BeforeReiteratedVersionNumber == null) { - _beforeVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; + this.BeforeReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber; } _canUndo = false; @@ -283,5 +281,10 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation } } #endregion + + internal class UndoTag : IUndoEditTag + { + public static readonly UndoTag Tag = new UndoTag(); + } } } diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs index 77f526c..646a47e 100644 --- a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs +++ b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs @@ -8,159 +8,165 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { using System; - using System.Collections.Generic; - using System.Text; + using System.Diagnostics; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Operations; - using System.Diagnostics; - class TextBufferUndoManager : ITextBufferUndoManager, IDisposable + internal sealed class TextBufferUndoManager : ITextBufferUndoManager, IDisposable { #region Private Members - ITextBuffer _textBuffer; - ITextUndoHistoryRegistry _undoHistoryRegistry; - ITextUndoHistory _undoHistory; - Queue<ITextVersion> _editVersionList = new Queue<ITextVersion>(); - bool _inPostChanged; + private ITextBuffer _textBuffer; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private ITextUndoHistory _undoHistory; + + // The plan had been to add the IUndoMetadataEditTag to allow people to create simple edits + // that would restore carets. That is being pushed back to 16.0 (maybe) but I didn't want to + // abandon the work in progress. +#if false + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; - #endregion + IEditorOperations _initiatingOperations = null; +#endif + ITextUndoTransaction _createdTransaction = null; +#endregion public TextBufferUndoManager(ITextBuffer textBuffer, ITextUndoHistoryRegistry undoHistoryRegistry) { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (undoHistoryRegistry == null) { - throw new ArgumentNullException("undoHistoryRegistry"); + throw new ArgumentNullException(nameof(undoHistoryRegistry)); } _textBuffer = textBuffer; - _undoHistoryRegistry = undoHistoryRegistry; +#if false + if (editorOperationsFactoryService == null) + { + throw new ArgumentNullException(nameof(editorOperationsFactoryService)); + } + + _editorOperationsFactoryService = editorOperationsFactoryService; +#endif + // Register the undo history - _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); + this.EnsureTextBufferUndoHistory(); // Listen for the buffer changed events so that we can make them undo/redo-able + _textBuffer.Changing += TextBufferChanging; _textBuffer.Changed += TextBufferChanged; _textBuffer.PostChanged += TextBufferPostChanged; - _textBuffer.Changing += TextBufferChanging; } - #region Private Methods +#region Private Methods private void TextBufferChanged(object sender, TextContentChangedEventArgs e) { - Debug.Assert((e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive) || - (_undoHistory.State != TextUndoHistoryState.Idle), - "We are undoing/redoing a change while UndoHistory.State is Idle. Something is wrong with the state."); - - // If this change didn't originate from undo, add a TextBufferChangeUndoPrimitive to our history. - if (_undoHistory.State == TextUndoHistoryState.Idle && - (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive)) + if (!(e.EditTag is IUndoEditTag)) { - // With projection, we sometimes get Changed events with no changes, or for "" -> "". - // We don't want to create undo actions for these. - bool nonNullChange = false; - foreach (ITextChange c in e.BeforeVersion.Changes) + if (this.TextBufferUndoHistory.State != TextUndoHistoryState.Idle) { - if (c.OldLength != 0 || c.NewLength != 0) + Debug.Fail("We are doing a normal edit in a non-idle undo state. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code."); + } + else + { + // With projection, we sometimes get Changed events with no changes, or for "" -> "". + // We don't want to create undo actions for these. + bool nonNullChange = false; + foreach (ITextChange c in e.BeforeVersion.Changes) { - nonNullChange = true; - break; + if (c.OldLength != 0 || c.NewLength != 0) + { + nonNullChange = true; + break; + } } - } - if (nonNullChange) - { - // Queue the edit, and actually add an undo primitive later (see comment on PostChanged). - _editVersionList.Enqueue(e.BeforeVersion); + if (nonNullChange) + { + // If there's an open undo transaction, add our edit (turned into a primitive) to it. Otherwise, create and undo transaction. + var currentTransaction = _undoHistory.CurrentTransaction; + if (currentTransaction == null) + { + // TODO remove this + // Hack to allow Cascade's local undo to light up if using v15.7 but behave using the old -- non-local -- undo before if running on 15.6. + // Cascade should really be marking its edits with IInvisibleEditTag (and will once it can take a hard requirement of VS 15.7). + if ((e.EditTag is IInvisibleEditTag) || ((e.EditTag != null) && (string.Equals(e.EditTag.ToString(), "CascadeRemoteEdit", StringComparison.Ordinal)))) + { + _createdTransaction = ((ITextUndoHistory2)_undoHistory).CreateInvisibleTransaction("<invisible>"); + } +#if false + else if (e.EditTag is IUndoMetadataEditTag metadata) + { + _createdTransaction = _undoHistory.CreateTransaction(metadata.Description); + if (_initiatingOperations == null) + { + var view = metadata.InitiatingView; + if (view != null) + { + _initiatingOperations = _editorOperationsFactoryService.GetEditorOperations(view); + _initiatingOperations.AddBeforeTextBufferChangePrimitive(); + } + } + } +#endif + else + { + _createdTransaction = _undoHistory.CreateTransaction(Strings.TextBufferChanged); + } + + currentTransaction = _createdTransaction; + } + + currentTransaction.AddUndo(new TextBufferChangeUndoPrimitive(_undoHistory, e.BeforeVersion)); + } } } } - /// <remarks> - /// Edits are queued up by our TextBufferChanged handler and then we finally add them to the - /// undo stack here in response to PostChanged. The reason and history behind why we do this - /// is as follows: - /// - /// Originally this was done for VB commit, which uses undo events (i.e. TransactionCompleted) to - /// trigger commit. Their commit logic relies on the buffer being in a state such that applying - /// an edit synchronously raises a Changed event (which is always the case for PostChanged, but - /// not for Changed if there are nested edits). - /// - /// JaredPar made a change (CS 1182244) that allowed VB to detect that UndoTransactionCompleted - /// was being fired from a nested edit, and therefore delay the actual commit until the following - /// PostChanged event. - /// - /// So this allowed us to move TextBufferUndoManager back to adding undo actions directly - /// from the TextBufferChanged handler (CS 1285117). This is preferable, as otherwise there's a - /// "delay" between when the edit happens and when we record the edit on the undo stack, - /// allowing other people to stick something on the undo stack (i.e. from - /// their ITextBuffer.Changed handler) in between. The result is actions being "out-of-order" - /// on the undo stack. - /// - /// Unfortunately, it turns out VB snippets actually rely on this "out-of-order" behavior - /// (see Dev10 834740) and so we are forced to revert CS 1285117) and return to the model - /// where we queue up edits and delay adding them to the undo stack until PostChanged. - /// - /// It would be good to revisit this at again, but we would need to work with VB - /// to fix their snippets / undo behavior, and verify that VB commit is also unaffected. - /// </remarks> - private void TextBufferPostChanged(object sender, EventArgs e) + void TextBufferChanging(object sender, TextContentChangingEventArgs e) { - // Only process a top level PostChanged event. Nested events will continue to process TextChange events - // which are added to the queue and will be processed below - if ( _inPostChanged ) - { - return; - } - - _inPostChanged = true; - try + // Note that VB explicitly forces undo edits to happen while the history is idle so we need to allow this here + // by always doing nothing for undo edits). This may be a bug in our code (e.g. not properly cleaning up when + // an undo transaction is cancelled in mid-flight) but changing that will require coordination with Roslyn. + if (!(e.EditTag is IUndoEditTag)) { - // Do not do a foreach loop here. It's perfectly possible, and in fact expected, that the Complete - // method below can trigger a series of events which leads to a nested edit and another - // ITextBuffer::Changed. That event will add to the _editVersionList queue and hence break a - // foreach loop - while ( _editVersionList.Count > 0 ) + if (this.TextBufferUndoHistory.State != TextUndoHistoryState.Idle) { - var cur = _editVersionList.Dequeue(); - using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.TextBufferChanged)) - { - TextBufferChangeUndoPrimitive undoPrimitive = new TextBufferChangeUndoPrimitive(_undoHistory, cur); - undoTransaction.AddUndo(undoPrimitive); - - undoTransaction.Complete(); - } + Debug.Fail("We are doing a normal edit in a non-idle undo state. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code."); + e.Cancel(); } } - finally - { - _editVersionList.Clear(); // Ensure we cleanup state in the face of an exception - _inPostChanged = false; - } } - void TextBufferChanging(object sender, TextContentChangingEventArgs e) + private void TextBufferPostChanged(object sender, EventArgs e) { - // See if somebody (other than us) is trying to edit the buffer during undo/redo. - if (_undoHistory.State != TextUndoHistoryState.Idle && - (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive)) + if (_createdTransaction != null) { - Debug.Fail("Attempt to edit the buffer during undo/redo has been denied. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code."); - e.Cancel(); +#if false + if (_initiatingOperations != null) + { + _initiatingOperations.AddAfterTextBufferChangePrimitive(); + } + + _initiatingOperations = null; +#endif + + _createdTransaction.Complete(); + _createdTransaction.Dispose(); + _createdTransaction = null; } } +#endregion - #endregion - - #region ITextBufferUndoManager Members +#region ITextBufferUndoManager Members public ITextBuffer TextBuffer { @@ -175,31 +181,50 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation // we are robust, always register the undo history. get { - _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); - return _undoHistory; + this.EnsureTextBufferUndoHistory(); + return _undoHistory; } } public void UnregisterUndoHistory() { // Unregister the undo history - _undoHistoryRegistry.RemoveHistory(_undoHistory); + if (_undoHistory != null) + { + _undoHistoryRegistry.RemoveHistory(_undoHistory); + _undoHistory = null; + } } - #endregion +#endregion + + private void EnsureTextBufferUndoHistory() + { + if (_textBuffer == null) + throw new ObjectDisposedException("TextBufferUndoManager"); + + // Note, right now, there is no way for us to know if an ITextUndoHistory + // has been unregistered (ie it can be unregistered by a third party) + // An issue has been logged with the Undo team, but in the mean time, to ensure that + // we are robust, always register the undo history. + _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); + } - #region IDisposable Members +#region IDisposable Members public void Dispose() { - UnregisterUndoHistory(); - _textBuffer.Changed -= TextBufferChanged; - _textBuffer.PostChanged -= TextBufferPostChanged; - _textBuffer.Changing -= TextBufferChanging; + if (_textBuffer != null) + { + _textBuffer.PostChanged -= TextBufferPostChanged; + _textBuffer.Changed -= TextBufferChanged; + _textBuffer.Changing -= TextBufferChanging; + _textBuffer = null; + } GC.SuppressFinalize(this); } - #endregion +#endregion } } diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs index 72eb736..c5a4823 100644 --- a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs +++ b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs @@ -8,18 +8,19 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { using System; - using System.Collections.Generic; + using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text; - using Microsoft.VisualStudio.Text.Projection; using Microsoft.VisualStudio.Text.Operations; - using System.ComponentModel.Composition; [Export(typeof(ITextBufferUndoManagerProvider))] internal sealed class TextBufferUndoManagerProvider : ITextBufferUndoManagerProvider { [Import] internal ITextUndoHistoryRegistry _undoHistoryRegistry { get; set; } - +#if false + [Import] + internal IEditorOperationsFactoryService _editorOperationsFactoryService { get; set; } +#endif /// <summary> /// Provides an <see cref="ITextBufferUndoManager"/> for the given <paramref name="textBuffer"/>. /// </summary> @@ -31,13 +32,16 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation // Validate if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } // See if there was already a TextBufferUndoManager created for the given textBuffer, we only ever want to create one ITextBufferUndoManager cachedBufferUndoManager; if (!textBuffer.Properties.TryGetProperty<ITextBufferUndoManager>(typeof(ITextBufferUndoManager), out cachedBufferUndoManager)) { +#if false + cachedBufferUndoManager = new TextBufferUndoManager(textBuffer, _undoHistoryRegistry, _editorOperationsFactoryService); +#endif cachedBufferUndoManager = new TextBufferUndoManager(textBuffer, _undoHistoryRegistry); textBuffer.Properties.AddProperty(typeof(ITextBufferUndoManager), cachedBufferUndoManager); } @@ -54,7 +58,7 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation // Validate if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } ITextBufferUndoManager cachedBufferUndoManager; diff --git a/src/Text/Impl/TextModel/BaseBuffer.cs b/src/Text/Impl/TextModel/BaseBuffer.cs index 4d4ed82..a318a35 100644 --- a/src/Text/Impl/TextModel/BaseBuffer.cs +++ b/src/Text/Impl/TextModel/BaseBuffer.cs @@ -106,7 +106,9 @@ namespace Microsoft.VisualStudio.Text.Implementation } } +#pragma warning disable CA1063 // Implement IDisposable Correctly public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { if (!this.applied && !this.canceled) { @@ -207,11 +209,11 @@ namespace Microsoft.VisualStudio.Text.Implementation CheckActive(); if (position < 0 || position > this.bufferLength) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } // Check for ReadOnly @@ -233,19 +235,19 @@ namespace Microsoft.VisualStudio.Text.Implementation CheckActive(); if (position < 0 || position > this.bufferLength) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } if (characterBuffer == null) { - throw new ArgumentNullException("characterBuffer"); + throw new ArgumentNullException(nameof(characterBuffer)); } if (startIndex < 0 || startIndex > characterBuffer.Length) { - throw new ArgumentOutOfRangeException("startIndex"); + throw new ArgumentOutOfRangeException(nameof(startIndex)); } if (length < 0 || startIndex + length > characterBuffer.Length) { - throw new ArgumentOutOfRangeException("length"); + throw new ArgumentOutOfRangeException(nameof(length)); } // Check for ReadOnly @@ -267,15 +269,15 @@ namespace Microsoft.VisualStudio.Text.Implementation CheckActive(); if (startPosition < 0 || startPosition > this.bufferLength) { - throw new ArgumentOutOfRangeException("startPosition"); + throw new ArgumentOutOfRangeException(nameof(startPosition)); } if (charsToReplace < 0 || startPosition + charsToReplace > this.bufferLength) { - throw new ArgumentOutOfRangeException("charsToReplace"); + throw new ArgumentOutOfRangeException(nameof(charsToReplace)); } if (replaceWith == null) { - throw new ArgumentNullException("replaceWith"); + throw new ArgumentNullException(nameof(replaceWith)); } // Check for ReadOnly @@ -297,11 +299,11 @@ namespace Microsoft.VisualStudio.Text.Implementation CheckActive(); if (replaceSpan.End > this.bufferLength) { - throw new ArgumentOutOfRangeException("replaceSpan"); + throw new ArgumentOutOfRangeException(nameof(replaceSpan)); } if (replaceWith == null) { - throw new ArgumentNullException("replaceWith"); + throw new ArgumentNullException(nameof(replaceWith)); } // Check for ReadOnly @@ -323,11 +325,11 @@ namespace Microsoft.VisualStudio.Text.Implementation CheckActive(); if (startPosition < 0 || startPosition > this.bufferLength) { - throw new ArgumentOutOfRangeException("startPosition"); + throw new ArgumentOutOfRangeException(nameof(startPosition)); } if (charsToDelete < 0 || startPosition + charsToDelete > this.bufferLength) { - throw new ArgumentOutOfRangeException("charsToDelete"); + throw new ArgumentOutOfRangeException(nameof(charsToDelete)); } // Check for ReadOnly @@ -349,7 +351,7 @@ namespace Microsoft.VisualStudio.Text.Implementation CheckActive(); if (deleteSpan.End > this.bufferLength) { - throw new ArgumentOutOfRangeException("deleteSpan"); + throw new ArgumentOutOfRangeException(nameof(deleteSpan)); } // Check for ReadOnly @@ -716,7 +718,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (newContentType == null) { - throw new ArgumentNullException("newContentType"); + throw new ArgumentNullException(nameof(newContentType)); } if (newContentType != this.contentType) @@ -764,7 +766,7 @@ namespace Microsoft.VisualStudio.Text.Implementation ReadOnlyQueryThreadCheck(); if ((position < 0) || (position > this.currentSnapshot.Length)) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } return IsReadOnlyImplementation(position, isEdit); @@ -780,7 +782,7 @@ namespace Microsoft.VisualStudio.Text.Implementation ReadOnlyQueryThreadCheck(); if (span.End > this.currentSnapshot.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } return IsReadOnlyImplementation(span, isEdit); @@ -809,7 +811,7 @@ namespace Microsoft.VisualStudio.Text.Implementation ReadOnlyQueryThreadCheck(); if (span.End > this.CurrentSnapshot.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } return GetReadOnlyExtentsImplementation(span); } diff --git a/src/Text/Impl/TextModel/BaseSnapshot.cs b/src/Text/Impl/TextModel/BaseSnapshot.cs index db723d3..b099424 100644 --- a/src/Text/Impl/TextModel/BaseSnapshot.cs +++ b/src/Text/Impl/TextModel/BaseSnapshot.cs @@ -9,6 +9,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { using System; using System.Collections.Generic; + using System.Globalization; using System.IO; using System.Text; using Microsoft.VisualStudio.Utilities; @@ -185,9 +186,11 @@ namespace Microsoft.VisualStudio.Text.Implementation public override string ToString() { - return String.Format("version: {0} lines: {1} length: {2} \r\n content: {3}", + return string.Format( + CultureInfo.InvariantCulture, + "version: {0} lines: {1} length: {2} \r\n content: {3}", Version.VersionNumber, LineCount, Length, - Microsoft.VisualStudio.Text.Utilities.TextUtilities.Escape(this.GetText(0, Math.Min(40, this.Length)))); + Utilities.TextUtilities.Escape(this.GetText(0, Math.Min(40, this.Length)))); } #if _DEBUG diff --git a/src/Text/Impl/TextModel/BufferFactoryService.cs b/src/Text/Impl/TextModel/BufferFactoryService.cs index 975cbf0..5ae8b2f 100644 --- a/src/Text/Impl/TextModel/BufferFactoryService.cs +++ b/src/Text/Impl/TextModel/BufferFactoryService.cs @@ -149,7 +149,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } return Make(contentType, StringRebuilder.Empty, false); } @@ -163,7 +163,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } StringRebuilder content = StringRebuilderFromSnapshotSpan(span); @@ -191,11 +191,11 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } return Make(contentType, StringRebuilder.Create(text), spurnGroup); } @@ -204,11 +204,11 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (reader == null) { - throw new ArgumentNullException("reader"); + throw new ArgumentNullException(nameof(reader)); } if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } if (length > int.MaxValue) { @@ -217,7 +217,7 @@ namespace Microsoft.VisualStudio.Text.Implementation bool hasConsistentLineEndings; int longestLineLength; - StringRebuilder content = TextImageLoader.Load(reader, length, traceId, out hasConsistentLineEndings, out longestLineLength); + StringRebuilder content = TextImageLoader.Load(reader, length, out hasConsistentLineEndings, out longestLineLength); ITextBuffer buffer = Make(contentType, content, false); if (!hasConsistentLineEndings) @@ -286,7 +286,7 @@ namespace Microsoft.VisualStudio.Text.Implementation bool hasConsistentLineEndings; int longestLineLength; - return CachingTextImage.Create(TextImageLoader.Load(reader, length, string.Empty, out hasConsistentLineEndings, out longestLineLength), null); + return CachingTextImage.Create(TextImageLoader.Load(reader, length, out hasConsistentLineEndings, out longestLineLength), null); } public ITextImage CreateTextImage(MemoryMappedFile source) @@ -318,11 +318,11 @@ namespace Microsoft.VisualStudio.Text.Implementation // projectionEditResolver is allowed to be null. if (trackingSpans == null) { - throw new ArgumentNullException("trackingSpans"); + throw new ArgumentNullException(nameof(trackingSpans)); } if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } IProjectionBuffer buffer = new ProjectionBuffer(this, projectionEditResolver, contentType, trackingSpans, _differenceService, _textDifferencingSelectorService.DefaultTextDifferencingService, options, _guardedOperations); @@ -337,7 +337,7 @@ namespace Microsoft.VisualStudio.Text.Implementation // projectionEditResolver is allowed to be null. if (trackingSpans == null) { - throw new ArgumentNullException("trackingSpans"); + throw new ArgumentNullException(nameof(trackingSpans)); } IProjectionBuffer buffer = @@ -354,15 +354,15 @@ namespace Microsoft.VisualStudio.Text.Implementation // projectionEditResolver is allowed to be null. if (exposedSpans == null) { - throw new ArgumentNullException("exposedSpans"); + throw new ArgumentNullException(nameof(exposedSpans)); } if (exposedSpans.Count == 0) { - throw new ArgumentOutOfRangeException("exposedSpans"); // really? + throw new ArgumentOutOfRangeException(nameof(exposedSpans)); // really? } if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } if (exposedSpans[0].Snapshot != exposedSpans[0].Snapshot.TextBuffer.CurrentSnapshot) diff --git a/src/Text/Impl/TextModel/EncodedStreamReader.cs b/src/Text/Impl/TextModel/EncodedStreamReader.cs index b11a34b..4e30901 100644 --- a/src/Text/Impl/TextModel/EncodedStreamReader.cs +++ b/src/Text/Impl/TextModel/EncodedStreamReader.cs @@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.Text.Implementation GuardedOperations guardedOperations) { if (stream == null) - throw new ArgumentNullException("stream"); + throw new ArgumentNullException(nameof(stream)); long position = stream.Position; diff --git a/src/Text/Impl/TextModel/FileNameKey.cs b/src/Text/Impl/TextModel/FileNameKey.cs new file mode 100644 index 0000000..3bbd354 --- /dev/null +++ b/src/Text/Impl/TextModel/FileNameKey.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.IO; + sealed class FileNameKey + { + private readonly string _fileName; + private readonly int _hashCode; + + public FileNameKey(string fileName) + { + //Gracefully catch errors getting the full path (which can happen if the file name is on a protected share). + try + { + _fileName = Path.GetFullPath(fileName); + } + catch + { + //This shouldn't happen (we are generally passed names associated with documents that we are expecting to open so + //we should have access). If we fail, we will, at worst not get the same underlying document when people create + //persistent spans using unnormalized names. + _fileName = fileName; + } + + _hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(_fileName); + } + + //Override equality and hash code + public override int GetHashCode() + { + return _hashCode; + } + + public override bool Equals(object obj) + { + var other = obj as FileNameKey; + return (other != null) && string.Equals(_fileName, other._fileName, StringComparison.OrdinalIgnoreCase); + } + + public override string ToString() + { + return _fileName; + } + } +} diff --git a/src/Text/Impl/TextModel/FileUtilities.cs b/src/Text/Impl/TextModel/FileUtilities.cs index 2c567c3..b6fbd82 100644 --- a/src/Text/Impl/TextModel/FileUtilities.cs +++ b/src/Text/Impl/TextModel/FileUtilities.cs @@ -185,7 +185,7 @@ namespace Microsoft.VisualStudio.Text.Implementation if (!(safeHandle.IsClosed || safeHandle.IsInvalid)) { BY_HANDLE_FILE_INFORMATION fi; - if (GetFileInformationByHandle(safeHandle, out fi)) + if (NativeMethods.GetFileInformationByHandle(safeHandle, out fi)) { if (fi.NumberOfLinks <= 1) { @@ -232,26 +232,5 @@ namespace Microsoft.VisualStudio.Text.Implementation temporaryPath = null; return new FileStream(filePath, fileMode, FileAccess.Write, FileShare.Read); } - - [StructLayout(LayoutKind.Sequential)] - struct BY_HANDLE_FILE_INFORMATION - { - public uint FileAttributes; - public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime; - public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime; - public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime; - public uint VolumeSerialNumber; - public uint FileSizeHigh; - public uint FileSizeLow; - public uint NumberOfLinks; - public uint FileIndexHigh; - public uint FileIndexLow; - } - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool GetFileInformationByHandle( - Microsoft.Win32.SafeHandles.SafeFileHandle hFile, - out BY_HANDLE_FILE_INFORMATION lpFileInformation - ); } -}
\ No newline at end of file +} diff --git a/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs b/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs index 4b8f0c2..938be92 100644 --- a/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs +++ b/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (behavior == null) { - throw new ArgumentNullException("behavior"); + throw new ArgumentNullException(nameof(behavior)); } this.behavior = behavior; this.customState = customState; diff --git a/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs b/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs index db784fb..5301a95 100644 --- a/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs +++ b/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs @@ -44,7 +44,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (fidelity != TrackingFidelityMode.UndoRedo && fidelity != TrackingFidelityMode.Backward) { - throw new ArgumentOutOfRangeException("fidelity"); + throw new ArgumentOutOfRangeException(nameof(fidelity)); } List<VersionNumberPosition> initialHistory = null; if (fidelity == TrackingFidelityMode.UndoRedo && version.VersionNumber > 0) diff --git a/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs b/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs index 6eb6ca4..e889804 100644 --- a/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs +++ b/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs @@ -50,7 +50,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (fidelity != TrackingFidelityMode.UndoRedo && fidelity != TrackingFidelityMode.Backward) { - throw new ArgumentOutOfRangeException("fidelity"); + throw new ArgumentOutOfRangeException(nameof(fidelity)); } List<VersionNumberPosition> startHistory = null; List<VersionNumberPosition> endHistory = null; diff --git a/src/Text/Impl/TextModel/MappingPoint.cs b/src/Text/Impl/TextModel/MappingPoint.cs index 46e34ec..2497607 100644 --- a/src/Text/Impl/TextModel/MappingPoint.cs +++ b/src/Text/Impl/TextModel/MappingPoint.cs @@ -20,15 +20,15 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (anchorPoint.Snapshot == null) { - throw new ArgumentNullException("anchorPoint"); + throw new ArgumentNullException(nameof(anchorPoint)); } if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (bufferGraph == null) { - throw new ArgumentNullException("bufferGraph"); + throw new ArgumentNullException(nameof(bufferGraph)); } this.anchorPoint = anchorPoint; this.trackingMode = trackingMode; @@ -49,7 +49,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (targetBuffer == null) { - throw new ArgumentNullException("targetBuffer"); + throw new ArgumentNullException(nameof(targetBuffer)); } ITextBuffer anchorBuffer = this.AnchorBuffer; SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); @@ -86,7 +86,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public SnapshotPoint? GetPoint(ITextSnapshot targetSnapshot, PositionAffinity affinity) { if (targetSnapshot == null) - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); SnapshotPoint? result = GetPoint(targetSnapshot.TextBuffer, affinity); if (result.HasValue && (result.Value.Snapshot != targetSnapshot)) @@ -101,7 +101,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } ITextBuffer anchorBuffer = this.AnchorBuffer; SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); @@ -143,7 +143,7 @@ namespace Microsoft.VisualStudio.Text.Implementation // always maps down if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } ITextBuffer anchorBuffer = this.AnchorBuffer; SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); diff --git a/src/Text/Impl/TextModel/MappingSpan.cs b/src/Text/Impl/TextModel/MappingSpan.cs index a42f0fe..741972c 100644 --- a/src/Text/Impl/TextModel/MappingSpan.cs +++ b/src/Text/Impl/TextModel/MappingSpan.cs @@ -9,6 +9,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { using System; using System.Collections.Generic; + using System.Globalization; using Microsoft.VisualStudio.Text.Projection; using Microsoft.VisualStudio.Text.Utilities; @@ -22,15 +23,15 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (anchorSpan.Snapshot == null) { - throw new ArgumentNullException("anchorSpan"); + throw new ArgumentNullException(nameof(anchorSpan)); } if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (bufferGraph == null) { - throw new ArgumentNullException("bufferGraph"); + throw new ArgumentNullException(nameof(bufferGraph)); } this.anchorSpan = anchorSpan; this.trackingMode = trackingMode; @@ -105,7 +106,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public NormalizedSnapshotSpanCollection GetSpans(ITextSnapshot targetSnapshot) { if (targetSnapshot == null) - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); NormalizedSnapshotSpanCollection results = GetSpans(targetSnapshot.TextBuffer); if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot)) @@ -126,7 +127,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } ITextBuffer anchorBuffer = this.AnchorBuffer; @@ -156,7 +157,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override string ToString() { - return String.Format("MappingSpan anchored at {0}", this.anchorSpan); + return String.Format(CultureInfo.CurrentCulture, "MappingSpan anchored at {0}", this.anchorSpan); } } -}
\ No newline at end of file +} diff --git a/src/Text/Impl/TextModel/NativeMethods.cs b/src/Text/Impl/TextModel/NativeMethods.cs new file mode 100644 index 0000000..78d507b --- /dev/null +++ b/src/Text/Impl/TextModel/NativeMethods.cs @@ -0,0 +1,28 @@ +using System.Runtime.InteropServices; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + [StructLayout(LayoutKind.Sequential)] + internal struct BY_HANDLE_FILE_INFORMATION + { + public uint FileAttributes; + public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime; + public uint VolumeSerialNumber; + public uint FileSizeHigh; + public uint FileSizeLow; + public uint NumberOfLinks; + public uint FileIndexHigh; + public uint FileIndexLow; + } + + internal static class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool GetFileInformationByHandle( + Microsoft.Win32.SafeHandles.SafeFileHandle hFile, + out BY_HANDLE_FILE_INFORMATION lpFileInformation + ); + } +} diff --git a/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs b/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs index 8e3352f..5a9e03b 100644 --- a/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs +++ b/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs @@ -16,7 +16,7 @@ namespace Microsoft.VisualStudio.Text.Implementation internal partial class NormalizedTextChangeCollection : INormalizedTextChangeCollection { - public static readonly NormalizedTextChangeCollection Empty = new NormalizedTextChangeCollection(new TextChange[0]); + public static readonly NormalizedTextChangeCollection Empty = new NormalizedTextChangeCollection(Array.Empty<TextChange>()); private readonly IReadOnlyList<TextChange> _changes; public static INormalizedTextChangeCollection Create(IReadOnlyList<TextChange> changes) @@ -36,7 +36,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (changes == null) { - throw new ArgumentNullException("changes"); + throw new ArgumentNullException(nameof(changes)); } if (changes.Count == 0) @@ -122,7 +122,7 @@ namespace Microsoft.VisualStudio.Text.Implementation } else if (changes.Count == 0) { - return new TextChange[0]; + return Array.Empty<TextChange>(); } TextChange[] work = TextUtilities.StableSort(changes, TextChange.Compare); @@ -215,10 +215,7 @@ namespace Microsoft.VisualStudio.Text.Implementation if (differenceOptions.HasValue) { - if (textDifferencingService == null) - { - throw new ArgumentNullException("stringDifferenceUtility"); - } + Requires.NotNull(textDifferencingService, nameof(textDifferencingService)); foreach (TextChange change in work) { if (change == null) continue; @@ -259,7 +256,7 @@ namespace Microsoft.VisualStudio.Text.Implementation string oldText = change.OldText; string newText = change.NewText; - if (oldText == newText) + if (string.Equals(oldText, newText, StringComparison.Ordinal)) { // This change simply evaporates. This case occurs frequently in Venus and it is much // better to short circuit it here than to fire up the differencing engine. diff --git a/src/Text/Impl/TextModel/PersistentSpan.cs b/src/Text/Impl/TextModel/PersistentSpan.cs index 79f205d..6ec563a 100644 --- a/src/Text/Impl/TextModel/PersistentSpan.cs +++ b/src/Text/Impl/TextModel/PersistentSpan.cs @@ -7,45 +7,42 @@ // namespace Microsoft.VisualStudio.Text.Implementation { - using Microsoft.VisualStudio.Text; - using Microsoft.VisualStudio.Utilities; using System; - using System.Diagnostics; internal sealed class PersistentSpan : IPersistentSpan { #region members - private PersistentSpanFactory _factory; + public PersistentSpanSet SpanSet; private ITrackingSpan _span; //null for spans on closed documents or disposed spans - private ITextDocument _document; //null for spans on closed documents or disposed spans - private string _filePath; //null for spans on opened documents or disposed spans private int _startLine; //these parameters are valid whether or not the document is open (but _start*,_end* may be stale). private int _startIndex; private int _endLine; private int _endIndex; - private Span _nonTrackingSpan; + private ITextVersion _originalVersion = null; + private Span _originalSpan; // This is either the span when this was created or when the document was reopened. + // It is default(Span) if either we were created (on an unopened document) with line/column indices or after the document was closed. private bool _useLineIndex; private readonly SpanTrackingMode _trackingMode; #endregion - internal PersistentSpan(ITextDocument document, SnapshotSpan span, SpanTrackingMode trackingMode, PersistentSpanFactory factory) + internal PersistentSpan(SnapshotSpan span, SpanTrackingMode trackingMode, PersistentSpanSet spanSet) { - //Arguments verified in factory - _document = document; - _span = span.Snapshot.CreateTrackingSpan(span, trackingMode); - _trackingMode = trackingMode; - _factory = factory; + _originalVersion = span.Snapshot.Version; + _originalSpan = span; + + PersistentSpan.SnapshotPointToLineIndex(span.Start, out _startLine, out _startIndex); + PersistentSpan.SnapshotPointToLineIndex(span.End, out _endLine, out _endIndex); + + _trackingMode = trackingMode; + this.SpanSet = spanSet; } - internal PersistentSpan(string filePath, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode, PersistentSpanFactory factory) + internal PersistentSpan(int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode, PersistentSpanSet spanSet) { - //Arguments verified in factory - _filePath = filePath; - _useLineIndex = true; _startLine = startLine; _startIndex = startIndex; @@ -53,27 +50,22 @@ namespace Microsoft.VisualStudio.Text.Implementation _endIndex = endIndex; _trackingMode = trackingMode; - - _factory = factory; + this.SpanSet = spanSet; } - internal PersistentSpan(string filePath, Span span, SpanTrackingMode trackingMode, PersistentSpanFactory factory) + internal PersistentSpan(Span span, SpanTrackingMode trackingMode, PersistentSpanSet spanSet) { - //Arguments verified in factory - _filePath = filePath; - _useLineIndex = false; - _nonTrackingSpan = span; + _originalSpan = span; _trackingMode = trackingMode; - - _factory = factory; + this.SpanSet = spanSet; } #region IPersistentSpan members - public bool IsDocumentOpen { get { return _document != null; } } + public bool IsDocumentOpen { get { return this.SpanSet.Document != null; } } - public ITextDocument Document { get { return _document; } } + public ITextDocument Document { get { return this.SpanSet.Document; } } public ITrackingSpan Span { get { return _span; } } @@ -81,84 +73,129 @@ namespace Microsoft.VisualStudio.Text.Implementation { get { - return (_document != null) ? _document.FilePath : _filePath; + if (this.SpanSet == null) + throw new ObjectDisposedException("PersistentSpan"); + + return (this.SpanSet.Document != null) ? this.SpanSet.Document.FilePath : this.SpanSet.FileKey.ToString(); } } public bool TryGetStartLineIndex(out int startLine, out int startIndex) { - if ((_document == null) && (_filePath == null)) + if (this.SpanSet == null) throw new ObjectDisposedException("PersistentSpan"); if (_span != null) - this.UpdateStartEnd(); - - startLine = _startLine; - startIndex = _startIndex; + { + SnapshotSpan span = _span.GetSpan(_span.TextBuffer.CurrentSnapshot); + PersistentSpan.SnapshotPointToLineIndex(span.Start, out startLine, out startIndex); + return true; + } + else if (_useLineIndex) + { + startLine = _startLine; + startIndex = _startIndex; + return true; + } - return ((_span != null) || _useLineIndex); + startLine = startIndex = 0; + return false; } public bool TryGetEndLineIndex(out int endLine, out int endIndex) { - if ((_document == null) && (_filePath == null)) + if (this.SpanSet == null) throw new ObjectDisposedException("PersistentSpan"); if (_span != null) - this.UpdateStartEnd(); + { + SnapshotSpan span = _span.GetSpan(_span.TextBuffer.CurrentSnapshot); + PersistentSpan.SnapshotPointToLineIndex(span.End, out endLine, out endIndex); + return true; + } + else if (_useLineIndex) + { + endLine = _endLine; + endIndex = _endIndex; + return true; + } - endLine = _endLine; - endIndex = _endIndex; - return ((_span != null) || _useLineIndex); + endLine = endIndex = 0; + return false; } public bool TryGetSpan(out Span span) { - if ((_document == null) && (_filePath == null)) + if (this.SpanSet == null) throw new ObjectDisposedException("PersistentSpan"); if (_span != null) - this.UpdateStartEnd(); + { + span = _span.GetSpan(_span.TextBuffer.CurrentSnapshot); + return true; + } + else if (!_useLineIndex) + { + span = _originalSpan; + return true; + } - span = _nonTrackingSpan; - return ((_span != null) || !_useLineIndex); + span = new Span(); + return false; } #endregion #region IDisposable members public void Dispose() { - if ((_document != null) || (_filePath != null)) + if (this.SpanSet != null) { - _factory.Delete(this); - + this.SpanSet.Delete(this); + this.SpanSet = null; + _originalVersion = null; _span = null; - _document = null; - _filePath = null; } } #endregion - #region private helpers - internal void DocumentClosed() + internal void SetSpanSet(PersistentSpanSet spanSet) { - this.UpdateStartEnd(); + if (this.SpanSet == null) + throw new ObjectDisposedException("PersistentSpan"); + + this.SpanSet = spanSet; + } + + internal void DocumentClosed(ITextSnapshot savedSnapshot) + { + Assumes.NotNull(_originalVersion); + + if ((savedSnapshot != null) && (savedSnapshot.Version.VersionNumber > _originalVersion.VersionNumber)) + { + // The document was saved and we want to line/column indices in the saved snapshot (& not the current snapshot) + var savedSpan = new SnapshotSpan(savedSnapshot, Tracking.TrackSpanForwardInTime(_trackingMode, _originalSpan, _originalVersion, savedSnapshot.Version)); + + PersistentSpan.SnapshotPointToLineIndex(savedSpan.Start, out _startLine, out _startIndex); + PersistentSpan.SnapshotPointToLineIndex(savedSpan.End, out _endLine, out _endIndex); + } + else + { + // The document was never saved (or was saved before we created) so continue to use the old line/column indices. + // Since those are set when either the span is created (against an open document) or when the document is reopened, + // they don't need to be changed. + } //We set this to false when the document is closed because we have an accurate line/index and that is more stable //than a simple offset. _useLineIndex = true; - _nonTrackingSpan = new Span(0, 0); - - _filePath = _document.FilePath; - _document = null; + _originalSpan = default(Span); + _originalVersion = null; _span = null; } - internal void DocumentReopened(ITextDocument document) + internal void DocumentReopened() { - _document = document; - - ITextSnapshot snapshot = document.TextBuffer.CurrentSnapshot; + ITextSnapshot snapshot = this.SpanSet.Document.TextBuffer.CurrentSnapshot; SnapshotPoint start; SnapshotPoint end; @@ -178,23 +215,27 @@ namespace Microsoft.VisualStudio.Text.Implementation } else { - start = new SnapshotPoint(snapshot, Math.Min(_nonTrackingSpan.Start, snapshot.Length)); - end = new SnapshotPoint(snapshot, Math.Min(_nonTrackingSpan.End, snapshot.Length)); + start = new SnapshotPoint(snapshot, Math.Min(_originalSpan.Start, snapshot.Length)); + end = new SnapshotPoint(snapshot, Math.Min(_originalSpan.End, snapshot.Length)); } - _span = snapshot.CreateTrackingSpan(new SnapshotSpan(start, end), _trackingMode); + var snapshotSpan = new SnapshotSpan(start, end); + _span = snapshot.CreateTrackingSpan(snapshotSpan, _trackingMode); + _originalSpan = snapshotSpan; - _filePath = null; + _originalVersion = snapshot.Version; + PersistentSpan.SnapshotPointToLineIndex(snapshotSpan.Start, out _startLine, out _startIndex); + PersistentSpan.SnapshotPointToLineIndex(snapshotSpan.End, out _endLine, out _endIndex); } - private void UpdateStartEnd() + private SnapshotSpan UpdateStartEnd() { SnapshotSpan span = _span.GetSpan(_span.TextBuffer.CurrentSnapshot); - _nonTrackingSpan = span; - PersistentSpan.SnapshotPointToLineIndex(span.Start, out _startLine, out _startIndex); PersistentSpan.SnapshotPointToLineIndex(span.End, out _endLine, out _endIndex); + + return span; } private static void SnapshotPointToLineIndex(SnapshotPoint p, out int line, out int index) @@ -207,10 +248,13 @@ namespace Microsoft.VisualStudio.Text.Implementation internal static SnapshotPoint LineIndexToSnapshotPoint(int line, int index, ITextSnapshot snapshot) { - ITextSnapshotLine l = snapshot.GetLineFromLineNumber(Math.Min(line, snapshot.LineCount - 1)); + if (line >= snapshot.LineCount) + { + return new SnapshotPoint(snapshot, snapshot.Length); + } + ITextSnapshotLine l = snapshot.GetLineFromLineNumber(line); return l.Start + Math.Min(index, l.Length); } - #endregion } -}
\ No newline at end of file +} diff --git a/src/Text/Impl/TextModel/PersistentSpanFactory.cs b/src/Text/Impl/TextModel/PersistentSpanFactory.cs index 6ea3a3d..7e62ae1 100644 --- a/src/Text/Impl/TextModel/PersistentSpanFactory.cs +++ b/src/Text/Impl/TextModel/PersistentSpanFactory.cs @@ -7,14 +7,8 @@ // namespace Microsoft.VisualStudio.Text.Implementation { - using Microsoft.VisualStudio.Text; - using Microsoft.VisualStudio.Utilities; - using Microsoft.VisualStudio.Text.Utilities; - using System; using System.Collections.Generic; using System.ComponentModel.Composition; - using System.Diagnostics; - using System.IO; [Export(typeof(IPersistentSpanFactory))] internal class PersistentSpanFactory : IPersistentSpanFactory @@ -22,17 +16,13 @@ namespace Microsoft.VisualStudio.Text.Implementation [Import] internal ITextDocumentFactoryService TextDocumentFactoryService; - private readonly Dictionary<object, FrugalList<PersistentSpan>> _spansOnDocuments = new Dictionary<object, FrugalList<PersistentSpan>>(); //Used for lock - + private readonly Dictionary<object, PersistentSpanSet> _spansOnDocuments = new Dictionary<object, PersistentSpanSet>(); //Used for lock private bool _eventsHooked; #region IPersistentSpanFactory members public bool CanCreate(ITextBuffer buffer) { - if (buffer == null) - { - throw new ArgumentNullException("buffer"); - } + Requires.NotNull(buffer, nameof(buffer)); ITextDocument document; return this.TextDocumentFactoryService.TryGetTextDocument(buffer, out document); @@ -40,14 +30,16 @@ namespace Microsoft.VisualStudio.Text.Implementation public IPersistentSpan Create(SnapshotSpan span, SpanTrackingMode trackingMode) { + Requires.NotNull(span.Snapshot, nameof(span.Snapshot)); + ITextDocument document; if (this.TextDocumentFactoryService.TryGetTextDocument(span.Snapshot.TextBuffer, out document)) { - PersistentSpan persistentSpan = new PersistentSpan(document, span, trackingMode, this); - - this.AddSpan(document, persistentSpan); - - return persistentSpan; + lock (_spansOnDocuments) + { + var spanSet = this.GetOrCreateSpanSet(null, document); + return spanSet.Create(span, trackingMode); + } } return null; @@ -55,21 +47,21 @@ namespace Microsoft.VisualStudio.Text.Implementation public IPersistentSpan Create(ITextSnapshot snapshot, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode) { + Requires.NotNull(snapshot, nameof(snapshot)); + Requires.Argument(startLine >= 0, nameof(startLine), "Must be non-negative."); + Requires.Argument(startIndex >= 0, nameof(startIndex), "Must be non-negative."); + Requires.Argument(endLine >= startLine, nameof(endLine), "Must be >= startLine."); + Requires.Argument((endIndex >= 0) && ((startLine != endLine) || (endIndex >= startIndex)), nameof(endIndex), "Must be non-negative and (endLine,endIndex) may not be before (startLine,startIndex)."); + Requires.Range(((int)trackingMode >= (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode <= (int)(SpanTrackingMode.EdgeNegative)), nameof(trackingMode)); + ITextDocument document; if (this.TextDocumentFactoryService.TryGetTextDocument(snapshot.TextBuffer, out document)) { - var start = PersistentSpan.LineIndexToSnapshotPoint(startLine, startIndex, snapshot); - var end = PersistentSpan.LineIndexToSnapshotPoint(endLine, endIndex, snapshot); - if (end < start) + lock (_spansOnDocuments) { - end = start; + var spanSet = this.GetOrCreateSpanSet(null, document); + return spanSet.Create(snapshot, startLine, startIndex, endLine, endIndex, trackingMode); } - - PersistentSpan persistentSpan = new PersistentSpan(document, new SnapshotSpan(start, end), trackingMode, this); - - this.AddSpan(document, persistentSpan); - - return persistentSpan; } return null; @@ -77,218 +69,134 @@ namespace Microsoft.VisualStudio.Text.Implementation public IPersistentSpan Create(string filePath, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode) { - if (string.IsNullOrEmpty(filePath)) - { - throw new ArgumentException("filePath"); - } - if (startLine < 0) - { - throw new ArgumentOutOfRangeException("startLine", "Must be non-negative."); - } - if (startIndex < 0) - { - throw new ArgumentOutOfRangeException("startIndex", "Must be non-negative."); - } - if (endLine < startLine) - { - throw new ArgumentOutOfRangeException("endLine", "Must be >= startLine."); - } - if ((endIndex < 0) || ((startLine == endLine) && (endIndex < startIndex))) - { - throw new ArgumentOutOfRangeException("endIndex", "Must be non-negative and (endLine,endIndex) may not be before (startLine,startIndex)."); - } - if (((int)trackingMode < (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode > (int)(SpanTrackingMode.EdgeNegative))) + Requires.NotNullOrEmpty(filePath, nameof(filePath)); + Requires.Argument(startLine >= 0, nameof(startLine), "Must be non-negative."); + Requires.Argument(startIndex >= 0, nameof(startIndex), "Must be non-negative."); + Requires.Argument(endLine >= startLine, nameof(endLine), "Must be >= startLine."); + Requires.Argument((endIndex >= 0) && ((startLine != endLine) || (endIndex >= startIndex)), nameof(endIndex), "Must be non-negative and (endLine,endIndex) may not be before (startLine,startIndex)."); + Requires.Range(((int)trackingMode >= (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode <= (int)(SpanTrackingMode.EdgeNegative)), nameof(trackingMode)); + + var key = new FileNameKey(filePath); + lock (_spansOnDocuments) { - throw new ArgumentOutOfRangeException("trackingMode"); + var spanSet = this.GetOrCreateSpanSet(key, null); + return spanSet.Create(startLine, startIndex, endLine, endIndex, trackingMode); } - - PersistentSpan persistentSpan = new PersistentSpan(filePath, startLine, startIndex, endLine, endIndex, trackingMode, this); - - this.AddSpan(new FileNameKey(filePath), persistentSpan); - - return persistentSpan; } public IPersistentSpan Create(string filePath, Span span, SpanTrackingMode trackingMode) { - if (string.IsNullOrEmpty(filePath)) - { - throw new ArgumentException("filePath"); - } - if (((int)trackingMode < (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode > (int)(SpanTrackingMode.EdgeNegative))) + Requires.NotNullOrEmpty(filePath, nameof(filePath)); + Requires.Range(((int)trackingMode >= (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode <= (int)(SpanTrackingMode.EdgeNegative)), nameof(trackingMode)); + + var key = new FileNameKey(filePath); + lock (_spansOnDocuments) { - throw new ArgumentOutOfRangeException("trackingMode"); + var spanSet = this.GetOrCreateSpanSet(key, null); + return spanSet.Create(span, trackingMode); } - - PersistentSpan persistentSpan = new PersistentSpan(filePath, span, trackingMode, this); - - this.AddSpan(new FileNameKey(filePath), persistentSpan); - - return persistentSpan; } #endregion - internal bool IsEmpty { get { return _spansOnDocuments.Count == 0; } } //For unit tests + internal bool IsEmpty { get { return _spansOnDocuments.Count == 0; } } //For unit tests - private void AddSpan(object key, PersistentSpan persistentSpan) + private PersistentSpanSet GetOrCreateSpanSet(FileNameKey filePath, ITextDocument document) { - lock (_spansOnDocuments) + object key = ((object)document) ?? filePath; + if (!_spansOnDocuments.TryGetValue(key, out PersistentSpanSet spanSet)) { - FrugalList<PersistentSpan> spans; - if (!_spansOnDocuments.TryGetValue(key, out spans)) + if (!_eventsHooked) { - this.EnsureEventsHooked(); + _eventsHooked = true; - spans = new FrugalList<PersistentSpan>(); - _spansOnDocuments.Add(key, spans); + this.TextDocumentFactoryService.TextDocumentCreated += OnTextDocumentCreated; + this.TextDocumentFactoryService.TextDocumentDisposed += OnTextDocumentDisposed; } - spans.Add(persistentSpan); + spanSet = new PersistentSpanSet(filePath, document, this); + _spansOnDocuments.Add(key, spanSet); } - } - - private void EnsureEventsHooked() - { - if (!_eventsHooked) - { - _eventsHooked = true; - this.TextDocumentFactoryService.TextDocumentCreated += OnTextDocumentCreated; - this.TextDocumentFactoryService.TextDocumentDisposed += OnTextDocumentDisposed; - } + return spanSet; } private void OnTextDocumentCreated(object sender, TextDocumentEventArgs e) { var path = new FileNameKey(e.TextDocument.FilePath); - FrugalList<PersistentSpan> spans; lock (_spansOnDocuments) { - if (_spansOnDocuments.TryGetValue(path, out spans)) + if (_spansOnDocuments.TryGetValue(path, out PersistentSpanSet spanSet)) { - foreach (var span in spans) - { - span.DocumentReopened(e.TextDocument); - } + spanSet.DocumentReopened(e.TextDocument); _spansOnDocuments.Remove(path); - _spansOnDocuments.Add(e.TextDocument, spans); + _spansOnDocuments.Add(e.TextDocument, spanSet); } } } private void OnTextDocumentDisposed(object sender, TextDocumentEventArgs e) { - FrugalList<PersistentSpan> spans; lock (_spansOnDocuments) { - if (_spansOnDocuments.TryGetValue(e.TextDocument, out spans)) + if (_spansOnDocuments.TryGetValue(e.TextDocument, out PersistentSpanSet spanSet)) { - foreach (var span in spans) - { - span.DocumentClosed(); - } - + spanSet.DocumentClosed(); _spansOnDocuments.Remove(e.TextDocument); - var path = new FileNameKey(e.TextDocument.FilePath); - FrugalList<PersistentSpan> existingSpansOnPath; - if (_spansOnDocuments.TryGetValue(path, out existingSpansOnPath)) + if (_spansOnDocuments.TryGetValue(spanSet.FileKey, out PersistentSpanSet existingSpansOnPath)) { - //Handle (badly) the case where a document is renamed to an existing closed document & then closed. - existingSpansOnPath.AddRange(spans); + // Handle (badly) the case where a document is renamed to an existing closed document & then closed. + // We should only end up in this case if we had spans on two open documents that were both renamed + // to the same file name & then closed. + foreach (var s in spanSet.Spans) + { + s.SetSpanSet(existingSpansOnPath); + existingSpansOnPath.Spans.Add(s); + } + + spanSet.Spans.Clear(); + spanSet.Dispose(); } else { - _spansOnDocuments.Add(path, spans); + _spansOnDocuments.Add(spanSet.FileKey, spanSet); } } } } - internal void Delete(PersistentSpan span) + internal void DocumentRenamed(PersistentSpanSet spanSet) { lock (_spansOnDocuments) { - ITextDocument document = span.Document; - if (document != null) + if (_spansOnDocuments.TryGetValue(spanSet.FileKey, out PersistentSpanSet existingSpansOnPath)) { - FrugalList<PersistentSpan> spans; - if (_spansOnDocuments.TryGetValue(document, out spans)) + // There were spans on a closed document with the same name as this one. Move all of those spans to this one + // and "open" them (note that this will probably do bad things to their positions but it is the best we + // can do). + foreach (var s in existingSpansOnPath.Spans) { - spans.Remove(span); + s.SetSpanSet(spanSet); + spanSet.Spans.Add(s); - if (spans.Count == 0) - { - //Last one ... remove all references to document. - _spansOnDocuments.Remove(document); - } + s.DocumentReopened(); } - else - { - Debug.Fail("There should have been an entry in SpanOnDocuments."); - } - } - else - { - var path = new FileNameKey(span.FilePath); - FrugalList<PersistentSpan> spans; - if (_spansOnDocuments.TryGetValue(path, out spans)) - { - spans.Remove(span); - if (spans.Count == 0) - { - //Last one ... remove all references to path. - _spansOnDocuments.Remove(path); - } - } - else - { - Debug.Fail("There should have been an entry in SpanOnDocuments."); - } + existingSpansOnPath.Spans.Clear(); + existingSpansOnPath.Dispose(); } } } - - private class FileNameKey + internal void Delete(PersistentSpanSet spanSet, PersistentSpan span) { - private readonly string _fileName; - private readonly int _hashCode; - - public FileNameKey(string fileName) + lock (_spansOnDocuments) { - //Gracefully catch errors getting the full path (which can happen if the file name is on a protected share). - try + if (spanSet.Spans.Remove(span) && (spanSet.Spans.Count == 0)) { - _fileName = Path.GetFullPath(fileName); + _spansOnDocuments.Remove(((object)(spanSet.Document)) ?? spanSet.FileKey); + spanSet.Dispose(); } - catch - { - //This shouldn't happen (we are generally passed names associated with documents that we are expecting to open so - //we should have access). If we fail, we will, at worst not get the same underlying document when people create - //persistent spans using unnormalized names. - _fileName = fileName; - } - - _hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(_fileName); - } - - //Override equality and hash code - public override int GetHashCode() - { - return _hashCode; - } - - public override bool Equals(object obj) - { - var other = obj as FileNameKey; - return (other != null) && string.Equals(_fileName, other._fileName, StringComparison.OrdinalIgnoreCase); - } - - public override string ToString() - { - return _fileName; } } } diff --git a/src/Text/Impl/TextModel/PersistentSpanSet.cs b/src/Text/Impl/TextModel/PersistentSpanSet.cs new file mode 100644 index 0000000..8370eeb --- /dev/null +++ b/src/Text/Impl/TextModel/PersistentSpanSet.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + + sealed class PersistentSpanSet : IDisposable + { + internal FileNameKey FileKey; + internal ITextDocument Document; + internal readonly HashSet<PersistentSpan> Spans = new HashSet<PersistentSpan>(); + private readonly PersistentSpanFactory Factory; + + private ITextSnapshot _savedSnapshot = null; + + internal PersistentSpanSet(FileNameKey filePath, ITextDocument document, PersistentSpanFactory factory) + { + this.FileKey = filePath; + this.Document = document; + this.Factory = factory; + + if (document != null) + { + document.FileActionOccurred += this.OnFileActionOccurred; + } + } + + public void Dispose() + { + Assumes.True(this.Spans.Count == 0); + + if (this.Document != null) + { + this.Document.FileActionOccurred -= this.OnFileActionOccurred; + this.Document = null; + } + } + + internal PersistentSpan Create(int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode) + { + PersistentSpan persistentSpan = new PersistentSpan(startLine, startIndex, endLine, endIndex, trackingMode, this); + this.Spans.Add(persistentSpan); + return persistentSpan; + } + + internal PersistentSpan Create(Span span, SpanTrackingMode trackingMode) + { + var persistentSpan = new PersistentSpan(span, trackingMode, this); + this.Spans.Add(persistentSpan); + return persistentSpan; + } + + internal PersistentSpan Create(ITextSnapshot snapshot, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode) + { + var start = PersistentSpan.LineIndexToSnapshotPoint(startLine, startIndex, snapshot); + var end = PersistentSpan.LineIndexToSnapshotPoint(endLine, endIndex, snapshot); + if (end < start) + { + end = start; + } + + return this.Create(new SnapshotSpan(start, end), trackingMode); + } + + internal PersistentSpan Create(SnapshotSpan span, SpanTrackingMode trackingMode) + { + var persistentSpan = new PersistentSpan(span, trackingMode, this); + this.Spans.Add(persistentSpan); + return persistentSpan; + } + + internal void Delete(PersistentSpan span) + { + this.Factory.Delete(this, span); + } + + internal void DocumentReopened(ITextDocument document) + { + Requires.NotNull(document, nameof(document)); + Assumes.Null(this.Document); + + this.Document = document; + document.FileActionOccurred += this.OnFileActionOccurred; + + foreach (var s in this.Spans) + { + s.DocumentReopened(); + } + } + + internal void DocumentClosed() + { + Assumes.NotNull(this.Document); + + this.FileKey = new FileNameKey(this.Document.FilePath); + + foreach (var s in this.Spans) + { + s.DocumentClosed(_savedSnapshot); + } + + this.Document.FileActionOccurred -= this.OnFileActionOccurred; + this.Document = null; + } + + private void OnFileActionOccurred(object sender, TextDocumentFileActionEventArgs e) + { + if (e.FileActionType == FileActionTypes.ContentSavedToDisk) + { + _savedSnapshot = this.Document.TextBuffer.CurrentSnapshot; + } + else if (e.FileActionType == FileActionTypes.DocumentRenamed) + { + this.FileKey = new FileNameKey(this.Document.FilePath); + this.Factory.DocumentRenamed(this); + } + } + } +} diff --git a/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs b/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs index 5dbdfc3..4a3572c 100644 --- a/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs +++ b/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs @@ -17,6 +17,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation using Microsoft.VisualStudio.Text.Differencing; using Microsoft.VisualStudio.Text.Utilities; using System.Collections.ObjectModel; + using System.Globalization; internal abstract class BaseProjectionBuffer : BaseBuffer, IProjectionBufferBase { @@ -184,7 +185,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation #endregion #region Debug support - [Conditional("_DEBUG")] + [Conditional("DEBUG")] protected void DumpPendingChanges(List<Tuple<ITextBuffer, List<TextChange>>> pendingSourceChanges) { if (BufferGroup.Tracing) @@ -203,7 +204,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation } } - [Conditional("_DEBUG")] + [Conditional("DEBUG")] protected void DumpPendingContentChangedEventArgs() { if (BufferGroup.Tracing) @@ -213,7 +214,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { sb.Append(TextUtilities.GetTag(args.Before.TextBuffer)); sb.Append(" V"); - sb.AppendLine(args.After.Version.VersionNumber.ToString()); + sb.AppendLine(args.After.Version.VersionNumber.ToString(CultureInfo.InvariantCulture)); foreach (var change in args.Changes) { sb.AppendLine(change.ToString()); diff --git a/src/Text/Impl/TextModel/Projection/BufferGraph.cs b/src/Text/Impl/TextModel/Projection/BufferGraph.cs index 87f42e8..da4d4eb 100644 --- a/src/Text/Impl/TextModel/Projection/BufferGraph.cs +++ b/src/Text/Impl/TextModel/Projection/BufferGraph.cs @@ -29,11 +29,11 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (topBuffer == null) { - throw new ArgumentNullException("topBuffer"); + throw new ArgumentNullException(nameof(topBuffer)); } if (guardedOperations == null) { - throw new ArgumentNullException("guardedOperations"); + throw new ArgumentNullException(nameof(guardedOperations)); } this.topBuffer = topBuffer; @@ -69,7 +69,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } FrugalList<ITextBuffer> buffers = new FrugalList<ITextBuffer>(); foreach (ITextBuffer buffer in this.importingProjectionBufferMap.Keys) @@ -100,19 +100,19 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position.Snapshot == null) { - throw new ArgumentNullException("position"); + throw new ArgumentNullException(nameof(position)); } if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) { - throw new ArgumentOutOfRangeException("affinity"); + throw new ArgumentOutOfRangeException(nameof(affinity)); } if (!this.importingProjectionBufferMap.ContainsKey(position.Snapshot.TextBuffer)) { @@ -146,15 +146,15 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position.Snapshot == null) { - throw new ArgumentNullException("position"); + throw new ArgumentNullException(nameof(position)); } if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } ITextBuffer currentBuffer = position.Snapshot.TextBuffer; @@ -184,19 +184,19 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position.Snapshot == null) { - throw new ArgumentNullException("position"); + throw new ArgumentNullException(nameof(position)); } if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (targetBuffer == null) { - throw new ArgumentNullException("targetBuffer"); + throw new ArgumentNullException(nameof(targetBuffer)); } if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) { - throw new ArgumentOutOfRangeException("affinity"); + throw new ArgumentOutOfRangeException(nameof(affinity)); } ITextBuffer currentBuffer = position.Snapshot.TextBuffer; @@ -228,7 +228,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (targetSnapshot == null) { - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); } SnapshotPoint? result = MapDownToBuffer(position, trackingMode, targetSnapshot.TextBuffer, affinity); @@ -250,7 +250,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (targetSnapshot == null) { - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); } SnapshotPoint? result = MapUpToBuffer(position, trackingMode, affinity, targetSnapshot.TextBuffer); @@ -266,7 +266,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } return CheckedMapUpToBuffer(point, trackingMode, match, affinity); } @@ -275,15 +275,15 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (point.Snapshot == null) { - throw new ArgumentNullException("point"); + throw new ArgumentNullException(nameof(point)); } if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) { - throw new ArgumentOutOfRangeException("affinity"); + throw new ArgumentOutOfRangeException(nameof(affinity)); } if (!this.importingProjectionBufferMap.ContainsKey(point.Snapshot.TextBuffer)) @@ -328,15 +328,15 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (span.Snapshot == null) { - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); } if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } if (!this.importingProjectionBufferMap.ContainsKey(span.Snapshot.TextBuffer)) @@ -372,7 +372,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (targetBuffer == null) { - throw new ArgumentNullException("targetBuffer"); + throw new ArgumentNullException(nameof(targetBuffer)); } if (!this.importingProjectionBufferMap.ContainsKey(targetBuffer)) @@ -389,7 +389,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (targetSnapshot == null) { - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); } NormalizedSnapshotSpanCollection results = MapDownToBuffer(span, trackingMode, targetSnapshot.TextBuffer); @@ -411,7 +411,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (targetSnapshot == null) { - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); } NormalizedSnapshotSpanCollection results = MapUpToBuffer(span, trackingMode, targetSnapshot.TextBuffer); @@ -482,7 +482,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } return CheckedMapUpToBuffer(span, trackingMode, match); } @@ -491,11 +491,11 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (span.Snapshot == null) { - throw new ArgumentNullException("span"); + throw new ArgumentNullException(nameof(span)); } if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } ITextBuffer buffer = span.Snapshot.TextBuffer; if (!this.importingProjectionBufferMap.ContainsKey(buffer)) diff --git a/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs b/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs index 7c47c06..f4b1cc1 100644 --- a/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs +++ b/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs @@ -21,7 +21,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } return textBuffer.Properties.GetOrCreateSingletonProperty<BufferGraph>(() => (new BufferGraph(textBuffer, GuardedOperations))); } diff --git a/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs b/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs index f47748e..6d30821 100644 --- a/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs +++ b/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs @@ -123,6 +123,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation this.sourceBuffer = sourceBuffer; this.sourceSnapshot = sourceBuffer.CurrentSnapshot; + Debug.Assert(sourceBuffer is BaseBuffer); BaseBuffer baseSourceBuffer = (BaseBuffer)sourceBuffer; this.eventHook = new WeakEventHook(this, baseSourceBuffer); @@ -219,11 +220,11 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if ((spansToElide.Count > 0) && (spansToElide[spansToElide.Count - 1].End > this.elBuffer.sourceSnapshot.Length)) { - throw new ArgumentOutOfRangeException("spansToElide"); + throw new ArgumentOutOfRangeException(nameof(spansToElide)); } if ((spansToExpand.Count > 0) && (spansToExpand[spansToExpand.Count - 1].End > this.elBuffer.sourceSnapshot.Length)) { - throw new ArgumentOutOfRangeException("spansToExpand"); + throw new ArgumentOutOfRangeException(nameof(spansToExpand)); } ElisionSourceSpansChangedEventArgs args = this.elBuffer.ApplySpanChanges(spansToElide, spansToExpand); if (args != null) @@ -251,7 +252,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (spansToElide == null) { - throw new ArgumentNullException("spansToElide"); + throw new ArgumentNullException(nameof(spansToElide)); } return ModifySpans(spansToElide, null); } @@ -260,7 +261,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (spansToExpand == null) { - throw new ArgumentNullException("spansToExpand"); + throw new ArgumentNullException(nameof(spansToExpand)); } return ModifySpans(null, spansToExpand); } diff --git a/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs b/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs index 36b0779..8c70f24 100644 --- a/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs +++ b/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs @@ -91,7 +91,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } return this.sourceSnapshot.TextBuffer == textBuffer ? this.sourceSnapshot : null; } @@ -100,7 +100,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (this.sourceSnapshot.TextBuffer == textBuffer) @@ -121,7 +121,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } if (match(this.sourceSnapshot.TextBuffer)) @@ -142,11 +142,11 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (startSpanIndex < 0) { - throw new ArgumentOutOfRangeException("startSpanIndex"); + throw new ArgumentOutOfRangeException(nameof(startSpanIndex)); } if (count < 0 || startSpanIndex + count > SpanCount) { - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); } return new ReadOnlyCollection<SnapshotSpan>(this.content.GetSourceSpans(this.sourceSnapshot, startSpanIndex, count)); } @@ -162,7 +162,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position < 0 || position > this.totalLength) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } FrugalList<SnapshotPoint> points = this.content.MapInsertionPointToSourceSnapshots(this, position); if (points.Count == 1) @@ -183,11 +183,11 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position < 0 || position > this.totalLength) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) { - throw new ArgumentOutOfRangeException("affinity"); + throw new ArgumentOutOfRangeException(nameof(affinity)); } return this.content.MapToSourceSnapshot(this.sourceSnapshot, position, affinity); } @@ -200,7 +200,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation } if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) { - throw new ArgumentOutOfRangeException("affinity"); + throw new ArgumentOutOfRangeException(nameof(affinity)); } return this.content.MapFromSourceSnapshot(this, point.Position); } @@ -209,7 +209,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (span.End > this.totalLength) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } FrugalList<SnapshotSpan> result = new FrugalList<SnapshotSpan>(); if (fillIn) diff --git a/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs b/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs index 67b747e..368db2f 100644 --- a/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs +++ b/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs @@ -554,10 +554,8 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { foreach (object spanToInsert in this.RawSpansToInsert) { - if (spanToInsert == null) - { - throw new ArgumentNullException("spansToInsert"); - } + Requires.NotNull(spanToInsert, nameof(spanToInsert)); + ITrackingSpan trackingSpanToInsert = spanToInsert as ITrackingSpan; if (trackingSpanToInsert != null) { @@ -731,15 +729,15 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position < 0 || position > this.projBuffer.sourceSpans.Count) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } if (spansToReplace < 0 || position + spansToReplace > this.projBuffer.sourceSpans.Count) { - throw new ArgumentOutOfRangeException("spansToReplace"); + throw new ArgumentOutOfRangeException(nameof(spansToReplace)); } if (spansToInsert == null) { - throw new ArgumentNullException("spansToInsert"); + throw new ArgumentNullException(nameof(spansToInsert)); } this.spanManager = new SpanManager(this.projBuffer, position, spansToReplace, spansToInsert, true, (this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0); @@ -1596,12 +1594,15 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation } } +#pragma warning disable CA1801 // Review unused parameters private StringRebuilder InsertionLiesInCustomSpan(ITextSnapshot afterSourceSnapshot, int spanPosition, ITextChange incomingChange, HashSet<SnapshotPoint> urPoints, int accumulatedDelta) { +#pragma warning disable CA1801 // Review unused parameters + // just evaluate the new span and see if it overlaps the insertion. ITrackingSpan sourceTrackingSpan = this.sourceSpans[spanPosition]; SnapshotSpan afterSpan = sourceTrackingSpan.GetSpan(afterSourceSnapshot); diff --git a/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs b/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs index 5414af1..67a4473 100644 --- a/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs +++ b/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs @@ -17,6 +17,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation using Microsoft.VisualStudio.Text.Utilities; using Strings = Microsoft.VisualStudio.Text.Implementation.Strings; + using System.Globalization; internal partial class ProjectionSnapshot : BaseProjectionSnapshot, IProjectionSnapshot { @@ -192,7 +193,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys) { @@ -208,7 +209,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys) { @@ -233,7 +234,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys) { @@ -263,11 +264,11 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (startSpanIndex < 0 || startSpanIndex > this.SpanCount) { - throw new ArgumentOutOfRangeException("startSpanIndex"); + throw new ArgumentOutOfRangeException(nameof(startSpanIndex)); } if (count < 0 || startSpanIndex + count > this.SpanCount) { - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); } // better using iterator or explicit successor func eventually @@ -290,7 +291,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (span.End > this.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>(); @@ -464,7 +465,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position < 0 || position > this.totalLength) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } ReadOnlyCollection<SnapshotPoint> points = this.MapInsertionPointToSourceSnapshots(position, this.projectionBuffer.literalBuffer); // should this be conditional on writable literal buffer? if (points.Count == 1) @@ -485,11 +486,11 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position < 0 || position > this.Length) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) { - throw new ArgumentOutOfRangeException("affinity"); + throw new ArgumentOutOfRangeException(nameof(affinity)); } int rover = affinity == PositionAffinity.Predecessor ? FindLowestSpanIndexOfPosition(position) @@ -508,7 +509,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) { - throw new ArgumentOutOfRangeException("affinity"); + throw new ArgumentOutOfRangeException(nameof(affinity)); } List<InvertedSource> orderedSources; @@ -576,7 +577,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation { if (position < 0 || position > this.Length) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } int rover = FindLowestSpanIndexOfPosition(position); @@ -740,7 +741,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation "{0,12} {1,10} {2,4} {3,12} {4}\r\n", new Span(cumulativeLength, sourceSpan.Length), TextUtilities.GetTagOrContentType(sourceSpan.Snapshot.TextBuffer), - "V" + sourceSpan.Snapshot.Version.VersionNumber.ToString(), + "V" + sourceSpan.Snapshot.Version.VersionNumber.ToString(CultureInfo.InvariantCulture), sourceSpan.Span, TextUtilities.Escape(sourceSpan.GetText())); cumulativeLength += sourceSpan.Length; diff --git a/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs b/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs index 06b0306..9e62620 100644 --- a/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs +++ b/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs @@ -48,7 +48,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation private void ConstructChanges() { - IDifferenceCollection<SnapshotSpan> diffs = differ.GetDifferences(); + var diffs = differ.GetDifferences(); List<TextChange> changes = new List<TextChange>(); int pos = this.textPosition; @@ -56,10 +56,10 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation // each difference generates a text change foreach (Difference diff in diffs) { - pos += GetMatchSize(differ.DeletedSpans, diff.Before); + pos += GetMatchSize(diffs.LeftSequence, diff.Before); TextChange change = TextChange.Create(pos, - BufferFactoryService.StringRebuilderFromSnapshotSpans(differ.DeletedSpans, diff.Left), - BufferFactoryService.StringRebuilderFromSnapshotSpans(differ.InsertedSpans, diff.Right), + BufferFactoryService.StringRebuilderFromSnapshotSpans(diffs.LeftSequence, diff.Left), + BufferFactoryService.StringRebuilderFromSnapshotSpans(diffs.RightSequence, diff.Right), this.currentSnapshot); changes.Add(change); pos += change.OldLength; @@ -67,7 +67,7 @@ namespace Microsoft.VisualStudio.Text.Projection.Implementation this.normalizedChanges = NormalizedTextChangeCollection.Create(changes); } - private static int GetMatchSize(ReadOnlyCollection<SnapshotSpan> spans, Match match) + private static int GetMatchSize(IList<SnapshotSpan> spans, Match match) { int size = 0; if (match != null) diff --git a/src/Text/Impl/TextModel/Storage/CharStream.cs b/src/Text/Impl/TextModel/Storage/CharStream.cs index 2179e9d..9a8e0f8 100644 --- a/src/Text/Impl/TextModel/Storage/CharStream.cs +++ b/src/Text/Impl/TextModel/Storage/CharStream.cs @@ -119,12 +119,12 @@ namespace Microsoft.VisualStudio.Text.Implementation } } - private char Make(byte hi, byte lo) + private static char Make(byte hi, byte lo) { return (char)((hi << 8) | lo); } - private void Split(char c, out byte hi, out byte lo) + private static void Split(char c, out byte hi, out byte lo) { hi = (byte)(c >> 8); lo = (byte)(c & 255); diff --git a/src/Text/Impl/TextModel/Storage/ILineBreaks.cs b/src/Text/Impl/TextModel/Storage/ILineBreaks.cs index ccf1358..2100860 100644 --- a/src/Text/Impl/TextModel/Storage/ILineBreaks.cs +++ b/src/Text/Impl/TextModel/Storage/ILineBreaks.cs @@ -32,4 +32,16 @@ namespace Microsoft.VisualStudio.Text.Implementation /// </summary> void Add(int start, int length); } + + public interface IPooledLineBreaksEditor : ILineBreaksEditor + { + /// <summary> + /// If the internal list of line breaks has excess capacity, copy it to a correctly sized list and return the oversized + /// list to a pool that can be reused. + /// </summary> + /// <remarks> + /// This method should be called when using calling <see cref="LineBreakManager.CreatePooledLineBreakEditor(int)"/>. + /// </remarks> + void ReleasePooledLineBreaks(); + } } diff --git a/src/Text/Impl/TextModel/Storage/LineBreakManager.cs b/src/Text/Impl/TextModel/Storage/LineBreakManager.cs index 2d3071b..f12a9a4 100644 --- a/src/Text/Impl/TextModel/Storage/LineBreakManager.cs +++ b/src/Text/Impl/TextModel/Storage/LineBreakManager.cs @@ -1,23 +1,39 @@ using System; -using System.Collections.Generic; +using System.Threading; using Microsoft.VisualStudio.Text.Utilities; namespace Microsoft.VisualStudio.Text.Implementation { public static class LineBreakManager { - public readonly static ILineBreaks Empty = new ShortLineBreaksEditor(0); + public readonly static ILineBreaks Empty = new ShortLineBreaksEditor(Array.Empty<ushort>()); + + /// <summary> + /// Create a line break editor using the pooled line break lists (which should have excess capacity). + /// </summary> + /// <remarks> + /// <para>ILineBreakEditor.ReleasePooledLineBreaks() should be called on the returne editors once all line breaks have been added.</para> + /// <para>Note that this method is thread-safe. If multiple PooledLineBreakEditor are created simultaneously on different threads then + /// only one will use the pooled line breaks (and the others will get freshly allocated line breaks).</para> + /// </remarks> + public static IPooledLineBreaksEditor CreatePooledLineBreakEditor(int maxLength) + { + return (maxLength <= short.MaxValue) + ? (IPooledLineBreaksEditor)(new ShortLineBreaksEditor(LineBreakListManager<ushort>.AcquireLineBreaks(ShortLineBreaksEditor.ExpectedNumberOfLines))) + : (IPooledLineBreaksEditor)(new IntLineBreaksEditor(LineBreakListManager<int>.AcquireLineBreaks(IntLineBreaksEditor.ExpectedNumberOfLines))); + } - public static ILineBreaksEditor CreateLineBreakEditor(int maxLength, int initialCapacity = 0) + // Create a line break editor using an allocated list (which should be sized to hold all the expected line breaks without reallocations), + public static ILineBreaksEditor CreateLineBreakEditor(int maxLength, int initialCapacity) { return (maxLength < short.MaxValue) - ? (ILineBreaksEditor)(new ShortLineBreaksEditor(initialCapacity)) - : (ILineBreaksEditor)(new IntLineBreaksEditor(initialCapacity)); + ? (ILineBreaksEditor)(new ShortLineBreaksEditor(new ushort[initialCapacity])) + : (ILineBreaksEditor)(new IntLineBreaksEditor(new int[initialCapacity])); } public static ILineBreaks CreateLineBreaks(string source) { - ILineBreaksEditor lineBreaks = null; + IPooledLineBreaksEditor lineBreaks = null; int index = 0; while (index < source.Length) @@ -30,112 +46,167 @@ namespace Microsoft.VisualStudio.Text.Implementation else { if (lineBreaks == null) - lineBreaks = LineBreakManager.CreateLineBreakEditor(source.Length); + lineBreaks = LineBreakManager.CreatePooledLineBreakEditor(source.Length); lineBreaks.Add(index, breakLength); index += breakLength; } } - return lineBreaks ?? Empty; + if (lineBreaks != null) + { + lineBreaks.ReleasePooledLineBreaks(); + return lineBreaks; + } + + return Empty; } - private class ShortLineBreaksEditor : ILineBreaksEditor + internal abstract class LineBreakListManager<T> : IPooledLineBreaksEditor { - private const ushort MaskForPosition = 0x7fff; - private const ushort MaskForLength = 0x8000; + internal static T[] _pooledLineBreaks = null; + + internal protected T[] LineBreaks; + + private int _length; - private readonly static List<ushort> Empty = new List<ushort>(0); - private List<ushort> _lineBreaks = Empty; + public int Length => _length; - public ShortLineBreaksEditor(int initialCapacity) + public LineBreakListManager(T[] lineBreaks) { - if (initialCapacity > 0) - _lineBreaks = new List<ushort>(initialCapacity); + this.LineBreaks = lineBreaks; } - public int Length => _lineBreaks.Count; + protected void Add(T value) + { + if (_length == this.LineBreaks.Length) + { + // Simulate a List.Add() + var newLineBreaks = new T[_length * 2]; + Array.Copy(this.LineBreaks, newLineBreaks, _length); - public int LengthOfLineBreak(int index) + this.LineBreaks = newLineBreaks; + } + + this.LineBreaks[_length++] = value; + } + + // In single threaded operations, we'll always be getting/reusing the same list of line breaks. We, however, need to handle + // the case of a file being simultaneously read on multiple threads (at which point one thread will get the pooled list, + // the other threads will allocate, and the largest list will end up back in the shared pool). + internal static T[] AcquireLineBreaks(int size) { - return ((_lineBreaks[index] & MaskForLength) != 0 ? 2 : 1); + T[] buffer = Volatile.Read(ref _pooledLineBreaks); + if (buffer != null && buffer.Length >= size) + { + if (buffer == Interlocked.CompareExchange(ref _pooledLineBreaks, null, buffer)) + { + return buffer; + } + } + + return new T[size]; } - public int StartOfLineBreak(int index) + public void ReleasePooledLineBreaks() { - return (int)(_lineBreaks[index] & MaskForPosition); + if (this.LineBreaks.Length != _length) + { + // We have excess capacity, so make an accurately sized copy of this.LineBreaks and return it to the pool. + T[] newLineBreaks; + if (_length > 0) + { + newLineBreaks = new T[_length]; + Array.Copy(this.LineBreaks, newLineBreaks, _length); + } + else + { + newLineBreaks = Array.Empty<T>(); + } + + T[] buffer = Volatile.Read(ref _pooledLineBreaks); + if ((buffer == null) || (buffer.Length < this.LineBreaks.Length)) + { + // We're done with this.LineBreaks and either there is nothing in the pool or + // this.LineBreaks are larger than the array in the pool so replace it with + // this.LineBreaks. + Interlocked.CompareExchange(ref _pooledLineBreaks, this.LineBreaks, buffer); + } + + this.LineBreaks = newLineBreaks; + } } - public int EndOfLineBreak(int index) + + public abstract void Add(int start, int length); + public abstract int StartOfLineBreak(int index); + public abstract int EndOfLineBreak(int index); + } + + private class ShortLineBreaksEditor : LineBreakListManager<ushort> + { + public const int ExpectedNumberOfLines = 500; // Guestimate on how many lines will be in a typical 16k block. + + private const ushort MaskForPosition = 0x7fff; + private const ushort MaskForLength = 0x8000; + + public ShortLineBreaksEditor(ushort[] lineBreaks) + : base(lineBreaks) + { } + + public override int StartOfLineBreak(int index) + { + return (int)(this.LineBreaks[index] & MaskForPosition); + } + + public override int EndOfLineBreak(int index) { - int lineBreak = _lineBreaks[index]; - return (lineBreak & MaskForPosition) + + int lineBreak = this.LineBreaks[index]; + return (lineBreak & MaskForPosition) + (((lineBreak & MaskForLength) != 0) ? 2 : 1); } - public void Add(int start, int length) + public override void Add(int start, int length) { if ((start < 0) || (start > short.MaxValue)) throw new ArgumentOutOfRangeException(nameof(start)); if ((length < 1) || (length > 2)) throw new ArgumentOutOfRangeException(nameof(length)); - if (_lineBreaks == Empty) - _lineBreaks = new List<ushort>(); - - if (length == 1) - _lineBreaks.Add((ushort)start); - else if (length == 2) - _lineBreaks.Add((ushort)(start | MaskForLength)); + this.Add((length == 1) ? (ushort)start : (ushort)(start | MaskForLength)); } } - private class IntLineBreaksEditor : ILineBreaksEditor + private class IntLineBreaksEditor : LineBreakListManager<int> { - private const uint MaskForPosition = 0x7fffffff; - private const uint MaskForLength = 0x80000000; - - private readonly static List<uint> Empty = new List<uint>(0); - private List<uint> _lineBreaks = Empty; - - public IntLineBreaksEditor(int initialCapacity) - { - if (initialCapacity > 0) - _lineBreaks = new List<uint>(initialCapacity); - } + public const int ExpectedNumberOfLines = 32000; // Guestimate on how many lines will be in a typical 1MB block. - public int Length => _lineBreaks.Count; + private const int MaskForPosition = int.MaxValue; //0x7fffffff + private const int MaskForLength = int.MinValue; //0x80000000 in an int-friendly way - public int LengthOfLineBreak(int index) - { - return (_lineBreaks[index] & MaskForLength) != 0 ? 2 : 1; - } + public IntLineBreaksEditor(int[] lineBreaks) + : base(lineBreaks) + { } - public int StartOfLineBreak(int index) + public override int StartOfLineBreak(int index) { - return (int)(_lineBreaks[index] & MaskForPosition); + return (int)(this.LineBreaks[index] & MaskForPosition); } - public int EndOfLineBreak(int index) + public override int EndOfLineBreak(int index) { - uint lineBreak = _lineBreaks[index]; - return (int)((lineBreak & MaskForPosition) + - (((lineBreak & MaskForLength) != 0) ? 2 : 1)); + int lineBreak = this.LineBreaks[index]; + return (lineBreak & MaskForPosition) + + (((lineBreak & MaskForLength) != 0) ? 2 : 1); } - public void Add(int start, int length) + public override void Add(int start, int length) { - if ((start < 0) || (start > int.MaxValue)) + if (start < 0) throw new ArgumentOutOfRangeException(nameof(start)); if ((length < 1) || (length > 2)) throw new ArgumentOutOfRangeException(nameof(length)); - if (_lineBreaks == Empty) - _lineBreaks = new List<uint>(); - - if (length == 1) - _lineBreaks.Add((uint)start); - else if (length == 2) - _lineBreaks.Add((uint)(start | MaskForLength)); + this.Add((length == 1) ? start : (start | MaskForLength)); } } } diff --git a/src/Text/Impl/TextModel/Storage/TextImageLoader.cs b/src/Text/Impl/TextModel/Storage/TextImageLoader.cs index 428d29c..da7646d 100644 --- a/src/Text/Impl/TextModel/Storage/TextImageLoader.cs +++ b/src/Text/Impl/TextModel/Storage/TextImageLoader.cs @@ -6,7 +6,6 @@ // Use at your own risk. // using System; -using System.Collections.Generic; using System.IO; using System.Threading; using Microsoft.VisualStudio.Text.Utilities; @@ -17,7 +16,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { public const int BlockSize = 16384; - internal static StringRebuilder Load(TextReader reader, long fileSize, string id, + internal static StringRebuilder Load(TextReader reader, long fileSize, out bool hasConsistentLineEndings, out int longestLineLength, int blockSize = 0, int minCompressedBlockSize = TextImageLoader.BlockSize) // Exposed for unit tests @@ -52,8 +51,7 @@ namespace Microsoft.VisualStudio.Text.Implementation if (read == 0) break; - var lineBreaks = LineBreakManager.CreateLineBreakEditor(read); - TextImageLoader.ParseBlock(buffer, read, lineBreaks, ref lineEnding, ref currentLineLength, ref longestLineLength); + var lineBreaks = TextImageLoader.ParseBlock(buffer, read, ref lineEnding, ref currentLineLength, ref longestLineLength); char[] bufferForStringBuilder = buffer; if (read < (buffer.Length / 2)) @@ -64,7 +62,7 @@ namespace Microsoft.VisualStudio.Text.Implementation } else { - // We're using most of bufferForStringRebuilder so allocate a new block for the next chunk. + // We're using most of buffer so allocate a new block for the next chunk. buffer = new char[blockSize]; } @@ -76,7 +74,6 @@ namespace Microsoft.VisualStudio.Text.Implementation } longestLineLength = Math.Max(longestLineLength, currentLineLength); - hasConsistentLineEndings = lineEnding != LineEndingState.Inconsistent; } finally { @@ -86,6 +83,8 @@ namespace Microsoft.VisualStudio.Text.Implementation } } + hasConsistentLineEndings = lineEnding != LineEndingState.Inconsistent; + return content; } @@ -110,9 +109,12 @@ namespace Microsoft.VisualStudio.Text.Implementation return read; } - private static void ParseBlock(char[] buffer, int length, ILineBreaksEditor lineBreaks, - ref LineEndingState lineEnding, ref int currentLineLength, ref int longestLineLength) + private static ILineBreaks ParseBlock(char[] buffer, int length, + ref LineEndingState lineEnding, ref int currentLineLength, ref int longestLineLength) { + // Note that the lineBreaks created here will (internally) use the pooled list of line breaks. + IPooledLineBreaksEditor lineBreaks = LineBreakManager.CreatePooledLineBreakEditor(length); + int index = 0; while (index < length) { @@ -161,6 +163,10 @@ namespace Microsoft.VisualStudio.Text.Implementation index += breakLength; } } + + lineBreaks.ReleasePooledLineBreaks(); + + return lineBreaks; } internal enum LineEndingState diff --git a/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs index e3e3581..26139fc 100644 --- a/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs +++ b/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs @@ -164,9 +164,9 @@ namespace Microsoft.VisualStudio.Text.Implementation public static StringRebuilder Create(StringRebuilder left, StringRebuilder right) { if (left == null) - throw new ArgumentNullException("left"); + throw new ArgumentNullException(nameof(left)); if (right == null) - throw new ArgumentNullException("right"); + throw new ArgumentNullException(nameof(right)); if (left.Length == 0) return right; @@ -203,7 +203,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override int GetLineNumberFromPosition(int position) { if ((position < 0) || (position > this.Length)) - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); return (position <= _left.Length) ? _left.GetLineNumberFromPosition(position) @@ -214,7 +214,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength) { if ((lineNumber < 0) || (lineNumber > this.LineBreakCount)) - throw new ArgumentOutOfRangeException("lineNumber"); + throw new ArgumentOutOfRangeException(nameof(lineNumber)); if (lineNumber < _left.LineBreakCount) { @@ -273,7 +273,7 @@ namespace Microsoft.VisualStudio.Text.Implementation get { if ((index < 0) || (index >= this.Length)) - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); return (index < _left.Length) ? _left[index] @@ -284,7 +284,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override string GetText(Span span) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); if (span.End <= _left.Length) return _left.GetText(span); @@ -338,9 +338,9 @@ namespace Microsoft.VisualStudio.Text.Implementation public override void Write(TextWriter writer, Span span) { if (writer == null) - throw new ArgumentNullException("writer"); + throw new ArgumentNullException(nameof(writer)); if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); if (span.Start >= _left.Length) _right.Write(writer, new Span(span.Start - _left.Length, span.Length)); @@ -356,7 +356,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override StringRebuilder GetSubText(Span span) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); if (span.Length == this.Length) return this; diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs index 1fe8b63..9134e56 100644 --- a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs @@ -34,7 +34,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public static StringRebuilder Create(string text) { if (text == null) - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); #if DEBUG Interlocked.Add(ref _totalCharactersScanned, text.Length); #endif @@ -258,10 +258,10 @@ namespace Microsoft.VisualStudio.Text.Implementation public char[] ToCharArray(int startIndex, int length) { if (startIndex < 0) - throw new ArgumentOutOfRangeException("startIndex"); + throw new ArgumentOutOfRangeException(nameof(startIndex)); if ((length < 0) || (startIndex + length > this.Length) || (startIndex + length < 0)) - throw new ArgumentOutOfRangeException("length"); + throw new ArgumentOutOfRangeException(nameof(length)); char[] copy = new char[length]; this.CopyTo(startIndex, copy, 0, length); @@ -331,9 +331,9 @@ namespace Microsoft.VisualStudio.Text.Implementation public StringRebuilder Insert(int position, StringRebuilder text) { if ((position < 0) || (position > this.Length)) - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); if (text == null) - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); return this.Assemble(Span.FromBounds(0, position), text, Span.FromBounds(position, this.Length)); } @@ -351,7 +351,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public StringRebuilder Delete(Span span) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); return this.Assemble(Span.FromBounds(0, span.Start), Span.FromBounds(span.End, this.Length)); } @@ -402,9 +402,9 @@ namespace Microsoft.VisualStudio.Text.Implementation public StringRebuilder Replace(Span span, StringRebuilder text) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); if (text == null) - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); return this.Assemble(Span.FromBounds(0, span.Start), text, Span.FromBounds(span.End, this.Length)); } diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs index a4f4293..70e932a 100644 --- a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs @@ -71,7 +71,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override StringRebuilder GetSubText(Span span) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); if (span.Length == 0) return StringRebuilder.Empty; diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs index ca80b09..59dd801 100644 --- a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs @@ -62,7 +62,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override StringRebuilder GetSubText(Span span) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); if (span.Length == this.Length) return this; diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs index 56e5c8a..75838f1 100644 --- a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs @@ -96,7 +96,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override StringRebuilder GetSubText(Span span) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); if (span.Length == 0) return StringRebuilder.Empty; diff --git a/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs index 3f53283..41f11fb 100644 --- a/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs +++ b/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs @@ -57,7 +57,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override int GetLineNumberFromPosition(int position) { if ((position < 0) || (position > this.Length)) - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); //Convert position to a position relative to the start of _text. if (position == this.Length) @@ -87,7 +87,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength) { if ((lineNumber < 0) || (lineNumber > this.LineBreakCount)) - throw new ArgumentOutOfRangeException("lineNumber"); + throw new ArgumentOutOfRangeException(nameof(lineNumber)); int absoluteLineNumber = _lineBreakSpanStart + lineNumber; @@ -122,7 +122,7 @@ namespace Microsoft.VisualStudio.Text.Implementation protected char GetChar(char[] content, int index) { if ((index < 0) || (index >= this.Length)) - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); #if DEBUG Interlocked.Increment(ref _totalCharactersReturned); @@ -134,7 +134,7 @@ namespace Microsoft.VisualStudio.Text.Implementation protected string GetText(char[] content, Span span) { if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); #if DEBUG Interlocked.Add(ref _totalCharactersReturned, span.Length); @@ -146,19 +146,19 @@ namespace Microsoft.VisualStudio.Text.Implementation protected void CopyTo(char[] content, int sourceIndex, char[] destination, int destinationIndex, int count) { if (sourceIndex < 0) - throw new ArgumentOutOfRangeException("sourceIndex"); + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); if (destination == null) - throw new ArgumentNullException("destination"); + throw new ArgumentNullException(nameof(destination)); if (destinationIndex < 0) - throw new ArgumentOutOfRangeException("destinationIndex"); + throw new ArgumentOutOfRangeException(nameof(destinationIndex)); if (count < 0) - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); if ((sourceIndex + count > this.Length) || (sourceIndex + count < 0)) - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); if ((destinationIndex + count > destination.Length) || (destinationIndex + count < 0)) - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); #if DEBUG Interlocked.Add(ref _totalCharactersCopied, count); @@ -170,9 +170,9 @@ namespace Microsoft.VisualStudio.Text.Implementation protected void Write(char[] content, TextWriter writer, Span span) { if (writer == null) - throw new ArgumentNullException("writer"); + throw new ArgumentNullException(nameof(writer)); if (span.End > this.Length) - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); writer.Write(content, span.Start + _textSpanStart, span.Length); } diff --git a/src/Text/Impl/TextModel/TextChange.cs b/src/Text/Impl/TextModel/TextChange.cs index 972854e..b1f3383 100644 --- a/src/Text/Impl/TextModel/TextChange.cs +++ b/src/Text/Impl/TextModel/TextChange.cs @@ -53,7 +53,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (oldPosition < 0) { - throw new ArgumentOutOfRangeException("oldPosition"); + throw new ArgumentOutOfRangeException(nameof(oldPosition)); } _oldPosition = oldPosition; @@ -106,7 +106,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (value < 0) { - throw new ArgumentOutOfRangeException("value"); + throw new ArgumentOutOfRangeException(nameof(value)); } _oldPosition = value; } @@ -119,7 +119,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (value < 0) { - throw new ArgumentOutOfRangeException("value"); + throw new ArgumentOutOfRangeException(nameof(value)); } _newPosition = value; } @@ -226,7 +226,7 @@ namespace Microsoft.VisualStudio.Text.Implementation internal void RecordMasterChangeOffset(int masterChangeOffset) { if (masterChangeOffset < 0) - throw new ArgumentOutOfRangeException("masterChangeOffset", "MasterChangeOffset should be non-negative."); + throw new ArgumentOutOfRangeException(nameof(masterChangeOffset), "MasterChangeOffset should be non-negative."); if (_masterChangeOffset != -1) throw new InvalidOperationException("MasterChangeOffset has already been set."); diff --git a/src/Text/Impl/TextModel/TextDocument.cs b/src/Text/Impl/TextModel/TextDocument.cs index e546e97..62d0f5a 100644 --- a/src/Text/Impl/TextModel/TextDocument.cs +++ b/src/Text/Impl/TextModel/TextDocument.cs @@ -15,7 +15,7 @@ namespace Microsoft.VisualStudio.Text.Implementation using Microsoft.VisualStudio.Utilities; using Microsoft.VisualStudio.Text.Editor; - internal partial class TextDocument : ITextDocument + internal sealed partial class TextDocument : ITextDocument { #region Private Members @@ -50,19 +50,19 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (filePath == null) { - throw new ArgumentNullException("filePath"); + throw new ArgumentNullException(nameof(filePath)); } if (textDocumentFactoryService == null) { - throw new ArgumentNullException("textDocumentFactoryService"); + throw new ArgumentNullException(nameof(textDocumentFactoryService)); } if (encoding == null) { - throw new ArgumentNullException("encoding"); + throw new ArgumentNullException(nameof(encoding)); } _textBuffer = textBuffer; @@ -125,7 +125,7 @@ namespace Microsoft.VisualStudio.Text.Implementation } if (newFilePath == null) { - throw new ArgumentNullException("newFilePath"); + throw new ArgumentNullException(nameof(newFilePath)); } _filePath = newFilePath; @@ -147,7 +147,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { bool hasConsistentLineEndings; int longestLineLength; - StringRebuilder newContent = TextImageLoader.Load(streamReader, fileSize, _filePath, out hasConsistentLineEndings, out longestLineLength); + StringRebuilder newContent = TextImageLoader.Load(streamReader, fileSize, out hasConsistentLineEndings, out longestLineLength); if (!hasConsistentLineEndings) { @@ -361,11 +361,11 @@ namespace Microsoft.VisualStudio.Text.Implementation } if (filePath == null) { - throw new ArgumentNullException("filePath"); + throw new ArgumentNullException(nameof(filePath)); } PerformSave(overwrite ? FileMode.Create : FileMode.CreateNew, filePath, createFolder); - UpdateSaveStatus(filePath, _filePath != filePath); + UpdateSaveStatus(filePath, !string.Equals(_filePath, filePath, StringComparison.Ordinal)); // file path won't be updated if the save fails (in which case PerformSave will throw an exception) @@ -376,7 +376,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (newContentType == null) { - throw new ArgumentNullException("newContentType"); + throw new ArgumentNullException(nameof(newContentType)); } SaveAs(filePath, overwrite, createFolder); // content type won't be changed if the save fails (in which case SaveAs will throw an exception) @@ -391,7 +391,7 @@ namespace Microsoft.VisualStudio.Text.Implementation } if (filePath == null) { - throw new ArgumentNullException("filePath"); + throw new ArgumentNullException(nameof(filePath)); } PerformSave(overwrite ? FileMode.Create : FileMode.CreateNew, filePath, createFolder); @@ -453,7 +453,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } Encoding oldEncoding = _encoding; diff --git a/src/Text/Impl/TextModel/TextDocumentFactoryService.cs b/src/Text/Impl/TextModel/TextDocumentFactoryService.cs index ff59ae3..67c75f9 100644 --- a/src/Text/Impl/TextModel/TextDocumentFactoryService.cs +++ b/src/Text/Impl/TextModel/TextDocumentFactoryService.cs @@ -48,17 +48,17 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (filePath == null) { - throw new ArgumentNullException("filePath"); + throw new ArgumentNullException(nameof(filePath)); } if (contentType == null) { - throw new ArgumentNullException("contentType"); + throw new ArgumentNullException(nameof(contentType)); } if (encoding == null) { - throw new ArgumentNullException("encoding"); + throw new ArgumentNullException(nameof(encoding)); } var fallbackDetector = new FallbackDetector(encoding.DecoderFallback); @@ -191,12 +191,12 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (filePath == null) { - throw new ArgumentNullException("filePath"); + throw new ArgumentNullException(nameof(filePath)); } TextDocument textDocument = new TextDocument(textBuffer, filePath, DateTime.UtcNow, this, Encoding.UTF8); @@ -209,7 +209,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } textDocument = null; diff --git a/src/Text/Impl/TextModel/TextImageVersion.cs b/src/Text/Impl/TextModel/TextImageVersion.cs index ef3e44a..783a32a 100644 --- a/src/Text/Impl/TextModel/TextImageVersion.cs +++ b/src/Text/Impl/TextModel/TextImageVersion.cs @@ -98,7 +98,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public int TrackTo(VersionedPosition other, PointTrackingMode mode) { if (other.Version == null) - throw new ArgumentException(nameof(other)); + throw new ArgumentException(nameof(other) + " version cannot be null"); if (other.Version.VersionNumber == this.VersionNumber) return other.Position; @@ -112,7 +112,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public Span TrackTo(VersionedSpan span, SpanTrackingMode mode) { if (span.Version == null) - throw new ArgumentException(nameof(span)); + throw new ArgumentException(nameof(span) + " version cannot be null"); if (span.Version.VersionNumber == this.VersionNumber) return span.Span; diff --git a/src/Text/Impl/TextModel/TextVersion.cs b/src/Text/Impl/TextModel/TextVersion.cs index 55727c5..3e16443 100644 --- a/src/Text/Impl/TextModel/TextVersion.cs +++ b/src/Text/Impl/TextModel/TextVersion.cs @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { using System; + using System.Globalization; /// <summary> /// An internal implementation of ITextVersion @@ -131,7 +132,7 @@ namespace Microsoft.VisualStudio.Text.Implementation // Forward fidelity is implicit if (trackingMode == SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } return new ForwardFidelityTrackingSpan(this, new Span(start, length), trackingMode); } @@ -146,7 +147,7 @@ namespace Microsoft.VisualStudio.Text.Implementation // Forward fidelity is implicit if (trackingMode == SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } return new ForwardFidelityTrackingSpan(this, span, trackingMode); } @@ -155,7 +156,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (trackingMode == SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } if (trackingFidelity == TrackingFidelityMode.Forward) { @@ -171,7 +172,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (behavior == null) { - throw new ArgumentNullException("behavior"); + throw new ArgumentNullException(nameof(behavior)); } if (trackingFidelity != TrackingFidelityMode.Forward) { @@ -183,7 +184,7 @@ namespace Microsoft.VisualStudio.Text.Implementation public override string ToString() { - return String.Format("V{0} (r{1})", VersionNumber, ReiteratedVersionNumber); + return String.Format(CultureInfo.CurrentCulture, "V{0} (r{1})", this.VersionNumber, ReiteratedVersionNumber); } } } diff --git a/src/Text/Impl/TextModel/TrackingPoint.cs b/src/Text/Impl/TextModel/TrackingPoint.cs index 02750f3..3549d03 100644 --- a/src/Text/Impl/TextModel/TrackingPoint.cs +++ b/src/Text/Impl/TextModel/TrackingPoint.cs @@ -21,15 +21,15 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (version == null) { - throw new ArgumentNullException("version"); + throw new ArgumentNullException(nameof(version)); } if (position < 0 | position > version.Length) { - throw new ArgumentOutOfRangeException("position"); + throw new ArgumentOutOfRangeException(nameof(position)); } if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } this.trackingMode = trackingMode; @@ -50,7 +50,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (version == null) { - throw new ArgumentNullException("version"); + throw new ArgumentNullException(nameof(version)); } if (version.TextBuffer != this.TextBuffer) { @@ -63,7 +63,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (snapshot.TextBuffer != this.TextBuffer) { diff --git a/src/Text/Impl/TextModel/TrackingSpan.cs b/src/Text/Impl/TextModel/TrackingSpan.cs index 8ffb5fe..82d5d55 100644 --- a/src/Text/Impl/TextModel/TrackingSpan.cs +++ b/src/Text/Impl/TextModel/TrackingSpan.cs @@ -21,15 +21,15 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (version == null) { - throw new ArgumentNullException("version"); + throw new ArgumentNullException(nameof(version)); } if (span.End > version.Length) { - throw new ArgumentOutOfRangeException("span"); + throw new ArgumentOutOfRangeException(nameof(span)); } if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.Custom) { - throw new ArgumentOutOfRangeException("trackingMode"); + throw new ArgumentOutOfRangeException(nameof(trackingMode)); } this.trackingMode = trackingMode; @@ -50,7 +50,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (version == null) { - throw new ArgumentNullException("version"); + throw new ArgumentNullException(nameof(version)); } if (version.TextBuffer != this.TextBuffer) { @@ -63,7 +63,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (snapshot == null) { - throw new ArgumentNullException("snapshot"); + throw new ArgumentNullException(nameof(snapshot)); } if (snapshot.TextBuffer != this.TextBuffer) { diff --git a/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs b/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs index bb70ebd..e92dbf9 100644 --- a/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs +++ b/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs @@ -39,7 +39,7 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (index != 0) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } return this; } @@ -95,11 +95,11 @@ namespace Microsoft.VisualStudio.Text.Implementation { if (array == null) { - throw new ArgumentNullException("array"); + throw new ArgumentNullException(nameof(array)); } if (arrayIndex < 0) { - throw new ArgumentOutOfRangeException("arrayIndex"); + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } if (array.Rank > 1 || arrayIndex >= array.Length) { diff --git a/src/Text/Impl/TextSearch/BackgroundSearch.cs b/src/Text/Impl/TextSearch/BackgroundSearch.cs index f0e6db9..3bbe3ec 100644 --- a/src/Text/Impl/TextSearch/BackgroundSearch.cs +++ b/src/Text/Impl/TextSearch/BackgroundSearch.cs @@ -25,7 +25,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation /// Once we've searched a section of the buffer we don't search it again unless it is modified. /// Even if we get multiple, nearly simultaneous requests to search a section of the buffer, we only search it once. /// </remarks> - internal class BackgroundSearch<T> : IDisposable where T : ITag + internal sealed class BackgroundSearch<T> : IDisposable where T : ITag { private ITextBuffer _buffer; private readonly ITextSearchService2 _textSearchService; @@ -429,6 +429,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation public void Dispose() { _isDisposed = true; + GC.SuppressFinalize(this); } #endregion diff --git a/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs b/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs index 278532f..a555f9b 100644 --- a/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs +++ b/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs @@ -25,7 +25,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (buffer == null) { - throw new ArgumentNullException("buffer"); + throw new ArgumentNullException(nameof(buffer)); } // Don't return a singleton since it's allowed to have multiple search navigators on the same buffer diff --git a/src/Text/Impl/TextSearch/TextSearchService.cs b/src/Text/Impl/TextSearch/TextSearchService.cs index 71c44e1..5146f4f 100644 --- a/src/Text/Impl/TextSearch/TextSearchService.cs +++ b/src/Text/Impl/TextSearch/TextSearchService.cs @@ -8,16 +8,15 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { using System; - using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.Composition; using System.Diagnostics; + using System.Linq; using System.Text.RegularExpressions; + using System.Threading; using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Text.Utilities; - using System.Linq; - using System.Threading; [Export(typeof(ITextSearchService))] [Export(typeof(ITextSearchService2))] @@ -30,18 +29,12 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation // Cache of recently used Regex expressions to save on construction // of Regex objects. - static IDictionary<string, WeakReference> _cachedRegexEngines; - static ReaderWriterLockSlim _regexCacheLock; + static IDictionary<string, WeakReference> _cachedRegexEngines = new Dictionary<string, WeakReference>(_maxCachedRegexEngines); + static ReaderWriterLockSlim _regexCacheLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); // Maximum number of Regex engines to cache const int _maxCachedRegexEngines = 10; - static TextSearchService() - { - _cachedRegexEngines = new Dictionary<string, WeakReference>(_maxCachedRegexEngines); - _regexCacheLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - } - #region ITextSearchService Members public SnapshotSpan? FindNext(int startIndex, bool wraparound, FindData findData) @@ -49,12 +42,12 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation // We allow startIndex to be at the end of the buffer if ((startIndex < 0) || (startIndex > findData.TextSnapshotToSearch.Length)) { - throw new ArgumentOutOfRangeException("startIndex"); + throw new ArgumentOutOfRangeException(nameof(startIndex)); } if (string.IsNullOrEmpty(findData.SearchString)) { - throw new ArgumentException("Search pattern can't be empty or null", "findData"); + throw new ArgumentException("Search pattern can't be empty or null", nameof(findData)); } FindOptions options = findData.FindOptions; @@ -102,7 +95,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(findData.SearchString)) { - throw new ArgumentException("Search pattern can't be empty or null", "findData"); + throw new ArgumentException("Search pattern can't be empty or null", nameof(findData)); } FindOptions options = findData.FindOptions; @@ -140,7 +133,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(searchPattern)) { - throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + throw new ArgumentException("Pattern can't be empty or null", nameof(searchPattern)); } return Find(startingPosition, new SnapshotSpan(startingPosition.Snapshot, Span.FromBounds(0, startingPosition.Snapshot.Length)), searchPattern, options); @@ -150,7 +143,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(searchPattern)) { - throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + throw new ArgumentException("Pattern can't be empty or null", nameof(searchPattern)); } if (searchRange.Snapshot != startingPosition.Snapshot) @@ -170,13 +163,10 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(searchPattern)) { - throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + throw new ArgumentException("Pattern can't be empty or null", nameof(searchPattern)); } - if (replacePattern == null) - { - throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern"); - } + Requires.NotNull(replacePattern, nameof(replacePattern)); return FindForReplace(startingPosition, new SnapshotSpan(startingPosition.Snapshot, Span.FromBounds(0, startingPosition.Snapshot.Length)), searchPattern, replacePattern, options, out expandedReplacePattern); @@ -186,13 +176,10 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(searchPattern)) { - throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + throw new ArgumentException("Pattern can't be empty or null", nameof(searchPattern)); } - if (replacePattern == null) - { - throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern"); - } + Requires.NotNull(replacePattern, nameof(replacePattern)); return FindForReplace(((options & FindOptions.SearchReverse) != FindOptions.SearchReverse) ? searchRange.Start : searchRange.End, searchRange, searchPattern, replacePattern, options, out expandedReplacePattern); } @@ -201,12 +188,12 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(searchPattern)) { - throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + throw new ArgumentException("Pattern can't be empty or null", nameof(searchPattern)); } if (searchRange.Length == 0) { - return new SnapshotSpan[] { }; + return Array.Empty<SnapshotSpan>(); } return FindAllForReplace(searchRange.Start, searchRange, searchPattern, null, options).Select(r => r.Item1); @@ -216,12 +203,12 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(searchPattern)) { - throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + throw new ArgumentException("Pattern can't be empty or null", nameof(searchPattern)); } if (searchRange.Length == 0) { - return new SnapshotSpan[] { }; + return Array.Empty<SnapshotSpan>(); } if (searchRange.Snapshot != startingPosition.Snapshot) @@ -241,13 +228,10 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (string.IsNullOrEmpty(searchPattern)) { - throw new ArgumentException("Search pattern can't be null or empty.", "searchPattern"); + throw new ArgumentException("Search pattern can't be null or empty.", nameof(searchPattern)); } - if (replacePattern == null) - { - throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern"); - } + Requires.NotNull(replacePattern, nameof(replacePattern)); return FindAllForReplace(searchRange.Start, searchRange, searchPattern, replacePattern, options); } diff --git a/src/Text/Impl/TextSearch/TextSearchTagger.cs b/src/Text/Impl/TextSearch/TextSearchTagger.cs index 6a8852b..150a67b 100644 --- a/src/Text/Impl/TextSearch/TextSearchTagger.cs +++ b/src/Text/Impl/TextSearch/TextSearchTagger.cs @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation /// <remarks> /// This tagger -- like most others -- will not raise a TagsChanged event when the buffer changes. /// </remarks> - class TextSearchTagger<T> : ITextSearchTagger<T> where T : ITag + internal sealed class TextSearchTagger<T> : ITextSearchTagger<T> where T : ITag { // search service to use for doing the real search ITextSearchService2 _searchService; @@ -103,12 +103,12 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if ((searchOptions & FindOptions.SearchReverse) == FindOptions.SearchReverse) { - throw new ArgumentException("FindOptions.SearchReverse is invalid as searches are performed forwards to ensure all matches in a requested search span are found.", "searchOptions"); + throw new ArgumentException("FindOptions.SearchReverse is invalid as searches are performed forwards to ensure all matches in a requested search span are found.", nameof(searchOptions)); } if ((searchOptions & FindOptions.Wrap) == FindOptions.Wrap) { - throw new ArgumentException("FindOptions.Wrap is invalid as searches are performed forwards with no wrapping to ensure all matches in a requested span are found.", "searchOptions"); + throw new ArgumentException("FindOptions.Wrap is invalid as searches are performed forwards with no wrapping to ensure all matches in a requested span are found.", nameof(searchOptions)); } _searchTerms.Add(new BackgroundSearch<T>(_searchService, _buffer, searchTerm, searchOptions, tagFactory, this.ResultsCalculated)); diff --git a/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs b/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs index fca8282..711fda8 100644 --- a/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs +++ b/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs @@ -25,7 +25,7 @@ namespace Microsoft.VisualStudio.Text.Find.Implementation { if (buffer == null) { - throw new ArgumentNullException("buffer"); + throw new ArgumentNullException(nameof(buffer)); } // Don't return singleton instances since multiple taggers can exist per buffer diff --git a/src/Text/Util/TextDataUtil/BufferTracker.cs b/src/Text/Util/TextDataUtil/BufferTracker.cs index f64e6bc..0ed007f 100644 --- a/src/Text/Util/TextDataUtil/BufferTracker.cs +++ b/src/Text/Util/TextDataUtil/BufferTracker.cs @@ -102,7 +102,7 @@ namespace Microsoft.VisualStudio.Text.Utilities foreach (KeyValuePair<Object, Object> pair in ((IPropertyOwner)buffer).Properties.PropertyList) { - if (pair.Key.ToString() != "tag") + if (!string.Equals(pair.Key.ToString(), "tag", StringComparison.Ordinal)) { string rhsType; if (pair.Value == null) diff --git a/src/Text/Util/TextDataUtil/FrugalList.cs b/src/Text/Util/TextDataUtil/FrugalList.cs index fe554fe..c6ac083 100644 --- a/src/Text/Util/TextDataUtil/FrugalList.cs +++ b/src/Text/Util/TextDataUtil/FrugalList.cs @@ -20,6 +20,7 @@ namespace Microsoft.VisualStudio.Text.Utilities } #endif +#pragma warning disable CA1710 // Identifiers should have correct suffix /// <summary> /// <para> /// This implementation is intended for lists that are usually empty or have a single element. @@ -36,6 +37,7 @@ namespace Microsoft.VisualStudio.Text.Utilities /// </summary> /// <typeparam name="T">The type of the list element.</typeparam> public class FrugalList<T> : IList<T>, IReadOnlyList<T> +#pragma warning restore CA1710 // Identifiers should have correct suffix { const int InitialTailSize = 2; // initial size of array list @@ -65,7 +67,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (elements == null) { - throw new ArgumentNullException("elements"); + throw new ArgumentNullException(nameof(elements)); } switch (elements.Count) { @@ -106,7 +108,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (list == null) { - throw new ArgumentNullException("list"); + throw new ArgumentNullException(nameof(list)); } for (int i = 0; i < list.Count; ++i) @@ -124,7 +126,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (match == null) { - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); } int removed = 0; for (int i = Count - 1; i >= 0; --i) @@ -180,7 +182,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (index < 0 || index > this.Count) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } if (index == 0) { @@ -214,7 +216,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (index < 0 || index >= Count) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } int count = Count; @@ -249,13 +251,15 @@ namespace Microsoft.VisualStudio.Text.Utilities } } + // Analyzer has a bug where it is giving a false positive in this location. +#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations public T this[int index] { get { if (index < 0 || index >= Count) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } if (index == 0) @@ -271,7 +275,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (index < 0 || index >= Count) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } if (index == 0) @@ -284,6 +288,7 @@ namespace Microsoft.VisualStudio.Text.Utilities } } } +#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations public void Clear() { @@ -312,7 +317,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (array == null) { - throw new ArgumentNullException("array"); + throw new ArgumentNullException(nameof(array)); } int count = Count; if (count > 0) diff --git a/src/Text/Util/TextDataUtil/GuardedOperations.cs b/src/Text/Util/TextDataUtil/GuardedOperations.cs index 4a9ff53..add89d2 100644 --- a/src/Text/Util/TextDataUtil/GuardedOperations.cs +++ b/src/Text/Util/TextDataUtil/GuardedOperations.cs @@ -25,7 +25,7 @@ namespace Microsoft.VisualStudio.Text.Utilities private List<Lazy<IExtensionErrorHandler>> _errorHandlerExports = null; [ImportMany] - private List<Lazy<IExtensionPerformanceTracker>> _perTrackerExports = null; + private List<Lazy<IExtensionPerformanceTracker>> _perfTrackerExports = null; [Import] private JoinableTaskContext _joinableTaskContext; @@ -38,6 +38,9 @@ namespace Microsoft.VisualStudio.Text.Utilities private FrugalList<IExtensionErrorHandler> _errorHandlers; private FrugalList<IExtensionPerformanceTracker> _perfTrackers; + private static Exception LastHandledException = null; + private static string LastHandleExceptionStackTrace = null; + public GuardedOperations() { } @@ -82,7 +85,7 @@ namespace Microsoft.VisualStudio.Text.Utilities } catch (Exception) { - Debug.Fail("Exception instantiating error handler!"); + GuardedOperations.Fail("Exception instantiating error handler!"); } } } @@ -98,13 +101,13 @@ namespace Microsoft.VisualStudio.Text.Utilities if (_perfTrackers == null) { _perfTrackers = new FrugalList<IExtensionPerformanceTracker>(); - if (_perTrackerExports != null) // can be null during unit testing + if (_perfTrackerExports != null) // can be null during unit testing { - foreach (var export in _perTrackerExports) + for (int i = 0; i < _perfTrackerExports.Count; i++) { try { - var perfTracker = export.Value; + var perfTracker = _perfTrackerExports[i].Value; if (perfTracker != null) { _perfTrackers.Add(perfTracker); @@ -112,7 +115,7 @@ namespace Microsoft.VisualStudio.Text.Utilities } catch (Exception) { - Debug.Fail("Exception instantiating perf tracker"); + GuardedOperations.Fail("Exception instantiating perf tracker"); } } } @@ -150,8 +153,9 @@ namespace Microsoft.VisualStudio.Text.Utilities where TMetadataView : IContentTypeMetadata { var candidates = new List<Lazy<TExtension, TMetadataView>>(); - foreach (var providerHandle in providerHandles) + for (int i = 0; (i < providerHandles.Count); ++i) { + var providerHandle = providerHandles[i]; foreach (string contentTypeName in providerHandle.Metadata.ContentTypes) { if (string.Compare(dataContentType.TypeName, contentTypeName, StringComparison.OrdinalIgnoreCase) == 0) @@ -385,6 +389,23 @@ namespace Microsoft.VisualStudio.Text.Utilities } } + public void CallExtensionPoint(object errorSource, Action call, Predicate<Exception> exceptionFilter) + { + try + { + BeforeCallingExtensionPoint(errorSource ?? call); + call(); + } + catch (Exception e) when (exceptionFilter(e)) + { + HandleException(errorSource, e); + } + finally + { + AfterCallingExtensionPoint(errorSource ?? call); + } + } + public T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow) { try @@ -418,24 +439,27 @@ namespace Microsoft.VisualStudio.Text.Utilities { try { - await asyncAction(); + await asyncAction().ConfigureAwait(true); } + catch (OperationCanceledException) { } // swallow OperationCanceledException in async method calls catch (Exception e) { HandleException(errorSource, e); } } - public async Task CallExtensionPointAsync(Func<Task> asyncAction) - { - await CallExtensionPointAsync(errorSource: null, asyncAction: asyncAction); - } + public Task CallExtensionPointAsync(Func<Task> asyncAction) => CallExtensionPointAsync(errorSource: null, asyncAction: asyncAction); public async Task<T> CallExtensionPointAsync<T>(object errorSource, Func<Task<T>> asyncCall, T valueOnThrow) { try { - return await asyncCall(); + return await asyncCall().ConfigureAwait(true); + } + catch (OperationCanceledException) + { + // swallow OperationCanceledException in async method calls + return valueOnThrow; } catch (Exception e) { @@ -444,10 +468,8 @@ namespace Microsoft.VisualStudio.Text.Utilities } } - public async Task<T> CallExtensionPointAsync<T>(Func<Task<T>> asyncCall, T valueOnThrow) - { - return await CallExtensionPointAsync<T>(errorSource: null, asyncCall: asyncCall, valueOnThrow: valueOnThrow); - } + public Task<T> CallExtensionPointAsync<T>(Func<Task<T>> asyncCall, T valueOnThrow) + => CallExtensionPointAsync<T>(errorSource: null, asyncCall: asyncCall, valueOnThrow: valueOnThrow); public void RaiseEvent(object sender, EventHandler eventHandlers) { @@ -458,8 +480,9 @@ namespace Microsoft.VisualStudio.Text.Utilities var handlers = eventHandlers.GetInvocationList(); - foreach (EventHandler handler in handlers) + for (int i = 0; (i < handlers.Length); ++i) { + var handler = (EventHandler)(handlers[i]); try { BeforeCallingEventHandler(handler); @@ -484,8 +507,9 @@ namespace Microsoft.VisualStudio.Text.Utilities } var handlers = eventHandlers.GetInvocationList(); - foreach (EventHandler<TArgs> handler in handlers) + for (int i = 0; (i < handlers.Length); ++i) { + var handler = (EventHandler<TArgs>)(handlers[i]); try { BeforeCallingEventHandler(handler); @@ -509,15 +533,16 @@ namespace Microsoft.VisualStudio.Text.Utilities return; } - foreach (var perfTracker in PerfTrackers) + for (int i = 0; i < PerfTrackers.Count; i++) { + var perfTracker = PerfTrackers[i]; try { - perfTracker.AfterCallingEventHandler(handler); + PerfTrackers[i].AfterCallingEventHandler(handler); } catch (Exception e) { - HandleException(perfTracker, e); + HandleException(PerfTrackers[i], e); } } } @@ -529,15 +554,16 @@ namespace Microsoft.VisualStudio.Text.Utilities return; } - foreach (var perfTracker in PerfTrackers) + for (int i = 0; i < PerfTrackers.Count; i++) { + var perfTracker = PerfTrackers[i]; try { - perfTracker.AfterCallingExtension(extensionPoint); + PerfTrackers[i].AfterCallingExtension(extensionPoint); } catch (Exception e) { - HandleException(perfTracker, e); + HandleException(PerfTrackers[i], e); } } } @@ -549,15 +575,16 @@ namespace Microsoft.VisualStudio.Text.Utilities return; } - foreach (var perfTracker in PerfTrackers) + for (int i = 0; i < PerfTrackers.Count; i++) { + var perfTracker = PerfTrackers[i]; try { - perfTracker.BeforeCallingEventHandler(handler); + PerfTrackers[i].BeforeCallingEventHandler(handler); } catch (Exception e) { - HandleException(perfTracker, e); + HandleException(PerfTrackers[i], e); } } } @@ -569,15 +596,16 @@ namespace Microsoft.VisualStudio.Text.Utilities return; } - foreach (var perfTracker in PerfTrackers) + for (int i = 0; i < PerfTrackers.Count; i++) { + var perfTracker = PerfTrackers[i]; try { - perfTracker.BeforeCallingExtension(extensionPoint); + PerfTrackers[i].BeforeCallingExtension(extensionPoint); } catch (Exception e) { - HandleException(perfTracker, e); + HandleException(PerfTrackers[i], e); } } } @@ -585,23 +613,27 @@ namespace Microsoft.VisualStudio.Text.Utilities public void HandleException(object errorSource, Exception e) { bool handled = false; - foreach (var errorHandler in ErrorHandlers) + for (int i = 0; (i < ErrorHandlers.Count); ++i) { + var errorHandler = ErrorHandlers[i]; try { + GuardedOperations.LastHandledException = e; + GuardedOperations.LastHandleExceptionStackTrace = e.StackTrace; + errorHandler.HandleError(errorSource, e); handled = true; } catch (Exception doubleFaultException) { // TODO: What is the right behavior here? - Debug.Fail(doubleFaultException.ToString()); + GuardedOperations.Fail(doubleFaultException.ToString()); } } if (!handled) { // TODO: What is the right behavior here? - Debug.Fail(e.ToString()); + GuardedOperations.Fail(e.ToString()); if (GuardedOperations.ReThrowIfNoHandlers) throw new Exception("Unhandled exception.", e); @@ -695,12 +727,13 @@ namespace Microsoft.VisualStudio.Text.Utilities var handlers = eventHandlers.GetInvocationList(); - foreach (AsyncEventHandler<TArgs> handler in handlers) + for (int i = 0; (i < handlers.Length); ++i) { + var handler = (AsyncEventHandler<TArgs>)(handlers[i]); try { BeforeCallingEventHandler(handler); - await handler(sender, args); + await handler(sender, args).ConfigureAwait(true); } catch (Exception e) { @@ -713,5 +746,18 @@ namespace Microsoft.VisualStudio.Text.Utilities } }).Task; } + + static bool _ignoreFailures = false; + + [Conditional("DEBUG")] + private static void Fail(string message) + { + if (!_ignoreFailures) + { + if (Debugger.IsAttached) + Debugger.Break(); + Debug.Fail(message); + } + } } } diff --git a/src/Text/Util/TextDataUtil/ListUtilities.cs b/src/Text/Util/TextDataUtil/ListUtilities.cs index a61d5cb..18013d7 100644 --- a/src/Text/Util/TextDataUtil/ListUtilities.cs +++ b/src/Text/Util/TextDataUtil/ListUtilities.cs @@ -14,11 +14,10 @@ namespace Microsoft.VisualStudio.Text.Utilities internal static class ListUtilities { /// <summary> - /// Do a binary search in <paramref name="list"/> for an element that matches <paramref name="target"/> + /// Do a binary search in <paramref name="list"/> for an element that matches an implicit target (built into the comparison function). /// </summary> /// <param name="list">List to search.</param> - /// <param name="target">Object of the search.</param> - /// <param name="compare">Comparison function between an element and target (returns < 0 if e comes before t, 0 if e matches, > 0 if e comes after). + /// <param name="compare">Comparison function between an element (returns < 0 if e comes before t, 0 if e matches, > 0 if e comes after). /// <param name="index">Index of the matching element (or, if there is no exact match, index of the element that follows it).</param> /// <returns>true if an exact match was found.</returns> /// <remarks>Yes, I know there is List.BinarySearch but that doesn't do exactly what I need most of the time.</remarks> @@ -61,4 +60,4 @@ namespace Microsoft.VisualStudio.Text.Utilities return source.Cast<T?>().FirstOrDefault(); } } - } +} diff --git a/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs b/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs index 3e6c110..121979a 100644 --- a/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs +++ b/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs @@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public SnapshotPoint? GetPoint(ITextBuffer targetBuffer, PositionAffinity affinity) { if (targetBuffer == null) - throw new ArgumentNullException("targetBuffer"); + throw new ArgumentNullException(nameof(targetBuffer)); if (_unmappable) return null; @@ -59,7 +59,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public SnapshotPoint? GetPoint(ITextSnapshot targetSnapshot, PositionAffinity affinity) { if (targetSnapshot == null) - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); if (_unmappable) return null; @@ -75,7 +75,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public SnapshotPoint? GetPoint(Predicate<ITextBuffer> match, PositionAffinity affinity) { if (match == null) - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); if (_unmappable) return null; @@ -94,7 +94,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public SnapshotPoint? GetInsertionPoint(Predicate<ITextBuffer> match) { if (match == null) - throw new ArgumentNullException("match"); + throw new ArgumentNullException(nameof(match)); if (_unmappable) return null; diff --git a/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs b/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs index 88714fd..6f758ad 100644 --- a/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs +++ b/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { using System; using System.Collections.Generic; + using System.Globalization; using Microsoft.VisualStudio.Text.Projection; internal class MappingSpanSnapshot : IMappingSpan @@ -41,7 +42,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public NormalizedSnapshotSpanCollection GetSpans(ITextBuffer targetBuffer) { if (targetBuffer == null) - throw new ArgumentNullException("targetBuffer"); + throw new ArgumentNullException(nameof(targetBuffer)); if (_unmappable) return NormalizedSnapshotSpanCollection.Empty; @@ -76,7 +77,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public NormalizedSnapshotSpanCollection GetSpans(ITextSnapshot targetSnapshot) { if (targetSnapshot == null) - throw new ArgumentNullException("targetSnapshot"); + throw new ArgumentNullException(nameof(targetSnapshot)); if (_unmappable) return NormalizedSnapshotSpanCollection.Empty; @@ -203,7 +204,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public override string ToString() { - return String.Format("MappingSpanSnapshot anchored at {0}", _anchor); + return string.Format(CultureInfo.CurrentCulture, "MappingSpanSnapshot anchored at {0}", _anchor); } } } diff --git a/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs b/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs index 2aa6e92..8af4993 100644 --- a/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs +++ b/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs @@ -224,7 +224,9 @@ namespace Microsoft.VisualStudio.Text.Utilities /// return a larger array to the pool than was originally allocated. /// </summary> [Conditional("DEBUG")] +#pragma warning disable CA1822 // Mark members as static internal void ForgetTrackedObject(T old, T replacement = null) +#pragma warning restore CA1822 // Mark members as static { #if DETECT_LEAKS LeakTracker tracker; diff --git a/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs b/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs index 951cf14..1a43cb9 100644 --- a/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs +++ b/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs @@ -5,6 +5,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { using System; + using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.VisualStudio.Text.Differencing; @@ -12,42 +13,15 @@ namespace Microsoft.VisualStudio.Text.Utilities internal class ProjectionSpanDiffer { - private IDifferenceService diffService; + private readonly IDifferenceService diffService; - private ReadOnlyCollection<SnapshotSpan> inputDeletedSnapSpans; - private ReadOnlyCollection<SnapshotSpan> inputInsertedSnapSpans; - private bool computed; + private readonly ReadOnlyCollection<SnapshotSpan> inputDeletedSnapSpans; + private readonly ReadOnlyCollection<SnapshotSpan> inputInsertedSnapSpans; // exposed to unit tests internal List<SnapshotSpan>[] deletedSurrogates; internal List<SnapshotSpan>[] insertedSurrogates; - public static ProjectionSpanDifference DiffSourceSpans(IDifferenceService diffService, - IProjectionSnapshot left, - IProjectionSnapshot right) - { - if (left == null) - { - throw new ArgumentNullException("left"); - } - if (right == null) - { - throw new ArgumentNullException("right"); - } - - if (!object.ReferenceEquals(left.TextBuffer, right.TextBuffer)) - { - throw new ArgumentException("left does not belong to the same text buffer as right"); - } - - ProjectionSpanDiffer differ = new ProjectionSpanDiffer - (diffService, - left.GetSourceSpans(), - right.GetSourceSpans()); - - return new ProjectionSpanDifference(differ.GetDifferences(), differ.InsertedSpans, differ.DeletedSpans); - } - public ProjectionSpanDiffer(IDifferenceService diffService, ReadOnlyCollection<SnapshotSpan> deletedSnapSpans, ReadOnlyCollection<SnapshotSpan> insertedSnapSpans) @@ -57,34 +31,31 @@ namespace Microsoft.VisualStudio.Text.Utilities this.inputInsertedSnapSpans = insertedSnapSpans; } - public ReadOnlyCollection<SnapshotSpan> DeletedSpans { get; private set; } - public ReadOnlyCollection<SnapshotSpan> InsertedSpans { get; private set; } + private IDifferenceCollection<SnapshotSpan> differences; public IDifferenceCollection<SnapshotSpan> GetDifferences() { - if (!this.computed) + if (this.differences == null) { DecomposeSpans(); - this.computed = true; - } - var deletedSpans = new List<SnapshotSpan>(); - var insertedSpans = new List<SnapshotSpan>(); + var deletedSpans = new List<SnapshotSpan>(); + var insertedSpans = new List<SnapshotSpan>(); - DeletedSpans = deletedSpans.AsReadOnly(); - InsertedSpans = insertedSpans.AsReadOnly(); + for (int s = 0; s < deletedSurrogates.Length; ++s) + { + deletedSpans.AddRange(deletedSurrogates[s]); + } - for (int s = 0; s < deletedSurrogates.Length; ++s) - { - deletedSpans.AddRange(deletedSurrogates[s]); - } + for (int s = 0; s < insertedSurrogates.Length; ++s) + { + insertedSpans.AddRange(insertedSurrogates[s]); + } - for (int s = 0; s < insertedSurrogates.Length; ++s) - { - insertedSpans.AddRange(insertedSurrogates[s]); + differences = this.diffService.DifferenceSequences(deletedSpans, insertedSpans); } - return this.diffService.DifferenceSequences(deletedSpans, insertedSpans); + return differences; } #region Internal (for testing) helpers @@ -277,11 +248,10 @@ namespace Microsoft.VisualStudio.Text.Utilities } } - private int Comparison(Thing left, Thing right) + private static int Comparison(Thing left, Thing right) { return left.span.Start - right.span.Start; } #endregion - } } diff --git a/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs b/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs deleted file mode 100644 index 99d3267..0000000 --- a/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// -using System.Collections.ObjectModel; -using Microsoft.VisualStudio.Text.Differencing; - -namespace Microsoft.VisualStudio.Text.Utilities -{ - /// <summary> - /// Represents the set of differences between two projection snapshots. - /// </summary> - public class ProjectionSpanDifference - { - /// <summary> - /// Initializes a new instance of <see cref="ProjectionSpanDifference"/>. - /// </summary> - /// <param name="differenceCollection">The collection of snapshot spans that include the differences.</param> - /// <param name="insertedSpans">A read-only collection of the inserted snapshot spans.</param> - /// <param name="deletedSpans">A read-only collection of the deleted snapshot spans.</param> - public ProjectionSpanDifference(IDifferenceCollection<SnapshotSpan> differenceCollection, ReadOnlyCollection<SnapshotSpan> insertedSpans, ReadOnlyCollection<SnapshotSpan> deletedSpans) - { - DifferenceCollection = differenceCollection; - InsertedSpans = insertedSpans; - DeletedSpans = deletedSpans; - } - - /// <summary> - /// The collection of differences between the two snapshots. - /// </summary> - public IDifferenceCollection<SnapshotSpan> DifferenceCollection { get; private set; } - - /// <summary> - /// The read-only collection of inserted snapshot spans. - /// </summary> - public ReadOnlyCollection<SnapshotSpan> InsertedSpans { get; private set; } - - /// <summary> - /// The read-only collection of deleted snapshot spans. - /// </summary> - public ReadOnlyCollection<SnapshotSpan> DeletedSpans { get; private set; } - } -} diff --git a/src/Text/Util/TextDataUtil/TextModelOptions.cs b/src/Text/Util/TextDataUtil/TextModelOptions.cs index 1863bfd..a07d797 100644 --- a/src/Text/Util/TextDataUtil/TextModelOptions.cs +++ b/src/Text/Util/TextDataUtil/TextModelOptions.cs @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { // these values should be set by MEF component in editor options component. defaults // are here just in case that doesn't happen in some configuration. +#pragma warning disable CA2211 // Non-constant fields should not be visible public static int CompressedStorageFileSizeThreshold = 5 * 1024 * 1024; // 5 MB file (typically 10 MB in memory) public static int CompressedStoragePageSize = 1 * 1024 * 1024; // 1 MB per page (so 10 pages at the low end) public static int CompressedStorageMaxLoadedPages = 3; // at most 3 pages loaded @@ -18,5 +19,6 @@ namespace Microsoft.VisualStudio.Text.Utilities public static int StringRebuilderMaxLinesToConsolidate = 8; // Combine adjacent pieces when number of lines is less than this public static int DiffSizeThreshold = 25 * 1024 * 1024; // threshold above which to do poor man's diff +#pragma warning restore CA2211 // Non-constant fields should not be visible } } diff --git a/src/Text/Util/TextDataUtil/TextUtilities.cs b/src/Text/Util/TextDataUtil/TextUtilities.cs index 26aa19a..e64703e 100644 --- a/src/Text/Util/TextDataUtil/TextUtilities.cs +++ b/src/Text/Util/TextDataUtil/TextUtilities.cs @@ -76,7 +76,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } int lines = 0; for (int i = 0; i < text.Length; ) @@ -106,7 +106,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (text == null) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } int lines = 0; for (int i = 0; i < length; ) @@ -257,11 +257,11 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (buffer == null) { - throw new ArgumentNullException("buffer"); + throw new ArgumentNullException(nameof(buffer)); } if (tag == null) { - throw new ArgumentNullException("tag"); + throw new ArgumentNullException(nameof(tag)); } string existingTag = ""; if (!buffer.Properties.TryGetProperty<string>("tag", out existingTag)) @@ -284,7 +284,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (buffer == null) { - throw new ArgumentNullException("buffer"); + throw new ArgumentNullException(nameof(buffer)); } string tag = ""; buffer.Properties.TryGetProperty<string>("tag", out tag); diff --git a/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs b/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs index 341ed2a..baf84fa 100644 --- a/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs +++ b/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs @@ -48,7 +48,9 @@ namespace Microsoft.VisualStudio.Text.Utilities get { return this.EditBuffer; } } - public void Dispose() +#pragma warning disable CA1063 // Implement IDisposable Correctly + public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly { GC.SuppressFinalize(this); } |