diff options
author | Sandy Armstrong <sandy@xamarin.com> | 2019-08-28 23:09:45 +0300 |
---|---|---|
committer | Sandy Armstrong <sandy@xamarin.com> | 2019-08-28 23:09:45 +0300 |
commit | cc54ccf435221210660b51f0f9ca49c0c99407fa (patch) | |
tree | 86a4b1cf2f5450f3964dc47362f4064e46bb1059 /src | |
parent | a566dffd4c3899a71da7c72071c448500ba8cb9b (diff) |
Sync with vs-editor-core@21efac1c7
Diffstat (limited to 'src')
52 files changed, 1801 insertions, 380 deletions
diff --git a/src/Editor/Core/Def/BaseUtility/IGuardedOperations.cs b/src/Editor/Core/Def/BaseUtility/IGuardedOperations.cs index 6180a0e..b1c7ef4 100644 --- a/src/Editor/Core/Def/BaseUtility/IGuardedOperations.cs +++ b/src/Editor/Core/Def/BaseUtility/IGuardedOperations.cs @@ -132,7 +132,9 @@ namespace Microsoft.VisualStudio.Utilities /// <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> + /// infrastructure and in general is not intended to be used directly from your code. + /// In Visual Studio, this method logs the exception to ActivityLogs and the telemetry, and displays an error message to the user if possible. + /// This method can be invoked from any thread.</remarks> void HandleException(object errorSource, Exception e); /// <summary> diff --git a/src/Editor/Core/Def/BaseUtility/IGuardedOperations2.cs b/src/Editor/Core/Def/BaseUtility/IGuardedOperations2.cs new file mode 100644 index 0000000..50ddf5d --- /dev/null +++ b/src/Editor/Core/Def/BaseUtility/IGuardedOperations2.cs @@ -0,0 +1,25 @@ +// +// 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.Utilities +{ + /// <summary> + /// 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> + public interface IGuardedOperations2 : IGuardedOperations + { + /// <summary> + /// Logs an exception silently, without notifying the user. + /// </summary> + /// <param name="errorSource">Reference to the extension object or event handler that threw the exception</param> + /// <param name="e">Exception to log</param> + /// <remarks>This method can be invoked from any thread.</remarks> + void LogException(object errorSource, Exception e); + } +} diff --git a/src/Editor/Core/Def/BaseUtility/IGuardedOperationsInternal.cs b/src/Editor/Core/Def/BaseUtility/IGuardedOperationsInternal.cs new file mode 100644 index 0000000..578b292 --- /dev/null +++ b/src/Editor/Core/Def/BaseUtility/IGuardedOperationsInternal.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 System; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Operations that guard calls to extensions code, track performance and log errors. + /// This interface contains method signatures which will be moved to <see cref="IGuardedOperations" /> + /// in future releases of Visual Studio. Microsoft reserves the right to modify this interface. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + internal interface IGuardedOperationsInternal : IGuardedOperations + { + /// <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> + /// <param name="exceptionToIgnore">Determines which exceptions should be ignored. This predicate is evaluated first</param> + /// <param name="exceptionToHandle">Determines which exceptions should be logged. This predicate is evaluated second. + /// If both predicates return <c>false</c>, then the exceptions remains unhandled and may be caught in the calling code.</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> + /// <example> + /// The following code will synchronously call <c>extension.GetData()</c>. + /// exceptionToIgnore predicate prevents logging the <c>OperationCanceledException</c> when relevant cancellation token is canceled. + /// exceptionToHandle predicate causes all other exceptions to be logged. + /// If both predicates returned false, the exception would remain unhandled and may be caught in the calling code. + /// <code> + /// var result = GuardedOperations.CallExtensionPoint( + /// errorSource: extension, + /// call: () => extension.GetData(token), + /// valueOnThrow: string.Empty, + /// exceptionToIgnore: (e) => e is OperationCanceledException && token.IsCancellationRequested, + /// exceptionToHandle: (e) => true); + /// </code> + /// </example> + T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow, Predicate<Exception> exceptionToIgnore, Predicate<Exception> exceptionToHandle); + + } +} diff --git a/src/Editor/Core/Def/ContentType/StandardContentTypeNames.cs b/src/Editor/Core/Def/ContentType/StandardContentTypeNames.cs new file mode 100644 index 0000000..df2fbba --- /dev/null +++ b/src/Editor/Core/Def/ContentType/StandardContentTypeNames.cs @@ -0,0 +1,36 @@ +// +// 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 static class StandardContentTypeNames + { + /// <summary> + /// Base content type of all contents types except for <see cref="Inert"/>. + /// </summary> + public const string Any = "any"; + + /// <summary> + /// Base content type of any content type use for a document. Note that <see cref="Projection"/> does not derive from <see cref="Text"/>. + /// </summary> + public const string Text = "text"; + + /// <summary> + /// Base content type of any document containing code. Derives from <see cref="Text"/>. + /// </summary> + public const string Code = "code"; + + /// <summary> + /// Base content type for a projection of a document that contains a mix of distinct content types (e.g. a .aspx file containing + /// html and embedded c#). + /// </summary> + public const string Projection = "projection"; + + /// <summary> + /// A content type for which no associated artifacts are automatically created. + /// </summary> + public const string Inert = "inert"; + } +} + diff --git a/src/Editor/Core/Def/CoreUtility.csproj b/src/Editor/Core/Def/CoreUtility.csproj index e89b22f..9920ca4 100644 --- a/src/Editor/Core/Def/CoreUtility.csproj +++ b/src/Editor/Core/Def/CoreUtility.csproj @@ -18,5 +18,6 @@ </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.VisualStudio.Threading" /> + <PackageReference Include="System.Collections.Immutable" /> </ItemGroup> </Project>
\ No newline at end of file diff --git a/src/Editor/Core/Def/CoreUtilityAssemblyInfo.cs b/src/Editor/Core/Def/CoreUtilityAssemblyInfo.cs index a47d690..446fe0d 100644 --- a/src/Editor/Core/Def/CoreUtilityAssemblyInfo.cs +++ b/src/Editor/Core/Def/CoreUtilityAssemblyInfo.cs @@ -4,9 +4,14 @@ // using System.Reflection; using System.Runtime.ConstrainedExecution; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Permissions; +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Language.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Platform.VSEditor, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Data.Utilities, PublicKey=" + ThisAssembly.PublicKey)] + // // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information diff --git a/src/Editor/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs b/src/Editor/Core/Def/PooledObjects/ArrayBuilder.Enumerator.cs index dc61741..cd70903 100644 --- a/src/Editor/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs +++ b/src/Editor/Core/Def/PooledObjects/ArrayBuilder.Enumerator.cs @@ -2,14 +2,14 @@ using System.Collections.Generic; -namespace Microsoft.VisualStudio.Text.Utilities +namespace Microsoft.VisualStudio.Utilities { - internal partial class ArrayBuilder<T> + public partial class ArrayBuilder<T> { /// <summary> /// struct enumerator used in foreach. /// </summary> - internal struct Enumerator : IEnumerator<T> + public struct Enumerator : IEnumerator<T> { private readonly ArrayBuilder<T> _builder; private int _index; diff --git a/src/Editor/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs b/src/Editor/Core/Def/PooledObjects/ArrayBuilder.cs index 7e205b3..02ce7d4 100644 --- a/src/Editor/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs +++ b/src/Editor/Core/Def/PooledObjects/ArrayBuilder.cs @@ -5,11 +5,11 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -namespace Microsoft.VisualStudio.Text.Utilities +namespace Microsoft.VisualStudio.Utilities { [DebuggerDisplay("Count = {Count,nq}")] [DebuggerTypeProxy(typeof(ArrayBuilder<>.DebuggerProxy))] - internal sealed partial class ArrayBuilder<T> : IReadOnlyCollection<T>, IReadOnlyList<T> + public sealed partial class ArrayBuilder<T> : IReadOnlyCollection<T>, IReadOnlyList<T> { #region DebuggerProxy @@ -28,7 +28,7 @@ namespace Microsoft.VisualStudio.Text.Utilities get { var result = new T[_builder.Count]; - for (int i = 0; i < result.Length; i++) + for (var i = 0; i < result.Length; i++) { result[i] = _builder[i]; } @@ -49,12 +49,12 @@ namespace Microsoft.VisualStudio.Text.Utilities _builder = ImmutableArray.CreateBuilder<T>(size); } - public ArrayBuilder() : - this(8) + public ArrayBuilder() + : this(8) { } - private ArrayBuilder(ObjectPool<ArrayBuilder<T>> pool) : - this() + private ArrayBuilder(ObjectPool<ArrayBuilder<T>> pool) + : this() { _pool = pool; } @@ -100,7 +100,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { while (index > _builder.Count) { - _builder.Add(default(T)); + _builder.Add(default); } if (index == _builder.Count) @@ -164,8 +164,8 @@ namespace Microsoft.VisualStudio.Text.Utilities public int FindIndex(int startIndex, int count, Predicate<T> match) { - int endIndex = startIndex + count; - for (int i = startIndex; i < endIndex; i++) + var endIndex = startIndex + count; + for (var i = startIndex; i < endIndex; i++) { if (match(_builder[i])) { @@ -176,6 +176,11 @@ namespace Microsoft.VisualStudio.Text.Utilities return -1; } + public bool Remove(T element) + { + return _builder.Remove(element); + } + public void RemoveAt(int index) { _builder.RemoveAt(index); @@ -241,7 +246,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { if (Count == 0) { - return default(ImmutableArray<T>); + return default; } return this.ToImmutable(); @@ -272,7 +277,20 @@ namespace Microsoft.VisualStudio.Text.Utilities /// </summary> public ImmutableArray<T> ToImmutableAndFree() { - var result = this.ToImmutable(); + ImmutableArray<T> result; + if (Count == 0) + { + result = ImmutableArray<T>.Empty; + } + else if (_builder.Capacity == Count) + { + result = _builder.MoveToImmutable(); + } + else + { + result = ToImmutable(); + } + this.Free(); return result; } @@ -302,7 +320,7 @@ namespace Microsoft.VisualStudio.Text.Utilities // while the chance that we will need their size is diminishingly small. // It makes sense to constrain the size to some "not too small" number. // Overall perf does not seem to be very sensitive to this number, so I picked 128 as a limit. - if (this.Count < 128) + if (_builder.Capacity < 128) { if (this.Count != 0) { @@ -341,7 +359,7 @@ namespace Microsoft.VisualStudio.Text.Utilities var builder = GetInstance(); builder.EnsureCapacity(capacity); - for (int i = 0; i < capacity; i++) + for (var i = 0; i < capacity; i++) { builder.Add(fillWithValue); } @@ -383,7 +401,7 @@ namespace Microsoft.VisualStudio.Text.Utilities if (this.Count == 1) { var dictionary1 = new Dictionary<K, ImmutableArray<T>>(1, comparer); - T value = this[0]; + var value = this[0]; dictionary1.Add(keySelector(value), ImmutableArray.Create(value)); return dictionary1; } @@ -396,7 +414,7 @@ namespace Microsoft.VisualStudio.Text.Utilities // bucketize // prevent reallocation. it may not have 'count' entries, but it won't have more. var accumulator = new Dictionary<K, ArrayBuilder<T>>(Count, comparer); - for (int i = 0; i < Count; i++) + for (var i = 0; i < Count; i++) { var item = this[i]; var key = keySelector(item); @@ -482,7 +500,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public void AddMany(T item, int count) { - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { Add(item); } @@ -492,8 +510,8 @@ namespace Microsoft.VisualStudio.Text.Utilities { var set = PooledHashSet<T>.GetInstance(); - int j = 0; - for (int i = 0; i < Count; i++) + var j = 0; + for (var i = 0; i < Count; i++) { if (set.Add(this[i])) { @@ -506,6 +524,28 @@ namespace Microsoft.VisualStudio.Text.Utilities set.Free(); } + public void SortAndRemoveDuplicates(IComparer<T> comparer) + { + if (Count <= 1) + { + return; + } + + Sort(comparer); + + int j = 0; + for (int i = 1; i < Count; i++) + { + if (comparer.Compare(this[j], this[i]) < 0) + { + j++; + this[j] = this[i]; + } + } + + Clip(j + 1); + } + public ImmutableArray<S> SelectDistinct<S>(Func<T, S> selector) { var result = ArrayBuilder<S>.GetInstance(Count); diff --git a/src/Editor/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs b/src/Editor/Core/Def/PooledObjects/ObjectPool`1.cs index 8af4993..43cbb17 100644 --- a/src/Editor/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs +++ b/src/Editor/Core/Def/PooledObjects/ObjectPool`1.cs @@ -18,7 +18,7 @@ using System.Threading; using System.Runtime.CompilerServices; #endif -namespace Microsoft.VisualStudio.Text.Utilities +namespace Microsoft.VisualStudio.Utilities { /// <summary> /// Generic implementation of object pooling pattern with predefined pool size limit. The main @@ -37,7 +37,7 @@ namespace Microsoft.VisualStudio.Text.Utilities /// Rationale: /// If there is no intent for reusing the object, do not use pool - just use "new". /// </summary> - internal class ObjectPool<T> where T : class + public class ObjectPool<T> where T : class { [DebuggerDisplay("{Value,nq}")] private struct Element @@ -133,7 +133,7 @@ namespace Microsoft.VisualStudio.Text.Utilities // Note that the initial read is optimistically not synchronized. That is intentional. // We will interlock only when we have a candidate. in a worst case we may miss some // recently returned objects. Not a big deal. - T inst = _firstItem; + var inst = _firstItem; if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst)) { inst = AllocateSlow(); @@ -155,12 +155,12 @@ namespace Microsoft.VisualStudio.Text.Utilities { var items = _items; - for (int i = 0; i < items.Length; i++) + for (var i = 0; i < items.Length; i++) { // Note that the initial read is optimistically not synchronized. That is intentional. // We will interlock only when we have a candidate. in a worst case we may miss some // recently returned objects. Not a big deal. - T inst = items[i].Value; + var inst = items[i].Value; if (inst != null) { if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst)) @@ -202,7 +202,7 @@ namespace Microsoft.VisualStudio.Text.Utilities private void FreeSlow(T obj) { var items = _items; - for (int i = 0; i < items.Length; i++) + for (var i = 0; i < items.Length; i++) { if (items[i].Value == null) { @@ -224,9 +224,8 @@ 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 + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822")] internal void ForgetTrackedObject(T old, T replacement = null) -#pragma warning restore CA1822 // Mark members as static { #if DETECT_LEAKS LeakTracker tracker; @@ -266,7 +265,7 @@ namespace Microsoft.VisualStudio.Text.Utilities Debug.Assert(_firstItem != obj, "freeing twice?"); var items = _items; - for (int i = 0; i < items.Length; i++) + for (var i = 0; i < items.Length; i++) { var value = items[i].Value; if (value == null) diff --git a/src/Editor/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs b/src/Editor/Core/Def/PooledObjects/PooledDictionary.cs index a98addd..c630e0a 100644 --- a/src/Editor/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs +++ b/src/Editor/Core/Def/PooledObjects/PooledDictionary.cs @@ -4,15 +4,16 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -namespace Microsoft.VisualStudio.Text.Utilities +namespace Microsoft.VisualStudio.Utilities { // Dictionary that can be recycled via an object pool // NOTE: these dictionaries always have the default comparer. - internal class PooledDictionary<K, V> : Dictionary<K, V> + public sealed class PooledDictionary<K, V> : Dictionary<K, V> { private readonly ObjectPool<PooledDictionary<K, V>> _pool; - private PooledDictionary(ObjectPool<PooledDictionary<K, V>> pool) + private PooledDictionary(ObjectPool<PooledDictionary<K, V>> pool, IEqualityComparer<K> keyComparer) + : base(keyComparer) { _pool = pool; } @@ -31,13 +32,13 @@ namespace Microsoft.VisualStudio.Text.Utilities } // global pool - private static readonly ObjectPool<PooledDictionary<K, V>> s_poolInstance = CreatePool(); + private static readonly ObjectPool<PooledDictionary<K, V>> s_poolInstance = CreatePool(EqualityComparer<K>.Default); // if someone needs to create a pool; - public static ObjectPool<PooledDictionary<K, V>> CreatePool() + public static ObjectPool<PooledDictionary<K, V>> CreatePool(IEqualityComparer<K> keyComparer) { ObjectPool<PooledDictionary<K, V>> pool = null; - pool = new ObjectPool<PooledDictionary<K, V>>(() => new PooledDictionary<K, V>(pool), 128); + pool = new ObjectPool<PooledDictionary<K, V>>(() => new PooledDictionary<K, V>(pool, keyComparer), 128); return pool; } diff --git a/src/Editor/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs b/src/Editor/Core/Def/PooledObjects/PooledHashSet.cs index 2969170..4e09136 100644 --- a/src/Editor/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs +++ b/src/Editor/Core/Def/PooledObjects/PooledHashSet.cs @@ -3,15 +3,16 @@ using System.Collections.Generic; using System.Diagnostics; -namespace Microsoft.VisualStudio.Text.Utilities +namespace Microsoft.VisualStudio.Utilities { // HashSet that can be recycled via an object pool // NOTE: these HashSets always have the default comparer. - internal class PooledHashSet<T> : HashSet<T> + public sealed class PooledHashSet<T> : HashSet<T> { private readonly ObjectPool<PooledHashSet<T>> _pool; - private PooledHashSet(ObjectPool<PooledHashSet<T>> pool) + private PooledHashSet(ObjectPool<PooledHashSet<T>> pool, IEqualityComparer<T> equalityComparer) : + base(equalityComparer) { _pool = pool; } @@ -23,13 +24,13 @@ namespace Microsoft.VisualStudio.Text.Utilities } // global pool - private static readonly ObjectPool<PooledHashSet<T>> s_poolInstance = CreatePool(); + private static readonly ObjectPool<PooledHashSet<T>> s_poolInstance = CreatePool(EqualityComparer<T>.Default); // if someone needs to create a pool; - public static ObjectPool<PooledHashSet<T>> CreatePool() + public static ObjectPool<PooledHashSet<T>> CreatePool(IEqualityComparer<T> equalityComparer) { ObjectPool<PooledHashSet<T>> pool = null; - pool = new ObjectPool<PooledHashSet<T>>(() => new PooledHashSet<T>(pool), 128); + pool = new ObjectPool<PooledHashSet<T>>(() => new PooledHashSet<T>(pool, equalityComparer), 128); return pool; } diff --git a/src/Editor/Core/Def/PooledObjects/PooledStopwatch.cs b/src/Editor/Core/Def/PooledObjects/PooledStopwatch.cs new file mode 100644 index 0000000..d81ca9b --- /dev/null +++ b/src/Editor/Core/Def/PooledObjects/PooledStopwatch.cs @@ -0,0 +1,38 @@ +// 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.Diagnostics; + +namespace Microsoft.VisualStudio.Utilities +{ + public class PooledStopwatch : Stopwatch + { + private static readonly ObjectPool<PooledStopwatch> s_poolInstance = CreatePool(); + + private readonly ObjectPool<PooledStopwatch> _pool; + + private PooledStopwatch(ObjectPool<PooledStopwatch> pool) + { + _pool = pool; + } + + public void Free() + { + Reset(); + _pool?.Free(this); + } + + public static ObjectPool<PooledStopwatch> CreatePool() + { + ObjectPool<PooledStopwatch> pool = null; + pool = new ObjectPool<PooledStopwatch>(() => new PooledStopwatch(pool), 128); + return pool; + } + + public static PooledStopwatch StartInstance() + { + var instance = s_poolInstance.Allocate(); + instance.Restart(); + return instance; + } + } +} diff --git a/src/Editor/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs b/src/Editor/Core/Def/PooledObjects/PooledStringBuilder.cs index b618095..abd3867 100644 --- a/src/Editor/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs +++ b/src/Editor/Core/Def/PooledObjects/PooledStringBuilder.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Text; -namespace Microsoft.VisualStudio.Text.Utilities +namespace Microsoft.VisualStudio.Utilities { /// <summary> /// The usage is: @@ -13,8 +13,9 @@ namespace Microsoft.VisualStudio.Text.Utilities /// ... sb.ToString() ... /// inst.Free(); /// </summary> - internal class PooledStringBuilder + public class PooledStringBuilder { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104")] public readonly StringBuilder Builder = new StringBuilder(); private readonly ObjectPool<PooledStringBuilder> _pool; @@ -53,7 +54,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public string ToStringAndFree() { - string result = this.Builder.ToString(); + var result = this.Builder.ToString(); this.Free(); return result; @@ -61,7 +62,7 @@ namespace Microsoft.VisualStudio.Text.Utilities public string ToStringAndFree(int startIndex, int length) { - string result = this.Builder.ToString(startIndex, length); + var result = this.Builder.ToString(startIndex, length); this.Free(); return result; @@ -71,10 +72,15 @@ namespace Microsoft.VisualStudio.Text.Utilities private static readonly ObjectPool<PooledStringBuilder> s_poolInstance = CreatePool(); // if someone needs to create a private pool; - public static ObjectPool<PooledStringBuilder> CreatePool() + /// <summary> + /// If someone need to create a private pool + /// </summary> + /// <param name="size">The size of the pool.</param> + /// <returns></returns> + public static ObjectPool<PooledStringBuilder> CreatePool(int size = 32) { ObjectPool<PooledStringBuilder> pool = null; - pool = new ObjectPool<PooledStringBuilder>(() => new PooledStringBuilder(pool), 32); + pool = new ObjectPool<PooledStringBuilder>(() => new PooledStringBuilder(pool), size); return pool; } diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionContext.cs b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionContext.cs index aa5d181..7e75d4e 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionContext.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionContext.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data { @@ -16,7 +14,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data /// <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); + public static CompletionContext Empty { get; } = new CompletionContext(ImmutableArray<CompletionItem>.Empty, ImmutableArray<CompletionFilterWithState>.Empty); /// <summary> /// Set of completion items available at a location @@ -24,6 +22,22 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data public ImmutableArray<CompletionItem> Items { get; } /// <summary> + /// <para> + /// Set of completion filters available for this session. + /// Each filter's <see cref="CompletionFilterWithState.IsSelected"/> property is used to determine initial selection. + /// The <see cref="CompletionFilterWithState.IsAvailable"/> property is ignored. + /// </para> + /// <para> + /// Typically, this is used to select <see cref="CompletionExpander"/>s that correspond to provided <see cref="CompletionItem"/>s, + /// in scenarios when the completion source provides expanded items by default. + /// </para> + /// </summary> + /// <remarks> + /// When the value is uninitialized, then <see cref="Items"/> need to be enumerated to find the filters. + /// </remarks> + public ImmutableArray<CompletionFilterWithState> Filters { 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> @@ -37,12 +51,32 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data public SuggestionItemOptions SuggestionItemOptions { get; } /// <summary> - /// Constructs <see cref="CompletionContext"/> with specified <see cref="CompletionItem"/>s, + /// [Deprecated] Constructs <see cref="CompletionContext"/> with specified <see cref="CompletionItem"/>s, /// with recommendation to not use suggestion mode and to use use regular selection. + /// Note: completion will iterate through all items to determine filters. + /// For better performance, use the overload which accepts <see cref="ImmutableArray{CompletionFilterWithState}"/> /// </summary> /// <param name="items">Available completion items. If none are available, use <c>CompletionContext.Default</c></param> public CompletionContext(ImmutableArray<CompletionItem> items) - : this(items, suggestionItemOptions: null, selectionHint: InitialSelectionHint.RegularSelection) + : this(items, + suggestionItemOptions: null, + selectionHint: InitialSelectionHint.RegularSelection, + filters: default) + { + } + + /// <summary> + /// Constructs <see cref="CompletionContext"/> with specified <see cref="CompletionItem"/>s and <see cref="CompletionFilterWithState"/>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 <c>CompletionContext.Default</c></param> + /// <param name="filters">Available completion filters. Each filter's <see cref="CompletionFilterWithState.IsSelected"/> property is used to determine initial selection. + /// The <see cref="CompletionFilterWithState.IsAvailable"/> property is ignored.</param> + public CompletionContext(ImmutableArray<CompletionItem> items, ImmutableArray<CompletionFilterWithState> filters) + : this(items, + suggestionItemOptions: null, + selectionHint: InitialSelectionHint.RegularSelection, + filters: filters) { } @@ -55,7 +89,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data public CompletionContext( ImmutableArray<CompletionItem> items, SuggestionItemOptions suggestionItemOptions) - : this(items, suggestionItemOptions, InitialSelectionHint.RegularSelection) + : this(items, + suggestionItemOptions, + selectionHint: InitialSelectionHint.RegularSelection, + filters: default) { } @@ -70,12 +107,33 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data ImmutableArray<CompletionItem> items, SuggestionItemOptions suggestionItemOptions, InitialSelectionHint selectionHint) + : this (items, + suggestionItemOptions, + selectionHint, + filters: default) + { } + + /// <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 <c>null</c></param> + /// <param name="selectionHint">Recommended selection mode. Suggestion mode automatically sets soft selection Default is <c>InitialSelectionHint.RegularSelection</c></param> + /// <param name="filters">Available completion filters. Each filter's <see cref="CompletionFilterWithState.IsSelected"/> property is used to determine initial selection. + /// The <see cref="CompletionFilterWithState.IsAvailable"/> property is ignored.</param> + public CompletionContext( + ImmutableArray<CompletionItem> items, + SuggestionItemOptions suggestionItemOptions, + InitialSelectionHint selectionHint, + ImmutableArray<CompletionFilterWithState> filters) { if (items.IsDefault) throw new ArgumentException("Array must be initialized", nameof(items)); Items = items; SelectionHint = selectionHint; SuggestionItemOptions = suggestionItemOptions; + Filters = filters; } } } diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionExpander.cs b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionExpander.cs new file mode 100644 index 0000000..cbb9533 --- /dev/null +++ b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionExpander.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using Microsoft.VisualStudio.Text.Adornments; + +namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data +{ + /// <summary> + /// Identifies an expander that adds <see cref="CompletionItem"/>s that reference it to the list of completions. + /// </summary> + /// <remarks> + /// <para> + /// Expander apperars in the UI alongside <see cref="CompletionFilter"/>s, but behaves differently: + /// When <see cref="CompletionFilter"/> is selected, then <see cref="CompletionItem"/>s that don't reference it are hidden + /// When <see cref="CompletionExpander"/> is selected, then <see cref="CompletionItem"/>s that reference it are visible + /// When no <see cref="CompletionFilter"/>s are selected, then all <see cref="CompletionItem"/>s that don't reference an expander are visible + /// When no <see cref="CompletionExpander"/>s are selected, then all <see cref="CompletionItem"/>s reference an expander are hidden + /// </para> + /// <para> + /// These instances should be singletons. All <see cref="CompletionItem"/>s that should be filtered + /// using the same expander button must use the same reference to the instance of <see cref="CompletionExpander"/>. + /// </para> + /// </remarks> + /// <example> + /// <code> + /// static CompletionExpander MyExpander = new CompletionFilter("Additional items", "a", MyAdditionalItemsImageElement); + /// </code> + /// </example> + [DebuggerDisplay("+ {DisplayText}")] + public class CompletionExpander : CompletionFilter + { + /// <summary> + /// Constructs an instance of <see cref="CompletionExpander"/>. + /// </summary> + /// <param name="displayText">Name of this expander</param> + /// <param name="accessKey">Key used in a keyboard shortcut that toggles this expander</param> + /// <param name="image">Image which represents this expander</param> + public CompletionExpander(string displayText, string accessKey, ImageElement image) + : base(displayText, accessKey, image) + { } + } +} diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionFilter.cs b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionFilter.cs index 9486259..a4c848b 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionFilter.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionFilter.cs @@ -18,7 +18,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data /// </code> /// </example> [DebuggerDisplay("{DisplayText}")] - public sealed class CompletionFilter : INotifyPropertyChanged + public class CompletionFilter : INotifyPropertyChanged { /// <summary> /// Localized name of this filter. @@ -36,11 +36,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data public ImageElement Image { get; } /// <summary> - /// Constructs an instance of CompletionFilter. + /// Constructs an instance of <see cref="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> + /// <param name="image">Image which represents this filter</param> public CompletionFilter(string displayText, string accessKey, ImageElement image) { if (string.IsNullOrWhiteSpace(displayText)) diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs index 2539c82..95709c6 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs @@ -55,6 +55,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data public static bool operator !=(CompletionItemWithHighlight left, CompletionItemWithHighlight right) => !(left == right); - public override int GetHashCode() => CompletionItem.GetHashCode() ^ HighlightedSpans.GetHashCode(); + /// <summary> + /// Assumption: We won't see two instances of <see cref="CompletionItemWithHighlight"/> with same <see cref="CompletionItem"/> but different highlighting. + /// Therefore, we don't calculate hash code for the highlights. + /// </summary> + /// <returns><see cref="CompletionItem.GetHashCode"/></returns> + public override int GetHashCode() => CompletionItem.GetHashCode(); } } diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/Data/ComputedCompletionItems.cs b/src/Editor/Language/Def/Language/AsyncCompletion/Data/ComputedCompletionItems.cs index 463b5f7..df2866b 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/Data/ComputedCompletionItems.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/Data/ComputedCompletionItems.cs @@ -26,7 +26,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data bool suggestionItemSelected, bool usesSoftSelection) { - _items = items; + _itemsWithoutHighlight = items; SuggestionItem = suggestionItem; SelectedItem = selectedItem; SuggestionItemSelected = suggestionItemSelected; @@ -61,13 +61,47 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data /// </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; + private ImmutableArray<CompletionItem> _itemsWithoutHighlight; + private ImmutableArray<CompletionItemWithHighlight> _itemsWithHighlight; + + private IEnumerable<CompletionItem> _computedItems = null; /// <summary> /// <see cref="CompletionItem"/>s displayed in the completion UI /// </summary> - public IEnumerable<CompletionItem> Items => _items ?? _itemsWithHighlight.Select(n => n.CompletionItem); + public IEnumerable<CompletionItem> Items + { + get + { + if (_computedItems == null) + { + // We were constructed with either items or itemsWithHighlight + if (_itemsWithoutHighlight != null) + { + _computedItems = _itemsWithoutHighlight.IsDefault + ? Enumerable.Empty<CompletionItem>() + : _itemsWithoutHighlight; + } + else + { + if (_itemsWithHighlight.IsDefault) + { + _computedItems = Enumerable.Empty<CompletionItem>(); + } + else + { + var items = new List<CompletionItem>(_itemsWithHighlight.Length); + for (int i = 0; i < _itemsWithHighlight.Length; i++) + { + items.Add(_itemsWithHighlight[i].CompletionItem); + } + _computedItems = items; + } + } + } + return _computedItems; + } + } /// <summary> /// Suggestion <see cref="CompletionItem"/> displayed in the UI, or null if no suggestion is displayed diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs index e425879..b011ea1 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs @@ -60,9 +60,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion /// 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> + /// <returns><c>true</c> if the unique item was committed</returns> bool CommitIfUnique(CancellationToken token); - + /// <summary> /// Returns the <see cref="ITextView"/> this session is active on. /// </summary> diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs index 6a307eb..88e22bf 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs @@ -1,4 +1,5 @@ using System.Threading; +using System.Threading.Tasks; using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -23,6 +24,16 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion bool IsStarted { get; } /// <summary> + /// Returns the intial trigger + /// </summary> + CompletionTrigger InitialTrigger { get; } + + /// <summary> + /// Returns the location of the initial trigger + /// </summary> + SnapshotPoint InitialTriggerLocation { get; } + + /// <summary> /// Enqueues selection a specified item. When all queued tasks are completed, the UI updates. /// </summary> void SelectCompletionItem(CompletionItem item); @@ -38,6 +49,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion void InvokeAndCommitIfUnique(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token); /// <summary> + /// Starts asynchronous computation, which results in either committing of the single <see cref="CompletionItem"/> or opening the completion UI. + /// Calling <see cref="OpenOrUpdate(CompletionTrigger, SnapshotPoint, CancellationToken)"/> cancels the operation and dismisses the session. + /// Must be called on the UI thread to correctly set state of the session. + /// </summary> + /// <param name="token">Token used to cancel this operation</param> + /// <returns><c>true</c> if the unique item was committed</returns> + Task<bool> CommitIfUniqueAsync(CancellationToken token); + + /// <summary> /// Enqueues selecting the next item. When all queued tasks are completed, the UI updates. /// </summary> void SelectDown(); diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs index 8db4447..080c327 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs @@ -8,7 +8,7 @@ using Microsoft.VisualStudio.Text.Editor; namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion { /// <summary> - /// Represents a class that provides <see cref="CompletionItem"/>s and other information + /// Represents an object that provides <see cref="CompletionItem"/>s and other information /// relevant to the completion feature at a specific <see cref="SnapshotPoint"/>. /// </summary> /// <remarks> @@ -26,7 +26,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion /// <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(IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token); + Task<CompletionContext> GetCompletionContextAsync( + IAsyncCompletionSession session, + CompletionTrigger trigger, + SnapshotPoint triggerLocation, + SnapshotSpan applicableToSpan, + CancellationToken token); /// <summary> /// Returns tooltip associated with provided <see cref="CompletionItem"/>. diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncExpandingCompletionSource.cs b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncExpandingCompletionSource.cs new file mode 100644 index 0000000..51ac57a --- /dev/null +++ b/src/Editor/Language/Def/Language/AsyncCompletion/IAsyncExpandingCompletionSource.cs @@ -0,0 +1,40 @@ +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 an object that provides <see cref="CompletionItem"/>s and other information + /// relevant to the completion feature at a specific <see cref="SnapshotPoint"/>. + /// Additionally, this object has capability to provide additional <see cref="CompletionItem"/>s + /// in a reaction to user interacting with <see cref="CompletionExpander"/>. If this capability + /// is not necessary, then it is sufficient to implement just <see cref="IAsyncCompletionSource"/>. + /// </summary> + /// <remarks> + /// Instances of this class should be created by <see cref="IAsyncCompletionSourceProvider"/>, which is a MEF part. + /// </remarks> + public interface IAsyncExpandingCompletionSource : IAsyncCompletionSource + { + /// <summary> + /// Called when user interacts with expander buttons, + /// requesting the completion source to provide additional completion items pertinent to the expander button. + /// For best performance, do not provide <see cref="CompletionContext.Filters"/> unless expansion should add new filters. + /// Called on a background thread. + /// </summary> + /// <param name="session">Reference to the active <see cref="IAsyncCompletionSession"/></param> + /// <param name="expander">Expander which caused this call</param> + /// <param name="initialTrigger">What initially caused the completion</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> GetExpandedCompletionContextAsync( + IAsyncCompletionSession session, + CompletionExpander expander, + CompletionTrigger initialTrigger, + SnapshotSpan applicableToSpan, + CancellationToken token); + } +} diff --git a/src/Editor/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs b/src/Editor/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs index 17becc9..5195588 100644 --- a/src/Editor/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs +++ b/src/Editor/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.Commanding; +using System; +using Microsoft.VisualStudio.Commanding; namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion { @@ -22,6 +23,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion /// </summary> public const string CompletionCommandHandler = "CompletionCommandHandler"; + [Obsolete("Use Microsoft.VisualStudio.Text.Editor.DefaultOptions.NonBlockingCompletionOptionName instead")] /// <summary> /// Name of the editor option that stores user's preference for dismissing completion rather than blocking for potentially long running tasks. /// </summary> @@ -36,5 +38,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion /// Name of the editor option that stores user's preference for the completion mode during debugging. /// </summary> public const string SuggestionModeInDebuggerCompletionOptionName = "SuggestionModeInDebuggerViewCompletion"; + + /// <summary> + /// Order your MEF part of type <see cref="Data.CompletionFilter"/> relatively to this name, + /// so that it tends to be the default expander (order before this name) or not be the default expander (order after this name). + /// </summary> + public const string DefaultCompletionExpander = "DefaultCompletionExpander"; } } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs index 798f68a..b873c88 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs @@ -20,7 +20,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement internal sealed class AsyncCompletionBroker : IAsyncCompletionBroker { [Import] - private IGuardedOperations GuardedOperations; + private IGuardedOperationsInternal GuardedOperations; [Import] private JoinableTaskContext JoinableTaskContext; @@ -84,7 +84,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public bool IsCompletionActive(ITextView textView) { - return textView.Properties.ContainsProperty(typeof(IAsyncCompletionSession)); + return textView?.Properties?.ContainsProperty(typeof(IAsyncCompletionSession)) == true; } public bool IsCompletionSupported(IContentType contentType) => CompletionAvailability.IsAvailable(contentType, roles: null); // This will call HasCompletionProviders among doing other checks @@ -133,7 +133,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement // 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.IsCurrentlyAvailable(textView, contentTypeToCheckBlacklist: triggerLocation.Snapshot.ContentType)) + if (!CompletionAvailability.IsCurrentlyAvailable(textView)) return null; if (textView.IsClosed) @@ -150,6 +150,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if (token.IsCancellationRequested || textView.IsClosed) return null; + // See if we can use more aggressive cancellation token for typing scenarios + if (trigger.Reason == CompletionTriggerReason.Insertion) + token = CompletionUtilities.GetResponsiveToken(textView, token); + GetCompletionSources(triggerLocation, GetItemSourceProviders, rootSnapshot, textView, textView.BufferGraph, trigger, telemetry, token, out var sourcesWithLocations, out var applicableToSpan); @@ -223,13 +227,16 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement var aggregatingSession = AsyncCompletionSession.CreateAggregatingSession(applicableToSpan, JoinableTaskContext, sourcesWithLocations, this, textView, telemetry, GuardedOperations); - var completionData = await aggregatingSession.ConnectToCompletionSources(trigger, triggerLocation, rootSnapshot, token).ConfigureAwait(true); + var completionData = await aggregatingSession.ConnectToCompletionSources( + trigger, triggerLocation, rootSnapshot, + getExpandedContext: false, initialItems: default, expander: default, + token: token).ConfigureAwait(true); if (completionData.IsCanceled) return AggregatedCompletionContext.Empty; var aggregateCompletionContext = new CompletionContext( - completionData.InitialCompletionItems, + completionData.Items, completionData.RequestedSuggestionItemOptions, completionData.InitialSelectionHint); return new AggregatedCompletionContext(aggregateCompletionContext, aggregatingSession); @@ -249,7 +256,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// is inappropriate, because it might be an elision buffer. If we map down from the elision buffer, /// we may locate incorrect points around elided text. /// - /// /// Note that the root snapshot cannot be use to realize the <see cref="IAsyncCompletionSession.ApplicableToSpan"/>, + /// Note that the root snapshot cannot be use to realize the <see cref="IAsyncCompletionSession.ApplicableToSpan"/>, /// which is always defined on the <see cref="ITextView.TextSnapshot"/> /// </summary> /// <param name="textView">TextView which will host completion</param> diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs index 0a13199..ca0f01c 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs @@ -5,9 +5,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Language.Intellisense.Implementation.AsyncCompletion; 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; @@ -25,11 +25,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement private readonly IList<(IAsyncCompletionSource Source, SnapshotPoint Point)> _completionSources; private readonly IList<(IAsyncCompletionCommitManager, ITextBuffer)> _commitManagers; private readonly IAsyncCompletionItemManager _completionItemManager; - private readonly JoinableTaskContext JoinableTaskContext; + private readonly JoinableTaskContext _jtc; private readonly ICompletionPresenterProvider _presenterProvider; private readonly AsyncCompletionBroker _broker; private readonly ITextView _textView; - private readonly IGuardedOperations _guardedOperations; + private readonly IGuardedOperationsInternal _guardedOperations; private readonly ImmutableArray<char> _potentialCommitChars; // Presentation: @@ -69,9 +69,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement } /// <summary> - /// Stores the initial reason this session was triggererd. + /// Stores the reason this session was initially triggererd. /// </summary> - private CompletionTrigger InitialTrigger { get; set; } + public CompletionTrigger InitialTrigger { get; private set; } + + /// <summary> + /// Stores the location this session was initially triggered. + /// </summary> + public SnapshotPoint InitialTriggerLocation { get; private set; } /// <summary> /// Text to display in place of suggestion mode when filtered text is empty. @@ -91,7 +96,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement private readonly CompletionSessionTelemetry _telemetry; /// <summary> - /// Records noteworthy event which led to committing or dismissing + /// Records noteworthy event which led to committing or dismissing. Used in End To End telemetry. + /// If left unset, it means that the scenario is unremarkable. /// </summary> CompletionSessionState _finalSessionState; @@ -121,6 +127,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public PropertyCollection Properties { get; } + // Allow a blocking operation to run on the background thread until canceled by user's action + private DeferredBlockingOperation<bool> DeferredOperation { get; set; } + public AsyncCompletionSession( SnapshotSpan initialApplicableToSpan, ImmutableArray<char> potentialCommitChars, @@ -132,10 +141,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement AsyncCompletionBroker broker, ITextView textView, CompletionSessionTelemetry telemetry, - IGuardedOperations guardedOperations) + IGuardedOperationsInternal guardedOperations) { _potentialCommitChars = potentialCommitChars; - JoinableTaskContext = joinableTaskContext; + _jtc = joinableTaskContext; _presenterProvider = presenterProvider; _broker = broker; _completionSources = completionSources; // still prorotype at the momemnt. @@ -163,7 +172,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement AsyncCompletionBroker broker, ITextView textView, CompletionSessionTelemetry telemetry, - IGuardedOperations guardedOperations) + IGuardedOperationsInternal guardedOperations) { return new AsyncCompletionSession( applicableToSpan, @@ -183,10 +192,25 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement { if (IsDismissed) return false; - if (!JoinableTaskContext.IsOnMainThread) + if (!_jtc.IsOnMainThread) throw new InvalidOperationException($"This method must be callled on the UI thread."); if (!_potentialCommitChars.Contains(typedChar)) return false; + if (DeferredOperation != null) + DeferredOperation.Cancel(); + + // Before we further block UI thread, let's see if we can dismiss in suggestion or non blocking mode + var inNonBlockingMode = CompletionUtilities.GetNonBlockingCompletionOption(_textView); + var inSuggestionMode = CompletionUtilities.GetSuggestionModeOption(_textView); + if (EligibleToQuicklyDismiss(_computation, typedChar, inSuggestionMode || inNonBlockingMode)) + { + // For simplicity of implementation, let's pretend that we want to commit, + // so that the commit code appropriately dismisses the session. + return true; + } + + // See if we can use more aggressive cancellation token + token = CompletionUtilities.GetResponsiveToken(_textView, token); var rootSnapshot = AsyncCompletionBroker.GetRootSnapshot(TextView); var points = MappingHelper.GetPointsAtLocation(triggerLocation, rootSnapshot); @@ -211,7 +235,16 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement var shouldCommit = _guardedOperations.CallExtensionPoint( errorSource: commitManager, call: () => commitManager.ShouldCommitCompletion(this, relevantPoint, typedChar, token), - valueOnThrow: false); + valueOnThrow: false, + exceptionToIgnore: (e) => e is OperationCanceledException && token.IsCancellationRequested, + exceptionToHandle: (e) => true); + + if (token.IsCancellationRequested) + { + _telemetry.RecordBlockingExtension(commitManager); + _finalSessionState = CompletionSessionState.DismissedDueToCancellation; + return false; + } if (shouldCommit) return true; @@ -224,7 +257,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if (IsDismissed) return false; - if (!JoinableTaskContext.IsOnMainThread) + if (!_jtc.IsOnMainThread) throw new InvalidOperationException($"This method must be callled on the UI thread."); _telemetry.UiStopwatch.Restart(); @@ -240,7 +273,64 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement { return false; } - else if (lastModel.UniqueItem != null) + + return CommitIfUniqueCore(lastModel, token); + } + + async Task<bool> IAsyncCompletionSessionOperations.CommitIfUniqueAsync(CancellationToken token) + { + if (IsDismissed) + return await Task.FromResult(false).ConfigureAwait(false); + + if (!_jtc.IsOnMainThread) + throw new InvalidOperationException($"This method must be callled on the UI thread."); + + if (DeferredOperation == null) + { + var deferredOperation = new DeferredBlockingOperation<bool>(_jtc, CommitIfUniqueAsyncOperation, token); + + // Assign the property to allow others to cancel this operation. + DeferredOperation = deferredOperation; + } + else + { + // DeferredOperation is already set (for example, user repeatedly pressed Ctrl+Space) + // Instead of resetting it, await completion of the existing DeferredOperation. + } + + var result = await DeferredOperation.Operation.Task.ConfigureAwait(false); + if (result) + Dismiss(); + + return result; + } + + private async Task<bool> CommitIfUniqueAsyncOperation(CancellationToken token) + { + _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; + } + + var responsiveCommitToken = CompletionUtilities.GetResponsiveToken(_textView, CancellationToken.None); + await _jtc.Factory.SwitchToMainThreadAsync(); + DeferredOperation = null; // we no longer need this + + return CommitIfUniqueCore(lastModel, responsiveCommitToken); + } + + private bool CommitIfUniqueCore(CompletionModel lastModel, CancellationToken token) + { + if (lastModel.UniqueItem != null) { _finalSessionState = CompletionSessionState.CommittedThroughCompleteWord; var behavior = CommitItem(default, lastModel.UniqueItem, ApplicableToSpan, token); @@ -283,31 +373,64 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if (IsDismissed) return CommitBehavior.None; - if (!JoinableTaskContext.IsOnMainThread) + if (!_jtc.IsOnMainThread) throw new InvalidOperationException($"This method must be callled on the UI thread."); - // We are in either low latency mode or suggestion mode - // user did not press tab, and we don't have results yet - // => dismiss - if ((_computation.RecentModel == default || _computation.RecentModel.Uninitialized) - && (CompletionUtilities.GetSuggestionModeOption(_textView) || CompletionUtilities.GetNonBlockingCompletionOption(_textView)) - && !(typedChar.Equals(default) || typedChar.Equals('\t'))) - { - _finalSessionState = CompletionSessionState.DismissedDueToNonBlockingMode; - ((IAsyncCompletionSession)this).Dismiss(); - return CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers; - } + if (DeferredOperation != null) + DeferredOperation.Cancel(); - CompletionModel lastModel; + var inSuggestionMode = CompletionUtilities.GetSuggestionModeOption(_textView); + var inNonBlockingMode = CompletionUtilities.GetNonBlockingCompletionOption(_textView); + var inResponisveMode = CompletionUtilities.GetResponsiveCompletionOption(_textView); - // We are in low latency mode - // => use recently computed model, but don't block waiting for one - if (CompletionUtilities.GetNonBlockingCompletionOption(_textView)) + CompletionModel lastModel; + if (EligibleToQuicklyDismiss(_computation, typedChar, inSuggestionMode || inNonBlockingMode || inResponisveMode)) { - lastModel = _computation.RecentModel; + // We haven't received the completion items yet. See if we are in any eligible modes. + if (inNonBlockingMode || inSuggestionMode) + { + // We are fully non blocking. Dismiss immediately + _finalSessionState = inNonBlockingMode ? CompletionSessionState.DismissedDueToNonBlockingMode : CompletionSessionState.DismissedDueToSuggestionMode; + ((IAsyncCompletionSession)this).Dismiss(); + return CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers; + } + else + { + // Await for completion of tasks, but no longer than either commanding or responsive token + var computationWaitToken = CompletionUtilities.GetResponsiveToken(_textView, token); + + _telemetry.UiStopwatch.Restart(); + lastModel = _computation.WaitAndGetResult(cancelUi: true, computationWaitToken); + _telemetry.UiStopwatch.Stop(); + + // We already waited a little bit. + // If lastModel is not null, we have finished all computation + // If lastModel is null, check if we at least received completion items. If that's the case, continue waiting for filtering + + if (lastModel == null && (_computation == default || _computation.RecentModel == default || _computation.RecentModel.Uninitialized)) + { + _telemetry.RecordBlockingWaitForComputation(_telemetry.UiStopwatch.ElapsedMilliseconds); + + // We still haven't received completion items. Dismiss. + _finalSessionState = CompletionSessionState.DismissedDueToResponsiveMode; + ((IAsyncCompletionSession)this).Dismiss(); + return CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers; + } + else + { + // Continue waiting for rest of computation (e.g. filtering) + _telemetry.UiStopwatch.Start(); + lastModel = _computation.WaitAndGetResult(cancelUi: true, token); + _telemetry.UiStopwatch.Stop(); + _telemetry.RecordBlockingWaitForComputation(_telemetry.UiStopwatch.ElapsedMilliseconds); + } + } } else { + // User explicitly wanted to commit, or we already had results. + // Wrap up remaining filtering tasks and continue with commit + _telemetry.UiStopwatch.Restart(); lastModel = _computation.WaitAndGetResult(cancelUi: true, token); _telemetry.UiStopwatch.Stop(); @@ -327,18 +450,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement } else if (lastModel.Uninitialized) { + _finalSessionState = CompletionSessionState.DismissedUninitialized; ((IAsyncCompletionSession)this).Dismiss(); return CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers; } else if (lastModel.UseSoftSelection - && !(typedChar.Equals(default) - || typedChar.Equals('\t') - || TypedCharShouldNotDismissInSoftSelection(typedChar))) + && !(IsTabOrEmpty(typedChar) || TypedCharShouldNotDismissInSoftSelection(typedChar))) { // In soft selection mode, allow commit under the following circumstances: // 1. User commits explicitly (click, tab) // 2. User typed a character which is excluded from list of potential commit characters in the given session // Otherwise, dismiss the session + _finalSessionState = CompletionSessionState.DismissedDueToSuggestionMode; ((IAsyncCompletionSession)this).Dismiss(); return CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers; } @@ -356,13 +479,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement else if (lastModel.PresentedItems.IsDefaultOrEmpty) { // There is nothing to commit + _finalSessionState = CompletionSessionState.DismissedDueToNoItems; Dismiss(); return CommitBehavior.None; } else { // Regular commit - _finalSessionState = CompletionSessionState.Committed; + _finalSessionState = IsTabOrEmpty(typedChar) ? CompletionSessionState.Committed : CompletionSessionState.CommittedThroughTypedChar; return CommitItem(typedChar, lastModel.PresentedItems[lastModel.SelectedIndex].CompletionItem, ApplicableToSpan, token); } } @@ -376,13 +500,20 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement _telemetry.UiStopwatch.Restart(); IAsyncCompletionCommitManager managerWhoCommitted = null; + var versionBeforeChange = applicableToSpan.TextBuffer.CurrentSnapshot.Version; + bool commitHandled = false; - foreach (var commitManager in _commitManagers) + foreach (var commitManagerWithBuffer in _commitManagers) { + var commitManager = commitManagerWithBuffer.Item1; + var textBuffer = commitManagerWithBuffer.Item2; + var commitResult = _guardedOperations.CallExtensionPoint( errorSource: commitManager, - call: () => commitManager.Item1.TryCommit(this, commitManager.Item2 /* buffer */, itemToCommit, typedChar, token), - valueOnThrow: CommitResult.Unhandled); + call: () => commitManager.TryCommit(this, textBuffer, itemToCommit, typedChar, token), + valueOnThrow: CommitResult.Unhandled, + exceptionToIgnore: (e) => e is OperationCanceledException && token.IsCancellationRequested, + exceptionToHandle: (e) => true); if (commitResult.Behavior == CommitBehavior.CancelCommit) { @@ -398,11 +529,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement commitHandled |= commitResult.IsHandled; if (commitResult.IsHandled) { - managerWhoCommitted = commitManager.Item1; + managerWhoCommitted = commitManager; break; } if (token.IsCancellationRequested) { + _telemetry.RecordBlockingExtension(commitManager); break; } } @@ -412,11 +544,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement InsertIntoBuffer(_textView, applicableToSpan, itemToCommit.InsertText); } + var versionAfterChange = applicableToSpan.TextBuffer.CurrentSnapshot.Version; + bool editsAreNoops = AreEditsNoops(versionBeforeChange, versionAfterChange); + _telemetry.UiStopwatch.Stop(); _telemetry.E2EStopwatch.Stop(); _guardedOperations.RaiseEvent(this, ItemCommitted, new CompletionItemEventArgs(itemToCommit)); - _telemetry.RecordCommitted(_telemetry.UiStopwatch.ElapsedMilliseconds, managerWhoCommitted); + _telemetry.RecordCommitted(_telemetry.UiStopwatch.ElapsedMilliseconds, editsAreNoops, managerWhoCommitted); Dismiss(); @@ -461,13 +596,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement _broker.ForgetSession(this); _textView.Caret.PositionChanged -= OnCaretPositionChanged; _computationCancellation.Cancel(); + DeferredOperation?.Cancel(); // This method may be invoked on any thread. We promised extenders we will raise Dismissed event on UI thread. if (Dismissed != null) { - JoinableTaskContext.Factory.Run(async () => + _jtc.Factory.Run(async () => { - await JoinableTaskContext.Factory.SwitchToMainThreadAsync(); + await _jtc.Factory.SwitchToMainThreadAsync(); _guardedOperations.RaiseEvent(this, Dismissed); }); } @@ -479,7 +615,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement errorSource: _gui, asyncAction: async () => { - await JoinableTaskContext.Factory.SwitchToMainThreadAsync(); + await _jtc.Factory.SwitchToMainThreadAsync(); _telemetry.UiStopwatch.Restart(); copyOfGui.FiltersChanged -= OnFiltersChanged; copyOfGui.CommitRequested -= OnCommitRequested; @@ -505,9 +641,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if (IsDismissed) return; - if (!JoinableTaskContext.IsOnMainThread) + if (!_jtc.IsOnMainThread) throw new InvalidOperationException($"This method must be callled on the UI thread."); + if (DeferredOperation != null) + DeferredOperation.Cancel(); + var rootSnapshot = AsyncCompletionBroker.GetRootSnapshot(TextView); commandToken.Register(_computationCancellation.Cancel); @@ -517,13 +656,22 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement { _computation = new ModelComputation<CompletionModel>( PrioritizedTaskScheduler.AboveNormalInstance, - JoinableTaskContext, + _jtc, (model, token) => GetInitialModel(trigger, triggerLocation, rootSnapshot, token), _computationCancellation.Token, _guardedOperations, this ); } + else if (trigger.Reason == CompletionTriggerReason.Invoke && _computation.RecentModel != default && !_computation.RecentModel.Uninitialized) + { + // Completion session already exists and it is in a well defined state. + // User invoked completion again - we will treat this as shortcut to toggle the first expander + // If no expander is available, UpdateCompletionByTogglingDefaultExpander will call UpdateSnapshot to preserve behavior + var expandTaskId = Interlocked.Increment(ref _lastFilteringTaskId); + _computation.Enqueue((model, token) => UpdateCompletionByTogglingDefaultExpander(model, trigger, triggerLocation, rootSnapshot, expandTaskId, token), updateUi: true); + return; + } var taskId = Interlocked.Increment(ref _lastFilteringTaskId); _computation.Enqueue((model, token) => UpdateSnapshot(model, trigger, triggerLocation, rootSnapshot, taskId, token), updateUi: true); @@ -532,9 +680,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement ComputedCompletionItems IAsyncCompletionSession.GetComputedItems(CancellationToken token) { if (_computation == null) - return ComputedCompletionItems.Empty; // Call OpenOrUpdate first to kick off computation + { + // Computation hasn't started yet. Call OpenOrUpdate first. + return ComputedCompletionItems.Empty; + } - var model = _computation.WaitAndGetResult(cancelUi: true, token); // We don't want user initiated action to hide UI + var model = _computation.WaitAndGetResult( + cancelUi: false, // Don't hide the UI on user or extension initiated action. As a tradeoff, we will wait for UI to render. + token: token); return ComputeCompletionItems(model); } @@ -552,10 +705,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement ((IAsyncCompletionSession)this).OpenOrUpdate(trigger, triggerLocation, token); } - if (((IAsyncCompletionSession)this).CommitIfUnique(token)) + // CancellationToken.None allows unlimited time to process this request. + // The CommitIfUnique can be anyways canceled by any user action. + _jtc.Factory.RunAsync(async () => { - ((IAsyncCompletionSession)this).Dismiss(); - } + // RunAsync allows us to remain on UI thread, so that so that DeferredOperation is set + var committed = await ((IAsyncCompletionSessionOperations)this).CommitIfUniqueAsync(CancellationToken.None).ConfigureAwait(false); + if (committed) + Dismiss(); + }); } public void SetSuggestionMode(bool useSuggestionMode) @@ -565,7 +723,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public void SelectDown() { - if (_computation.RecentModel == default || _computation.RecentModel.Uninitialized) + if (DeferredOperation != null || _computation.RecentModel == default || _computation.RecentModel.Uninitialized) { // https://github.com/dotnet/roslyn/issues/31131 Dismiss completion so that up and down gestures are not blocked Dismiss(); @@ -575,7 +733,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public void SelectPageDown() { - if (_computation.RecentModel == default || _computation.RecentModel.Uninitialized) + if (DeferredOperation != null || _computation.RecentModel == default || _computation.RecentModel.Uninitialized) { // https://github.com/dotnet/roslyn/issues/31131 Dismiss completion so that up and down gestures are not blocked Dismiss(); @@ -585,7 +743,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public void SelectUp() { - if (_computation.RecentModel == default || _computation.RecentModel.Uninitialized) + if (DeferredOperation != null || _computation.RecentModel == default || _computation.RecentModel.Uninitialized) { // https://github.com/dotnet/roslyn/issues/31131 Dismiss completion so that up and down gestures are not blocked Dismiss(); @@ -595,7 +753,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public void SelectPageUp() { - if (_computation.RecentModel == default || _computation.RecentModel.Uninitialized) + if (DeferredOperation != null || _computation.RecentModel == default || _computation.RecentModel.Uninitialized) { // https://github.com/dotnet/roslyn/issues/31131 Dismiss completion so that up and down gestures are not blocked Dismiss(); @@ -605,6 +763,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public void SelectCompletionItem(CompletionItem item) { + if (DeferredOperation != null) + DeferredOperation.Cancel(); + // To prevent inifinite loops, UI interacts with computation using the OnItemSelected event handler _computation.Enqueue((model, token) => UpdateSelectedItem(model, item, false, token), updateUi: true); } @@ -630,6 +791,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement private void OnFiltersChanged(object sender, CompletionFilterChangedEventArgs args) { + if (DeferredOperation != null) + DeferredOperation.Cancel(); + var taskId = Interlocked.Increment(ref _lastFilteringTaskId); _computation.Enqueue((model, token) => UpdateFilters(model, args.Filters, taskId, token), updateUi: true); } @@ -724,7 +888,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement async Task IModelComputationCallbackHandler<CompletionModel>.UpdateUI(CompletionModel model, CancellationToken token) { if (_presenterProvider == null) return; - await JoinableTaskContext.Factory.SwitchToMainThreadAsync(token); + await _jtc.Factory.SwitchToMainThreadAsync(token); if (token.IsCancellationRequested) return; UpdateUiInner(model); } @@ -741,7 +905,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement throw new ArgumentNullException(nameof(model)); if (model.Uninitialized) return; // Language service wishes to not show completion yet. - if (!JoinableTaskContext.IsOnMainThread) + if (!_jtc.IsOnMainThread) throw new InvalidOperationException($"This method must be callled on the UI thread."); // TODO: Consider building CompletionPresentationViewModel in BG and passing it here @@ -780,23 +944,30 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement } /// <summary> - /// Gets <see cref="CompletionContext"/> from available <see cref="IAsyncCompletionSource"/>s + /// Gets <see cref="CompletionContext"/> from available <see cref="IAsyncCompletionSource"/>s, + /// or <see cref="IAsyncExpandingCompletionSource"/>s if <paramref name="getExpandedContext"/> is set. /// </summary> /// <returns>Aggregate data built from all received <see cref="CompletionContext"/>s</returns> - internal async Task<CompletionSourceConnectionResult>ConnectToCompletionSources(CompletionTrigger trigger, SnapshotPoint triggerLocation, ITextSnapshot rootSnapshot, CancellationToken token) + internal async Task<CompletionSourceConnectionResult>ConnectToCompletionSources(CompletionTrigger trigger, SnapshotPoint triggerLocation, ITextSnapshot rootSnapshot, bool getExpandedContext, ImmutableArray<CompletionItem> initialItems, CompletionExpander expander, CancellationToken token) { bool sourceUsesSuggestionMode = false; SuggestionItemOptions requestedSuggestionItemOptions = null; InitialSelectionHint initialSelectionHint = InitialSelectionHint.RegularSelection; var initialItemsBuilder = ImmutableArray.CreateBuilder<CompletionItem>(); + if (!initialItems.IsDefaultOrEmpty) + initialItemsBuilder.AddRange(initialItems); + var filterBuilder = ImmutableArray.CreateBuilder<CompletionFilterWithState>(); - // We use rootSnapshot to obtain buffers that participate in comple + // We use rootSnapshot to obtain buffers which participate in completion var points = MappingHelper.GetPointsAtLocation(triggerLocation, rootSnapshot); for (int i = 0; i < _completionSources.Count; i++) { var sourceAndLocation = _completionSources[i]; // Capture the source, since `i` will change during the async call + if (getExpandedContext && !(sourceAndLocation.Source is IAsyncExpandingCompletionSource)) + continue; + _telemetry.ComputationStopwatch.Restart(); var context = await _guardedOperations.CallExtensionPointAsync( errorSource: sourceAndLocation.Source, @@ -808,7 +979,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement return Task.FromResult(CompletionContext.Empty); // Don't use rootSnapshot: ApplicableToSpan is defined on the triggerLocation's snapshot - return sourceAndLocation.Source.GetCompletionContextAsync(this, trigger, mappedPoint, ApplicableToSpan.GetSpan(triggerLocation.Snapshot), token); + if (getExpandedContext) + return ((IAsyncExpandingCompletionSource)sourceAndLocation.Source).GetExpandedCompletionContextAsync(this, expander, InitialTrigger, ApplicableToSpan.GetSpan(triggerLocation.Snapshot), token); + else + return sourceAndLocation.Source.GetCompletionContextAsync(this, trigger, mappedPoint, ApplicableToSpan.GetSpan(triggerLocation.Snapshot), token); }, valueOnThrow: null ).ConfigureAwait(true); @@ -816,7 +990,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement _telemetry.RecordObtainingSourceContext(sourceAndLocation.Source, _telemetry.ComputationStopwatch.ElapsedMilliseconds); if (token.IsCancellationRequested) + { + _telemetry.RecordBlockingExtension(sourceAndLocation.Source); return CompletionSourceConnectionResult.Canceled; + } if (context == null) continue; @@ -828,11 +1005,22 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if (!context.Items.IsDefaultOrEmpty) initialItemsBuilder.AddRange(context.Items); + if (context.Filters.IsDefault) + { + // Iterate through items to get filters + filterBuilder.AddRange(context.Items.SelectMany(n => n.Filters).Distinct().Select(n => new CompletionFilterWithState(n, isAvailable: false, isSelected: false))); + } + else + { + filterBuilder.AddRange(context.Filters); + } + // We use SuggestionModeOptions of the first source that provides it if (requestedSuggestionItemOptions == null && context.SuggestionItemOptions != null) requestedSuggestionItemOptions = context.SuggestionItemOptions; } - return new CompletionSourceConnectionResult(sourceUsesSuggestionMode, requestedSuggestionItemOptions, initialSelectionHint, initialItemsBuilder.ToImmutable()); + + return new CompletionSourceConnectionResult(sourceUsesSuggestionMode, requestedSuggestionItemOptions, initialSelectionHint, initialItemsBuilder.ToImmutable(), filterBuilder.ToImmutable()); } /// <summary> @@ -840,14 +1028,16 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// </summary> private async Task<CompletionModel> GetInitialModel(CompletionTrigger trigger, SnapshotPoint triggerLocation, ITextSnapshot rootSnapshot, CancellationToken token) { - var completionData = await ConnectToCompletionSources(trigger, triggerLocation, rootSnapshot, token).ConfigureAwait(true); + var completionData = await ConnectToCompletionSources(trigger, triggerLocation, rootSnapshot, + getExpandedContext: false, initialItems: default, expander: default, + token: token).ConfigureAwait(true); // Do not continue without items if (completionData.IsCanceled) { return default; } - else if (completionData.InitialCompletionItems.IsDefaultOrEmpty) + else if (completionData.Items.IsDefaultOrEmpty) { return CompletionModel.GetUninitializedModel(triggerLocation.Snapshot); } @@ -857,13 +1047,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement // Store the data that won't change throughout the session InitialTrigger = trigger; + InitialTriggerLocation = triggerLocation; SuggestionModeCompletionItemSource = new SuggestionModeCompletionItemSource(SuggestionItemOptions); - var availableFilters = completionData.InitialCompletionItems - .SelectMany(n => n.Filters) - .Distinct() - .Select(n => new CompletionFilterWithState(n, true)) - .ToImmutableArray(); + var primedExpanders = GetPrimedExpanders(completionData.Filters); var viewUsesSuggestionMode = CompletionUtilities.GetSuggestionModeOption(_textView); var useSuggestionMode = completionData.SourceUsesSuggestionMode || viewUsesSuggestionMode; @@ -877,15 +1064,85 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement errorSource: _completionItemManager, asyncCall: () => _completionItemManager.SortCompletionListAsync( session: this, - data: new AsyncCompletionSessionInitialDataSnapshot(completionData.InitialCompletionItems, triggerLocation.Snapshot, InitialTrigger), + data: new AsyncCompletionSessionInitialDataSnapshot(completionData.Items, triggerLocation.Snapshot, InitialTrigger), token: token), - valueOnThrow: completionData.InitialCompletionItems).ConfigureAwait(true); + valueOnThrow: completionData.Items).ConfigureAwait(true); _telemetry.ComputationStopwatch.Stop(); - _telemetry.RecordProcessing(_telemetry.ComputationStopwatch.ElapsedMilliseconds, completionData.InitialCompletionItems.Length); + _telemetry.RecordProcessing(_telemetry.ComputationStopwatch.ElapsedMilliseconds, completionData.Items.Length); _telemetry.RecordKeystroke(); + if (token.IsCancellationRequested) + { + _telemetry.RecordBlockingExtension(_completionItemManager); + } - return new CompletionModel(completionData.InitialCompletionItems, sortedList, triggerLocation.Snapshot, - availableFilters, useSoftSelection, useSuggestionMode, selectSuggestionItem, suggestionItem: null); + return new CompletionModel(completionData.Items, sortedList, triggerLocation.Snapshot, + completionData.Filters, primedExpanders, useSoftSelection, useSuggestionMode, selectSuggestionItem, suggestionItem: null); + } + + private async Task<CompletionModel> ExpandModel(CompletionModel model, CompletionExpander expander, ITextSnapshot rootSnapshot, CancellationToken token) + { + var triggerLocation = ApplicableToSpan.GetStartPoint(model.Snapshot); // it's made up + var completionData = await ConnectToCompletionSources(InitialTrigger, triggerLocation, rootSnapshot, + getExpandedContext: true, initialItems: model.InitialItems, expander: expander, + token: token).ConfigureAwait(true); + + // Do not continue without items + if (completionData.IsCanceled || completionData.Items.IsDefaultOrEmpty) + return model; + // Ignore the part of CompletionData which pertains soft selection and suggestion mode + // So far, nobody made a request that we make adjust suggestion mode or selection during expansion. + + // Mark currently used expander as primed, together with any other expanders provided by the language service + var primedExpanders = model.PrimedExpanders.AddRange(GetPrimedExpanders(completionData.Filters)).Add(expander); + var deduplicatedItems = completionData.Items.Distinct().ToImmutableArray(); + + // Sort items + _telemetry.ComputationStopwatch.Restart(); + var sortedList = await _guardedOperations.CallExtensionPointAsync( + errorSource: _completionItemManager, + asyncCall: () => _completionItemManager.SortCompletionListAsync( + session: this, + data: new AsyncCompletionSessionInitialDataSnapshot(deduplicatedItems, triggerLocation.Snapshot, InitialTrigger), + token: token), + valueOnThrow: deduplicatedItems).ConfigureAwait(true); + _telemetry.ComputationStopwatch.Stop(); + _telemetry.RecordProcessing(_telemetry.ComputationStopwatch.ElapsedMilliseconds, deduplicatedItems.Length); + _telemetry.RecordKeystroke(); + if (token.IsCancellationRequested) + { + _telemetry.RecordBlockingExtension(_completionItemManager); + } + + // Combine existing filters with potential new items + // Ensure that previously selected filters remain selected, and that newly selected filters are selected + var filters = model.Filters; + if (completionData.Filters.Any()) + { + var filterBuilder = ImmutableArray.CreateBuilder<CompletionFilterWithState>(model.Filters.Length); + for (int i = 0; i < completionData.Filters.Length; i++) + { + var newFilter = completionData.Filters[i]; + var existingFilter = model.Filters.FirstOrDefault(n => n.Filter == newFilter.Filter); + if (existingFilter != null && existingFilter.IsSelected) + { + filterBuilder.Add(existingFilter); + } + else + { + filterBuilder.Add(newFilter); + } + } + filters = filterBuilder.ToImmutableArray(); + } + return model.WithExpansion(deduplicatedItems, sortedList, filters, primedExpanders); + } + + private static ImmutableArray<CompletionExpander> GetPrimedExpanders(ImmutableArray<CompletionFilterWithState> filters) + { + return filters + .Where(n => n.IsSelected && n.Filter is CompletionExpander) + .Select(n => (CompletionExpander)n.Filter) + .ToImmutableArray(); } /// <summary> @@ -1040,6 +1297,13 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement valueOnThrow: null).ConfigureAwait(true); // Error cases are handled by logging them above and dismissing the session. + if (token.IsCancellationRequested) + { + _telemetry.RecordBlockingExtension(_completionItemManager); + _finalSessionState = CompletionSessionState.DismissedDueToCancellation; + ((IAsyncCompletionSession)this).Dismiss(); + return model; + } if (filteredCompletion == null) { _finalSessionState = CompletionSessionState.DismissedDuringFiltering; @@ -1172,7 +1436,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// <returns></returns> private async Task<bool> TryDismissSafely(int currentTaskId) { - await JoinableTaskContext.Factory.SwitchToMainThreadAsync(); + await _jtc.Factory.SwitchToMainThreadAsync(); // Tasks are enqueued on the UI thread, so we know that _lastFilteringTaskId won't change if (currentTaskId < _lastFilteringTaskId) @@ -1196,7 +1460,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement _telemetry.RecordChangingFilters(); _telemetry.RecordKeystroke(); - // This operation just got preempted, preserve new filters until next time we have a chance to update the completion list. + // See if any of the selected filters are expanders used for the first time + model = await ExpandCompletionWithSpecificFilter(model, newFilters, token).ConfigureAwait(false); + + // Filtering just got preempted, preserve new filters until next time we have a chance to update the completion list. if (token.IsCancellationRequested || thisId != _lastFilteringTaskId) return model.WithFilters(newFilters); @@ -1216,9 +1483,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement valueOnThrow: null).ConfigureAwait(true); // Handle error cases by logging the issue and discarding the request to filter - if (filteredCompletion == null) + if (token.IsCancellationRequested) + { + _telemetry.RecordBlockingExtension(_completionItemManager); + _finalSessionState = CompletionSessionState.DismissedDueToCancellation; + ((IAsyncCompletionSession)this).Dismiss(); return model; - if (filteredCompletion.Filters.Length != newFilters.Length) + } + else if (filteredCompletion == null) + { + return model; + } + else if (filteredCompletion.Filters.Length != newFilters.Length) { _guardedOperations.HandleException( errorSource: _completionItemManager, @@ -1229,6 +1505,37 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement return model.WithFilters(filteredCompletion.Filters).WithPresentedItems(filteredCompletion.Items, filteredCompletion.SelectedItemIndex); } + private async Task<CompletionModel> UpdateCompletionByTogglingDefaultExpander(CompletionModel model, CompletionTrigger trigger, SnapshotPoint triggerLocation, ITextSnapshot rootSnapshot, int thisId, CancellationToken token) + { + if (token.IsCancellationRequested) + return model; + + if (!(model.Filters.FirstOrDefault()?.Filter is CompletionExpander)) + { + // Preserve the default behavior + return await UpdateSnapshot(model, trigger, triggerLocation, rootSnapshot, thisId, token).ConfigureAwait(false); + } + + var filtersWithToggledExpander = ImmutableArray.CreateRange(model.Filters.Select((n, i) => i == 0 ? n.WithSelected(!n.IsSelected) : n)); + return await UpdateFilters(model, filtersWithToggledExpander, thisId, token).ConfigureAwait(false); + } + + private async Task<CompletionModel> ExpandCompletionWithSpecificFilter(CompletionModel model, ImmutableArray<CompletionFilterWithState> newFilters, CancellationToken token) + { + for (int i = 0; i < newFilters.Length; i++) + { + if (newFilters[i].IsSelected && newFilters[i].Filter is CompletionExpander expander) + { + if (!model.PrimedExpanders.Contains(expander)) + { + var rootSnapshot = AsyncCompletionBroker.GetRootSnapshot(TextView); + model = await ExpandModel(model, expander, rootSnapshot, token).ConfigureAwait(true); + } + } + } + return model; + } + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CA1801 // Parameter token is never used /// <summary> @@ -1371,5 +1678,65 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement suggestionItemSelected: model.SelectSuggestionItem, usesSoftSelection: model.UseSoftSelection); } + + /// <summary> + /// Checks whether all versions between <paramref name="startVersion"/> and <paramref name="endVersion"/> + /// have no associated changes. + /// </summary> + /// <param name="startVersion"></param> + /// <param name="endVersion"></param> + /// <returns></returns> + private static bool AreEditsNoops(ITextVersion startVersion, ITextVersion endVersion) + { + if (startVersion.TextBuffer != endVersion.TextBuffer) + throw new ArgumentException("Versions must apply to the same buffer"); + + if (startVersion.VersionNumber > endVersion.VersionNumber) + throw new ArgumentException($"{nameof(startVersion)} must be before {nameof(endVersion)}."); + + if (startVersion == endVersion) + { + return false; + } + else + { + var inspectedVersion = startVersion; + while (inspectedVersion.VersionNumber < endVersion.VersionNumber || inspectedVersion == null) + { + if (inspectedVersion.Changes.Count > 0) + { + return true; + } + inspectedVersion = inspectedVersion.Next; + } + return false; + } + } + + /// <summary> + /// Returns whether there are no available items, user did not perform a blocking operation, + /// and completion session is in one of the eligible modes. Specifically, + /// 1. In suggestion mode, we don't wait on computation. Instead, we just hide the UI (dismiss) on commit + /// 2. Non blocking mode is set by langauge services to immediately stop computation when user presses a commit character + /// 3. Responsive mode is a moderate version of non blocking mode, where language services get grace period to finish computation. + /// we introduced Responsive mode because most delays, if any, are less than 20ms. + /// </summary> + private static bool EligibleToQuicklyDismiss(ModelComputation<CompletionModel> computation, char typedChar, bool inEligibleMode) + { + return (computation == default || computation.RecentModel == default || computation.RecentModel.Uninitialized) + && inEligibleMode + && !IsTabOrEmpty(typedChar); + } + + /// <summary> + /// Returns whether <paramref name="typedChar"/> represents Tab or empty character, + /// which commit completion differently than any other character. + /// </summary> + /// <param name="typedChar">Character to examine</param> + /// <returns><c>true</c> if <paramref name="typedChar"/> is <c>'\0'</c> or <c>'\t'</c> </returns> + private static bool IsTabOrEmpty(char typedChar) + { + return typedChar.Equals(default) || typedChar.Equals('\t'); + } } } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionAvailabilityUtility.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionAvailabilityUtility.cs index 6d1cc36..14fe115 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionAvailabilityUtility.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionAvailabilityUtility.cs @@ -12,33 +12,19 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement { /// <summary> /// Provides information whether modern completion should be enabled, - /// based on the state of <see cref="IExperimentationServiceInternal"/> and <see cref="IFeatureServiceFactory" /> + /// based on the state of <see cref="PredefinedEditorFeatureNames.AsyncCompletion"/> in <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. - [Import] - private IEditorOptionsFactoryService EditorOptionsFactory; - - // Black list by content type - private const string CompletionFlightName = "CompletionAPI"; - private const string RoslynLanguagesContentType = "Roslyn Languages"; - private const string RazorContentType = "Razor"; - private const string LanguageServerContentType = "code-languageserver-preview"; - private bool _treatmentFlightDataInitialized; - // Quick access data: - private bool _treatmentFlightEnabled; private IFeatureCookie _globalCompletionCookie; private IFeatureCookie GlobalCompletionCookie => _globalCompletionCookie @@ -47,69 +33,21 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// <summary> /// Returns whether completion is available for the given <see cref="IContentType"/> and <see cref="ITextViewRoleSet" />. /// </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> + /// <returns>true if 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, ITextViewRoleSet roles) { - if (!GlobalCompletionCookie.IsEnabled) - return false; - - if (!Broker.HasCompletionProviders(contentType, roles)) - 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(LanguageServerContentType) && (contentType.IsOfType(RoslynLanguagesContentType) || contentType.IsOfType(RazorContentType))) - return false; - - return true; + return GlobalCompletionCookie.IsEnabled + && Broker.HasCompletionProviders(contentType, roles); } /// <summary> /// Returns whether completion feature 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 IsCurrentlyAvailable(ITextView textView, IContentType contentTypeToCheckBlacklist) + /// <returns>true if feature is enabled in <see cref="ITextView"/>'s scope</returns> + internal bool IsCurrentlyAvailable(ITextView textView) { var featureService = FeatureServiceFactory.GetOrCreate(textView); - if (!featureService.IsEnabled(PredefinedEditorFeatureNames.AsyncCompletion)) - 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(LanguageServerContentType) && (contentTypeToCheckBlacklist.IsOfType(RoslynLanguagesContentType) || contentTypeToCheckBlacklist.IsOfType(RazorContentType))) - return false; - - return true; - } - - private bool IsExperimentEnabled() - { - int userSetting = 0; - if (EditorOptionsFactory.GlobalOptions.IsOptionDefined(UseAsyncCompletionOptionDefinition.OptionName, localScopeOnly: false)) - { - userSetting = EditorOptionsFactory.GlobalOptions.GetOptionValue<int>(UseAsyncCompletionOptionDefinition.OptionName); - } - - if (userSetting == 1) - { - return true; - } - else if (userSetting == -1) - { - return false; - } - else - { - if (_treatmentFlightDataInitialized) - return _treatmentFlightEnabled; - - _treatmentFlightEnabled = ExperimentationService.IsCachedFlightEnabled(CompletionFlightName); - _treatmentFlightDataInitialized = true; - return _treatmentFlightEnabled; - } + return featureService.IsEnabled(PredefinedEditorFeatureNames.AsyncCompletion); } } } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs index c3201c8..c4a5621 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs @@ -1,13 +1,15 @@ using System; using System.ComponentModel.Composition; +using System.Globalization; using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.BraceCompletion; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Text.Operations; -using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Text.UI.Utilities; using Microsoft.VisualStudio.Utilities; using CommonImplementation = Microsoft.VisualStudio.Language.Intellisense.Implementation; @@ -33,6 +35,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement IDynamicCommandHandler<EscapeKeyCommandArgs>, ICommandHandler<InsertSnippetCommandArgs>, ICommandHandler<InvokeCompletionListCommandArgs>, + IDynamicCommandHandler<InvokeCompletionListCommandArgs>, ICommandHandler<PageDownKeyCommandArgs>, ICommandHandler<PageUpKeyCommandArgs>, ICommandHandler<PasteCommandArgs>, @@ -205,6 +208,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement // Execute other commands in the chain to see the change in the buffer. nextCommandHandler(); + if (args.TextView.TextSnapshot == snapshotBeforeEdit) + { + // Buffer has not changed. Don't invoke completion. + return; + } + var session = Broker.GetSession(args.TextView); var location = args.TextView.Caret.Position.BufferPosition; var trigger = new CompletionTrigger(CompletionTriggerReason.Backspace, snapshotBeforeEdit); @@ -241,6 +250,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement CommandState ICommandHandler<InvokeCompletionListCommandArgs>.GetCommandState(InvokeCompletionListCommandArgs args) => GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView); + bool IDynamicCommandHandler<InvokeCompletionListCommandArgs>.CanExecuteCommand(InvokeCompletionListCommandArgs args) + => CompletionAvailability.IsAvailable(args.SubjectBuffer.ContentType, args.TextView.Roles); + bool ICommandHandler<InvokeCompletionListCommandArgs>.ExecuteCommand(InvokeCompletionListCommandArgs args, CommandExecutionContext executionContext) { if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable) @@ -357,6 +369,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement // Execute other commands in the chain to see the change in the buffer. nextCommandHandler(); + if (args.TextView.TextSnapshot == snapshotBeforeEdit) + { + // Buffer has not changed. Don't invoke completion. + return; + } + var session = Broker.GetSession(args.TextView); var location = args.TextView.Caret.Position.BufferPosition; var trigger = new CompletionTrigger(CompletionTriggerReason.Deletion, snapshotBeforeEdit); @@ -470,6 +488,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement var session = Broker.GetSession(args.TextView); if (session != null) { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Return: begin commit"); var commitBehavior = session.Commit(typedChar, executionContext.OperationContext.UserCancellationToken); session.Dismiss(); @@ -479,23 +499,42 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if ((commitBehavior & CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers) == 0 || CompletionUtilities.IsDebuggerTextView(args.TextView) || CompletionUtilities.IsImmediateTextView(args.TextView)) + { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Return: do nothing after commit", commitBehavior); return; + } } var snapshotBeforeEdit = args.TextView.TextSnapshot; + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Return: next handler"); nextCommandHandler(); + if (args.TextView.TextSnapshot == snapshotBeforeEdit) + { + // Buffer has not changed. Don't invoke completion. + return; + } + // Buffer has changed. Update it for when we try to trigger new session. var location = args.TextView.Caret.Position.BufferPosition; + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Return: try make new session"); var trigger = new CompletionTrigger(CompletionTriggerReason.Insertion, snapshotBeforeEdit, typedChar); var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken); if (newSession is IAsyncCompletionSessionOperations sessionInternal) { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Return: created new session"); RealizeVirtualSpaceUpdateApplicableToSpan(sessionInternal, args.TextView); } location = args.TextView.Caret.Position.BufferPosition; // Buffer may have changed. Update the location. newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Return: finish"); }); } @@ -514,6 +553,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement var session = Broker.GetSession(args.TextView); if (session != null) { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Tab: begin commit"); + var commitBehavior = session.Commit(typedChar, executionContext.OperationContext.UserCancellationToken); session.Dismiss(); @@ -523,17 +565,39 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if ((commitBehavior & CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers) == 0 || CompletionUtilities.IsDebuggerTextView(args.TextView) || CompletionUtilities.IsImmediateTextView(args.TextView)) + { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Tab: do nothing after commit", commitBehavior); return; + } } var snapshotBeforeEdit = args.TextView.TextSnapshot; + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Tab: next handler"); nextCommandHandler(); + if (args.TextView.TextSnapshot == snapshotBeforeEdit) + { + // Buffer has not changed. Don't invoke completion. + return; + } + // Buffer has changed. Update it for when we try to trigger new session. var location = args.TextView.Caret.Position.BufferPosition; + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Tab: try make new session"); var trigger = new CompletionTrigger(CompletionTriggerReason.Insertion, snapshotBeforeEdit, typedChar); var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken); + if (newSession is IAsyncCompletionSessionOperations sessionInternal) + { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Tab: created new session"); + } newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); + + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("Commit with tab: finish"); }); } @@ -547,6 +611,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement { RunOnceIfAvailable(args, nextCommandHandler, () => { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar: ", args.TypedChar); + var view = args.TextView; var location = view.Caret.Position.BufferPosition; var initialTextSnapshot = args.SubjectBuffer.CurrentSnapshot; @@ -566,33 +633,68 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement ((AsyncCompletionSession)sessionToCommit).IgnoreCaretMovement(ignore: true); } + // BraceCompletionManager is accessible through well known property name. + IBraceCompletionManager braceCompletionManager; + args.TextView.Properties.TryGetProperty("BraceCompletionManager", out braceCompletionManager); + var braceCompletionSessionsBeforeEdit = braceCompletionManager?.ActiveSessionCount; var snapshotBeforeEdit = args.TextView.TextSnapshot; + // 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 + + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar invokes nextCommandHandler..."); + 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; + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("...TypeChar invoked nextCommandHandler"); + + var braceCompletionSessionAfterEdit = braceCompletionManager?.ActiveSessionCount; + + if (args.TextView.TextSnapshot == snapshotBeforeEdit + && braceCompletionSessionAfterEdit == braceCompletionSessionsBeforeEdit) + { + // Buffer has not changed, and neither did state of brace completion. + // Don't invoke completion. + return; + } + + // If brace completion just closed, we will not undo the last type char + var dontUndoBraceCompletion = braceCompletionSessionAfterEdit < braceCompletionSessionsBeforeEdit; // 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) { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar: ShouldCommit"); + // 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) + // Undo the typechar, because that's what language service expects + // Note that Roslyn expects brace to be there, because it can't handle undoing brace completion + if (!dontUndoBraceCompletion) + { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar: commit. roll back"); UndoUtilities.RollbackToBeforeTypeChar(initialTextSnapshot, args.SubjectBuffer); - // Now the buffer doesn't have the commit character nor the matching brace, if any + } + // Now the buffer doesn't have the commit character, but may have a matching brace var commitBehavior = sessionToCommit.Commit(args.TypedChar, executionContext.OperationContext.UserCancellationToken); + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar: commit. behavior: ", commitBehavior); - if (!braceCompletionSpecialHandling && (commitBehavior & CommitBehavior.SuppressFurtherTypeCharCommandHandlers) == 0) + if (!dontUndoBraceCompletion && (commitBehavior & CommitBehavior.SuppressFurtherTypeCharCommandHandlers) == 0) + { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar: commit. nextCommandHandler"); nextCommandHandler(); // Replay the key, so that we get brace completion. + } // Complete the transaction before stopping it. undoTransaction.Complete(); @@ -612,10 +714,16 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement var session = Broker.GetSession(args.TextView); if (session != null) { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar: Update session"); + session.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); } else { + if (DiagnosticLogger.IsLoggingEnabled(args.TextView)) + DiagnosticLogger.Add("TypeChar: Create new session"); + var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken); newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken); } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionModel.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionModel.cs index 2d75b0e..bc1fef4 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionModel.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionModel.cs @@ -31,6 +31,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public readonly ImmutableArray<CompletionFilterWithState> Filters; /// <summary> + /// Invoked expansions involved in this completion model. + /// </summary> + public readonly ImmutableArray<CompletionExpander> PrimedExpanders; + + /// <summary> /// Items to be displayed in the UI. /// </summary> public readonly ImmutableArray<CompletionItemWithHighlight> PresentedItems; @@ -85,20 +90,21 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// </summary> public static CompletionModel GetUninitializedModel(ITextSnapshot snapshot) { - return new CompletionModel(default, default, snapshot, default, default, default, default, default, default, default, default, default, uninitialized: true); + return new CompletionModel(default, default, snapshot, default, 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, + ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, ImmutableArray<CompletionExpander> primedExpanders, bool useSoftSelection, bool displaySuggestionItem, bool selectSuggestionItem, CompletionItem suggestionItem) { InitialItems = initialItems; SortedItems = sortedItems; Snapshot = snapshot; Filters = filters; + PrimedExpanders = primedExpanders; SelectedIndex = 0; UseSoftSelection = useSoftSelection; DisplaySuggestionItem = displaySuggestionItem; @@ -113,7 +119,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// Private constructor for the With* methods /// </summary> private CompletionModel(ImmutableArray<CompletionItem> initialItems, ImmutableArray<CompletionItem> sortedItems, - ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, ImmutableArray<CompletionItemWithHighlight> presentedItems, + ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, ImmutableArray<CompletionExpander> primedExpanders, ImmutableArray<CompletionItemWithHighlight> presentedItems, bool useSoftSelection, bool displaySuggestionItem, int selectedIndex, bool selectSuggestionItem, CompletionItem suggestionItem, CompletionItem uniqueItem, bool applicableToSpanWasEmpty, bool uninitialized) { @@ -121,6 +127,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement SortedItems = sortedItems; Snapshot = snapshot; Filters = filters; + PrimedExpanders = primedExpanders; PresentedItems = presentedItems; SelectedIndex = selectedIndex; UseSoftSelection = useSoftSelection; @@ -139,6 +146,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: newPresentedItems, // Updated useSoftSelection: UseSoftSelection, displaySuggestionItem: DisplaySuggestionItem, @@ -158,6 +166,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: newSnapshot, // Updated filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, displaySuggestionItem: DisplaySuggestionItem, @@ -177,6 +186,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: newFilters, // Updated + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, displaySuggestionItem: DisplaySuggestionItem, @@ -196,6 +206,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: preserveSoftSelection ? UseSoftSelection : false, // Updated conditionally displaySuggestionItem: DisplaySuggestionItem, @@ -215,6 +226,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: false, // Explicit selection and soft selection are mutually exclusive displaySuggestionItem: DisplaySuggestionItem, @@ -234,6 +246,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection | newDisplaySuggestionItem, // Enabling suggestion mode also enables soft selection displaySuggestionItem: newDisplaySuggestionItem, // Updated @@ -257,6 +270,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, displaySuggestionItem: DisplaySuggestionItem, @@ -276,6 +290,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: newSoftSelection, // Updated displaySuggestionItem: DisplaySuggestionItem, @@ -296,6 +311,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: snapshot, // Updated filters: filters, // Updated + primedExpanders: PrimedExpanders, presentedItems: presentedItems, // Updated useSoftSelection: UseSoftSelection, displaySuggestionItem: DisplaySuggestionItem, @@ -315,6 +331,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement sortedItems: SortedItems, snapshot: Snapshot, filters: Filters, + primedExpanders: PrimedExpanders, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, displaySuggestionItem: DisplaySuggestionItem, @@ -326,5 +343,29 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement uninitialized: Uninitialized ); } + + internal CompletionModel WithExpansion( + ImmutableArray<CompletionItem> expandedItems, + ImmutableArray<CompletionItem> sortedItems, + ImmutableArray<CompletionFilterWithState> filters, + ImmutableArray<CompletionExpander> primedExpanders) + { + return new CompletionModel( + initialItems: expandedItems, // updated + sortedItems: sortedItems, // updated + snapshot: Snapshot, + filters: filters, // updated + primedExpanders: primedExpanders, // Updated + presentedItems: PresentedItems, + useSoftSelection: UseSoftSelection, + displaySuggestionItem: DisplaySuggestionItem, + selectedIndex: SelectedIndex, + selectSuggestionItem: SelectSuggestionItem, + suggestionItem: SuggestionItem, + uniqueItem: UniqueItem, + applicableToSpanWasEmpty: ApplicableToSpanWasEmpty, + uninitialized: Uninitialized + ); + } } } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionSourceConnectionResult.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionSourceConnectionResult.cs index 2869bea..aecfefe 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionSourceConnectionResult.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionSourceConnectionResult.cs @@ -3,28 +3,31 @@ using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation { - internal struct CompletionSourceConnectionResult + internal sealed class CompletionSourceConnectionResult { internal bool SourceUsesSuggestionMode { get; set; } internal SuggestionItemOptions RequestedSuggestionItemOptions { get; set; } internal InitialSelectionHint InitialSelectionHint { get; set; } - internal ImmutableArray<CompletionItem> InitialCompletionItems { get; set; } + internal ImmutableArray<CompletionItem> Items { get; set; } + internal ImmutableArray<CompletionFilterWithState> Filters { get; set; } internal bool IsCanceled { get; set; } internal CompletionSourceConnectionResult(bool sourceUsesSuggestionMode, SuggestionItemOptions requestedSuggestionItemOptions, InitialSelectionHint initialSelectionHint, ImmutableArray<CompletionItem> initialCompletionItems, + ImmutableArray<CompletionFilterWithState> initialCompletionFilters, bool isCanceled = false) { SourceUsesSuggestionMode = sourceUsesSuggestionMode; RequestedSuggestionItemOptions = requestedSuggestionItemOptions; InitialSelectionHint = initialSelectionHint; - InitialCompletionItems = initialCompletionItems; + Items = initialCompletionItems; + Filters = initialCompletionFilters; IsCanceled = isCanceled; } internal static CompletionSourceConnectionResult Canceled - => new CompletionSourceConnectionResult(default, default, default, default, isCanceled: true); + => new CompletionSourceConnectionResult(default, default, default, default, default, isCanceled: true); } } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionTelemetry.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionTelemetry.cs index 6818dd6..01a8c66 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionTelemetry.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionTelemetry.cs @@ -64,7 +64,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement internal long BlockingComputationDuration { get; private set; } // Additional data for the E2E telemetry - public CompletionSessionState CompletionState { get; private set; } + internal CompletionSessionState CompletionState { get; private set; } + internal bool NoChanges { get; private set; } + internal bool UserWaitedForNoChanges { get; private set; } + internal Dictionary<string, int> BlockingExtensionCounter { get; } = new Dictionary<string, int>(); // Additional parameters related to work done by IAsyncCompletionItemManager internal bool UserEverScrolled { get; private set; } @@ -119,11 +122,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement NumberOfKeystrokes++; } - internal void RecordCommitted(long duration, + internal void RecordCommitted(long duration, bool noChanges, IAsyncCompletionCommitManager manager) { CommitManagerName = CompletionTelemetryHost.GetCommitManagerName(manager); CommitDuration = duration; + NoChanges = noChanges; } internal void RecordClosing(long duration) @@ -139,6 +143,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement ItemManagerName = CompletionTelemetryHost.GetItemManagerName(itemManager); PresenterProviderName = CompletionTelemetryHost.GetPresenterProviderName(presenterProvider); CompletionState = state; + if (NoChanges && BlockingComputationDuration > 0) + UserWaitedForNoChanges = true; _telemetryHost.Add(this); } @@ -168,6 +174,33 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement { BlockingComputationDuration = elapsedMilliseconds; } + + internal void RecordBlockingExtension(object extension) + { + if (extension == null) + return; + + string extensionName; + switch (extension) + { + case IAsyncCompletionSource source: + extensionName = CompletionTelemetryHost.GetSourceName(source); + break; + case IAsyncCompletionItemManager itemManager: + extensionName = CompletionTelemetryHost.GetItemManagerName(itemManager); + break; + case IAsyncCompletionCommitManager commitManager: + extensionName = CompletionTelemetryHost.GetCommitManagerName(commitManager); + break; + default: + extensionName = extension.GetType().ToString(); + break; + } + + if (!BlockingExtensionCounter.ContainsKey(extensionName)) + BlockingExtensionCounter[extensionName] = 0; + BlockingExtensionCounter[extensionName]++; + } } /// <summary> @@ -241,16 +274,21 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement internal int CommittedThroughClick; internal int CommittedThroughCompleteWord; internal int CommittedSuggestionItem; + internal int CommittedThroughTypedChar; internal int Dismissed; internal int DismissedDueToBackspace; internal int DismissedDueToCancellation; internal int DismissedDueToCaretLeaving; internal int DismissedDuringFiltering; + internal int DismissedDueToNoItems; internal int DismissedDueToNonBlockingMode; + internal int DismissedDueToResponsiveMode; + internal int DismissedDueToSuggestionMode; internal int DismissedDueToUnhandledError; internal int DismissedThroughUI; internal int DismissedUninitialized; + // Measuring distribution of time spent between triggering session and displaying UI or committing the item, whichever is sooner internal int HistogramBucket25; internal int HistogramBucket50; internal int HistogramBucket100; @@ -261,12 +299,23 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement internal int HistogramBucketLast; internal int HistogramBucketCanceled; internal int HistogramBucketInvalid; + + // Measuring distribution of user action and reaction of completion session + internal int HistogramNoChanges; + internal int HistogramNoChangesAndUserWaited; + internal int HistogramNoChangesThroughTypedChar; + internal int HistogramNoChangesAndUserWaitedThroughTypedChar; + internal int HistogramChanges; + internal int HistogramChangesAndUserWaited; + internal int HistogramChangesThroughTypedChar; + internal int HistogramChangesAndUserWaitedThroughTypedChar; } 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>(); + Dictionary<string, int> BlockingExtensionData = new Dictionary<string, int>(); AggregateE2EData E2EData = new AggregateE2EData(); private readonly ILoggingServiceInternal _logger; @@ -299,6 +348,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement AddCommitManagerData(telemetry, CommitManagerData); AddPresenterData(telemetry, PresenterData); AddE2EData(telemetry, E2EData); + AddBlockingExtensionData(telemetry, BlockingExtensionData); } /// <summary> @@ -399,16 +449,35 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement (E2ECommittedClick, E2EData.CommittedThroughClick), (E2ECommittedCompleteWord, E2EData.CommittedThroughCompleteWord), (E2ECommittedSuggestionItem, E2EData.CommittedSuggestionItem), + (E2ECommittedThroughTypedChar, E2EData.CommittedThroughTypedChar), (E2EDismissedStandard, E2EData.Dismissed), (E2EDismissedBackspace, E2EData.DismissedDueToBackspace), (E2EDismissedCancellation, E2EData.DismissedDueToCancellation), (E2EDismissedCaretLeaving, E2EData.DismissedDueToCaretLeaving), (E2EDismissedFiltering, E2EData.DismissedDuringFiltering), + (E2EDismissedNoItems, E2EData.DismissedDueToNoItems), (E2EDismissedNonBlocking, E2EData.DismissedDueToNonBlockingMode), + (E2EDismissedResponsive, E2EData.DismissedDueToResponsiveMode), + (E2EDismissedSuggestion, E2EData.DismissedDueToSuggestionMode), (E2EDismissedUnhandledError, E2EData.DismissedDueToUnhandledError), (E2EDismissedUI, E2EData.DismissedThroughUI), - (E2EDismissedUninitialized, E2EData.DismissedUninitialized) + (E2EDismissedUninitialized, E2EData.DismissedUninitialized), + (E2EScenarioNoChanges, E2EData.HistogramNoChanges), + (E2EScenarioUserWaitedForNoChanges, E2EData.HistogramNoChangesAndUserWaited) ); + + foreach (var data in BlockingExtensionData) + { + if (data.Value == 0) + continue; + + _logger.PostEvent(TelemetryEventType.Operation, + BlockingExtensionEventName, + TelemetryResult.Success, + (BlockingExtensionName, data.Key), + (BlockingCount, data.Value) + ); + } } /// <summary> @@ -538,6 +607,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement case CompletionSessionState.CommittedThroughCompleteWord: e2eData.CommittedThroughCompleteWord++; break; + case CompletionSessionState.CommittedThroughTypedChar: + e2eData.CommittedThroughTypedChar++; + break; case CompletionSessionState.DismissedDueToBackspace: e2eData.DismissedDueToBackspace++; break; @@ -550,9 +622,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement case CompletionSessionState.DismissedDuringFiltering: e2eData.DismissedDuringFiltering++; break; + case CompletionSessionState.DismissedDueToNoItems: + e2eData.DismissedDueToNoItems++; + break; case CompletionSessionState.DismissedDueToNonBlockingMode: e2eData.DismissedDueToNonBlockingMode++; break; + case CompletionSessionState.DismissedDueToResponsiveMode: + e2eData.DismissedDueToResponsiveMode++; + break; + case CompletionSessionState.DismissedDueToSuggestionMode: + e2eData.DismissedDueToSuggestionMode++; + break; case CompletionSessionState.DismissedDueToUnhandledError: e2eData.DismissedDueToUnhandledError++; break; @@ -593,6 +674,42 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement else e2eData.HistogramBucketLast++; } + + if (telemetry.CompletionState == CompletionSessionState.Committed || telemetry.CompletionState == CompletionSessionState.CommittedThroughTypedChar || telemetry.CompletionState == CompletionSessionState.CommittedThroughCompleteWord) + { + if (telemetry.NoChanges) + { + if (telemetry.CompletionState == CompletionSessionState.CommittedThroughTypedChar && telemetry.UserWaitedForNoChanges) + e2eData.HistogramNoChangesAndUserWaitedThroughTypedChar++; + else if (telemetry.CompletionState == CompletionSessionState.CommittedThroughTypedChar && !telemetry.UserWaitedForNoChanges) + e2eData.HistogramNoChangesThroughTypedChar++; + else if (telemetry.CompletionState == CompletionSessionState.CommittedThroughTypedChar && telemetry.UserWaitedForNoChanges) + e2eData.HistogramNoChangesAndUserWaited++; + else if (telemetry.CompletionState == CompletionSessionState.CommittedThroughTypedChar && !telemetry.UserWaitedForNoChanges) + e2eData.HistogramNoChanges++; + } + else + { + if (telemetry.CompletionState == CompletionSessionState.CommittedThroughTypedChar && telemetry.UserWaitedForNoChanges) + e2eData.HistogramChangesAndUserWaitedThroughTypedChar++; + else if (telemetry.CompletionState == CompletionSessionState.CommittedThroughTypedChar && !telemetry.UserWaitedForNoChanges) + e2eData.HistogramChangesThroughTypedChar++; + else if (telemetry.CompletionState != CompletionSessionState.CommittedThroughTypedChar && telemetry.UserWaitedForNoChanges) + e2eData.HistogramChangesAndUserWaited++; + else if (telemetry.CompletionState != CompletionSessionState.CommittedThroughTypedChar && !telemetry.UserWaitedForNoChanges) + e2eData.HistogramChanges++; + } + } + } + + private static void AddBlockingExtensionData(CompletionSessionTelemetry telemetry, Dictionary<string, int> blockingExtensionData) + { + foreach (var blockingExtension in telemetry.BlockingExtensionCounter) + { + if (!blockingExtensionData.ContainsKey(blockingExtension.Key)) + blockingExtensionData[blockingExtension.Key] = 0; + blockingExtensionData[blockingExtension.Key] += blockingExtension.Value; + } } // Property and event names @@ -628,6 +745,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement internal const string SourceAverageSetupDuration = "Property.Source.SetupDuration"; internal const string SourceMaxSetupDuration = "Property.Source.MaxSetupDuration"; + internal const string BlockingExtensionEventName = "VS/Editor/Completion/BlockingExtensionData"; + internal const string BlockingExtensionName = "Property.Extension.Name"; + internal const string BlockingCount = "Property.Extension.GetBlockingCount"; + internal const string E2EEventName = "VS/Editor/Completion/E2EData"; internal const string E2EContentType = "Property.E2E.ContentType"; internal const string E2EBucket25 = "Property.E2E.Bucket.25"; @@ -644,15 +765,21 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement internal const string E2ECommittedClick = "Property.E2E.Committed.ThroughClick"; internal const string E2ECommittedCompleteWord = "Property.E2E.Committed.CompleteWord"; internal const string E2ECommittedSuggestionItem = "Property.E2E.Committed.SuggestionItem"; + internal const string E2ECommittedThroughTypedChar = "Property.E2E.Committed.TypedChar"; internal const string E2EDismissedStandard = "Property.E2E.Dismissed.Standard"; internal const string E2EDismissedBackspace = "Property.E2E.Dismissed.Backspace"; internal const string E2EDismissedCancellation = "Property.E2E.Dismissed.Cancellation"; internal const string E2EDismissedCaretLeaving = "Property.E2E.Dismissed.CaretLeaving"; internal const string E2EDismissedFiltering = "Property.E2E.Dismissed.Filtering"; + internal const string E2EDismissedNoItems = "Property.E2E.Dismissed.NoItems"; internal const string E2EDismissedNonBlocking = "Property.E2E.Dismissed.NonBlocking"; + internal const string E2EDismissedResponsive = "Property.E2E.Dismissed.Responsive"; + internal const string E2EDismissedSuggestion = "Property.E2E.Dismissed.Suggestion"; internal const string E2EDismissedUnhandledError = "Property.E2E.Dismissed.UnhandledError"; internal const string E2EDismissedUI = "Property.E2E.Dismissed.UI"; internal const string E2EDismissedUninitialized = "Property.E2E.Dismissed.Uninitialized"; + internal const string E2EScenarioNoChanges = "Property.E2E.Scenario.NoChanges"; + internal const string E2EScenarioUserWaitedForNoChanges = "Property.E2E.Scenario.UserWaitedForNoChanges"; } /// <summary> @@ -666,7 +793,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// </summary> Default, /// <summary> - /// Session committed through typing, enter, tab or programmatically + /// Session committed through enter, tab or programmatically. Excludes committing through typed char. /// </summary> Committed, /// <summary> @@ -682,6 +809,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// </summary> CommittedSuggestionItem, /// <summary> + /// Session committed through typing + /// </summary> + CommittedThroughTypedChar, + /// <summary> /// Session dismissed because user erased its contents /// </summary> DismissedDueToBackspace, @@ -698,10 +829,22 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// </summary> DismissedDuringFiltering, /// <summary> - /// Session dismissed because computation has not finished before attempt to commit. + /// Session dismissed because there was no item to commit + /// </summary> + DismissedDueToNoItems, + /// <summary> + /// Session dismissed because computation has not finished before attempt to commit /// </summary> DismissedDueToNonBlockingMode, /// <summary> + /// Session dismissed because computation has not finished within grace period before attempt to commit + /// </summary> + DismissedDueToResponsiveMode, + /// <summary> + /// Session dismissed because it was in suggestion mode and user did not use tab to commit it + /// </summary> + DismissedDueToSuggestionMode, + /// <summary> /// Session dismissed because an error brought down the computation /// </summary> DismissedDueToUnhandledError, diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs index ceea5ec..8281c6f 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; using System.Linq; +using System.Threading; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -43,10 +44,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement /// <returns>True if the view has "COMMANDVIEW" text view role.</returns> internal static bool IsImmediateTextView(ITextView textView) => textView.Roles.Contains("COMMANDVIEW"); - static readonly EditorOptionKey<bool> NonBlockingCompletionOptionKey = new EditorOptionKey<bool>(PredefinedCompletionNames.NonBlockingCompletionOptionName); static readonly EditorOptionKey<bool> SuggestionModeOptionKey = new EditorOptionKey<bool>(PredefinedCompletionNames.SuggestionModeInCompletionOptionName); static readonly EditorOptionKey<bool> SuggestionModeInDebuggerCompletionOptionKey = new EditorOptionKey<bool>(PredefinedCompletionNames.SuggestionModeInDebuggerCompletionOptionName); - private const bool NonBlockingCompletionDefaultValue = false; private const bool UseSuggestionModeDefaultValue = false; private const bool UseSuggestionModeInDebuggerCompletionDefaultValue = true; @@ -72,17 +71,6 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement public override string Name => PredefinedCompletionNames.SuggestionModeInDebuggerCompletionOptionName; } - [Export(typeof(EditorOptionDefinition))] - [Name(PredefinedCompletionNames.NonBlockingCompletionOptionName)] - class NonBlockingCompletionOptionDefinition : EditorOptionDefinition - { - public override object DefaultValue => NonBlockingCompletionDefaultValue; - - public override Type ValueType => typeof(bool); - - public override string Name => PredefinedCompletionNames.NonBlockingCompletionOptionName; - } - internal static bool GetSuggestionModeOption(ITextView textView) { var options = textView.Options.GlobalOptions; @@ -107,18 +95,31 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement internal static bool GetNonBlockingCompletionOption(ITextView textView) { - var options = textView.Options.GlobalOptions; - if (!(options.IsOptionDefined(NonBlockingCompletionOptionKey, localScopeOnly: false))) - { - options.SetOptionValue(NonBlockingCompletionOptionKey, NonBlockingCompletionDefaultValue); - } - return options.GetOptionValue(NonBlockingCompletionOptionKey); + return textView.Options.GetOptionValue(DefaultOptions.NonBlockingCompletionOptionId); } - internal static void SetNonBlockingModeOption(ITextView textView, bool value) + internal static bool GetResponsiveCompletionOption(ITextView textView) { - var options = textView.Options.GlobalOptions; - options.SetOptionValue(NonBlockingCompletionOptionKey, value); + return textView.Options.GetOptionValue(DefaultOptions.ResponsiveCompletionOptionId) + && textView.Options.GetOptionValue(DefaultOptions.RemoteControlledResponsiveCompletionOptionId); + } + + internal static int GetResponsiveCompletionThresholdOption(ITextView textView) + { + return textView.Options.GetOptionValue(DefaultOptions.ResponsiveCompletionThresholdOptionId); + } + + internal static CancellationToken GetResponsiveToken(ITextView textView, CancellationToken commandingToken) + { + var inResponisveMode = CompletionUtilities.GetResponsiveCompletionOption(textView); + if (!inResponisveMode) + return commandingToken; + + var responsiveCompletionThreshold = CompletionUtilities.GetResponsiveCompletionThresholdOption(textView); + var responsiveCancellationSource = new CancellationTokenSource(responsiveCompletionThreshold); + var responsiveToken = responsiveCancellationSource.Token; + var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(commandingToken, responsiveToken); + return combinedCancellationSource.Token; } } } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs index 8a4ab8b..125f1b0 100644 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs @@ -69,22 +69,38 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement // 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)); + .Where(n => (filterText.Length == 1 || patternMatcher.HasInvalidPattern || 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)))); + // When no items are available for a given filter, it becomes unavailable. Expanders always appear available. + var updatedFilters = ImmutableArray.CreateRange(data.SelectedFilters.Select(n => n.WithAvailability( + n.Filter is CompletionExpander ? true : 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)) + if (data.SelectedFilters.Any(n => (n.Filter is CompletionExpander))) { - filterFilteredList = matches.Where(n => ShouldBeInCompletionList(n.completionItem, data.SelectedFilters)); + filterFilteredList = matches.Where(n => ShouldBeInExpandedCompletionList(n.completionItem, data.SelectedFilters)); + } + if (data.SelectedFilters.Any(n => !(n.Filter is CompletionExpander) && n.IsSelected)) + { + filterFilteredList = filterFilteredList.Where(n => ShouldBeInCompletionList(n.completionItem, data.SelectedFilters)); + } + + (CompletionItem completionItem, PatternMatch? patternMatch) bestMatch; + if (patternMatcher.HasInvalidPattern) + { + // In a rare edge case where the pattern is invalid (e.g. it is just punctuation), see if any items match directly what user typed. + bestMatch = filterFilteredList.FirstOrDefault(n => string.Equals(n.completionItem.FilterText, filterText, StringComparison.OrdinalIgnoreCase)); + } + else + { + // 99.% cases fall here + bestMatch = filterFilteredList.OrderByDescending(n => n.Item2.HasValue).ThenBy(n => n.Item2).FirstOrDefault(); } - var bestMatch = filterFilteredList.OrderByDescending(n => n.Item2.HasValue).ThenBy(n => n.Item2).FirstOrDefault(); var listWithHighlights = filterFilteredList.Select(n => { ImmutableArray<Span> safeMatchedSpans = ImmutableArray<Span>.Empty; @@ -116,6 +132,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement }).ToImmutableArray(); int selectedItemIndex = 0; + var selectionHint = UpdateSelectionHint.NoChange; if (data.DisplaySuggestionItem) { selectedItemIndex = -1; @@ -127,12 +144,13 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement if (listWithHighlights[i].CompletionItem == bestMatch.completionItem) { selectedItemIndex = i; + selectionHint = UpdateSelectionHint.Selected; break; } } } - return Task.FromResult(new FilteredCompletionModel(listWithHighlights, selectedItemIndex, updatedFilters)); + return Task.FromResult(new FilteredCompletionModel(listWithHighlights, selectedItemIndex, updatedFilters, selectionHint, centerSelection: true, uniqueItem: null)); } Task<ImmutableArray<CompletionItem>> IAsyncCompletionItemManager.SortCompletionListAsync @@ -147,7 +165,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement CompletionItem item, ImmutableArray<CompletionFilterWithState> filtersWithState) { - foreach (var filterWithState in filtersWithState.Where(n => n.IsSelected)) + // Filter out items which don't have a filter which matches selected Filter Button + foreach (var filterWithState in filtersWithState.Where(n => !(n.Filter is CompletionExpander) && n.IsSelected)) { if (item.Filters.Any(n => n == filterWithState.Filter)) { @@ -157,6 +176,21 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement return false; } + private static bool ShouldBeInExpandedCompletionList( + CompletionItem item, + ImmutableArray<CompletionFilterWithState> filtersWithState) + { + // Remove items which have a filter which matches deselected Expander Button + foreach (var filterWithState in filtersWithState.Where(n => n.Filter is CompletionExpander && !(n.IsSelected))) + { + if (item.Filters.Any(n => n is CompletionExpander && n == filterWithState.Filter)) + { + return false; + } + } + return true; + } + #endregion } } diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/DeferredBlockingOperation.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/DeferredBlockingOperation.cs new file mode 100644 index 0000000..8d8384a --- /dev/null +++ b/src/Editor/Language/Impl/Language/AsyncCompletion/DeferredBlockingOperation.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Language.Intellisense.Implementation.AsyncCompletion +{ + internal class DeferredBlockingOperation<T> + { + public JoinableTask<T> Operation { get; } + + private CancellationTokenSource CancellationSource { get; } + private bool _canceled = false; + + /// <summary> + /// Create instance of <see cref="DeferredBlockingOperation"/>, which wraps a blocking <paramref name="operation"/> + /// that is immediately run on the background thread, can be canceled via <paramref name="token"/> + /// and accessed via <see cref="Operation"/> + /// </summary> + /// <param name="jtc">Reference to <see cref="JoinableTaskContext"/></param> + /// <param name="operation">Blocking operation</param> + /// <param name="token">Token used to cancel the blocking operation</param> + public DeferredBlockingOperation(JoinableTaskContext jtc, Func<CancellationToken, Task<T>> operation, CancellationToken token) + { + CancellationSource = new CancellationTokenSource(); + var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationSource.Token, token); + Operation = jtc.Factory.RunAsync<T>(async () => + { + await TaskScheduler.Default; // switch to background thread + return await operation(linkedSource.Token).ConfigureAwait(false); // run the blocking operation + }); + } + + internal void Cancel() + { + if (_canceled) + return; + + CancellationSource.Cancel(); + CancellationSource.Dispose(); + _canceled = true; + } + } +} diff --git a/src/Editor/Language/Impl/Language/AsyncCompletion/UseAsyncCompletionOptionDefinition.cs b/src/Editor/Language/Impl/Language/AsyncCompletion/UseAsyncCompletionOptionDefinition.cs deleted file mode 100644 index 5f648c7..0000000 --- a/src/Editor/Language/Impl/Language/AsyncCompletion/UseAsyncCompletionOptionDefinition.cs +++ /dev/null @@ -1,30 +0,0 @@ -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.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation -{ - [Export(typeof(EditorOptionDefinition))] - [Name(OptionName)] - internal class UseAsyncCompletionOptionDefinition : EditorOptionDefinition - { - public const string OptionName = "UseAsyncCompletion"; - - /// <summary> - /// The meaning of this option definition's values: - /// -1 - user disabled async completion - /// 0 - no changes from the user; check the experimentation service for whether to use async completion - /// 1 - user enabled async completion - /// </summary> - public override object DefaultValue => 0; - - public override Type ValueType => typeof(int); - - public override string Name => OptionName; - } -} diff --git a/src/Editor/Text/Def/Internal/TextUI/IBraceCompletionManager.cs b/src/Editor/Text/Def/Internal/TextUI/IBraceCompletionManager.cs index cb633af..ebc02b6 100644 --- a/src/Editor/Text/Def/Internal/TextUI/IBraceCompletionManager.cs +++ b/src/Editor/Text/Def/Internal/TextUI/IBraceCompletionManager.cs @@ -23,6 +23,11 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion bool HasActiveSessions { get; } /// <summary> + /// Returns number of currently active sessions. + /// </summary> + int ActiveSessionCount { get; } + + /// <summary> /// Opening brace characters the brace completion manager is currently registered to handle. /// </summary> string OpeningBraces { get; } diff --git a/src/Editor/Text/Def/TextData/Model/IExtensionErrorHandler.cs b/src/Editor/Text/Def/TextData/Model/IExtensionErrorHandler.cs index 3a9a071..caf1057 100644 --- a/src/Editor/Text/Def/TextData/Model/IExtensionErrorHandler.cs +++ b/src/Editor/Text/Def/TextData/Model/IExtensionErrorHandler.cs @@ -16,7 +16,7 @@ namespace Microsoft.VisualStudio.Text public interface IExtensionErrorHandler { /// <summary> - /// Notifies that an exception has occured. + /// Logs an exception to ActivityLogs and the telemetry, and notifies that an exception has occured. /// </summary> /// <param name="sender">The extension object or event handler that threw the exception.</param> /// <param name="exception">The exception that was thrown.</param> diff --git a/src/Editor/Text/Def/TextData/Model/IExtensionErrorHandler2.cs b/src/Editor/Text/Def/TextData/Model/IExtensionErrorHandler2.cs new file mode 100644 index 0000000..811bef7 --- /dev/null +++ b/src/Editor/Text/Def/TextData/Model/IExtensionErrorHandler2.cs @@ -0,0 +1,25 @@ +// +// 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> + /// Allows editor hosts to detect exceptions that get captured at extension points. + /// </summary> + /// <remarks>This is a MEF component part, and should be imported as follows: + /// [Import] + /// IExtensionErrorHandler2 errorHandler = null; + /// </remarks> + public interface IExtensionErrorHandler2 : IExtensionErrorHandler + { + /// <summary> + /// Logs an exception to ActivityLogs and the telemetry. + /// </summary> + /// <param name="sender">The extension object or event handler that threw the exception.</param> + /// <param name="exception">The exception to log.</param> + void LogError(object sender, Exception exception); + } +} diff --git a/src/Editor/Text/Def/TextLogic/AssemblyInfo.cs b/src/Editor/Text/Def/TextLogic/AssemblyInfo.cs index 6b99226..9f96b01 100644 --- a/src/Editor/Text/Def/TextLogic/AssemblyInfo.cs +++ b/src/Editor/Text/Def/TextLogic/AssemblyInfo.cs @@ -27,3 +27,6 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("Microsoft.VisualStudio.UI.Text.Commanding.Implementation, PublicKey=" + ThisAssembly.PublicKey)] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Platform.VSEditor, PublicKey=" + ThisAssembly.PublicKey)] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.UI.Utilities, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Language.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.TextViewUnitTestHelper, PublicKey=" + ThisAssembly.PublicKey)] diff --git a/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs b/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs index 996e758..5ddac79 100644 --- a/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs +++ b/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs @@ -249,6 +249,52 @@ namespace Microsoft.VisualStudio.Text.Editor /// </summary> internal static readonly EditorOptionKey<bool> EnableTypingLatencyGuardOptionId = new EditorOptionKey<bool>(EnableTypingLatencyGuardOptionName); internal const string EnableTypingLatencyGuardOptionName = "EnableTypingLatencyGuard"; + + /// <summary> + /// Option that defines the fallback font for the editor. + /// </summary> + /// <remarks> + /// Note that, unlike most other options, this value is only checked once at startup on <see cref="IEditorOptionsFactoryService.GlobalOptions"/> + /// and we do not react to changes. + /// </remarks> + public static readonly EditorOptionKey<string> FallbackFontId = new EditorOptionKey<string>(FallbackFontName); + public const string FallbackFontName = "FallbackFont"; + + /// <summary> + /// Option that defines when Editor should not block waiting for computation of completion items, + /// and either use the last good computed set of completion items, or dismiss completion if no completion items were computed so far. + /// </summary> + public static readonly EditorOptionKey<bool> NonBlockingCompletionOptionId = new EditorOptionKey<bool>(NonBlockingCompletionOptionName); + public const string NonBlockingCompletionOptionName = "NonBlockingCompletion"; + + /// <summary> + /// Option that defines how long Editor should block waiting for computation of completion items, in miliseconds, + /// and either use the last good computed set of completion items, or dismiss completion if no completion items were computed so far. + /// </summary> + public static readonly EditorOptionKey<bool> ResponsiveCompletionOptionId = new EditorOptionKey<bool>(ResponsiveCompletionOptionName); + public const string ResponsiveCompletionOptionName = "ResponsiveCompletion"; + + /// <summary> + /// Option that defines how long Editor should block waiting for computation of completion items, in miliseconds, + /// and either use the last good computed set of completion items, or dismiss completion if no completion items were computed so far. + /// </summary> + public static readonly EditorOptionKey<int> ResponsiveCompletionThresholdOptionId = new EditorOptionKey<int>(ResponsiveCompletionThresholdOptionName); + public const string ResponsiveCompletionThresholdOptionName = "ResponsiveCompletionThreshold"; + + /// <summary> + /// Option that keeps track of whether the responsive mode is enabled using remotely controlled feature flags. + /// If set to false, the feature is off, despite user's choice stored in <see cref="ResponsiveCompletionOptionId"/>. + /// </summary> + internal static readonly EditorOptionKey<bool> RemoteControlledResponsiveCompletionOptionId = new EditorOptionKey<bool>(RemoteControlledResponsiveCompletionOptionName); + internal const string RemoteControlledResponsiveCompletionOptionName = "RemoteControlledResponsiveCompletion"; + + /// <summary> + /// Option that keeps track of whether user toggled the <see cref="DiagnosticModeOptionId"/>. + /// If set to true, Editor will produce a detailed log for a particular scenario of interest. + /// </summary> + internal static readonly EditorOptionKey<bool> DiagnosticModeOptionId = new EditorOptionKey<bool>(DiagnosticModeOptionName); + internal const string DiagnosticModeOptionName = "DiagnosticMode"; + #endregion } @@ -519,6 +565,9 @@ namespace Microsoft.VisualStudio.Text.Editor public override EditorOptionKey<bool> Key { get { return DefaultOptions.EnableTypingLatencyGuardOptionId; } } } + // The option definition for DefaultOptions.FallbackFontId is in the implementation DLLs (since the name of the default fallback will depend + // on the rendering system). + /// <summary> /// The option definition that determines maximum allowed typing latency value in milliseconds. Its value comes either /// from remote settings or from <see cref="UserCustomMaximumTypingLatencyOption"/> if user specifies it in @@ -550,5 +599,72 @@ namespace Microsoft.VisualStudio.Text.Editor public override int Default { get { return Timeout.Infinite; } } public override EditorOptionKey<int> Key { get { return DefaultOptions.UserCustomMaximumTypingLatencyOptionId; } } } + + /// <summary> + /// The option definition that determines whether editor uses non blocking completion mode, + /// where editor does not wait for completion items to arrive when user presses a commit character. + /// This option is not exposed to the users. It is controllable by laguage services. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.NonBlockingCompletionOptionName)] + public sealed class NonBlockingCompletionOption : EditorOptionDefinition<bool> + { + public override bool Default => false; + public override EditorOptionKey<bool> Key => DefaultOptions.NonBlockingCompletionOptionId; + } + + /// <summary> + /// The option definition that determines whether editor uses responsive completion mode, + /// where editor waits short amount of time for completion items when user presses a commit character. + /// If completion items still don't exist after the delay, completion is dismissed. + /// This option is exposed to the users at Tools/Options/Text Editor/Advanced page. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.ResponsiveCompletionOptionName)] + public sealed class ResponsiveCompletionOption : EditorOptionDefinition<bool> + { + public override bool Default => true; + public override EditorOptionKey<bool> Key => DefaultOptions.ResponsiveCompletionOptionId; + } + + /// <summary> + /// The option definition that determines the maximum allowed delay in responsive completion mode, + /// where editor waits specified amount of time for completion items when user presses a commit character. + /// If completion items still don't exist after the delay, completion is dismissed. + /// This option is not exposed to the users. It is controllable by remote setting. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.ResponsiveCompletionThresholdOptionName)] + public sealed class ResponsiveCompletionThresholdOption : EditorOptionDefinition<int> + { + public override int Default => 250; + public override EditorOptionKey<int> Key => DefaultOptions.ResponsiveCompletionThresholdOptionId; + } + + /// <summary> + /// The option definition that determines whether responsive mode should be disabled. + /// This option is set using remotely controllable feature flag, and is set to <c>true</c> + /// so that responsive mode remains enabled when feature flag service may not be reached. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.RemoteControlledResponsiveCompletionOptionName)] + internal sealed class RemoteControlledResponsiveCompletionOption : EditorOptionDefinition<bool> + { + public override bool Default => true; + public override EditorOptionKey<bool> Key => DefaultOptions.RemoteControlledResponsiveCompletionOptionId; + } + + /// <summary> + /// The option definition that puts Editor in a special diagnostic mode + /// where <c>DiagnosticLogger</c> class stores logs that can be later retrieved from a crash dump. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.DiagnosticModeOptionName)] + internal sealed class DiagnosticModeOption : EditorOptionDefinition<bool> + { + public override bool Default => false; + public override EditorOptionKey<bool> Key => DefaultOptions.DiagnosticModeOptionId; + } + #endregion } diff --git a/src/Editor/Text/Def/TextLogic/PatternMatching/IPatternMatcher.cs b/src/Editor/Text/Def/TextLogic/PatternMatching/IPatternMatcher.cs index 27e6e2a..7851134 100644 --- a/src/Editor/Text/Def/TextLogic/PatternMatching/IPatternMatcher.cs +++ b/src/Editor/Text/Def/TextLogic/PatternMatching/IPatternMatcher.cs @@ -39,5 +39,11 @@ namespace Microsoft.VisualStudio.Text.PatternMatching /// To determine sort order, call <see cref="PatternMatch.CompareTo(PatternMatch)"/>. /// </remarks> PatternMatch? TryMatch(string candidate); + + /// <summary> + /// Determines whether given pattern is invalid, + /// in which case <see cref="TryMatch(string)"/> would return no <see cref="PatternMatch"/>es. + /// </summary> + bool HasInvalidPattern { get; } } } diff --git a/src/Editor/Text/Def/TextLogic/PatternMatching/IPatternMatcherFactory2.cs b/src/Editor/Text/Def/TextLogic/PatternMatching/IPatternMatcherFactory2.cs new file mode 100644 index 0000000..1b58499 --- /dev/null +++ b/src/Editor/Text/Def/TextLogic/PatternMatching/IPatternMatcherFactory2.cs @@ -0,0 +1,56 @@ +namespace Microsoft.VisualStudio.Text.PatternMatching +{ + using Microsoft.VisualStudio.Text.PatternMatching; + + /// <summary> + /// Provides instances of a <see cref="IPatternMatcher"/> for a given + /// search string and creation options. + /// </summary> + /// <remarks>This is a MEF component part, and should be imported as follows: + /// [Import] + /// IPatternMatcherFactory2 factory = null; + /// </remarks> + public interface IPatternMatcherFactory2 : IPatternMatcherFactory + { + /// <summary> + /// Gets an <see cref="IPatternMatcher"/> given a search pattern and search options. + /// </summary> + /// <param name="pattern">Describes the search pattern that candidate strings will be compared against for relevancy.</param> + /// <param name="creationOptions">Defines parameters for what should be considered relevant in a match.</param> + /// <param name="linkedMatcher">A matcher whose cache should be shared with the created matcher.</param> + /// <remarks> + /// <para> + /// As opposed to <see cref="IPatternMatcherFactory.CreatePatternMatcher(string, PatternMatcherCreationOptions)"/>, this overload + /// creates a <see cref="IPatternMatcher"/> with a shared cache. Use this overload in contexts with frequently changing <paramref name="pattern"/>s + /// to reduce allocations and throw-away work. Note that sharing the cache between <see cref="IPatternMatcher"/>s used from multiple + /// threads may lead to lock contention. It's recommended to profile prior to opting in. + /// </para> + /// <para> + /// This pattern matcher uses the concepts of a 'Pattern' and a 'Candidate' to to differentiate between what the user types to search + /// and what the system compares against. The pattern and some <see cref="PatternMatcherCreationOptions"/> are specified in here in order to obtain an <see cref="IPatternMatcher"/>. + /// + /// The user can then call <see cref="IPatternMatcher.TryMatch(string)"/> repeatedly with multiple candidates to filter out non-matches, and obtain sortable <see cref="PatternMatch"/> objects to help decide + /// what the user actually wanted. + /// + /// A few examples are useful here. Suppose the user obtains an IPatternMatcher using the following: + /// Pattern = "PatMat" + /// + /// The following calls to TryMatch could expect these results: + /// Candidate = "PatternMatcher" + /// Returns a match containing <see cref="PatternMatchKind.CamelCaseExact"/>. + /// + /// Candidate = "IPatternMatcher" + /// Returns a match containing <see cref="PatternMatchKind.CamelCaseSubstring"/> + /// + /// Candidate = "patmat" + /// Returns a match containing <see cref="PatternMatchKind.Exact"/>, but <see cref="PatternMatch.IsCaseSensitive"/> will be false. + /// + /// Candidate = "Not A Match" + /// Returns <see langword="null"/>. + /// + /// To determine sort order, call <see cref="PatternMatch.CompareTo(PatternMatch)"/>. + /// </para> + /// </remarks> + IPatternMatcher CreatePatternMatcher(string pattern, PatternMatcherCreationOptions creationOptions, IPatternMatcher linkedMatcher); + } +} diff --git a/src/Editor/Text/Impl/BraceCompletion/BraceCompletionManager.cs b/src/Editor/Text/Impl/BraceCompletion/BraceCompletionManager.cs index b5f9689..8346c6c 100644 --- a/src/Editor/Text/Impl/BraceCompletion/BraceCompletionManager.cs +++ b/src/Editor/Text/Impl/BraceCompletion/BraceCompletionManager.cs @@ -8,7 +8,6 @@ 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; @@ -74,6 +73,11 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation get { return _stack.TopSession != null; } } + public int ActiveSessionCount + { + get { return _stack.Sessions.Count; } + } + public string OpeningBraces { get @@ -97,7 +101,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation IBraceCompletionSession session = _stack.TopSession; // check for an existing session first - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: session, () => { if (session.ClosingBrace.Equals(character) && IsCaretOnBuffer(session.SubjectBuffer)) { @@ -149,7 +153,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation } else if (_postSession != null) { - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: _postSession, () => { if (_postSession.ClosingBrace.Equals(character)) { @@ -172,7 +176,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { IBraceCompletionSession session = _stack.TopSession; - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: session, () => { if (IsSingleLine(session.OpeningPoint, session.ClosingPoint)) { @@ -193,7 +197,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { if (_postSession != null) { - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: _postSession, () => { _postSession.PostTab(); }); @@ -212,7 +216,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { IBraceCompletionSession session = _stack.TopSession; - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: session, () => { if (session.OpeningPoint != null && session.ClosingPoint != null) { @@ -233,7 +237,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { if (_postSession != null) { - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: _postSession, () => { _postSession.PostBackspace(); }); @@ -252,7 +256,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { IBraceCompletionSession session = _stack.TopSession; - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: session, () => { if (session.OpeningPoint != null && session.ClosingPoint != null) { @@ -273,7 +277,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { if (_postSession != null) { - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: _postSession, () => { _postSession.PostDelete(); }); @@ -292,7 +296,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { IBraceCompletionSession session = _stack.TopSession; - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: session, () => { if (IsSingleLine(session.OpeningPoint, session.ClosingPoint)) { @@ -313,7 +317,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { if (_postSession != null) { - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: _postSession, () => { _postSession.PostReturn(); }); @@ -330,17 +334,29 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { _textView.Closed += textView_Closed; _textView.Options.OptionChanged += Options_OptionChanged; + _textView.TextDataModel.ContentTypeChanged += OnContentTypeChanged; + } + + private void OnContentTypeChanged(object sender, TextDataModelContentTypeChangedEventArgs e) + { + // Unhook everything if the view's content type goes inert. + if (!e.AfterContentType.IsOfType(StandardContentTypeNames.Any)) + { + this.textView_Closed(null, null); + } } private void textView_Closed(object sender, EventArgs e) { UnregisterEvents(); + _textView.Properties.RemoveProperty("BraceCompletionManager"); } private void UnregisterEvents() { _textView.Closed -= textView_Closed; _textView.Options.OptionChanged -= Options_OptionChanged; + _textView.TextDataModel.ContentTypeChanged -= OnContentTypeChanged; } private void Options_OptionChanged(object sender, EditorOptionChangedEventArgs e) @@ -396,7 +412,7 @@ namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation ITrackingPoint closingPoint = null; IBraceCompletionSession session = _stack.TopSession; - _guardedOperations.CallExtensionPoint(() => + _guardedOperations.CallExtensionPoint(errorSource: session, () => { // only set these if they are on the same buffer if (session.OpeningPoint != null && session.ClosingPoint != null diff --git a/src/Editor/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs b/src/Editor/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs index 6106223..dbb45b2 100644 --- a/src/Editor/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs +++ b/src/Editor/Text/Impl/PatternMatching/AllLowerCamelCaseMatcher.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Immutable; using System.Globalization; -using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; using TextSpan = Microsoft.VisualStudio.Text.Span; namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation diff --git a/src/Editor/Text/Impl/PatternMatching/CamelCaseResult.cs b/src/Editor/Text/Impl/PatternMatching/CamelCaseResult.cs index f11c61b..24dffc3 100644 --- a/src/Editor/Text/Impl/PatternMatching/CamelCaseResult.cs +++ b/src/Editor/Text/Impl/PatternMatching/CamelCaseResult.cs @@ -1,7 +1,7 @@ // 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.Diagnostics; -using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; using TextSpan = Microsoft.VisualStudio.Text.Span; namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation diff --git a/src/Editor/Text/Impl/PatternMatching/ContainerPatternMatcher.cs b/src/Editor/Text/Impl/PatternMatching/ContainerPatternMatcher.cs index 55aa996..671d1c4 100644 --- a/src/Editor/Text/Impl/PatternMatching/ContainerPatternMatcher.cs +++ b/src/Editor/Text/Impl/PatternMatching/ContainerPatternMatcher.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Microsoft.VisualStudio.Text.Utilities; -using Microsoft.VisualStudio.Utilities; namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation { @@ -31,8 +29,9 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation CultureInfo culture, bool allowFuzzyMatching = false, bool allowSimpleSubstringMatching = false, - bool includeMatchedSpans = false) - : base(includeMatchedSpans, culture, allowFuzzyMatching, allowSimpleSubstringMatching) + bool includeMatchedSpans = false, + PatternMatcher linkedMatcher = null) + : base(includeMatchedSpans, culture, allowFuzzyMatching, allowSimpleSubstringMatching, linkedMatcher) { _containerSplitCharacters = containerSplitCharacters.ToArray(); @@ -40,7 +39,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation .Select(text => new PatternSegment(text.Trim(), allowFuzzyMatching: allowFuzzyMatching)) .ToArray(); - _invalidPattern = _patternSegments.Length == 0 || _patternSegments.Any(s => s.IsInvalid); + HasInvalidPattern = _patternSegments.Length == 0 || _patternSegments.Any(s => s.IsInvalid); } #pragma warning disable CA1063 diff --git a/src/Editor/Text/Impl/PatternMatching/PatternMatchExtensions.cs b/src/Editor/Text/Impl/PatternMatching/PatternMatchExtensions.cs index 55ca719..791a840 100644 --- a/src/Editor/Text/Impl/PatternMatching/PatternMatchExtensions.cs +++ b/src/Editor/Text/Impl/PatternMatching/PatternMatchExtensions.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; +using Microsoft.VisualStudio.Utilities; using System.Collections.Immutable; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Text.Utilities; namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation { diff --git a/src/Editor/Text/Impl/PatternMatching/PatternMatcher.cs b/src/Editor/Text/Impl/PatternMatching/PatternMatcher.cs index 793a632..2ab675c 100644 --- a/src/Editor/Text/Impl/PatternMatching/PatternMatcher.cs +++ b/src/Editor/Text/Impl/PatternMatching/PatternMatcher.cs @@ -6,7 +6,6 @@ using System.Collections.Immutable; using System.Diagnostics.Contracts; using System.Globalization; using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Utilities; using Microsoft.VisualStudio.Utilities; using TextSpan = Microsoft.VisualStudio.Text.Span; @@ -26,19 +25,20 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation public const int CamelCaseMatchesFromStartBonus = 2; public const int CamelCaseMaxWeight = CamelCaseContiguousBonus + CamelCaseMatchesFromStartBonus; - private readonly object _gate = new object(); + private readonly object _gate; private readonly bool _includeMatchedSpans; private readonly bool _allowFuzzyMatching; private readonly bool _allowSimpleSubstringMatching; - private readonly Dictionary<string, StringBreaks> _stringToWordSpans = new Dictionary<string, StringBreaks>(); + private readonly Dictionary<string, StringBreaks> _stringToWordSpans; private static readonly Func<string, StringBreaks> _breakIntoWordSpans = StringBreaker.BreakIntoWordParts; // PERF: Cache the culture's compareInfo to avoid the overhead of asking for them repeatedly in inner loops private readonly CompareInfo _compareInfo; - private bool _invalidPattern; + public bool HasInvalidPattern { get; private set; } + /// <summary> /// Construct a new PatternMatcher using the specified culture. /// </summary> @@ -49,25 +49,32 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation bool includeMatchedSpans, CultureInfo culture, bool allowFuzzyMatching = false, - bool allowSimpleSubstringMatching = false) + bool allowSimpleSubstringMatching = false, + PatternMatcher linkedMatcher = null) { culture = culture ?? CultureInfo.CurrentCulture; _compareInfo = culture.CompareInfo; _includeMatchedSpans = includeMatchedSpans; _allowFuzzyMatching = allowFuzzyMatching; _allowSimpleSubstringMatching = allowSimpleSubstringMatching; + _stringToWordSpans = linkedMatcher?._stringToWordSpans ?? new Dictionary<string, StringBreaks>(); + _gate = linkedMatcher?._gate ?? new object(); } #pragma warning disable CA1063 public virtual void Dispose() #pragma warning restore CA1063 { - foreach (var kvp in _stringToWordSpans) + // Disposing this pattern matcher will dispose any linked matchers as well. + lock (_gate) { - kvp.Value.Dispose(); - } + foreach (var kvp in _stringToWordSpans) + { + kvp.Value.Dispose(); + } - _stringToWordSpans.Clear(); + _stringToWordSpans.Clear(); + } } public static PatternMatcher CreateSimplePatternMatcher( @@ -75,21 +82,35 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation CultureInfo culture = null, bool includeMatchedSpans = false, bool allowFuzzyMatching = false, - bool allowSimpleSubstringMatching = false) + bool allowSimpleSubstringMatching = false, + PatternMatcher linkedMatcher = null) { - return new SimplePatternMatcher(pattern, culture, includeMatchedSpans, allowFuzzyMatching, allowSimpleSubstringMatching); + return new SimplePatternMatcher( + pattern, + culture, + includeMatchedSpans, + allowFuzzyMatching, + allowSimpleSubstringMatching, + linkedMatcher); } - public static PatternMatcher CreateContainerPatternMatcher( + internal static PatternMatcher CreateContainerPatternMatcher( string[] patternParts, IReadOnlyCollection<char> containerSplitCharacters, CultureInfo culture = null, bool allowFuzzyMatching = false, bool allowSimpleSubstringMatching = false, - bool includeMatchedSpans = false) + bool includeMatchedSpans = false, + PatternMatcher linkedMatcher = null) { return new ContainerPatternMatcher( - patternParts, containerSplitCharacters, culture, allowFuzzyMatching, allowSimpleSubstringMatching, includeMatchedSpans); + patternParts, + containerSplitCharacters, + culture, + allowFuzzyMatching, + allowSimpleSubstringMatching, + includeMatchedSpans, + linkedMatcher); } internal static (string name, string containerOpt) GetNameAndContainer(string pattern) @@ -104,7 +125,7 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation public abstract PatternMatch? TryMatch(string candidate); private bool SkipMatch(string candidate) - => _invalidPattern || string.IsNullOrWhiteSpace(candidate); + => HasInvalidPattern || string.IsNullOrWhiteSpace(candidate); private StringBreaks GetWordSpans(string word) { diff --git a/src/Editor/Text/Impl/PatternMatching/PatternMatcherFactory.cs b/src/Editor/Text/Impl/PatternMatching/PatternMatcherFactory.cs index ccf1625..fc6c4a5 100644 --- a/src/Editor/Text/Impl/PatternMatching/PatternMatcherFactory.cs +++ b/src/Editor/Text/Impl/PatternMatching/PatternMatcherFactory.cs @@ -6,10 +6,16 @@ using static Microsoft.VisualStudio.Text.PatternMatching.PatternMatcherCreationF namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation { [Export(typeof(IPatternMatcherFactory))] - internal class PatternMatcherFactory : IPatternMatcherFactory + public class PatternMatcherFactory : IPatternMatcherFactory2 { public IPatternMatcher CreatePatternMatcher(string pattern, PatternMatcherCreationOptions creationOptions) { + return this.CreatePatternMatcher(pattern, creationOptions, linkedMatcher: null); + } + +#pragma warning disable CA1822 + public IPatternMatcher CreatePatternMatcher(string pattern, PatternMatcherCreationOptions creationOptions, IPatternMatcher linkedMatcher) + { if (string.IsNullOrWhiteSpace(pattern)) { throw new ArgumentException("A non-empty pattern is required to create a pattern matcher", nameof(pattern)); @@ -20,6 +26,8 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation throw new ArgumentNullException(nameof(creationOptions)); } + var matcher = linkedMatcher as PatternMatcher; + if (creationOptions.ContainerSplitCharacters == null) { return PatternMatcher.CreateSimplePatternMatcher( @@ -27,7 +35,8 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation creationOptions.CultureInfo, creationOptions.Flags.HasFlag(IncludeMatchedSpans), creationOptions.Flags.HasFlag(AllowFuzzyMatching), - creationOptions.Flags.HasFlag(AllowSimpleSubstringMatching)); + creationOptions.Flags.HasFlag(AllowSimpleSubstringMatching), + matcher); } else { @@ -37,8 +46,10 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation creationOptions.CultureInfo, creationOptions.Flags.HasFlag(AllowFuzzyMatching), creationOptions.Flags.HasFlag(AllowSimpleSubstringMatching), - creationOptions.Flags.HasFlag(IncludeMatchedSpans)); + creationOptions.Flags.HasFlag(IncludeMatchedSpans), + matcher); } } +#pragma warning restore CA1822 } } diff --git a/src/Editor/Text/Impl/PatternMatching/SimplePatternMatcher.cs b/src/Editor/Text/Impl/PatternMatching/SimplePatternMatcher.cs index db0e83d..c23ad30 100644 --- a/src/Editor/Text/Impl/PatternMatching/SimplePatternMatcher.cs +++ b/src/Editor/Text/Impl/PatternMatching/SimplePatternMatcher.cs @@ -1,7 +1,7 @@ // 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.Collections.Generic; using System.Globalization; -using Microsoft.VisualStudio.Text.Utilities; using Microsoft.VisualStudio.Utilities; using TextSpan = Microsoft.VisualStudio.Text.Span; @@ -18,13 +18,14 @@ namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation CultureInfo culture, bool includeMatchedSpans, bool allowFuzzyMatching, - bool allowSimpleSubstringMatching = false) - : base(includeMatchedSpans, culture, allowFuzzyMatching, allowSimpleSubstringMatching) + bool allowSimpleSubstringMatching = false, + PatternMatcher linkedMatcher = null) + : base(includeMatchedSpans, culture, allowFuzzyMatching, allowSimpleSubstringMatching, linkedMatcher) { pattern = pattern.Trim(); _fullPatternSegment = new PatternSegment(pattern, allowFuzzyMatching); - _invalidPattern = _fullPatternSegment.IsInvalid; + HasInvalidPattern = _fullPatternSegment.IsInvalid; } public override void Dispose() diff --git a/src/Editor/Text/Impl/PatternMatching/StringBreaker.cs b/src/Editor/Text/Impl/PatternMatching/StringBreaker.cs index 343953c..044fed2 100644 --- a/src/Editor/Text/Impl/PatternMatching/StringBreaker.cs +++ b/src/Editor/Text/Impl/PatternMatching/StringBreaker.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; using TextSpan = Microsoft.VisualStudio.Text.Span; namespace Microsoft.VisualStudio.Text.PatternMatching.Implementation diff --git a/src/Editor/Text/Util/TextDataUtil/GuardedOperations.cs b/src/Editor/Text/Util/TextDataUtil/GuardedOperations.cs index 92a8ad8..3b0d866 100644 --- a/src/Editor/Text/Util/TextDataUtil/GuardedOperations.cs +++ b/src/Editor/Text/Util/TextDataUtil/GuardedOperations.cs @@ -18,8 +18,10 @@ namespace Microsoft.VisualStudio.Text.Utilities /// </summary> [Export] [Export(typeof(IGuardedOperations))] + [Export(typeof(IGuardedOperations2))] + [Export(typeof(IGuardedOperationsInternal))] [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class GuardedOperations : IGuardedOperations + internal sealed class GuardedOperations : IGuardedOperations, IGuardedOperations2, IGuardedOperationsInternal { [ImportMany] private List<Lazy<IExtensionErrorHandler>> _errorHandlerExports = null; @@ -66,7 +68,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { get { - if (_errorHandlers == null) + if (_errorHandlers == null) { _errorHandlers = new FrugalList<IExtensionErrorHandler>(); if (_errorHandlerExports != null) // can be null during unit testing @@ -131,16 +133,20 @@ namespace Microsoft.VisualStudio.Text.Utilities where TMetadataView : IContentTypeMetadata where TExtensionFactory : class { - var factory = InvokeBestMatchingFactory(providerHandles, dataContentType, contentTypeRegistryService, errorSource); - - if (factory == null) + var factories = GetOrderedMatchingExtensions(providerHandles, dataContentType, contentTypeRegistryService); + foreach (var factoryExport in factories) { - return default(TExtensionInstance); + TExtensionFactory factory = InstantiateExtension(errorSource, factoryExport); + if (factory != null) + { + TExtensionInstance extensionInstance = default(TExtensionInstance); + this.CallExtensionPoint(errorSource, () => extensionInstance = getter(factory)); + if (extensionInstance != null) + return extensionInstance; + } } - TExtensionInstance extensionInstance = default(TExtensionInstance); - this.CallExtensionPoint(errorSource, () => extensionInstance = getter(factory)); - return extensionInstance; + return default(TExtensionInstance); } public TExtension InvokeBestMatchingFactory<TExtension, TMetadataView> @@ -150,22 +156,36 @@ namespace Microsoft.VisualStudio.Text.Utilities object errorSource) where TMetadataView : IContentTypeMetadata { + var extensions = GetOrderedMatchingExtensions(providerHandles, dataContentType, contentTypeRegistryService); + foreach (var extension in extensions) + { + TExtension factory = InstantiateExtension(errorSource, extension); + if (factory != null) + { + return factory; + } + } + + // no suitable provider found + return default(TExtension); + } + + /// <summary> + /// Return a list of uninstantiated extensions sorted by the specificity of the content type (assets with more specific content types come first). + /// </summary> + private static IEnumerable<Lazy<TExtension, TMetadataView>> GetOrderedMatchingExtensions<TExtension, TMetadataView> + (IList<Lazy<TExtension, TMetadataView>> providerHandles, + IContentType dataContentType, + IContentTypeRegistryService contentTypeRegistryService) + where TMetadataView : IContentTypeMetadata + { var candidates = new List<Lazy<TExtension, TMetadataView>>(); 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) - { - // we have an exact match--no need to look further if this one is happy - TExtension factory = InstantiateExtension(errorSource, providerHandle); - if (factory != null) - { - return factory; - } - } - else if (dataContentType.IsOfType(contentTypeName)) + if (dataContentType.IsOfType(contentTypeName)) { candidates.Add(providerHandle); break; @@ -175,17 +195,7 @@ namespace Microsoft.VisualStudio.Text.Utilities SortCandidates(candidates, dataContentType, contentTypeRegistryService); - for (int c = 0; c < candidates.Count; ++c) - { - TExtension factory = InstantiateExtension(errorSource, candidates[c]); - if (factory != null) - { - return factory; - } - } - - // no suitable provider found - return default(TExtension); + return candidates; } public List<TExtensionInstance> InvokeMatchingFactories<TExtensionInstance, TExtensionFactory, TMetadataView> @@ -423,6 +433,28 @@ namespace Microsoft.VisualStudio.Text.Utilities } } + public T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow, Predicate<Exception> exceptionToIgnore, Predicate<Exception> exceptionToHandle) + { + try + { + BeforeCallingExtensionPoint(errorSource ?? call); + return call(); + } + catch (Exception e) when (exceptionToIgnore(e)) + { + return valueOnThrow; + } + catch (Exception e) when (exceptionToHandle(e)) + { + HandleException(errorSource, e); + return valueOnThrow; + } + finally + { + AfterCallingExtensionPoint(errorSource ?? call); + } + } + public void CallExtensionPoint(Action call) { this.CallExtensionPoint(errorSource: null, call: call); @@ -608,6 +640,36 @@ namespace Microsoft.VisualStudio.Text.Utilities } } + public void LogException(object errorSource, Exception e) + { + var logged = false; + for (int i = 0; (i < ErrorHandlers.Count); ++i) + { + var errorHandler = ErrorHandlers[i] as IExtensionErrorHandler2; + if (errorHandler != null) + { + try + { + GuardedOperations.LastHandledException = e; + GuardedOperations.LastHandleExceptionStackTrace = e.StackTrace; + + errorHandler.LogError(errorSource, e); + logged = true; + } + catch (Exception doubleFaultException) + { + // TODO: What is the right behavior here? + GuardedOperations.Fail(doubleFaultException.ToString()); + } + } + } + + if (!logged) + { + Debug.WriteLine(e.Message); + } + } + public void HandleException(object errorSource, Exception e) { bool handled = false; @@ -662,7 +724,7 @@ namespace Microsoft.VisualStudio.Text.Utilities contentTypes.Sort(CompareContentTypes); candidates.Sort((left, right) => { - int leftIndex = BestContentTypeScore(left.Metadata.ContentTypes, contentTypes); + int leftIndex = BestContentTypeScore(left.Metadata.ContentTypes, contentTypes); int rightIndex = BestContentTypeScore(right.Metadata.ContentTypes, contentTypes); return leftIndex - rightIndex; // Sort these in ascending order. diff --git a/src/Editor/Text/Util/TextUIUtil/DiagnosticLogger.cs b/src/Editor/Text/Util/TextUIUtil/DiagnosticLogger.cs new file mode 100644 index 0000000..21f2b07 --- /dev/null +++ b/src/Editor/Text/Util/TextUIUtil/DiagnosticLogger.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Text.UI.Utilities +{ + public static class DiagnosticLogger + { + private static bool WasLoggingEnabled; + private static List<(long, string)> Log = new List<(long, string)>(); + + public static bool IsLoggingEnabled(ITextView textView) + { + var currentValue = textView.Options.GetOptionValue(DefaultOptions.DiagnosticModeOptionId); + if (!WasLoggingEnabled && currentValue) + { + Add("--- Begin new log"); + } + if (WasLoggingEnabled != currentValue) + { + WasLoggingEnabled = currentValue; + } + return currentValue; + } + + public static void Add(string message) + { + Log.Add((DateTime.Now.Ticks, message)); + } + + public static void Add(string message, object param) + { + Log.Add((DateTime.Now.Ticks, message + param.ToString())); + } + } +} |